mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
9316acf9b2
## 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
449 lines
16 KiB
Python
449 lines
16 KiB
Python
import asyncio
|
|
import queue
|
|
import threading
|
|
import time
|
|
from collections import deque
|
|
from dataclasses import dataclass, field
|
|
from typing import AsyncIterator, Awaitable, Callable, Iterator
|
|
|
|
from .generated.notification_registry import notification_turn_id
|
|
from .generated.v2_all import (
|
|
ThreadGoalClearedNotification,
|
|
ThreadGoalStatus,
|
|
ThreadGoalUpdatedNotification,
|
|
Turn,
|
|
TurnCompletedNotification,
|
|
TurnStartedNotification,
|
|
TurnStatus,
|
|
)
|
|
from .models import Notification, UnknownNotification
|
|
|
|
|
|
class _GoalStreamClosed(Exception):
|
|
"""Wake a notification reader after its logical stream closes."""
|
|
|
|
|
|
def _terminal_goal_status(status: ThreadGoalStatus | None) -> bool:
|
|
return status in {
|
|
ThreadGoalStatus.paused,
|
|
ThreadGoalStatus.blocked,
|
|
ThreadGoalStatus.usage_limited,
|
|
ThreadGoalStatus.budget_limited,
|
|
ThreadGoalStatus.complete,
|
|
}
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class _GoalOperationState:
|
|
"""Private state for one goal operation exposed as a logical turn."""
|
|
|
|
thread_id: str
|
|
logical_turn_id: str | None = None
|
|
current_turn_id: str | None = None
|
|
status: ThreadGoalStatus | None = None
|
|
started_turn: Turn | None = None
|
|
completed_turn: Turn | None = None
|
|
interrupted: bool = False
|
|
interrupt_requested: bool = False
|
|
cleared: bool = False
|
|
_condition: threading.Condition = field(default_factory=threading.Condition)
|
|
_notifications: queue.Queue[Notification | BaseException] = field(default_factory=queue.Queue)
|
|
_failure: BaseException | None = None
|
|
_finished: bool = False
|
|
_turn_routing_active: bool = False
|
|
|
|
def observe(self, notification: Notification) -> bool:
|
|
payload = notification.payload
|
|
with self._condition:
|
|
if not self._turn_routing_active and not isinstance(
|
|
payload,
|
|
ThreadGoalClearedNotification | ThreadGoalUpdatedNotification,
|
|
):
|
|
return False
|
|
if isinstance(payload, TurnStartedNotification):
|
|
if self.logical_turn_id is None:
|
|
self.logical_turn_id = payload.turn.id
|
|
self.current_turn_id = payload.turn.id
|
|
if self.started_turn is None:
|
|
self.started_turn = payload.turn
|
|
elif isinstance(payload, TurnCompletedNotification):
|
|
self.completed_turn = payload.turn
|
|
if self.current_turn_id == payload.turn.id:
|
|
self.current_turn_id = None
|
|
elif isinstance(payload, ThreadGoalUpdatedNotification):
|
|
self.status = payload.goal.status
|
|
if self.status == ThreadGoalStatus.active:
|
|
self.cleared = False
|
|
elif isinstance(payload, ThreadGoalClearedNotification):
|
|
self.cleared = True
|
|
if (
|
|
self.current_turn_id is None
|
|
and self.completed_turn is not None
|
|
and (self.cleared or _terminal_goal_status(self.status))
|
|
):
|
|
self._finished = True
|
|
self._condition.notify_all()
|
|
self._notifications.put(notification)
|
|
return True
|
|
|
|
def activate_turn_routing(self) -> None:
|
|
"""Accept physical turns after the previous stored goal is cleared."""
|
|
with self._condition:
|
|
self._turn_routing_active = True
|
|
|
|
def wait_for_start(self, timeout: float) -> str | None:
|
|
"""Wait for the runtime-generated first turn without consuming its event."""
|
|
deadline = time.monotonic() + timeout
|
|
with self._condition:
|
|
while self.started_turn is None or self.logical_turn_id is None:
|
|
if self._failure is not None:
|
|
raise self._failure
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
return None
|
|
self._condition.wait(remaining)
|
|
return self.logical_turn_id
|
|
|
|
def fail(self, exc: BaseException) -> None:
|
|
with self._condition:
|
|
self._failure = exc
|
|
self._condition.notify_all()
|
|
self._notifications.put(exc)
|
|
|
|
def next_notification(self) -> Notification:
|
|
item = self._notifications.get()
|
|
if isinstance(item, BaseException):
|
|
raise item
|
|
return item
|
|
|
|
def finish(self) -> None:
|
|
"""Mark the logical operation inactive and wake waiting controls."""
|
|
with self._condition:
|
|
self._finished = True
|
|
self.current_turn_id = None
|
|
self._condition.notify_all()
|
|
|
|
def is_finished(self) -> bool:
|
|
with self._condition:
|
|
return self._finished
|
|
|
|
def begin_interrupt(self) -> bool:
|
|
with self._condition:
|
|
if self._finished:
|
|
return False
|
|
self.interrupt_requested = True
|
|
return True
|
|
|
|
def confirm_interrupt(self) -> None:
|
|
with self._condition:
|
|
self.interrupted = True
|
|
self.interrupt_requested = False
|
|
self._condition.notify_all()
|
|
|
|
def cancel_interrupt(self) -> None:
|
|
with self._condition:
|
|
self.interrupt_requested = False
|
|
self._condition.notify_all()
|
|
|
|
def explicit_interrupt(self) -> bool:
|
|
with self._condition:
|
|
while self.interrupt_requested:
|
|
self._condition.wait()
|
|
return self.interrupted
|
|
|
|
def active_turn(self, *, after: str | None = None) -> str | None:
|
|
"""Wait for the current turn, or return None once the goal has ended."""
|
|
with self._condition:
|
|
while True:
|
|
if self._failure is not None:
|
|
raise self._failure
|
|
if self._finished:
|
|
return None
|
|
if self.current_turn_id is not None and self.current_turn_id != after:
|
|
return self.current_turn_id
|
|
if self.cleared or _terminal_goal_status(self.status):
|
|
return None
|
|
self._condition.wait()
|
|
|
|
def current_turn(self) -> str | None:
|
|
"""Return the current physical turn without waiting for rollover."""
|
|
with self._condition:
|
|
return self.current_turn_id
|
|
|
|
def resolve_active_turn(self, expected: str, active: str) -> None:
|
|
"""Adopt a server-reported active id when routed state is still stale."""
|
|
with self._condition:
|
|
if self.current_turn_id in {None, expected}:
|
|
self.current_turn_id = active
|
|
self._condition.notify_all()
|
|
|
|
def turn_for_interrupt(self) -> str | None:
|
|
"""Return an active or stale turn id that can resolve rollover races."""
|
|
with self._condition:
|
|
if self.current_turn_id is not None:
|
|
return self.current_turn_id
|
|
if self.completed_turn is not None:
|
|
return self.completed_turn.id
|
|
if self.started_turn is not None:
|
|
return self.started_turn.id
|
|
return None
|
|
|
|
def wake_notification_reader(self) -> None:
|
|
"""Release a reader blocked after its stream has been closed."""
|
|
self._notifications.put(_GoalStreamClosed())
|
|
|
|
|
|
def _logical_notification(notification: Notification, logical_turn_id: str) -> Notification:
|
|
"""Return a copy whose turn metadata uses the logical operation id."""
|
|
payload = notification.payload
|
|
if isinstance(payload, UnknownNotification):
|
|
params = dict(payload.params)
|
|
if isinstance(params.get("turnId"), str):
|
|
params["turnId"] = logical_turn_id
|
|
turn = params.get("turn")
|
|
if isinstance(turn, dict) and isinstance(turn.get("id"), str):
|
|
params["turn"] = {**turn, "id": logical_turn_id}
|
|
return Notification(notification.method, UnknownNotification(params))
|
|
|
|
turn_id = notification_turn_id(payload)
|
|
if turn_id is None:
|
|
return notification
|
|
if hasattr(payload, "turn_id"):
|
|
return Notification(
|
|
notification.method,
|
|
payload.model_copy(update={"turn_id": logical_turn_id}),
|
|
)
|
|
if hasattr(payload, "turn"):
|
|
logical_turn = payload.turn.model_copy(update={"id": logical_turn_id})
|
|
return Notification(
|
|
notification.method,
|
|
payload.model_copy(update={"turn": logical_turn}),
|
|
)
|
|
return notification
|
|
|
|
|
|
def _logical_completion(
|
|
completed: TurnCompletedNotification,
|
|
*,
|
|
logical_turn_id: str,
|
|
started: Turn | None,
|
|
interrupted: bool,
|
|
) -> TurnCompletedNotification:
|
|
"""Coalesce the final physical completion into one logical completion."""
|
|
final_turn = completed.turn
|
|
started_at = started.started_at if started is not None else final_turn.started_at
|
|
duration_ms = final_turn.duration_ms
|
|
if started_at is not None and final_turn.completed_at is not None:
|
|
duration_ms = max(0, final_turn.completed_at - started_at) * 1000
|
|
updates: dict[str, object] = {
|
|
"id": logical_turn_id,
|
|
"started_at": started_at,
|
|
"duration_ms": duration_ms,
|
|
}
|
|
if interrupted:
|
|
updates["status"] = TurnStatus.interrupted
|
|
return completed.model_copy(update={"turn": final_turn.model_copy(update=updates)})
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class _GoalStreamCursor:
|
|
"""Consume physical goal events as one ordered logical turn stream."""
|
|
|
|
state: _GoalOperationState
|
|
started: Turn | None = None
|
|
last_completed: TurnCompletedNotification | None = None
|
|
failed_completion: TurnCompletedNotification | None = None
|
|
status: ThreadGoalStatus | None = None
|
|
active: bool = False
|
|
cleared: bool = False
|
|
|
|
def process(self, notification: Notification) -> tuple[list[Notification], bool]:
|
|
logical_turn_id = self.state.logical_turn_id
|
|
if logical_turn_id is None:
|
|
raise RuntimeError("goal operation has not been bound to a logical turn id")
|
|
|
|
payload = notification.payload
|
|
if isinstance(payload, TurnStartedNotification):
|
|
self.active = True
|
|
if self.started is not None:
|
|
return [], False
|
|
self.started = payload.turn
|
|
return [_logical_notification(notification, logical_turn_id)], False
|
|
|
|
if isinstance(payload, TurnCompletedNotification):
|
|
self.active = False
|
|
self.last_completed = payload
|
|
if payload.turn.status == TurnStatus.interrupted:
|
|
return [
|
|
self._completion(
|
|
notification.method,
|
|
self.failed_completion or payload,
|
|
)
|
|
], True
|
|
if payload.turn.status == TurnStatus.failed:
|
|
self.failed_completion = payload
|
|
if self.cleared or _terminal_goal_status(self.status):
|
|
self.state.finish()
|
|
return [self._completion(notification.method, payload)], True
|
|
return [], False
|
|
if self.status is None and not self.cleared:
|
|
raise RuntimeError(
|
|
"the connected Codex runtime did not activate goal mode for this turn"
|
|
)
|
|
if self.cleared or _terminal_goal_status(self.status):
|
|
self.state.finish()
|
|
return [
|
|
self._completion(
|
|
notification.method,
|
|
self.failed_completion or payload,
|
|
)
|
|
], True
|
|
return [], False
|
|
|
|
events = [_logical_notification(notification, logical_turn_id)]
|
|
if isinstance(payload, ThreadGoalUpdatedNotification):
|
|
self.status = payload.goal.status
|
|
if self.status == ThreadGoalStatus.active:
|
|
self.cleared = False
|
|
events = []
|
|
elif isinstance(payload, ThreadGoalClearedNotification):
|
|
self.cleared = True
|
|
events = []
|
|
|
|
if (
|
|
not self.active
|
|
and self.last_completed is not None
|
|
and (self.cleared or _terminal_goal_status(self.status))
|
|
):
|
|
self.state.finish()
|
|
events.append(
|
|
self._completion(
|
|
"turn/completed",
|
|
self.failed_completion or self.last_completed,
|
|
)
|
|
)
|
|
return events, True
|
|
return events, False
|
|
|
|
def _completion(
|
|
self,
|
|
method: str,
|
|
payload: TurnCompletedNotification,
|
|
) -> Notification:
|
|
logical_turn_id = self.state.logical_turn_id
|
|
if logical_turn_id is None:
|
|
raise RuntimeError("goal operation has not been bound to a logical turn id")
|
|
return Notification(
|
|
method,
|
|
_logical_completion(
|
|
payload,
|
|
logical_turn_id=logical_turn_id,
|
|
started=self.started,
|
|
interrupted=self.state.explicit_interrupt(),
|
|
),
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class _GoalNotificationStream(Iterator[Notification]):
|
|
"""Closeable synchronous view of one logical goal operation."""
|
|
|
|
state: _GoalOperationState
|
|
next_notification: Callable[[], Notification]
|
|
unregister: Callable[[], None]
|
|
cancel_goal: Callable[[], None]
|
|
_cursor: _GoalStreamCursor = field(init=False)
|
|
_pending: deque[Notification] = field(default_factory=deque)
|
|
_closed: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
self._cursor = _GoalStreamCursor(self.state)
|
|
|
|
def __iter__(self) -> "_GoalNotificationStream":
|
|
return self
|
|
|
|
def __next__(self) -> Notification:
|
|
if self._closed:
|
|
raise StopIteration
|
|
try:
|
|
while not self._pending:
|
|
notification = self.next_notification()
|
|
events, completed = self._cursor.process(notification)
|
|
self._pending.extend(events)
|
|
if completed:
|
|
self._finish()
|
|
return self._pending.popleft()
|
|
except _GoalStreamClosed:
|
|
self.close()
|
|
raise StopIteration from None
|
|
except KeyboardInterrupt:
|
|
self.cancel_goal()
|
|
self.close()
|
|
raise
|
|
except BaseException:
|
|
self.close()
|
|
raise
|
|
|
|
def _finish(self) -> None:
|
|
if self._closed:
|
|
return
|
|
self.state.finish()
|
|
self.state.wake_notification_reader()
|
|
self.unregister()
|
|
self._closed = True
|
|
|
|
def close(self) -> None:
|
|
self._finish()
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class _AsyncGoalNotificationStream(AsyncIterator[Notification]):
|
|
"""Closeable asynchronous view of one logical goal operation."""
|
|
|
|
state: _GoalOperationState
|
|
next_notification: Callable[[], Awaitable[Notification]]
|
|
unregister: Callable[[], None]
|
|
cancel_goal: Callable[[], Awaitable[None]]
|
|
_cursor: _GoalStreamCursor = field(init=False)
|
|
_pending: deque[Notification] = field(default_factory=deque)
|
|
_closed: bool = False
|
|
|
|
def __post_init__(self) -> None:
|
|
self._cursor = _GoalStreamCursor(self.state)
|
|
|
|
def __aiter__(self) -> "_AsyncGoalNotificationStream":
|
|
return self
|
|
|
|
async def __anext__(self) -> Notification:
|
|
if self._closed:
|
|
raise StopAsyncIteration
|
|
try:
|
|
while not self._pending:
|
|
notification = await self.next_notification()
|
|
events, completed = self._cursor.process(notification)
|
|
self._pending.extend(events)
|
|
if completed:
|
|
self._finish()
|
|
return self._pending.popleft()
|
|
except _GoalStreamClosed:
|
|
await self.aclose()
|
|
raise StopAsyncIteration from None
|
|
except asyncio.CancelledError:
|
|
await self.cancel_goal()
|
|
await self.aclose()
|
|
raise
|
|
except BaseException:
|
|
await self.aclose()
|
|
raise
|
|
|
|
def _finish(self) -> None:
|
|
if self._closed:
|
|
return
|
|
self.state.finish()
|
|
self.state.wake_notification_reader()
|
|
self.unregister()
|
|
self._closed = True
|
|
|
|
async def aclose(self) -> None:
|
|
self._finish()
|