mirror of
https://github.com/openai/codex.git
synced 2026-05-20 11:12:43 +00:00
[codex] Accept string input for Python turns (#23162)
## Summary
- Allow thread.turn and turn.steer, including async variants, to accept
RunInput so plain strings work alongside typed input objects.
- Export RunInput and update the SDK artifact generator so regenerated
turn methods keep the same signature and normalization.
- Update docs, examples, notebook cells, and tests to use string
shorthand for text-only turns while keeping typed inputs for multimodal
input.
## Validation
- uv run --extra dev ruff format .
- uv run --extra dev ruff check --output-format=github .
- python3 -m py_compile sdk/python/src/openai_codex/__init__.py
sdk/python/src/openai_codex/api.py
sdk/python/src/openai_codex/_inputs.py
sdk/python/scripts/update_sdk_artifacts.py
sdk/python/tests/test_public_api_signatures.py
sdk/python/tests/test_app_server_streaming.py
sdk/python/tests/test_app_server_turn_controls.py
sdk/python/tests/test_real_app_server_integration.py
- python3 -c "import json;
json.load(open('sdk/python/notebooks/sdk_walkthrough.ipynb'))"
- sdk/python/.venv/bin/python -c "import inspect, openai_codex; from
openai_codex import Thread, AsyncThread, TurnHandle, AsyncTurnHandle,
RunInput; funcs=[Thread.run, Thread.turn, AsyncThread.run,
AsyncThread.turn, TurnHandle.steer, AsyncTurnHandle.steer]; assert
all(inspect.signature(fn).parameters['input'].annotation == 'RunInput'
for fn in funcs); assert RunInput is openai_codex.RunInput"
This commit is contained in:
@@ -102,6 +102,8 @@ target wheel. The SDK package version and runtime package version must match.
|
||||
|
||||
- `Codex()` is eager and performs startup + `initialize` in the constructor.
|
||||
- Use context managers (`with Codex() as codex:`) to ensure shutdown.
|
||||
- Plain strings are accepted anywhere a turn input is accepted; they are
|
||||
shorthand for `TextInput(...)`.
|
||||
- Prefer `thread.run("...")` for the common case. Use `thread.turn(...)` when
|
||||
you need streaming, steering, or interrupt control.
|
||||
- For transient overload, use `retry_on_overload` from the package root.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Public surface of `openai_codex` for app-server v2.
|
||||
|
||||
This SDK surface is experimental. Turn streams are routed by turn ID so one client can consume multiple active turns concurrently.
|
||||
Thread and turn starts expose `approval_mode`. `ApprovalMode.auto_review` is the default; use `ApprovalMode.deny_all` to deny escalated permissions.
|
||||
Thread starts default to `ApprovalMode.auto_review`; turn starts accept an optional `approval_mode` override.
|
||||
|
||||
## Package Entry
|
||||
|
||||
@@ -23,6 +23,7 @@ from openai_codex import (
|
||||
TurnResult,
|
||||
Input,
|
||||
InputItem,
|
||||
RunInput,
|
||||
TextInput,
|
||||
ImageInput,
|
||||
LocalImageInput,
|
||||
@@ -147,16 +148,16 @@ attempt. API-key login completes synchronously and does not return a handle.
|
||||
|
||||
### Thread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnResult`
|
||||
- `turn(input: Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> TurnHandle`
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=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_policy=None, service_tier=None, summary=None) -> TurnHandle`
|
||||
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
|
||||
- `set_name(name: str) -> ThreadSetNameResponse`
|
||||
- `compact() -> ThreadCompactStartResponse`
|
||||
|
||||
### AsyncThread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
|
||||
- `turn(input: Input, *, approval_mode=ApprovalMode.auto_review, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, summary=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=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_policy=None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
|
||||
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
|
||||
- `compact() -> Awaitable[ThreadCompactStartResponse]`
|
||||
@@ -185,7 +186,7 @@ Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
|
||||
|
||||
### TurnHandle
|
||||
|
||||
- `steer(input: Input) -> TurnSteerResponse`
|
||||
- `steer(input: str | Input) -> TurnSteerResponse`
|
||||
- `interrupt() -> TurnInterruptResponse`
|
||||
- `stream() -> Iterator[Notification]`
|
||||
- `run() -> TurnResult`
|
||||
@@ -197,7 +198,7 @@ Behavior notes:
|
||||
|
||||
### AsyncTurnHandle
|
||||
|
||||
- `steer(input: Input) -> Awaitable[TurnSteerResponse]`
|
||||
- `steer(input: str | Input) -> Awaitable[TurnSteerResponse]`
|
||||
- `interrupt() -> Awaitable[TurnInterruptResponse]`
|
||||
- `stream() -> AsyncIterator[Notification]`
|
||||
- `run() -> Awaitable[TurnResult]`
|
||||
@@ -218,8 +219,12 @@ Behavior notes:
|
||||
|
||||
InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput
|
||||
Input = list[InputItem] | InputItem
|
||||
RunInput = Input | str
|
||||
```
|
||||
|
||||
Use a plain `str` as shorthand for `TextInput(...)` anywhere a turn input is accepted:
|
||||
`thread.run("...")`, `thread.turn("...")`, and `turn.steer("...")`.
|
||||
|
||||
## Public Types
|
||||
|
||||
The SDK wrappers return and accept public app-server models wherever possible:
|
||||
|
||||
@@ -72,6 +72,7 @@ What happened:
|
||||
- `thread_start(...)` created a thread.
|
||||
- `thread.run("...")` started a turn, consumed events until completion, and returned `TurnResult` with turn metadata, final assistant response, collected items, and usage.
|
||||
- `result.final_response` is `None` when no final-answer or phase-less assistant message item completes for the turn.
|
||||
- plain strings are accepted anywhere a turn input is accepted; typed inputs are still available for multimodal and structured cases
|
||||
- use `thread.turn(...)` when you need a `TurnHandle` for streaming, steering, or interrupting before collecting `TurnResult`
|
||||
- one client can consume multiple active turns concurrently; turn streams are routed by turn ID
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ ensure_local_sdk_src()
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -19,7 +19,7 @@ async def main() -> None:
|
||||
thread = await codex.thread_start(
|
||||
model="gpt-5.4", config={"model_reasoning_effort": "high"}
|
||||
)
|
||||
turn = await thread.turn(TextInput("Give 3 bullets about SIMD."))
|
||||
turn = await thread.turn("Give 3 bullets about SIMD.")
|
||||
result = await turn.run()
|
||||
|
||||
print("thread_id:", thread.id)
|
||||
|
||||
@@ -9,11 +9,11 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
result = thread.turn(TextInput("Give 3 bullets about SIMD.")).run()
|
||||
result = thread.turn("Give 3 bullets about SIMD.").run()
|
||||
|
||||
print("thread_id:", thread.id)
|
||||
print("turn_id:", result.id)
|
||||
|
||||
@@ -11,7 +11,7 @@ ensure_local_sdk_src()
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -19,7 +19,7 @@ async def main() -> None:
|
||||
thread = await codex.thread_start(
|
||||
model="gpt-5.4", config={"model_reasoning_effort": "high"}
|
||||
)
|
||||
turn = await thread.turn(TextInput("Explain SIMD in 3 short bullets."))
|
||||
turn = await thread.turn("Explain SIMD in 3 short bullets.")
|
||||
|
||||
event_count = 0
|
||||
saw_started = False
|
||||
|
||||
@@ -9,11 +9,11 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
turn = thread.turn(TextInput("Explain SIMD in 3 short bullets."))
|
||||
turn = thread.turn("Explain SIMD in 3 short bullets.")
|
||||
|
||||
event_count = 0
|
||||
saw_started = False
|
||||
|
||||
@@ -11,7 +11,7 @@ ensure_local_sdk_src()
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -20,12 +20,12 @@ async def main() -> None:
|
||||
model="gpt-5.4", config={"model_reasoning_effort": "high"}
|
||||
)
|
||||
|
||||
first_turn = await original.turn(TextInput("Tell me one fact about Saturn."))
|
||||
first_turn = await original.turn("Tell me one fact about Saturn.")
|
||||
_ = await first_turn.run()
|
||||
print("Created thread:", original.id)
|
||||
|
||||
resumed = await codex.thread_resume(original.id)
|
||||
second_turn = await resumed.turn(TextInput("Continue with one more fact."))
|
||||
second_turn = await resumed.turn("Continue with one more fact.")
|
||||
second = await second_turn.run()
|
||||
print(second.final_response)
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
# Create an initial thread and turn so we have a real thread to resume.
|
||||
original = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
first = original.turn(TextInput("Tell me one fact about Saturn.")).run()
|
||||
first = original.turn("Tell me one fact about Saturn.").run()
|
||||
print("Created thread:", original.id)
|
||||
|
||||
# Resume the existing thread by ID.
|
||||
resumed = codex.thread_resume(original.id)
|
||||
second = resumed.turn(TextInput("Continue with one more fact.")).run()
|
||||
second = resumed.turn("Continue with one more fact.").run()
|
||||
print(second.final_response)
|
||||
|
||||
@@ -11,7 +11,7 @@ ensure_local_sdk_src()
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -19,10 +19,8 @@ async def main() -> None:
|
||||
thread = await codex.thread_start(
|
||||
model="gpt-5.4", config={"model_reasoning_effort": "high"}
|
||||
)
|
||||
first = await (
|
||||
await thread.turn(TextInput("One sentence about structured planning."))
|
||||
).run()
|
||||
second = await (await thread.turn(TextInput("Now restate it for a junior engineer."))).run()
|
||||
first = await (await thread.turn("One sentence about structured planning.")).run()
|
||||
second = await (await thread.turn("Now restate it for a junior engineer.")).run()
|
||||
|
||||
reopened = await codex.thread_resume(thread.id)
|
||||
listing_active = await codex.thread_list(limit=20, archived=False)
|
||||
@@ -38,13 +36,11 @@ async def main() -> None:
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
resumed_result = await (
|
||||
await resumed.turn(TextInput("Continue in one short sentence."))
|
||||
).run()
|
||||
resumed_result = await (await resumed.turn("Continue in one short sentence.")).run()
|
||||
|
||||
forked = await codex.thread_fork(unarchived.id, model="gpt-5.4")
|
||||
forked_result = await (
|
||||
await forked.turn(TextInput("Take a different angle in one short sentence."))
|
||||
await forked.turn("Take a different angle in one short sentence.")
|
||||
).run()
|
||||
|
||||
compact_result = await unarchived.compact()
|
||||
|
||||
@@ -9,12 +9,12 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
first = thread.turn(TextInput("One sentence about structured planning.")).run()
|
||||
second = thread.turn(TextInput("Now restate it for a junior engineer.")).run()
|
||||
first = thread.turn("One sentence about structured planning.").run()
|
||||
second = thread.turn("Now restate it for a junior engineer.").run()
|
||||
|
||||
reopened = codex.thread_resume(thread.id)
|
||||
listing_active = codex.thread_list(limit=20, archived=False)
|
||||
@@ -30,10 +30,10 @@ with Codex(config=runtime_config()) as codex:
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
resumed_result = resumed.turn(TextInput("Continue in one short sentence.")).run()
|
||||
resumed_result = resumed.turn("Continue in one short sentence.").run()
|
||||
|
||||
forked = codex.thread_fork(unarchived.id, model="gpt-5.4")
|
||||
forked_result = forked.turn(TextInput("Take a different angle in one short sentence.")).run()
|
||||
forked_result = forked.turn("Take a different angle in one short sentence.").run()
|
||||
|
||||
compact_result = unarchived.compact()
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ from _bootstrap import ensure_local_sdk_src, runtime_config, server_label
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex 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"})
|
||||
turn = thread.turn(TextInput("Say hello in one sentence."))
|
||||
turn = thread.turn("Say hello in one sentence.")
|
||||
result = turn.run()
|
||||
|
||||
print("Thread:", thread.id)
|
||||
|
||||
@@ -18,7 +18,6 @@ from openai_codex import (
|
||||
AsyncCodex,
|
||||
JsonRpcError,
|
||||
ServerBusyError,
|
||||
TextInput,
|
||||
is_retryable_error,
|
||||
)
|
||||
from openai_codex.types import TurnStatus
|
||||
@@ -82,7 +81,7 @@ async def main() -> None:
|
||||
|
||||
def _run_turn(thread, prompt: str):
|
||||
async def _inner():
|
||||
turn = await thread.turn(TextInput(prompt))
|
||||
turn = await thread.turn(prompt)
|
||||
return await turn.run()
|
||||
|
||||
return _inner
|
||||
|
||||
@@ -13,7 +13,6 @@ from openai_codex import (
|
||||
Codex,
|
||||
JsonRpcError,
|
||||
ServerBusyError,
|
||||
TextInput,
|
||||
retry_on_overload,
|
||||
)
|
||||
from openai_codex.types import TurnStatus
|
||||
@@ -23,7 +22,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
|
||||
try:
|
||||
result = retry_on_overload(
|
||||
lambda: thread.turn(TextInput("Summarize retry best practices in 3 bullets.")).run(),
|
||||
lambda: thread.turn("Summarize retry best practices in 3 bullets.").run(),
|
||||
max_attempts=3,
|
||||
initial_delay_s=0.25,
|
||||
max_delay_s=2.0,
|
||||
|
||||
@@ -13,7 +13,6 @@ import asyncio
|
||||
|
||||
from openai_codex import (
|
||||
AsyncCodex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
ThreadTokenUsageUpdatedNotification,
|
||||
@@ -51,7 +50,7 @@ async def main() -> None:
|
||||
if user_input in {"/exit", "/quit"}:
|
||||
break
|
||||
|
||||
turn = await thread.turn(TextInput(user_input))
|
||||
turn = await thread.turn(user_input)
|
||||
usage = None
|
||||
status = None
|
||||
error = None
|
||||
|
||||
@@ -11,7 +11,6 @@ ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import (
|
||||
Codex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
ThreadTokenUsageUpdatedNotification,
|
||||
@@ -46,7 +45,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
if user_input in {"/exit", "/quit"}:
|
||||
break
|
||||
|
||||
turn = thread.turn(TextInput(user_input))
|
||||
turn = thread.turn(user_input)
|
||||
usage = None
|
||||
status = None
|
||||
error = None
|
||||
|
||||
@@ -14,7 +14,6 @@ import asyncio
|
||||
|
||||
from openai_codex import (
|
||||
AsyncCodex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
@@ -49,7 +48,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
turn = await thread.turn(
|
||||
TextInput(PROMPT),
|
||||
PROMPT,
|
||||
output_schema=OUTPUT_SCHEMA,
|
||||
personality=Personality.pragmatic,
|
||||
summary=SUMMARY,
|
||||
|
||||
@@ -12,7 +12,6 @@ ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import (
|
||||
Codex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
@@ -43,7 +42,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
|
||||
turn = thread.turn(
|
||||
TextInput(PROMPT),
|
||||
PROMPT,
|
||||
output_schema=OUTPUT_SCHEMA,
|
||||
personality=Personality.pragmatic,
|
||||
summary=SUMMARY,
|
||||
|
||||
@@ -13,7 +13,6 @@ import asyncio
|
||||
|
||||
from openai_codex import (
|
||||
AsyncCodex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
@@ -91,7 +90,7 @@ async def main() -> None:
|
||||
)
|
||||
|
||||
first_turn = await thread.turn(
|
||||
TextInput("Give one short sentence about reliable production releases."),
|
||||
"Give one short sentence about reliable production releases.",
|
||||
model=selected_model.model,
|
||||
effort=selected_effort,
|
||||
)
|
||||
@@ -101,7 +100,7 @@ async def main() -> None:
|
||||
print("items:", len(first.items))
|
||||
|
||||
second_turn = await thread.turn(
|
||||
TextInput("Return JSON for a safe feature-flag rollout plan."),
|
||||
"Return JSON for a safe feature-flag rollout plan.",
|
||||
cwd=str(Path.cwd()),
|
||||
effort=selected_effort,
|
||||
model=selected_model.model,
|
||||
|
||||
@@ -11,7 +11,6 @@ ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import (
|
||||
Codex,
|
||||
TextInput,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
@@ -88,7 +87,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
)
|
||||
|
||||
first = thread.turn(
|
||||
TextInput("Give one short sentence about reliable production releases."),
|
||||
"Give one short sentence about reliable production releases.",
|
||||
model=selected_model.model,
|
||||
effort=selected_effort,
|
||||
).run()
|
||||
@@ -97,7 +96,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
print("items:", len(first.items))
|
||||
|
||||
second = thread.turn(
|
||||
TextInput("Return JSON for a safe feature-flag rollout plan."),
|
||||
"Return JSON for a safe feature-flag rollout plan.",
|
||||
cwd=str(Path.cwd()),
|
||||
effort=selected_effort,
|
||||
model=selected_model.model,
|
||||
|
||||
@@ -11,7 +11,7 @@ ensure_local_sdk_src()
|
||||
|
||||
import asyncio
|
||||
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -19,10 +19,8 @@ async def main() -> None:
|
||||
thread = await codex.thread_start(
|
||||
model="gpt-5.4", config={"model_reasoning_effort": "high"}
|
||||
)
|
||||
steer_turn = await thread.turn(
|
||||
TextInput("Count from 1 to 40 with commas, then one summary sentence.")
|
||||
)
|
||||
steer_result = await steer_turn.steer(TextInput("Keep it brief and stop after 10 numbers."))
|
||||
steer_turn = await thread.turn("Count from 1 to 40 with commas, then one summary sentence.")
|
||||
steer_result = await steer_turn.steer("Keep it brief and stop after 10 numbers.")
|
||||
|
||||
steer_event_count = 0
|
||||
steer_completed_status = None
|
||||
@@ -40,7 +38,7 @@ async def main() -> None:
|
||||
steer_preview = "".join(steer_deltas).strip()
|
||||
|
||||
interrupt_turn = await thread.turn(
|
||||
TextInput("Count from 1 to 200 with commas, then one summary sentence.")
|
||||
"Count from 1 to 200 with commas, then one summary sentence."
|
||||
)
|
||||
interrupt_result = await interrupt_turn.interrupt()
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ from _bootstrap import ensure_local_sdk_src, runtime_config
|
||||
|
||||
ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
|
||||
steer_turn = thread.turn(
|
||||
TextInput("Count from 1 to 40 with commas, then one summary sentence.")
|
||||
)
|
||||
steer_result = steer_turn.steer(TextInput("Keep it brief and stop after 10 numbers."))
|
||||
steer_turn = thread.turn("Count from 1 to 40 with commas, then one summary sentence.")
|
||||
steer_result = steer_turn.steer("Keep it brief and stop after 10 numbers.")
|
||||
|
||||
steer_event_count = 0
|
||||
steer_completed_status = None
|
||||
@@ -33,9 +31,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
raise RuntimeError("stream ended without turn/completed")
|
||||
steer_preview = "".join(steer_deltas).strip()
|
||||
|
||||
interrupt_turn = thread.turn(
|
||||
TextInput("Count from 1 to 200 with commas, then one summary sentence.")
|
||||
)
|
||||
interrupt_turn = thread.turn("Count from 1 to 200 with commas, then one summary sentence.")
|
||||
interrupt_result = interrupt_turn.interrupt()
|
||||
|
||||
interrupt_event_count = 0
|
||||
|
||||
@@ -8,6 +8,9 @@ Each example folder contains runnable versions:
|
||||
All examples intentionally use only public SDK exports from `openai_codex`
|
||||
and `openai_codex.types`.
|
||||
|
||||
Examples use plain strings for text-only turns and typed input objects for
|
||||
multimodal or structured input lists.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python `>=3.10`
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1b6614a5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -97,6 +98,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "137a6d64",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -115,6 +117,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5fae892d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -122,46 +125,42 @@
|
||||
"with Codex() as codex:\n",
|
||||
" # Open this URL and call `wait()` without canceling when completing login for real.\n",
|
||||
" login = codex.login_chatgpt()\n",
|
||||
" canceled = login.cancel()\n",
|
||||
" print('Please complete login at:', login.auth_url)\n",
|
||||
" completed = login.wait()\n",
|
||||
" account = codex.account()\n",
|
||||
"\n",
|
||||
" print('login.id:', login.login_id)\n",
|
||||
" print('login.auth_url:', login.auth_url)\n",
|
||||
" print('login.cancel.status:', canceled.status)\n",
|
||||
" print('login.completed.success:', completed.success)\n",
|
||||
" print('account.requires_openai_auth:', account.requires_openai_auth)\n"
|
||||
" print('account:', account.email)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ebdc04d9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Cell 3: simple sync conversation\n",
|
||||
"with Codex() as codex:\n",
|
||||
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
" turn = thread.turn(TextInput('Explain gradient descent in 3 bullets.'))\n",
|
||||
" result = turn.run()\n",
|
||||
" print('server:', server_label(codex.metadata))\n",
|
||||
" print('status:', result.status)\n",
|
||||
" print(result.final_response)\n",
|
||||
" print('items:', len(result.items))\n"
|
||||
" result = thread.run('Explain gradient descent in 3 bullets.')\n",
|
||||
" print(result.final_response)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bb4abb96",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Cell 4: multi-turn continuity in same thread\n",
|
||||
"with Codex() as codex:\n",
|
||||
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
"\n",
|
||||
" first = thread.turn(TextInput('Give a short summary of transformers.')).run()\n",
|
||||
" second = thread.turn(TextInput('Now explain that to a high-school student.')).run()\n",
|
||||
" first = thread.turn('Give a short summary of transformers.').run()\n",
|
||||
" second = thread.turn('Now explain that to a high-school student.').run()\n",
|
||||
" print('first status:', first.status)\n",
|
||||
" print('second status:', second.status)\n",
|
||||
" print('second text:', second.final_response)\n"
|
||||
@@ -170,14 +169,15 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8b0c80fd",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Cell 5: full thread lifecycle and branching (sync)\n",
|
||||
"with Codex() as codex:\n",
|
||||
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
" first = thread.turn(TextInput('One sentence about structured planning.')).run()\n",
|
||||
" second = thread.turn(TextInput('Now restate it for a junior engineer.')).run()\n",
|
||||
" first = thread.turn('One sentence about structured planning.').run()\n",
|
||||
" second = thread.turn('Now restate it for a junior engineer.').run()\n",
|
||||
"\n",
|
||||
" reopened = codex.thread_resume(thread.id)\n",
|
||||
" listing_active = codex.thread_list(limit=20, archived=False)\n",
|
||||
@@ -193,10 +193,10 @@
|
||||
" model='gpt-5.4',\n",
|
||||
" config={'model_reasoning_effort': 'high'},\n",
|
||||
" )\n",
|
||||
" resumed_result = resumed.turn(TextInput('Continue in one short sentence.')).run()\n",
|
||||
" resumed_result = resumed.turn('Continue in one short sentence.').run()\n",
|
||||
"\n",
|
||||
" forked = codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
|
||||
" forked_result = forked.turn(TextInput('Take a different angle in one short sentence.')).run()\n",
|
||||
" forked_result = forked.turn('Take a different angle in one short sentence.').run()\n",
|
||||
"\n",
|
||||
" compact_result = unarchived.compact()\n",
|
||||
"\n",
|
||||
@@ -214,6 +214,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "310db8c0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -242,7 +243,7 @@
|
||||
"with Codex() as codex:\n",
|
||||
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
" turn = thread.turn(\n",
|
||||
" TextInput('Propose a safe production feature-flag rollout. Return JSON matching the schema.'),\n",
|
||||
" 'Propose a safe production feature-flag rollout. Return JSON matching the schema.',\n",
|
||||
" cwd=str(Path.cwd()),\n",
|
||||
" effort=ReasoningEffort.medium,\n",
|
||||
" model='gpt-5.4',\n",
|
||||
@@ -259,6 +260,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7a33c97d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -321,7 +323,7 @@
|
||||
" thread = codex.thread_start(model=selected_model.model, config={'model_reasoning_effort': selected_effort.value})\n",
|
||||
"\n",
|
||||
" first = thread.turn(\n",
|
||||
" TextInput('Give one short sentence about reliable production releases.'),\n",
|
||||
" 'Give one short sentence about reliable production releases.',\n",
|
||||
" model=selected_model.model,\n",
|
||||
" effort=selected_effort,\n",
|
||||
" ).run()\n",
|
||||
@@ -329,7 +331,7 @@
|
||||
" print('items:', len(first.items))\n",
|
||||
"\n",
|
||||
" second = thread.turn(\n",
|
||||
" TextInput('Return JSON for a safe feature-flag rollout plan.'),\n",
|
||||
" 'Return JSON for a safe feature-flag rollout plan.',\n",
|
||||
" cwd=str(Path.cwd()),\n",
|
||||
" effort=selected_effort,\n",
|
||||
" model=selected_model.model,\n",
|
||||
@@ -345,6 +347,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e9aef26a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -364,6 +367,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "a0cecc6c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -382,6 +386,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "91afa2b8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -390,7 +395,7 @@
|
||||
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
"\n",
|
||||
" result = retry_on_overload(\n",
|
||||
" lambda: thread.turn(TextInput('List 5 failure modes in distributed systems.')).run(),\n",
|
||||
" lambda: thread.turn('List 5 failure modes in distributed systems.').run(),\n",
|
||||
" max_attempts=3,\n",
|
||||
" initial_delay_s=0.25,\n",
|
||||
" max_delay_s=2.0,\n",
|
||||
@@ -402,6 +407,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "103be934",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -412,8 +418,8 @@
|
||||
"async def async_lifecycle_demo():\n",
|
||||
" async with AsyncCodex() as codex:\n",
|
||||
" thread = await codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
" first = await (await thread.turn(TextInput('One sentence about structured planning.'))).run()\n",
|
||||
" second = await (await thread.turn(TextInput('Now restate it for a junior engineer.'))).run()\n",
|
||||
" first = await (await thread.turn('One sentence about structured planning.')).run()\n",
|
||||
" second = await (await thread.turn('Now restate it for a junior engineer.')).run()\n",
|
||||
"\n",
|
||||
" reopened = await codex.thread_resume(thread.id)\n",
|
||||
" listing_active = await codex.thread_list(limit=20, archived=False)\n",
|
||||
@@ -429,10 +435,10 @@
|
||||
" model='gpt-5.4',\n",
|
||||
" config={'model_reasoning_effort': 'high'},\n",
|
||||
" )\n",
|
||||
" resumed_result = await (await resumed.turn(TextInput('Continue in one short sentence.'))).run()\n",
|
||||
" resumed_result = await (await resumed.turn('Continue in one short sentence.')).run()\n",
|
||||
"\n",
|
||||
" forked = await codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
|
||||
" forked_result = await (await forked.turn(TextInput('Take a different angle in one short sentence.'))).run()\n",
|
||||
" forked_result = await (await forked.turn('Take a different angle in one short sentence.')).run()\n",
|
||||
"\n",
|
||||
" compact_result = await unarchived.compact()\n",
|
||||
"\n",
|
||||
@@ -453,6 +459,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "365aa10c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -463,9 +470,9 @@
|
||||
"async def async_stream_demo():\n",
|
||||
" async with AsyncCodex() as codex:\n",
|
||||
" thread = await codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
|
||||
" steer_turn = await thread.turn(TextInput('Count from 1 to 40 with commas, then one summary sentence.'))\n",
|
||||
" steer_turn = await thread.turn('Count from 1 to 40 with commas, then one summary sentence.')\n",
|
||||
"\n",
|
||||
" steer_result = await steer_turn.steer(TextInput('Keep it brief and stop after 10 numbers.'))\n",
|
||||
" steer_result = await steer_turn.steer('Keep it brief and stop after 10 numbers.')\n",
|
||||
"\n",
|
||||
" steer_event_count = 0\n",
|
||||
" steer_completed_status = None\n",
|
||||
@@ -482,7 +489,7 @@
|
||||
" raise RuntimeError('stream ended without turn/completed')\n",
|
||||
" steer_preview = ''.join(steer_deltas).strip()\n",
|
||||
"\n",
|
||||
" interrupt_turn = await thread.turn(TextInput('Count from 1 to 200 with commas, then one summary sentence.'))\n",
|
||||
" interrupt_turn = await thread.turn('Count from 1 to 200 with commas, then one summary sentence.')\n",
|
||||
" interrupt_result = await interrupt_turn.interrupt()\n",
|
||||
"\n",
|
||||
" interrupt_event_count = 0\n",
|
||||
@@ -516,13 +523,21 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"version": "3.10+"
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.14.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -1040,12 +1040,12 @@ def _render_thread_block(
|
||||
lines = [
|
||||
" def turn(",
|
||||
" self,",
|
||||
" input: Input,",
|
||||
" input: RunInput,",
|
||||
" *,",
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(turn_fields),
|
||||
" ) -> TurnHandle:",
|
||||
" wire_input = _to_wire_input(input)",
|
||||
" wire_input = _to_wire_input(_normalize_run_input(input))",
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = TurnStartParams(",
|
||||
" thread_id=self.id,",
|
||||
@@ -1065,13 +1065,13 @@ def _render_async_thread_block(
|
||||
lines = [
|
||||
" async def turn(",
|
||||
" self,",
|
||||
" input: Input,",
|
||||
" input: RunInput,",
|
||||
" *,",
|
||||
*_approval_mode_override_signature_lines(),
|
||||
*_kw_signature_lines(turn_fields),
|
||||
" ) -> AsyncTurnHandle:",
|
||||
" await self._codex._ensure_initialized()",
|
||||
" wire_input = _to_wire_input(input)",
|
||||
" wire_input = _to_wire_input(_normalize_run_input(input))",
|
||||
_approval_mode_assignment_line("_approval_mode_override_settings"),
|
||||
" params = TurnStartParams(",
|
||||
" thread_id=self.id,",
|
||||
|
||||
@@ -14,6 +14,7 @@ from .api import (
|
||||
InputItem,
|
||||
LocalImageInput,
|
||||
MentionInput,
|
||||
RunInput,
|
||||
SkillInput,
|
||||
TextInput,
|
||||
Thread,
|
||||
@@ -54,6 +55,7 @@ __all__ = [
|
||||
"TurnResult",
|
||||
"Input",
|
||||
"InputItem",
|
||||
"RunInput",
|
||||
"TextInput",
|
||||
"ImageInput",
|
||||
"LocalImageInput",
|
||||
|
||||
@@ -12,7 +12,7 @@ from ._approval_mode import (
|
||||
from ._initialize_metadata import validate_initialize_metadata
|
||||
from ._inputs import (
|
||||
ImageInput as ImageInput,
|
||||
Input,
|
||||
Input as Input,
|
||||
InputItem as InputItem,
|
||||
LocalImageInput as LocalImageInput,
|
||||
MentionInput as MentionInput,
|
||||
@@ -534,7 +534,7 @@ class Thread:
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnResult:
|
||||
turn = self.turn(
|
||||
_normalize_run_input(input),
|
||||
input,
|
||||
approval_mode=approval_mode,
|
||||
cwd=cwd,
|
||||
effort=effort,
|
||||
@@ -554,7 +554,7 @@ class Thread:
|
||||
# BEGIN GENERATED: Thread.flat_methods
|
||||
def turn(
|
||||
self,
|
||||
input: Input,
|
||||
input: RunInput,
|
||||
*,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
cwd: str | None = None,
|
||||
@@ -566,7 +566,7 @@ class Thread:
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnHandle:
|
||||
wire_input = _to_wire_input(input)
|
||||
wire_input = _to_wire_input(_normalize_run_input(input))
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = TurnStartParams(
|
||||
thread_id=self.id,
|
||||
@@ -617,7 +617,7 @@ class AsyncThread:
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnResult:
|
||||
turn = await self.turn(
|
||||
_normalize_run_input(input),
|
||||
input,
|
||||
approval_mode=approval_mode,
|
||||
cwd=cwd,
|
||||
effort=effort,
|
||||
@@ -637,7 +637,7 @@ class AsyncThread:
|
||||
# BEGIN GENERATED: AsyncThread.flat_methods
|
||||
async def turn(
|
||||
self,
|
||||
input: Input,
|
||||
input: RunInput,
|
||||
*,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
cwd: str | None = None,
|
||||
@@ -650,7 +650,7 @@ class AsyncThread:
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> AsyncTurnHandle:
|
||||
await self._codex._ensure_initialized()
|
||||
wire_input = _to_wire_input(input)
|
||||
wire_input = _to_wire_input(_normalize_run_input(input))
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
params = TurnStartParams(
|
||||
thread_id=self.id,
|
||||
@@ -694,8 +694,12 @@ class TurnHandle:
|
||||
thread_id: str
|
||||
id: str
|
||||
|
||||
def steer(self, input: Input) -> TurnSteerResponse:
|
||||
return self._client.turn_steer(self.thread_id, self.id, _to_wire_input(input))
|
||||
def steer(self, input: RunInput) -> TurnSteerResponse:
|
||||
return self._client.turn_steer(
|
||||
self.thread_id,
|
||||
self.id,
|
||||
_to_wire_input(_normalize_run_input(input)),
|
||||
)
|
||||
|
||||
def interrupt(self) -> TurnInterruptResponse:
|
||||
return self._client.turn_interrupt(self.thread_id, self.id)
|
||||
@@ -730,12 +734,12 @@ class AsyncTurnHandle:
|
||||
thread_id: str
|
||||
id: str
|
||||
|
||||
async def steer(self, input: Input) -> TurnSteerResponse:
|
||||
async def steer(self, input: RunInput) -> TurnSteerResponse:
|
||||
await self._codex._ensure_initialized()
|
||||
return await self._codex._client.turn_steer(
|
||||
self.thread_id,
|
||||
self.id,
|
||||
_to_wire_input(input),
|
||||
_to_wire_input(_normalize_run_input(input)),
|
||||
)
|
||||
|
||||
async def interrupt(self) -> TurnInterruptResponse:
|
||||
|
||||
@@ -11,7 +11,7 @@ from app_server_helpers import (
|
||||
streaming_response,
|
||||
)
|
||||
|
||||
from openai_codex import AsyncCodex, Codex, TextInput
|
||||
from openai_codex import AsyncCodex, Codex
|
||||
from openai_codex.generated.v2_all import (
|
||||
AgentMessageDeltaNotification,
|
||||
TurnCompletedNotification,
|
||||
@@ -26,8 +26,9 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
thread = codex.thread_start()
|
||||
stream = thread.turn(TextInput("stream please")).stream()
|
||||
stream = thread.turn("stream please").stream()
|
||||
events = list(stream)
|
||||
request = harness.responses.single_request()
|
||||
|
||||
assert {
|
||||
"deltas": [
|
||||
@@ -36,6 +37,7 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
if isinstance(event.payload, AgentMessageDeltaNotification)
|
||||
],
|
||||
"agent_messages": agent_message_texts(events),
|
||||
"request_user_texts": request.message_input_texts("user")[-1:],
|
||||
"completed_statuses": [
|
||||
event.payload.turn.status
|
||||
for event in events
|
||||
@@ -44,6 +46,7 @@ def test_sync_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
} == {
|
||||
"deltas": ["he", "llo"],
|
||||
"agent_messages": ["hello"],
|
||||
"request_user_texts": ["stream please"],
|
||||
"completed_statuses": [TurnStatus.completed],
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ def test_turn_run_returns_completed_turn(tmp_path) -> None:
|
||||
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
thread = codex.thread_start()
|
||||
turn = thread.turn(TextInput("complete this turn"))
|
||||
turn = thread.turn("complete this turn")
|
||||
completed = turn.run()
|
||||
|
||||
assert {
|
||||
@@ -83,8 +86,9 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
|
||||
async with AsyncCodex(config=harness.app_server_config()) as codex:
|
||||
thread = await codex.thread_start()
|
||||
turn = await thread.turn(TextInput("async stream please"))
|
||||
turn = await thread.turn("async stream please")
|
||||
events = [event async for event in turn.stream()]
|
||||
request = harness.responses.single_request()
|
||||
|
||||
assert {
|
||||
"deltas": [
|
||||
@@ -93,6 +97,7 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
if isinstance(event.payload, AgentMessageDeltaNotification)
|
||||
],
|
||||
"agent_messages": agent_message_texts(events),
|
||||
"request_user_texts": request.message_input_texts("user")[-1:],
|
||||
"completed_statuses": [
|
||||
event.payload.turn.status
|
||||
for event in events
|
||||
@@ -101,6 +106,7 @@ def test_async_stream_routes_text_deltas_and_completion(tmp_path) -> None:
|
||||
} == {
|
||||
"deltas": ["as", "ync"],
|
||||
"agent_messages": ["async"],
|
||||
"request_user_texts": ["async stream please"],
|
||||
"completed_statuses": [TurnStatus.completed],
|
||||
}
|
||||
|
||||
@@ -178,8 +184,8 @@ def test_interleaved_sync_turn_streams_route_by_turn_id(tmp_path) -> None:
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
first_thread = codex.thread_start()
|
||||
second_thread = codex.thread_start()
|
||||
first_turn = first_thread.turn(TextInput("first"))
|
||||
second_turn = second_thread.turn(TextInput("second"))
|
||||
first_turn = first_thread.turn("first")
|
||||
second_turn = second_thread.turn("second")
|
||||
|
||||
first_stream = first_turn.stream()
|
||||
second_stream = second_turn.stream()
|
||||
@@ -231,8 +237,8 @@ def test_interleaved_async_turn_streams_route_by_turn_id(tmp_path) -> None:
|
||||
async with AsyncCodex(config=harness.app_server_config()) as codex:
|
||||
first_thread = await codex.thread_start()
|
||||
second_thread = await codex.thread_start()
|
||||
first_turn = await first_thread.turn(TextInput("async first"))
|
||||
second_turn = await second_thread.turn(TextInput("async second"))
|
||||
first_turn = await first_thread.turn("async first")
|
||||
second_turn = await second_thread.turn("async second")
|
||||
|
||||
first_stream = first_turn.stream()
|
||||
second_stream = second_turn.stream()
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from app_server_harness import AppServerHarness
|
||||
from app_server_helpers import agent_message_texts, streaming_response
|
||||
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
from openai_codex.generated.v2_all import TurnStatus
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ def test_turn_steer_adds_follow_up_input(tmp_path) -> None:
|
||||
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
thread = codex.thread_start()
|
||||
turn = thread.turn(TextInput("Start a steerable turn."))
|
||||
turn = thread.turn("Start a steerable turn.")
|
||||
harness.responses.wait_for_requests(1)
|
||||
steer = turn.steer(TextInput("Use this steering input."))
|
||||
steer = turn.steer("Use this steering input.")
|
||||
events = list(turn.stream())
|
||||
requests = harness.responses.wait_for_requests(2)
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_turn_interrupt_stops_active_turn_and_follow_up_runs(tmp_path) -> None:
|
||||
|
||||
with Codex(config=harness.app_server_config()) as codex:
|
||||
thread = codex.thread_start()
|
||||
interrupted_turn = thread.turn(TextInput("Start a long turn."))
|
||||
interrupted_turn = thread.turn("Start a long turn.")
|
||||
harness.responses.wait_for_requests(1)
|
||||
interrupt_response = interrupted_turn.interrupt()
|
||||
completed = interrupted_turn.run()
|
||||
|
||||
@@ -40,6 +40,7 @@ EXPECTED_ROOT_EXPORTS = [
|
||||
"TurnResult",
|
||||
"Input",
|
||||
"InputItem",
|
||||
"RunInput",
|
||||
"TextInput",
|
||||
"ImageInput",
|
||||
"LocalImageInput",
|
||||
@@ -166,6 +167,22 @@ def test_turn_run_methods_return_turn_result() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_turn_input_methods_accept_string_shortcut() -> None:
|
||||
"""Every public turn-input method should accept strings and typed inputs."""
|
||||
funcs = [
|
||||
Thread.run,
|
||||
Thread.turn,
|
||||
AsyncThread.run,
|
||||
AsyncThread.turn,
|
||||
TurnHandle.steer,
|
||||
AsyncTurnHandle.steer,
|
||||
]
|
||||
|
||||
assert {fn: inspect.signature(fn).parameters["input"].annotation for fn in funcs} == (
|
||||
dict.fromkeys(funcs, "RunInput")
|
||||
)
|
||||
|
||||
|
||||
def test_root_exports_approval_mode() -> None:
|
||||
"""The root package should expose the high-level approval mode enum."""
|
||||
assert [(mode.name, mode.value) for mode in ApprovalMode] == [
|
||||
|
||||
@@ -234,14 +234,14 @@ def test_real_thread_and_turn_start_smoke(runtime_env: PreparedRuntimeEnv) -> No
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex() as codex:
|
||||
thread = codex.thread_start(
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
result = thread.turn(TextInput("hello")).run()
|
||||
result = thread.turn("hello").run()
|
||||
print(json.dumps({
|
||||
"thread_id": thread.id,
|
||||
"turn_id": result.id,
|
||||
@@ -331,7 +331,7 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from openai_codex import AsyncCodex, TextInput
|
||||
from openai_codex import AsyncCodex
|
||||
|
||||
async def main():
|
||||
async with AsyncCodex() as codex:
|
||||
@@ -339,7 +339,7 @@ def test_real_async_thread_turn_usage_and_ids_smoke(
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
result = await (await thread.turn(TextInput("say ok"))).run()
|
||||
result = await (await thread.turn("say ok")).run()
|
||||
print(json.dumps({
|
||||
"thread_id": thread.id,
|
||||
"turn_id": result.id,
|
||||
@@ -458,14 +458,14 @@ def test_real_streaming_smoke_turn_completed(runtime_env: PreparedRuntimeEnv) ->
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex() as codex:
|
||||
thread = codex.thread_start(
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
turn = thread.turn(TextInput("Reply with one short sentence."))
|
||||
turn = thread.turn("Reply with one short sentence.")
|
||||
saw_delta = False
|
||||
saw_completed = False
|
||||
for event in turn.stream():
|
||||
@@ -491,16 +491,16 @@ def test_real_turn_interrupt_smoke(runtime_env: PreparedRuntimeEnv) -> None:
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import json
|
||||
from openai_codex import Codex, TextInput
|
||||
from openai_codex import Codex
|
||||
|
||||
with Codex() as codex:
|
||||
thread = codex.thread_start(
|
||||
model="gpt-5.4",
|
||||
config={"model_reasoning_effort": "high"},
|
||||
)
|
||||
turn = thread.turn(TextInput("Count from 1 to 200 with commas."))
|
||||
turn = thread.turn("Count from 1 to 200 with commas.")
|
||||
turn.interrupt()
|
||||
follow_up = thread.turn(TextInput("Say 'ok' only.")).run()
|
||||
follow_up = thread.turn("Say 'ok' only.").run()
|
||||
print(json.dumps({"status": follow_up.status.value}))
|
||||
"""
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user