Accept string input for Python turns

This commit is contained in:
Ahmed Ibrahim
2026-05-17 07:03:40 -07:00
parent b2becbfa87
commit 7a7fdbf22f
31 changed files with 209 additions and 140 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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] == [

View File

@@ -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}))
"""
),