[2/4] Add private Python goal operations (#27111)

## Why

The Python SDK must treat the runtime's initial goal turn and its
continuations as one logical operation. That requires a private
lifecycle engine before the public API can return the existing turn
handle and result types.

## What

- start goals by composing the existing clear/set goal RPCs
- enforce persisted, idle threads and a bounded startup handshake
- coalesce continuation notifications under a stable logical turn ID
- aggregate items, usage, timing, and terminal status
- support rollover-aware steering, interruption, cancellation, and
cleanup
- provide equivalent sync and async internals

This is the second PR in the stack and intentionally adds no public API.

## Test plan

- online CI, including the Python SDK suite
- behavioral coverage is added in the following two stack PRs
This commit is contained in:
Ahmed Ibrahim
2026-06-09 15:32:15 -07:00
committed by GitHub
Unverified
parent cc8325f181
commit 9316acf9b2
5 changed files with 597 additions and 23 deletions
@@ -1,7 +1,9 @@
from __future__ import annotations
import asyncio
import threading
from collections.abc import Iterator
from concurrent.futures import Future
from typing import AsyncIterator, Callable, ParamSpec, TypeVar
from pydantic import BaseModel
@@ -227,6 +229,58 @@ class AsyncCodexClient:
"""Pause the active goal through the wrapped sync client."""
return await self._call_sync(self._sync.pause_goal, thread_id)
async def cancel_goal_operation(self, state: _GoalOperationState) -> None:
"""Stop continuation work after a logical goal operation is cancelled."""
await self._call_sync(self._sync.cancel_goal_operation, state)
async def start_goal_operation(
self,
thread_id: str,
objective: str,
) -> tuple[_GoalOperationState, str]:
"""Start a logical goal through the wrapped sync client."""
operation: Future[tuple[_GoalOperationState, str]] = Future()
def start_operation() -> None:
try:
operation.set_result(self._sync.start_goal_operation(thread_id, objective))
except BaseException as exc:
operation.set_exception(exc)
worker = threading.Thread(
target=start_operation,
name="codex-goal-start",
daemon=True,
)
worker.start()
try:
return await asyncio.shield(asyncio.wrap_future(operation))
except asyncio.CancelledError:
def cleanup_cancelled_start(
completed: Future[tuple[_GoalOperationState, str]],
) -> None:
try:
state, _ = completed.result()
except BaseException:
return
def stop_cancelled_goal() -> None:
try:
self._sync.cancel_goal_operation(state)
finally:
state.finish()
self._sync.unregister_goal_operation(state)
threading.Thread(
target=stop_cancelled_goal,
name="codex-goal-start-cleanup",
daemon=True,
).start()
operation.add_done_callback(cleanup_cancelled_start)
raise
async def turn_start(
self,
thread_id: str,