Files
codex/sdk/python/src/codex_app_server/_run.py
Shaqayeq 4fd2774614 Add Python SDK thread.run convenience methods (#15088)
## TL;DR
Add `thread.run(...)` / `async thread.run(...)` convenience methods to
the Python SDK for the common case.

- add `RunInput = Input | str` and `RunResult` with `final_response`,
collected `items`, and optional `usage`
- keep `thread.turn(...)` strict and lower-level for streaming,
steering, interrupting, and raw generated `Turn` access
- update Python SDK docs, quickstart examples, and tests for the sync
and async convenience flows

## Validation
- `python3 -m pytest sdk/python/tests/test_public_api_signatures.py
sdk/python/tests/test_public_api_runtime_behavior.py`
- `python3 -m pytest
sdk/python/tests/test_real_app_server_integration.py -k
'thread_run_convenience or async_thread_run_convenience'` (skipped in
this environment)

---------

Co-authored-by: Codex <noreply@openai.com>
2026-03-19 00:57:48 +00:00

113 lines
3.6 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import AsyncIterator, Iterator
from .generated.v2_all import (
AgentMessageThreadItem,
ItemCompletedNotification,
MessagePhase,
ThreadItem,
ThreadTokenUsage,
ThreadTokenUsageUpdatedNotification,
Turn as AppServerTurn,
TurnCompletedNotification,
TurnStatus,
)
from .models import Notification
@dataclass(slots=True)
class RunResult:
final_response: str | None
items: list[ThreadItem]
usage: ThreadTokenUsage | None
def _agent_message_item_from_thread_item(
item: ThreadItem,
) -> AgentMessageThreadItem | None:
thread_item = item.root if hasattr(item, "root") else item
if isinstance(thread_item, AgentMessageThreadItem):
return thread_item
return None
def _final_assistant_response_from_items(items: list[ThreadItem]) -> str | None:
last_unknown_phase_response: str | None = None
for item in reversed(items):
agent_message = _agent_message_item_from_thread_item(item)
if agent_message is None:
continue
if agent_message.phase == MessagePhase.final_answer:
return agent_message.text
if agent_message.phase is None and last_unknown_phase_response is None:
last_unknown_phase_response = agent_message.text
return last_unknown_phase_response
def _raise_for_failed_turn(turn: AppServerTurn) -> None:
if turn.status != TurnStatus.failed:
return
if turn.error is not None and turn.error.message:
raise RuntimeError(turn.error.message)
raise RuntimeError(f"turn failed with status {turn.status.value}")
def _collect_run_result(stream: Iterator[Notification], *, turn_id: str) -> RunResult:
completed: TurnCompletedNotification | None = None
items: list[ThreadItem] = []
usage: ThreadTokenUsage | None = None
for event in stream:
payload = event.payload
if isinstance(payload, ItemCompletedNotification) and payload.turn_id == turn_id:
items.append(payload.item)
continue
if isinstance(payload, ThreadTokenUsageUpdatedNotification) and payload.turn_id == turn_id:
usage = payload.token_usage
continue
if isinstance(payload, TurnCompletedNotification) and payload.turn.id == turn_id:
completed = payload
if completed is None:
raise RuntimeError("turn completed event not received")
_raise_for_failed_turn(completed.turn)
return RunResult(
final_response=_final_assistant_response_from_items(items),
items=items,
usage=usage,
)
async def _collect_async_run_result(
stream: AsyncIterator[Notification], *, turn_id: str
) -> RunResult:
completed: TurnCompletedNotification | None = None
items: list[ThreadItem] = []
usage: ThreadTokenUsage | None = None
async for event in stream:
payload = event.payload
if isinstance(payload, ItemCompletedNotification) and payload.turn_id == turn_id:
items.append(payload.item)
continue
if isinstance(payload, ThreadTokenUsageUpdatedNotification) and payload.turn_id == turn_id:
usage = payload.token_usage
continue
if isinstance(payload, TurnCompletedNotification) and payload.turn.id == turn_id:
completed = payload
if completed is None:
raise RuntimeError("turn completed event not received")
_raise_for_failed_turn(completed.turn)
return RunResult(
final_response=_final_assistant_response_from_items(items),
items=items,
usage=usage,
)