Skip to content
Closed
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
146 changes: 146 additions & 0 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Codex App Server Python SDK (Experimental)

Experimental Python SDK for `codex app-server` JSON-RPC v2.
The generated wire models come from the bundled v2 schema and use snake_case Python fields while preserving camelCase wire serialization.

It gives you a small typed API for:

- starting or resuming threads
- creating turns from Python
- streaming events or waiting for a final `TurnResult`
- using the same shape in sync and async code

## Experimental

This SDK is still experimental.

- it is not published yet
- API details may still change before the first release
- packaging and release workflow are still evolving

Use it for local development, dogfooding, and iteration inside this repo. Do not treat it as a stable public package yet.

## What You Need

- Python `>=3.10`
- local Codex auth/session already configured
- this repo checked out locally

## Install From Source

```bash
cd sdk/python
python -m pip install -e .
```

The package includes bundled Codex runtime binaries and automatically selects the binary for the current platform through `AppServerConfig().codex_bin`.

## Core Model

The public API is intentionally small:

- `Codex` / `AsyncCodex`: session entrypoint
- `Thread` / `AsyncThread`: a conversation thread
- `Turn` / `AsyncTurn`: one user turn within a thread
- `TurnResult`: final status, text, items, and usage

Typical flow:

1. create a `Codex` client
2. start or resume a thread
3. create a turn from input
4. call `run()` or iterate `stream()`

## Quickstart

### Sync

```python
from codex_app_server import Codex, TextInput

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

print("status:", result.status)
print("text:", result.text)
```

### Async

```python
import asyncio

from codex_app_server import AsyncCodex, TextInput


async def main() -> None:
async with AsyncCodex() as codex:
thread = await codex.thread_start(
model="gpt-5",
config={"model_reasoning_effort": "high"},
)
turn = await thread.turn(TextInput("Say hello in one sentence."))
result = await turn.run()

print("status:", result.status)
print("text:", result.text)


asyncio.run(main())
```

## Current Limitations

- Only one active `Turn.stream()` or `Turn.run()` consumer is supported per client instance.
- Starting a second active turn consumer on the same `Codex` or `AsyncCodex` raises `RuntimeError`.
- `Codex()` is eager and performs startup plus `initialize` in the constructor.

## Behavior Notes

- `AsyncCodex` is intended to be used with `async with AsyncCodex() as codex:`.
- `TurnResult.text` prefers streamed assistant deltas and falls back to completed raw response items when no deltas are emitted.
- For transient overload handling, use `retry_on_overload(...)`.

## Learn By Example

Runnable examples:

```bash
cd sdk/python
python examples/01_quickstart_constructor/sync.py
python examples/01_quickstart_constructor/async.py
```

More docs:

- Getting started: `docs/getting-started.md`
- API reference: `docs/api-reference.md`
- FAQ and pitfalls: `docs/faq.md`
- Examples index: `examples/README.md`
- Notebook walkthrough: `notebooks/sdk_walkthrough.ipynb`

## Maintainer Workflow

Refresh bundled binaries and generated artifacts with:

```bash
cd sdk/python
python scripts/update_sdk_artifacts.py --channel stable --bundle-all-platforms
```

or:

```bash
cd sdk/python
python scripts/update_sdk_artifacts.py --channel alpha --bundle-all-platforms
```

## Compatibility

- Package name: `codex-app-server-sdk`
- SDK version in this repo: `0.2.0`
- Target protocol: Codex `app-server` JSON-RPC v2
180 changes: 180 additions & 0 deletions sdk/python/docs/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Codex App Server SDK — API Reference

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

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

## Package Entry

```python
from codex_app_server import (
Codex,
AsyncCodex,
Thread,
AsyncThread,
Turn,
AsyncTurn,
TurnResult,
InitializeResult,
Input,
InputItem,
TextInput,
ImageInput,
LocalImageInput,
SkillInput,
MentionInput,
ThreadItem,
TurnStatus,
)
```

- Version: `codex_app_server.__version__`
- Requires Python >= 3.10

## Codex (sync)

```python
Codex(config: AppServerConfig | None = None)
```

Properties/methods:

- `metadata -> InitializeResult`
- `close() -> None`
- `thread_start(*, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread`
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponse`
- `thread_resume(thread_id: str, *, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread`
- `thread_fork(thread_id: str, *, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox=None) -> Thread`
- `thread_archive(thread_id: str) -> ThreadArchiveResponse`
- `thread_unarchive(thread_id: str) -> Thread`
- `models(*, include_hidden: bool = False) -> ModelListResponse`

Context manager:

```python
with Codex() as codex:
...
```

## AsyncCodex (async parity)

```python
AsyncCodex(config: AppServerConfig | None = None)
```

Properties/methods:

- `metadata -> InitializeResult`
- `close() -> Awaitable[None]`
- `thread_start(*, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]`
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> Awaitable[ThreadListResponse]`
- `thread_resume(thread_id: str, *, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]`
- `thread_fork(thread_id: str, *, approval_policy=None, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox=None) -> Awaitable[AsyncThread]`
- `thread_archive(thread_id: str) -> Awaitable[ThreadArchiveResponse]`
- `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]`
- `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]`

Async context manager:

```python
async with AsyncCodex() as codex:
...
```

## Thread / AsyncThread

`Thread` and `AsyncThread` share the same shape and intent.

### Thread

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

### AsyncThread

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

## Turn / AsyncTurn

### Turn

- `steer(input: Input) -> TurnSteerResponse`
- `interrupt() -> TurnInterruptResponse`
- `stream() -> Iterator[Notification]`
- `run() -> TurnResult`

Behavior notes:

- `stream()` and `run()` are exclusive per client instance in the current experimental build
- starting a second turn consumer on the same `Codex` instance raises `RuntimeError`

### AsyncTurn

- `steer(input: Input) -> Awaitable[TurnSteerResponse]`
- `interrupt() -> Awaitable[TurnInterruptResponse]`
- `stream() -> AsyncIterator[Notification]`
- `run() -> Awaitable[TurnResult]`

Behavior notes:

- `stream()` and `run()` are exclusive per client instance in the current experimental build
- starting a second turn consumer on the same `AsyncCodex` instance raises `RuntimeError`

## TurnResult

```python
@dataclass
class TurnResult:
thread_id: str
turn_id: str
status: TurnStatus
error: TurnError | None
text: str
items: list[ThreadItem]
usage: ThreadTokenUsageUpdatedNotification | None
```

## Inputs

```python
@dataclass class TextInput: text: str
@dataclass class ImageInput: url: str
@dataclass class LocalImageInput: path: str
@dataclass class SkillInput: name: str; path: str
@dataclass class MentionInput: name: str; path: str

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

## Retry + errors

```python
from codex_app_server import (
retry_on_overload,
JsonRpcError,
MethodNotFoundError,
InvalidParamsError,
ServerBusyError,
is_retryable_error,
)
```

- `retry_on_overload(...)` retries transient overload errors with exponential backoff + jitter.
- `is_retryable_error(exc)` checks if an exception is transient/overload-like.

## Example

```python
from codex_app_server import Codex, TextInput

with Codex() as codex:
thread = codex.thread_start(model="gpt-5", config={"model_reasoning_effort": "high"})
result = thread.turn(TextInput("Say hello in one sentence.")).run()
print(result.text)
```
Loading
Loading