Add dedicated Python goal operations

This commit is contained in:
Ahmed Ibrahim
2026-06-08 12:07:38 -07:00
parent 05704377c3
commit e502792f49
13 changed files with 375 additions and 258 deletions
+3 -4
View File
@@ -204,7 +204,7 @@ class MockResponsesServer:
class AppServerHarness:
"""Test fixture that points an app-server process at MockResponsesServer."""
"""Test fixture that points a pinned runtime app-server at MockResponsesServer."""
def __init__(
self,
@@ -232,10 +232,9 @@ class AppServerHarness:
shutil.rmtree(self.codex_home, ignore_errors=True)
shutil.rmtree(self.workspace, ignore_errors=True)
def app_server_config(self, *, codex_bin: str | None = None) -> CodexConfig:
"""Build SDK config for an isolated app-server process."""
def app_server_config(self) -> CodexConfig:
"""Build SDK config for an isolated pinned-runtime app-server process."""
return CodexConfig(
codex_bin=codex_bin,
cwd=str(self.workspace),
env={
"CODEX_HOME": str(self.codex_home),
+118 -107
View File
@@ -1,5 +1,4 @@
import asyncio
import os
import pytest
from app_server_harness import (
@@ -17,30 +16,17 @@ from app_server_helpers import (
streaming_response,
)
from openai_codex import AsyncCodex, Codex, CodexConfig, TextInput
from openai_codex import AsyncCodex, Codex
from openai_codex.errors import InvalidRequestError, TransportClosedError
from openai_codex.generated.notification_registry import notification_turn_id
from openai_codex.generated.v2_all import (
AgentMessageDeltaNotification,
ThreadGoalGetResponse,
ThreadGoalSetResponse,
ThreadGoalStatus,
TurnCompletedNotification,
TurnStatus,
)
SOURCE_CODEX_BIN = os.environ.get("CODEX_EXEC_PATH")
pytestmark = pytest.mark.skipif(
SOURCE_CODEX_BIN is None,
reason="requires CODEX_EXEC_PATH pointing to the checkout-built Codex binary",
)
def _source_config(harness: AppServerHarness) -> CodexConfig:
assert SOURCE_CODEX_BIN is not None
return harness.app_server_config(codex_bin=SOURCE_CODEX_BIN)
def _enqueue_completed_goal(
harness: AppServerHarness,
@@ -189,29 +175,14 @@ def test_sync_goal_run_aggregates_automatic_continuation(tmp_path) -> None:
final_text="Goal complete.",
)
with Codex(config=_source_config(harness)) as codex:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
turn = thread.turn(
[
TextInput(" Improve benchmark coverage "),
TextInput("Document the results"),
],
goal=True,
model="goal-test-model",
output_schema={
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
"additionalProperties": False,
},
)
result = turn.run()
result = thread.run_goal(" Improve benchmark coverage ")
requests = harness.responses.wait_for_requests(3)
usage = result.usage.model_dump(by_alias=True, mode="json") if result.usage else None
first_body = requests[0].body_json()
assert {
"id": result.id,
"id_is_present": bool(result.id),
"status": result.status,
"messages": agent_message_texts_from_items(result.items),
"final_response": result.final_response,
@@ -223,15 +194,10 @@ def test_sync_goal_run_aggregates_automatic_continuation(tmp_path) -> None:
if result.started_at is not None and result.completed_at is not None
else False,
),
"initial_input": requests[0].message_input_texts("user")[-2:],
"model": first_body["model"],
"output_schema": first_body["text"]["format"]["schema"],
"continuation_has_objective": (
"<objective>\nImprove benchmark coverage\n\nDocument the results\n</objective>"
in _continuation_text(requests[1])
),
"continuation_has_objective": "<objective>\nImprove benchmark coverage\n</objective>"
in _continuation_text(requests[0]),
} == {
"id": turn.id,
"id_is_present": True,
"status": TurnStatus.completed,
"messages": ["Initial pass complete.", "Goal complete."],
"final_response": "Goal complete.",
@@ -253,14 +219,6 @@ def test_sync_goal_run_aggregates_automatic_continuation(tmp_path) -> None:
},
},
"timing": (True, True, True),
"initial_input": [" Improve benchmark coverage ", "Document the results"],
"model": "goal-test-model",
"output_schema": {
"type": "object",
"properties": {"summary": {"type": "string"}},
"required": ["summary"],
"additionalProperties": False,
},
"continuation_has_objective": True,
}
@@ -303,8 +261,8 @@ def test_goal_stream_exposes_one_logical_lifecycle(tmp_path) -> None:
)
)
with Codex(config=_source_config(harness)) as codex:
turn = codex.thread_start().turn("Finish the integration suite", goal=True)
with Codex(config=harness.app_server_config()) as codex:
turn = codex.thread_start().start_goal("Finish the integration suite")
events = list(turn.stream())
lifecycle = [event for event in events if event.method in {"turn/started", "turn/completed"}]
@@ -362,8 +320,8 @@ def test_goal_can_complete_within_the_initial_server_turn(tmp_path) -> None:
response_id="single-turn-final",
)
with Codex(config=_source_config(harness)) as codex:
turn = codex.thread_start().turn("Finish in the initial turn", goal=True)
with Codex(config=harness.app_server_config()) as codex:
turn = codex.thread_start().start_goal("Finish in the initial turn")
result = turn.run()
requests = harness.responses.wait_for_requests(2)
with pytest.raises(InvalidRequestError) as steer_error:
@@ -394,19 +352,14 @@ def test_goal_replaces_an_existing_persisted_goal(tmp_path) -> None:
final_text="Replacement complete.",
)
with Codex(config=_source_config(harness)) as codex:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
previous = codex._client.request(
"thread/goal/set",
{
"threadId": thread.id,
"objective": "Keep the old benchmark objective",
"status": ThreadGoalStatus.paused.value,
"tokenBudget": 500,
},
response_model=ThreadGoalSetResponse,
previous = codex._client.thread_goal_set(
thread.id,
objective="Keep the old benchmark objective",
status=ThreadGoalStatus.paused,
).goal
result = thread.run("Publish the replacement objective", goal=True)
result = thread.run_goal("Publish the replacement objective")
persisted = codex._client.request(
"thread/goal/get",
{"threadId": thread.id},
@@ -415,7 +368,7 @@ def test_goal_replaces_an_existing_persisted_goal(tmp_path) -> None:
requests = harness.responses.wait_for_requests(3)
assert {
"previous": (previous.objective, previous.status, previous.token_budget),
"previous": (previous.objective, previous.status),
"result": (result.status, result.final_response),
"persisted": (
persisted.objective if persisted else None,
@@ -423,17 +376,13 @@ def test_goal_replaces_an_existing_persisted_goal(tmp_path) -> None:
persisted.token_budget if persisted else None,
),
"continuation_has_replacement": (
"Publish the replacement objective" in _continuation_text(requests[1])
"Publish the replacement objective" in _continuation_text(requests[0])
),
"continuation_has_previous": (
"Keep the old benchmark objective" in _continuation_text(requests[1])
"Keep the old benchmark objective" in _continuation_text(requests[0])
),
} == {
"previous": (
"Keep the old benchmark objective",
ThreadGoalStatus.paused,
500,
),
"previous": ("Keep the old benchmark objective", ThreadGoalStatus.paused),
"result": (TurnStatus.completed, "Replacement complete."),
"persisted": (
"Publish the replacement objective",
@@ -456,8 +405,8 @@ def test_goal_steer_targets_an_active_continuation(tmp_path) -> None:
final_text="Steered goal complete.",
)
with Codex(config=_source_config(harness)) as codex:
turn = codex.thread_start().turn("Start a goal that needs refinement", goal=True)
with Codex(config=harness.app_server_config()) as codex:
turn = codex.thread_start().start_goal("Start a goal that needs refinement")
harness.responses.wait_for_requests(2)
steer = turn.steer("Prioritize the edge-case coverage.")
result = turn.run()
@@ -491,9 +440,9 @@ def test_goal_interrupt_pauses_continuation_and_leaves_thread_usable(tmp_path) -
follow_up_text="Ordinary follow-up complete.",
)
with Codex(config=_source_config(harness)) as codex:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
turn = thread.turn("Start interruptible goal work", goal=True)
turn = thread.start_goal("Start interruptible goal work")
harness.responses.wait_for_requests(2)
interrupt = turn.interrupt()
interrupted = turn.run()
@@ -532,10 +481,10 @@ def test_terminal_goal_failure_stops_continuation_and_releases_routing(tmp_path)
response_id="goal-failure-follow-up",
)
with Codex(config=_source_config(harness)) as codex:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
with pytest.raises(RuntimeError, match="goal model failed"):
thread.run("Fail this goal turn", goal=True)
thread.run_goal("Fail this goal turn")
follow_up = thread.run("Run after the goal failure")
harness.responses.wait_for_requests(2)
@@ -553,7 +502,7 @@ def test_terminal_goal_failure_stops_continuation_and_releases_routing(tmp_path)
def test_closing_goal_stream_releases_real_process_routing(tmp_path) -> None:
"""Closing a public stream should immediately unregister its logical operation."""
"""Closing a stream should release SDK routing without pausing the persisted goal."""
with AppServerHarness(tmp_path, enable_goals=True) as harness:
harness.responses.enqueue_sse(
streaming_response(
@@ -564,14 +513,26 @@ def test_closing_goal_stream_releases_real_process_routing(tmp_path) -> None:
delay_between_events_s=0.5,
)
with Codex(config=_source_config(harness)) as codex:
turn = codex.thread_start().turn("Close this goal stream", goal=True)
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
turn = thread.start_goal("Close this goal stream")
harness.responses.wait_for_requests(1)
stream = turn.stream()
stream.close()
registered_goals = dict(codex._client._router._goal_operations)
persisted = codex._client.request(
"thread/goal/get",
{"threadId": thread.id},
response_model=ThreadGoalGetResponse,
).goal
assert registered_goals == {}
assert {
"registered_goals": registered_goals,
"persisted_status": persisted.status if persisted else None,
} == {
"registered_goals": {},
"persisted_status": ThreadGoalStatus.active,
}
def test_app_server_exit_unblocks_goal_stream_and_releases_routing(tmp_path) -> None:
@@ -586,8 +547,8 @@ def test_app_server_exit_unblocks_goal_stream_and_releases_routing(tmp_path) ->
delay_between_events_s=0.5,
)
with Codex(config=_source_config(harness)) as codex:
turn = codex.thread_start().turn("Stop the app-server during this goal", goal=True)
with Codex(config=harness.app_server_config()) as codex:
turn = codex.thread_start().start_goal("Stop the app-server during this goal")
harness.responses.wait_for_requests(1)
process = codex._client._proc
assert process is not None
@@ -609,29 +570,89 @@ def test_failed_goal_starts_release_routing_without_model_requests(tmp_path) ->
response_id="validation-follow-up",
)
with Codex(config=_source_config(harness)) as codex:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
with pytest.raises(InvalidRequestError) as empty_error:
thread.turn(" ", goal=True)
with pytest.raises(ValueError) as empty_error:
thread.start_goal(" ")
with pytest.raises(TypeError) as type_error:
thread.start_goal(123) # type: ignore[arg-type]
ephemeral = codex.thread_start(ephemeral=True)
with pytest.raises(InvalidRequestError) as ephemeral_error:
ephemeral.turn("Persist this goal", goal=True)
ephemeral.start_goal("Persist this goal")
follow_up = thread.run("Run after rejected goals")
requests = harness.responses.wait_for_requests(1)
registered_goals = dict(codex._client._router._goal_operations)
assert {
"errors": [empty_error.value.message, ephemeral_error.value.message],
"errors": [str(empty_error.value), str(type_error.value), ephemeral_error.value.message],
"follow_up": (follow_up.status, follow_up.final_response),
"request_count": len(requests),
"registered_goals": registered_goals,
} == {
"errors": [
"goal objective must not be empty",
f"ephemeral thread does not support goals: {ephemeral.id}",
"goal objective must be a string",
f"thread must be persisted before starting a goal: {ephemeral.id}",
],
"follow_up": (TurnStatus.completed, "Ordinary turn complete."),
"request_count": 1,
"registered_goals": {},
}
def test_disabled_goals_fail_before_model_work_or_routing(tmp_path) -> None:
"""A runtime with goals disabled should reject startup without leaking state."""
with AppServerHarness(tmp_path) as harness:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
with pytest.raises(InvalidRequestError) as error:
thread.start_goal("This goal must not start")
registered_goals = dict(codex._client._router._goal_operations)
requests = harness.responses.requests()
assert {
"error": error.value.message,
"registered_goals": registered_goals,
"request_count": len(requests),
} == {
"error": "goals feature is disabled",
"registered_goals": {},
"request_count": 0,
}
def test_active_thread_rejects_goal_start_and_keeps_ordinary_turn_usable(tmp_path) -> None:
"""Goal startup should require an idle thread without disturbing active work."""
with AppServerHarness(tmp_path, enable_goals=True) as harness:
harness.responses.enqueue_sse(
streaming_response(
"active-ordinary-turn",
"msg-active-ordinary-turn",
["ordinary ", "work"],
),
delay_between_events_s=0.5,
)
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
ordinary = thread.turn("Keep this ordinary turn active")
harness.responses.wait_for_requests(1)
with pytest.raises(InvalidRequestError) as error:
thread.start_goal("Do not replace active work")
ordinary.interrupt()
result = ordinary.run()
registered_goals = dict(codex._client._router._goal_operations)
assert {
"error": error.value.message,
"ordinary_result": (result.id, result.status),
"registered_goals": registered_goals,
} == {
"error": f"thread must be idle before starting a goal: {thread.id}",
"ordinary_result": (ordinary.id, TurnStatus.interrupted),
"registered_goals": {},
}
@@ -647,30 +668,23 @@ def test_async_goal_run_matches_sync_logical_result(tmp_path) -> None:
final_text="Async goal complete.",
)
async with AsyncCodex(config=_source_config(harness)) as codex:
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start()
turn = await thread.turn("Finish the async goal", goal=True)
result = await turn.run()
result = await thread.run_goal("Finish the async goal")
requests = harness.responses.wait_for_requests(3)
with pytest.raises(InvalidRequestError) as steer_error:
await turn.steer("Keep working")
with pytest.raises(InvalidRequestError) as interrupt_error:
await turn.interrupt()
assert {
"status": result.status,
"messages": agent_message_texts_from_items(result.items),
"final_response": result.final_response,
"continuation_has_objective": (
"Finish the async goal" in _continuation_text(requests[1])
"Finish the async goal" in _continuation_text(requests[0])
),
"inactive_errors": [steer_error.value.message, interrupt_error.value.message],
} == {
"status": TurnStatus.completed,
"messages": ["Async initial pass.", "Async goal complete."],
"final_response": "Async goal complete.",
"continuation_has_objective": True,
"inactive_errors": ["no active turn to steer", "no active turn to interrupt"],
}
asyncio.run(scenario())
@@ -689,12 +703,9 @@ def test_async_goal_steer_targets_an_active_continuation(tmp_path) -> None:
final_text="Async steered goal complete.",
)
async with AsyncCodex(config=_source_config(harness)) as codex:
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start()
turn = await thread.turn(
"Start an async goal that needs refinement",
goal=True,
)
turn = await thread.start_goal("Start an async goal that needs refinement")
await asyncio.to_thread(harness.responses.wait_for_requests, 2)
steer = await turn.steer("Prioritize async edge-case coverage.")
result = await turn.run()
@@ -732,9 +743,9 @@ def test_async_goal_interrupts_an_active_continuation(tmp_path) -> None:
follow_up_text="Async ordinary follow-up complete.",
)
async with AsyncCodex(config=_source_config(harness)) as codex:
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start()
turn = await thread.turn("Start async interruptible goal work", goal=True)
turn = await thread.start_goal("Start async interruptible goal work")
await asyncio.to_thread(harness.responses.wait_for_requests, 2)
interrupt = await turn.interrupt()
interrupted = await turn.run()
+23 -11
View File
@@ -185,17 +185,29 @@ def test_turn_input_methods_accept_string_shortcut() -> None:
)
def test_turn_result_producers_expose_goal_mode() -> None:
"""Goal mode belongs to APIs that produce a turn result or handle."""
funcs = [Thread.run, Thread.turn, AsyncThread.run, AsyncThread.turn]
def test_dedicated_goal_operations_have_pythonic_signatures() -> None:
"""Goal operations should accept only an objective and return existing turn types."""
goal_methods = {
Thread.run_goal: "TurnResult",
Thread.start_goal: "TurnHandle",
AsyncThread.run_goal: "TurnResult",
AsyncThread.start_goal: "AsyncTurnHandle",
}
ordinary_methods = [Thread.run, Thread.turn, AsyncThread.run, AsyncThread.turn]
assert {
fn: (
inspect.signature(fn).parameters["goal"].annotation,
inspect.signature(fn).parameters["goal"].default,
list(inspect.signature(fn).parameters),
inspect.signature(fn).parameters["objective"].annotation,
inspect.signature(fn).return_annotation,
)
for fn in funcs
} == dict.fromkeys(funcs, ("bool", False))
for fn in goal_methods
} == {
fn: (["self", "objective"], "str", return_type) for fn, return_type in goal_methods.items()
}
assert {fn: "goal" in inspect.signature(fn).parameters for fn in ordinary_methods} == (
dict.fromkeys(ordinary_methods, False)
)
def test_root_exports_approval_mode() -> None:
@@ -241,6 +253,10 @@ def test_curated_public_api_has_builtin_help_documentation() -> None:
"thread_resume": Codex.thread_resume,
"thread_run": Thread.run,
"thread_turn": Thread.turn,
"thread_run_goal": Thread.run_goal,
"thread_start_goal": Thread.start_goal,
"async_thread_run_goal": AsyncThread.run_goal,
"async_thread_start_goal": AsyncThread.start_goal,
}
assert {name: inspect.getdoc(value) is not None for name, value in documented.items()} == (
@@ -383,7 +399,6 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"thread_source",
],
Thread.turn: [
"goal",
"approval_mode",
"cwd",
"effort",
@@ -395,7 +410,6 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"summary",
],
Thread.run: [
"goal",
"approval_mode",
"cwd",
"effort",
@@ -460,7 +474,6 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"thread_source",
],
AsyncThread.turn: [
"goal",
"approval_mode",
"cwd",
"effort",
@@ -472,7 +485,6 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"summary",
],
AsyncThread.run: [
"goal",
"approval_mode",
"cwd",
"effort",