mirror of
https://github.com/openai/codex.git
synced 2026-05-16 09:12:54 +00:00
Route Python SDK turn notifications by ID (#21778)
## Why The Python SDK previously protected the stdio transport with a single active turn-consumer guard. That avoided competing reads from stdout, but it also meant one `Codex`/`AsyncCodex` client could not stream multiple active turns at the same time. Notifications could also arrive before the caller received a `TurnHandle` and registered for streaming, so the SDK needed an explicit routing layer instead of letting individual API calls read directly from the shared transport. ## What Changed - Added a private `MessageRouter` that owns per-request response queues, per-turn notification queues, pending turn-notification replay, and global notification delivery behind a single stdout reader thread. - Generated typed notification routing metadata so turn IDs come from known payload shapes instead of router-side attribute guessing, with explicit fallback handling for unknown notification payloads. - Updated sync and async turn streaming so `TurnHandle.stream()`/`run()` and `stream_text()` consume only notifications for their own turn ID, while `AsyncAppServerClient` no longer serializes all transport calls behind one async lock. - Cleared pending turn-notification buffers when unregistered turns complete so never-consumed turn handles do not leave stale queues behind. - Removed the internal stream-until helper now that turn completion waiting can register directly with routed turn notifications. - Updated Python SDK docs and focused tests for concurrent transport calls, interleaved turn routing, buffered early notifications, unknown notification routing, async delegation, and routed turn completion behavior. ## Validation - `uv run --extra dev ruff format scripts/update_sdk_artifacts.py src/codex_app_server/_message_router.py src/codex_app_server/client.py src/codex_app_server/generated/notification_registry.py tests/test_client_rpc_methods.py tests/test_public_api_runtime_behavior.py tests/test_async_client_behavior.py` - `uv run --extra dev ruff check scripts/update_sdk_artifacts.py src/codex_app_server/_message_router.py src/codex_app_server/client.py src/codex_app_server/generated/notification_registry.py tests/test_client_rpc_methods.py tests/test_public_api_runtime_behavior.py tests/test_async_client_behavior.py` - `uv run --extra dev pytest tests/test_client_rpc_methods.py tests/test_public_api_runtime_behavior.py tests/test_async_client_behavior.py` - `git diff --check` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -585,6 +585,43 @@ def _notification_specs() -> list[tuple[str, str]]:
|
||||
return specs
|
||||
|
||||
|
||||
def _notification_turn_id_specs(
|
||||
specs: list[tuple[str, str]],
|
||||
) -> tuple[list[str], list[str]]:
|
||||
server_notifications = json.loads(
|
||||
(schema_root_dir() / "ServerNotification.json").read_text()
|
||||
)
|
||||
definitions = server_notifications.get("definitions", {})
|
||||
if not isinstance(definitions, dict):
|
||||
return ([], [])
|
||||
|
||||
direct: list[str] = []
|
||||
nested: list[str] = []
|
||||
for _, class_name in specs:
|
||||
definition = definitions.get(class_name)
|
||||
if not isinstance(definition, dict):
|
||||
continue
|
||||
props = definition.get("properties", {})
|
||||
if not isinstance(props, dict):
|
||||
continue
|
||||
if "turnId" in props:
|
||||
direct.append(class_name)
|
||||
continue
|
||||
turn = props.get("turn")
|
||||
if isinstance(turn, dict) and turn.get("$ref") == "#/definitions/Turn":
|
||||
nested.append(class_name)
|
||||
|
||||
return (sorted(set(direct)), sorted(set(nested)))
|
||||
|
||||
|
||||
def _type_tuple_source(class_names: list[str]) -> str:
|
||||
if not class_names:
|
||||
return "()"
|
||||
if len(class_names) == 1:
|
||||
return f"({class_names[0]},)"
|
||||
return "(\n" + "".join(f" {class_name},\n" for class_name in class_names) + ")"
|
||||
|
||||
|
||||
def generate_notification_registry() -> None:
|
||||
out = (
|
||||
sdk_root()
|
||||
@@ -595,6 +632,7 @@ def generate_notification_registry() -> None:
|
||||
)
|
||||
specs = _notification_specs()
|
||||
class_names = sorted({class_name for _, class_name in specs})
|
||||
direct_turn_id_types, nested_turn_types = _notification_turn_id_specs(specs)
|
||||
|
||||
lines = [
|
||||
"# Auto-generated by scripts/update_sdk_artifacts.py",
|
||||
@@ -616,7 +654,26 @@ def generate_notification_registry() -> None:
|
||||
)
|
||||
for method, class_name in specs:
|
||||
lines.append(f' "{method}": {class_name},')
|
||||
lines.extend(["}", ""])
|
||||
lines.extend(
|
||||
[
|
||||
"}",
|
||||
"",
|
||||
"DIRECT_TURN_ID_NOTIFICATION_TYPES: tuple[type[BaseModel], ...] = "
|
||||
f"{_type_tuple_source(direct_turn_id_types)}",
|
||||
"",
|
||||
"NESTED_TURN_NOTIFICATION_TYPES: tuple[type[BaseModel], ...] = "
|
||||
f"{_type_tuple_source(nested_turn_types)}",
|
||||
"",
|
||||
"",
|
||||
"def notification_turn_id(payload: BaseModel) -> str | None:",
|
||||
" if isinstance(payload, DIRECT_TURN_ID_NOTIFICATION_TYPES):",
|
||||
" return payload.turn_id if isinstance(payload.turn_id, str) else None",
|
||||
" if isinstance(payload, NESTED_TURN_NOTIFICATION_TYPES):",
|
||||
" return payload.turn.id",
|
||||
" return None",
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
out.write_text("\n".join(lines))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user