Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ installs the pinned runtime package automatically.
## Quickstart

```python
from codex_app_server import Codex, TextInput
from codex_app_server import Codex

with Codex() as codex:
thread = codex.thread_start(model="gpt-5")
completed_turn = thread.turn(TextInput("Say hello in one sentence.")).run()
print(completed_turn.status)
print(completed_turn.id)
result = thread.run("Say hello in one sentence.")
print(result.final_response)
print(len(result.items))
```

`result.final_response` is `None` when the turn completes without a final-answer
or phase-less assistant message item.

## Docs map

- Golden path tutorial: `docs/getting-started.md`
Expand Down Expand Up @@ -95,4 +98,6 @@ This supports the CI release flow:

- `Codex()` is eager and performs startup + `initialize` in the constructor.
- Use context managers (`with Codex() as codex:`) to ensure shutdown.
- Prefer `thread.run("...")` for the common case. Use `thread.turn(...)` when
you need streaming, steering, or interrupt control.
- For transient overload, use `codex_app_server.retry.retry_on_overload`.
27 changes: 22 additions & 5 deletions sdk/python/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

Public surface of `codex_app_server` for app-server v2.

This SDK surface is experimental. The current implementation intentionally allows only one active `TurnHandle.stream()` or `TurnHandle.run()` consumer per client instance at a time.
This SDK surface is experimental. The current implementation intentionally allows only one active turn consumer (`Thread.run()`, `TurnHandle.stream()`, or `TurnHandle.run()`) per client instance at a time.

## Package Entry

```python
from codex_app_server import (
Codex,
AsyncCodex,
RunResult,
Thread,
AsyncThread,
TurnHandle,
Expand All @@ -24,7 +25,7 @@ from codex_app_server import (
MentionInput,
TurnStatus,
)
from codex_app_server.generated.v2_all import ThreadItem
from codex_app_server.generated.v2_all import ThreadItem, ThreadTokenUsage
```

- Version: `codex_app_server.__version__`
Expand Down Expand Up @@ -97,18 +98,34 @@ async with AsyncCodex() as codex:

### Thread

- `run(input: str | Input, *, approval_policy=None, approvals_reviewer=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> RunResult`
- `turn(input: Input, *, approval_policy=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> TurnHandle`
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
- `set_name(name: str) -> ThreadSetNameResponse`
- `compact() -> ThreadCompactStartResponse`

### AsyncThread

- `run(input: str | Input, *, approval_policy=None, approvals_reviewer=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[RunResult]`
- `turn(input: Input, *, approval_policy=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> Awaitable[AsyncTurnHandle]`
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
- `compact() -> Awaitable[ThreadCompactStartResponse]`

`run(...)` is the common-case convenience path. It accepts plain strings, starts
the turn, consumes notifications until completion, and returns a small result
object with:

- `final_response: str | None`
- `items: list[ThreadItem]`
- `usage: ThreadTokenUsage | None`

`final_response` is `None` when the turn finishes without a final-answer or
phase-less assistant message item.

Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
`interrupt()`) or the canonical generated `Turn` from `TurnHandle.run()`.

## TurnHandle / AsyncTurnHandle

