Add Python SDK goal turns

This commit is contained in:
Ahmed Ibrahim
2026-06-07 15:48:51 -07:00
parent e093d81982
commit 851a85b369
50 changed files with 2909 additions and 136 deletions

View File

@@ -527,6 +527,18 @@ def _normalized_schema_bundle_text(schema_dir: Path) -> str:
schema = json.loads(schema_bundle_path(schema_dir).read_text())
definitions = schema.get("definitions", {})
if isinstance(definitions, dict):
turn_start = definitions.get("TurnStartParams")
if isinstance(turn_start, dict):
properties = turn_start.get("properties")
if isinstance(properties, dict):
properties["goal"] = {
"default": False,
"description": (
"Replace the thread's active goal with an objective derived "
"from this turn's text input."
),
"type": "boolean",
}
for definition in definitions.values():
if isinstance(definition, dict):
_flatten_string_enum_one_of(definition)
@@ -648,6 +660,27 @@ def _notification_turn_id_specs(
return (sorted(set(direct)), sorted(set(nested)))
def _notification_thread_id_specs(
schema_dir: Path,
specs: list[tuple[str, str]],
) -> list[str]:
"""Return notification payloads that carry a direct thread id."""
server_notifications = json.loads((schema_dir / "ServerNotification.json").read_text())
definitions = server_notifications.get("definitions", {})
if not isinstance(definitions, dict):
return []
direct: list[str] = []
for _, class_name in specs:
definition = definitions.get(class_name)
if not isinstance(definition, dict):
continue
props = definition.get("properties", {})
if isinstance(props, dict) and "threadId" in props:
direct.append(class_name)
return sorted(set(direct))
def _type_tuple_source(class_names: list[str]) -> str:
"""Render a generated tuple literal for notification payload classes."""
if not class_names:
@@ -666,6 +699,7 @@ def generate_notification_registry(schema_dir: Path) -> None:
schema_dir,
specs,
)
direct_thread_id_types = _notification_thread_id_specs(schema_dir, specs)
lines = [
"# Auto-generated by scripts/update_sdk_artifacts.py",
@@ -697,6 +731,9 @@ def generate_notification_registry(schema_dir: Path) -> None:
"NESTED_TURN_NOTIFICATION_TYPES: tuple[type[BaseModel], ...] = "
f"{_type_tuple_source(nested_turn_types)}",
"",
"DIRECT_THREAD_ID_NOTIFICATION_TYPES: tuple[type[BaseModel], ...] = "
f"{_type_tuple_source(direct_thread_id_types)}",
"",
"",
"def notification_turn_id(payload: BaseModel) -> str | None:",
' """Return the turn id carried by generated notification payload metadata."""',
@@ -706,6 +743,13 @@ def generate_notification_registry(schema_dir: Path) -> None:
" return payload.turn.id",
" return None",
"",
"",
"def notification_thread_id(payload: BaseModel) -> str | None:",
' """Return the thread id carried by generated notification payload metadata."""',
" if isinstance(payload, DIRECT_THREAD_ID_NOTIFICATION_TYPES):",
" return payload.thread_id",
" return None",
"",
]
)
@@ -1077,6 +1121,7 @@ def _render_thread_block(
" self,",
" input: RunInput,",
" *,",
" goal: bool = False,",
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> TurnHandle:",
@@ -1089,8 +1134,16 @@ def _render_thread_block(
*_approval_mode_model_arg_lines(),
*_model_arg_lines(turn_fields),
" )",
" turn = self._client.turn_start(self.id, wire_input, params=params)",
" return TurnHandle(self._client, self.id, turn.turn.id)",
" goal_state = self._client.register_goal_operation(self.id) if goal else None",
" try:",
" turn = self._client.turn_start(self.id, wire_input, params=params, goal=goal)",
" except BaseException:",
" if goal_state is not None:",
" self._client.unregister_goal_operation(goal_state)",
" raise",
" if goal_state is not None:",
" self._client.bind_goal_operation(goal_state, turn.turn.id)",
" return TurnHandle(self._client, self.id, turn.turn.id, _goal=goal_state)",
]
return "\n".join(lines)
@@ -1103,6 +1156,7 @@ def _render_async_thread_block(
" self,",
" input: RunInput,",
" *,",
" goal: bool = False,",
*_approval_mode_override_signature_lines(),
*_kw_signature_lines(turn_fields),
" ) -> AsyncTurnHandle:",
@@ -1116,12 +1170,21 @@ def _render_async_thread_block(
*_approval_mode_model_arg_lines(),
*_model_arg_lines(turn_fields),
" )",
" turn = await self._codex._client.turn_start(",
" self.id,",
" wire_input,",
" params=params,",
" )",
" return AsyncTurnHandle(self._codex, self.id, turn.turn.id)",
" goal_state = self._codex._client.register_goal_operation(self.id) if goal else None",
" try:",
" turn = await self._codex._client.turn_start(",
" self.id,",
" wire_input,",
" params=params,",
" goal=goal,",
" )",
" except BaseException:",
" if goal_state is not None:",
" self._codex._client.unregister_goal_operation(goal_state)",
" raise",
" if goal_state is not None:",
" self._codex._client.bind_goal_operation(goal_state, turn.turn.id)",
" return AsyncTurnHandle(self._codex, self.id, turn.turn.id, _goal=goal_state)",
]
return "\n".join(lines)
@@ -1163,9 +1226,9 @@ def generate_public_api_flat_methods() -> None:
turn_start_fields = _load_public_fields(
"openai_codex.generated.v2_all",
"TurnStartParams",
# Keep the wire model current without exposing this app-server field
# through the ergonomic Python API yet.
exclude={"thread_id", "input", "client_user_message_id", *approval_fields},
# `goal` has a stable bool default and private routing setup, so render
# it explicitly rather than inheriting the generated model default.
exclude={"thread_id", "input", "client_user_message_id", "goal", *approval_fields},
)
turn_start_fields = _replace_public_sandbox_field(turn_start_fields, wire_name="sandbox_policy")