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
13 changes: 13 additions & 0 deletions sdk/python/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ attempt. API-key login completes synchronously and does not return a handle.

- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnResult`
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnHandle`
- `run_goal(objective: str) -> TurnResult`
- `start_goal(objective: str) -> TurnHandle`
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
- `set_name(name: str) -> ThreadSetNameResponse`
- `compact() -> ThreadCompactStartResponse`
Expand All @@ -160,6 +162,8 @@ attempt. API-key login completes synchronously and does not return a handle.

- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
- `run_goal(objective: str) -> Awaitable[TurnResult]`
- `start_goal(objective: str) -> Awaitable[AsyncTurnHandle]`
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
- `compact() -> Awaitable[ThreadCompactStartResponse]`
Expand All @@ -184,6 +188,15 @@ phase-less assistant message item.
Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
`interrupt()`) before collecting the turn result.

Use `run_goal(...)` or `start_goal(...)` for an objective that can continue
through multiple internal turns. Goal operations require an idle, persisted
thread and use its existing configuration. Starting one replaces any stored
goal. Streaming, steering, interruption, and the returned `TurnResult` still
present one logical turn with one stable ID.

Closing a goal stream releases its SDK routing state. It does not pause the
persisted goal or stop work already running on the server.

## Sandbox

Use `sandbox=` consistently on thread lifecycle methods and turns:
Expand Down
11 changes: 11 additions & 0 deletions sdk/python/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ with Codex() as codex:
Use `Thread.turn(...)` when you need a `TurnHandle` for streaming, steering,
or interrupting an active turn.

For a longer objective on an idle, persisted thread, run a dedicated goal
operation:

```python
result = thread.run_goal("Improve the benchmark coverage in this repository.")
```

A goal may continue working internally, but it streams and returns as one
logical turn. It uses the thread's existing configuration and replaces any
stored goal.

## 4. Choose Sandbox Access

Use one enum for the initial thread and later turn overrides:
Expand Down
26 changes: 26 additions & 0 deletions sdk/python/examples/16_goal_turns/async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import sys
from pathlib import Path

_EXAMPLES_ROOT = Path(__file__).resolve().parents[1]
if str(_EXAMPLES_ROOT) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_ROOT))

from _bootstrap import ensure_local_sdk_src, runtime_config

ensure_local_sdk_src()

import asyncio

from openai_codex import AsyncCodex


async def main() -> None:
async with AsyncCodex(config=runtime_config()) as codex:
thread = await codex.thread_start()
goal = await thread.start_goal("Improve the benchmark coverage in this repository.")
result = await goal.run()
print(result.final_response)


if __name__ == "__main__":
asyncio.run(main())
17 changes: 17 additions & 0 deletions sdk/python/examples/16_goal_turns/sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sys
from pathlib import Path

_EXAMPLES_ROOT = Path(__file__).resolve().parents[1]
if str(_EXAMPLES_ROOT) not in sys.path:
sys.path.insert(0, str(_EXAMPLES_ROOT))

from _bootstrap import ensure_local_sdk_src, runtime_config

ensure_local_sdk_src()

from openai_codex import Codex

with Codex(config=runtime_config()) as codex:
thread = codex.thread_start()
result = thread.run_goal("Improve the benchmark coverage in this repository.")
print(result.final_response)
2 changes: 2 additions & 0 deletions sdk/python/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,5 @@ python examples/01_quickstart_constructor/async.py
- separate `steer()` and `interrupt()` demos with concise summaries
- `15_login_and_account/`
- browser-login handle lifecycle, cancellation, and account inspection
- `16_goal_turns/`
- run a longer autonomous objective as one logical turn
51 changes: 51 additions & 0 deletions sdk/python/src/openai_codex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@
)
from .models import InitializeResponse, JsonObject, Notification

_MAX_THREAD_GOAL_OBJECTIVE_CHARS = 4_000


def _normalize_goal_objective(objective: str) -> str:
if not isinstance(objective, str):
raise TypeError("goal objective must be a string")
objective = objective.strip()
if not objective:
raise ValueError("goal objective must not be empty")
if len(objective) > _MAX_THREAD_GOAL_OBJECTIVE_CHARS:
raise ValueError(
f"goal objective must be at most {_MAX_THREAD_GOAL_OBJECTIVE_CHARS} characters"
)
Comment on lines +87 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Cap goal objectives by tokens

This 4,000-character limit can still admit high-token-density objectives over 1k tokens, and the objective is later injected into the goal continuation context. Please cap by tokens or lower the limit before exposing this path. guidance

Useful? React with 👍 / 👎.

return objective


class Codex:
"""Synchronous client for creating threads and running Codex turns.
Expand Down Expand Up @@ -574,6 +589,10 @@ def run(
finally:
stream.close()

def run_goal(self, objective: str) -> TurnResult:
"""Run a persisted goal to completion as one logical turn."""
return self.start_goal(objective).run()

# BEGIN GENERATED: Thread.flat_methods
def turn(
self,
Expand Down Expand Up @@ -611,6 +630,19 @@ def turn(

# END GENERATED: Thread.flat_methods

def start_goal(self, objective: str) -> TurnHandle:
"""Activate a persisted goal and return its logical turn handle."""
objective = _normalize_goal_objective(objective)
state, turn_id = self._client.start_goal_operation(self.id, objective)
handle = TurnHandle(self._client, self.id, turn_id)
handle._goal = _GoalTurnHandleAdapter(
client=self._client,
state=state,
thread_id=self.id,
logical_turn_id=turn_id,
)
return handle

def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
return self._client.thread_read(self.id, include_turns=include_turns)
Expand Down Expand Up @@ -662,6 +694,11 @@ async def run(
finally:
await stream.aclose()

async def run_goal(self, objective: str) -> TurnResult:
"""Run a persisted goal asynchronously as one logical turn."""
goal = await self.start_goal(objective)
return await goal.run()

# BEGIN GENERATED: AsyncThread.flat_methods
async def turn(
self,
Expand Down Expand Up @@ -704,6 +741,20 @@ async def turn(

# END GENERATED: AsyncThread.flat_methods

async def start_goal(self, objective: str) -> AsyncTurnHandle:
"""Activate a persisted goal and return its async logical turn handle."""
objective = _normalize_goal_objective(objective)
await self._codex._ensure_initialized()
state, turn_id = await self._codex._client.start_goal_operation(self.id, objective)
handle = AsyncTurnHandle(self._codex, self.id, turn_id)
handle._goal = _AsyncGoalTurnHandleAdapter(
client=self._codex._client,
state=state,
thread_id=self.id,
logical_turn_id=turn_id,
)
return handle

async def read(self, *, include_turns: bool = False) -> ThreadReadResponse:
"""Read this thread, optionally including its turn history."""
await self._codex._ensure_initialized()
Expand Down
Loading
Loading