### TurnHandle
Expand Down Expand Up @@ -181,10 +198,10 @@ from codex_app_server import (
## Example

```python
from codex_app_server import Codex, TextInput
from codex_app_server import Codex

with Codex() as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
completed_turn = thread.turn(TextInput("Say hello in one sentence.")).run()
print(completed_turn.id, completed_turn.status)
result = thread.run("Say hello in one sentence.")
print(result.final_response)
```
38 changes: 19 additions & 19 deletions sdk/python/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,42 @@ Requirements:
## 2) Run your first turn (sync)

```python
from codex_app_server import Codex, TextInput
from codex_app_server import Codex

with Codex() as codex:
server = codex.metadata.serverInfo
print("Server:", None if server is None else server.name, None if server is None else server.version)

thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
completed_turn = thread.turn(TextInput("Say hello in one sentence.")).run()
result = thread.run("Say hello in one sentence.")

print("Thread:", thread.id)
print("Turn:", completed_turn.id)
print("Status:", completed_turn.status)
print("Items:", len(completed_turn.items or []))
print("Text:", result.final_response)
print("Items:", len(result.items))
```

What happened:

- `Codex()` started and initialized `codex app-server`.
- `thread_start(...)` created a thread.
- `turn(...).run()` consumed events until `turn/completed` and returned the canonical generated app-server `Turn` model.
- one client can have only one active `TurnHandle.stream()` / `TurnHandle.run()` consumer at a time in the current experimental build
- `thread.run("...")` started a turn, consumed events until completion, and returned the final assistant response plus collected items and usage.
- `result.final_response` is `None` when no final-answer or phase-less assistant message item completes for the turn.
- use `thread.turn(...)` when you need a `TurnHandle` for streaming, steering, interrupting, or turn IDs/status
- one client can have only one active turn consumer (`thread.run(...)`, `TurnHandle.stream()`, or `TurnHandle.run()`) at a time in the current experimental build

## 3) Continue the same thread (multi-turn)

```python
from codex_app_server import Codex, TextInput
from codex_app_server import Codex

with Codex() as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})

first = thread.turn(TextInput("Summarize Rust ownership in 2 bullets.")).run()
second = thread.turn(TextInput("Now explain it to a Python developer.")).run()
first = thread.run("Summarize Rust ownership in 2 bullets.")
second = thread.run("Now explain it to a Python developer.")

print("first:", first.id, first.status)
print("second:", second.id, second.status)
print("first:", first.final_response)
print("second:", second.final_response)
```

## 4) Async parity
Expand All @@ -66,15 +67,14 @@ initializes lazily, and context entry makes startup/shutdown explicit.

```python
import asyncio
from codex_app_server import AsyncCodex, TextInput
from codex_app_server import AsyncCodex


async def main() -> None:
async with AsyncCodex() as codex:
thread = await codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
turn = await thread.turn(TextInput("Continue where we left off."))
completed_turn = await turn.run()
print(completed_turn.id, completed_turn.status)
result = await thread.run("Continue where we left off.")
print(result.final_response)


asyncio.run(main())
Expand All @@ -83,14 +83,14 @@ asyncio.run(main())
## 5) Resume an existing thread

```python
from codex_app_server import Codex, TextInput
from codex_app_server import Codex

THREAD_ID = "thr_123" # replace with a real id

with Codex() as codex:
thread = codex.thread_resume(THREAD_ID)
completed_turn = thread.turn(TextInput("Continue where we left off.")).run()
print(completed_turn.id, completed_turn.status)
result = thread.run("Continue where we left off.")
print(result.final_response)
```

## 6) Generated models
Expand Down
14 changes: 4 additions & 10 deletions sdk/python/examples/01_quickstart_constructor/async.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
sys.path.insert(0, str(_EXAMPLES_ROOT))

from _bootstrap import (
assistant_text_from_turn,
ensure_local_sdk_src,
find_turn_by_id,
runtime_config,
server_label,
)
Expand All @@ -17,21 +15,17 @@

import asyncio

from codex_app_server import AsyncCodex, TextInput
from codex_app_server import AsyncCodex


async def main() -> None:
async with AsyncCodex(config=runtime_config()) as codex:
print("Server:", server_label(codex.metadata))

thread = await codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
turn = await thread.turn(TextInput("Say hello in one sentence."))
result = await turn.run()
persisted = await thread.read(include_turns=True)
persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)

print("Status:", result.status)
print("Text:", assistant_text_from_turn(persisted_turn))
result = await thread.run("Say hello in one sentence.")
print("Items:", len(result.items))
print("Text:", result.final_response)


if __name__ == "__main__":
Expand Down
12 changes: 4 additions & 8 deletions sdk/python/examples/01_quickstart_constructor/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,19 @@
sys.path.insert(0, str(_EXAMPLES_ROOT))

from _bootstrap import (
assistant_text_from_turn,
ensure_local_sdk_src,
find_turn_by_id,
runtime_config,
server_label,
)

ensure_local_sdk_src()

from codex_app_server import Codex, TextInput
from codex_app_server import Codex

with Codex(config=runtime_config()) as codex:
print("Server:", server_label(codex.metadata))

thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
result = thread.turn(TextInput("Say hello in one sentence.")).run()
persisted = thread.read(include_turns=True)
persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)
print("Status:", result.status)
print("Text:", assistant_text_from_turn(persisted_turn))
result = thread.run("Say hello in one sentence.")
print("Items:", len(result.items))
print("Text:", result.final_response)
2 changes: 2 additions & 0 deletions sdk/python/src/codex_app_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
InputItem,
LocalImageInput,
MentionInput,
RunResult,
SkillInput,
TextInput,
Thread,
Expand All @@ -68,6 +69,7 @@
"TurnHandle",
"AsyncTurnHandle",
"InitializeResponse",
"RunResult",
"Input",
"InputItem",
"TextInput",
Expand Down
63 changes: 63 additions & 0 deletions sdk/python/src/codex_app_server/_inputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from dataclasses import dataclass

from .models import JsonObject


@dataclass(slots=True)
class TextInput:
text: str


@dataclass(slots=True)
class ImageInput:
url: str


@dataclass(slots=True)
class LocalImageInput:
path: str


@dataclass(slots=True)
class SkillInput:
name: str
path: str


@dataclass(slots=True)
class MentionInput:
name: str
path: str


InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput
Input = list[InputItem] | InputItem
RunInput = Input | str


def _to_wire_item(item: InputItem) -> JsonObject:
if isinstance(item, TextInput):
return {"type": "text", "text": item.text}
if isinstance(item, ImageInput):
return {"type": "image", "url": item.url}
if isinstance(item, LocalImageInput):
return {"type": "localImage", "path": item.path}
if isinstance(item, SkillInput):
return {"type": "skill", "name": item.name, "path": item.path}
if isinstance(item, MentionInput):
return {"type": "mention", "name": item.name, "path": item.path}
raise TypeError(f"unsupported input item: {type(item)!r}")


def _to_wire_input(input: Input) -> list[JsonObject]:
if isinstance(input, list):
return [_to_wire_item(i) for i in input]
return [_to_wire_item(input)]


def _normalize_run_input(input: RunInput) -> Input:
if isinstance(input, str):
return TextInput(input)
return input
Loading
Loading