Files
codex/sdk/python/src/openai_codex/_run.py
T
Ahmed Ibrahim eb1cc3824c [codex] Prepare Python SDK beta documentation and package metadata (#24836)
## Why

The initial public `openai-codex` beta should read and install like a
normal published Python package before a release tag is created. This
follows merged PR #24828, which establishes the independent SDK beta
release plumbing and exact runtime dependency.

## What changed

- Rewrote `sdk/python/README.md` as a compact PyPI-facing beta package
page: published installation, one quickstart, short login examples,
built-in help, and links to deeper guides.
- Updated the getting-started guide, API reference, FAQ, and examples
index to present the published beta consistently without repeating
onboarding in the package landing page or reference page.
- Made `pip install openai-codex` the primary install path while beta
releases are the only published SDK releases, with `--pre` documented
for opting into prereleases after a stable release exists.
- Added curated `help()` / `pydoc` docstrings across the public API and
generated public convenience methods through
`scripts/update_sdk_artifacts.py`.
- Declared the repository `Apache-2.0` license expression and
Documentation URL in package metadata, without introducing a duplicated
SDK-local license file.
- Kept the source distribution focused on installable package material
(`src/openai_codex`, `README.md`, and `pyproject.toml`); the repository
docs and runnable examples remain linked from the PyPI README.
- Built release artifacts in an Alpine container on the Ubuntu runner,
matching Python SDK CI and allowing type generation to install the
published `musllinux` runtime wheel.
- Added `twine check --strict` to the release workflow so malformed PyPI
metadata or rendered README content fails before publishing.
- Added focused SDK assertions for beta metadata, the exact runtime pin,
source distribution contents, and the built-in Python documentation
surface.

## Validation

- Ran `uv run --frozen --extra dev ruff check
scripts/update_sdk_artifacts.py src/openai_codex
tests/test_public_api_signatures.py
tests/test_artifact_workflow_and_binaries.py` before the final
README-only reductions and review-fix follow-ups.
- Built `openai_codex-0.1.0b1-py3-none-any.whl` and
`openai_codex-0.1.0b1.tar.gz` before the final README-only reductions
and review-fix follow-ups.
- Ran `python -m twine check --strict` on both built artifacts before
the final README-only reductions and review-fix follow-ups.
- Verified artifact metadata reports `Apache-2.0` without a duplicated
SDK-local license file.
- Verified `inspect.getdoc(...)` resolves documentation for the package,
`Codex`, `CodexConfig`, and key generated thread methods.
- Rebased the documentation/readiness change onto merged PR #24828
without changing the intended SDK or workflow file contents.
- Final verification is delegated to online CI for this PR.
2026-05-27 18:29:05 -07:00

136 lines
4.2 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,
TurnCompletedNotification,
TurnError,
TurnStatus,
)
from .models import Notification
@dataclass(slots=True)
class TurnResult:
"""Collected result returned after a turn completes."""
id: str
status: TurnStatus
error: TurnError | None
started_at: int | None
completed_at: int | None
duration_ms: int | None
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: Turn) -> 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_turn_result(stream: Iterator[Notification], *, turn_id: str) -> TurnResult:
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)
turn = completed.turn
return TurnResult(
id=turn.id,
status=turn.status,
error=turn.error,
started_at=turn.started_at,
completed_at=turn.completed_at,
duration_ms=turn.duration_ms,
final_response=_final_assistant_response_from_items(items),
items=items,
usage=usage,
)
async def _collect_async_turn_result(
stream: AsyncIterator[Notification], *, turn_id: str
) -> TurnResult:
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)
turn = completed.turn
return TurnResult(
id=turn.id,
status=turn.status,
error=turn.error,
started_at=turn.started_at,
completed_at=turn.completed_at,
duration_ms=turn.duration_ms,
final_response=_final_assistant_response_from_items(items),
items=items,
usage=usage,
)