Add untrusted external messages to the Python SDK (#44086)

## Why

Applications need to deliver content from other agents, tools, or services with tool-level authority, without treating it as user input or granting authorization.

## What changed

- Export `ExternalMessage` for sync and async `run(...)` and `turn(...)`, accepting text or structured content with a tool name and optional namespace. Send it through `toolOutput` and require CLI 0.151.0 or newer.
- Support starting a turn or joining an active regular turn while preserving external content as function output in history. Keep external messages separate from user-input lists and `steer(...)`.
- Give turn handles independent subscriptions, replaying completed items and latest usage to joining handles. Release consumed transient events and clean up subscriptions on closure, failure, or cancellation.
- Document the authority boundary and add sync and async examples.

## Testing

Add coverage for wire representations, input validation, runtime compatibility, tool authority across resume, active-turn joins, and tool-output truncation. Add subscription tests for replay, concurrent consumers, early completion, cancellation, and cleanup.

GitOrigin-RevId: 6106327085fd9c4bd11b71e20b3d8e74738b8bb5
This commit is contained in:
Ahmed Ibrahim
2026-09-09 06:46:01 +00:00
committed by copyberry
parent 8afccec87a
commit 1a4096e273
19 changed files with 1031 additions and 87 deletions

View File

@@ -1259,7 +1259,8 @@ def _render_thread_block(turn_fields: list[PublicFieldSpec], *, is_async: bool =
" ) -> TurnResult:",
' """Run a complete turn and collect its final result.',
"",
" Accepts the same input and options as turn().",
" Accepts the same input and options as turn(), including ExternalMessage",
" for untrusted external content with tool-level authority.",
' """',
f" turn = {await_prefix}self.turn(",
" input,",
@@ -1275,18 +1276,21 @@ def _render_thread_block(turn_fields: list[PublicFieldSpec], *, is_async: bool =
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
f" ) -> {handle_type}:",
' """Start a turn and return a handle for streaming or control.',
' """Start a turn or join an active regular turn and return its handle.',
"",
" ExternalMessage supplies untrusted content with tool-level authority;",
" it does not establish user authorization or approval.",
" turn_service_tier applies only to this new turn; service_tier updates",
" the thread default. source labels what initiated a new turn and grants",
" no authority. Both turn_service_tier and source are ignored when joining.",
' """',
" wire_input = _to_wire_input(_normalize_run_input(input))",
" wire_input, tool_output = _to_wire_turn_input(input)",
*([" await self._codex._ensure_initialized()"] if is_async else []),
_approval_mode_assignment_line("_approval_mode_override_settings"),
" params = TurnStartParams(",
" thread_id=self.id,",
" input=wire_input,",
" tool_output=tool_output,",
*_approval_mode_model_arg_lines(),
*_model_arg_lines(turn_fields),
" )",