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:54:18 +00:00
committed by copyberry
parent 8afccec87a
commit 1a4096e273
19 changed files with 1031 additions and 87 deletions
+3 -2
View File
@@ -84,6 +84,7 @@ def _initialized_client(
@pytest.mark.parametrize(
("method", "params"),
[
("turn/start", {"input": [], "toolOutput": {"name": "delegate", "output": "Investigate"}}),
("turn/start", {"input": [], "turnTrigger": "automation"}),
("turn/start", {"input": [], "serviceTierForTurn": "default"}),
("thread/resume", {"threadId": "thread-1", "excludeTurns": False}),
@@ -117,7 +118,7 @@ def test_new_options_accept_supported_runtime_metadata(
monkeypatch: pytest.MonkeyPatch, metadata: JsonObject
) -> None:
client, requests = _initialized_client(monkeypatch, metadata)
params = {"input": [], "turnTrigger": "automation"}
params = {"input": [], "toolOutput": {"name": "delegate", "output": "Investigate"}}
client.request("turn/start", params, response_model=InitializeResponse)
@@ -665,7 +666,7 @@ def test_turn_notification_router_clears_unregistered_turn_when_completed() -> N
)
)
assert client._router._pending_turn_notifications == {}
assert client._router._turn_states == {}
def test_turn_notification_router_routes_unknown_turn_notifications() -> None: