diff --git a/SCHEMAS.md b/SCHEMAS.md index 0009fc9..019281e 100644 --- a/SCHEMAS.md +++ b/SCHEMAS.md @@ -30,6 +30,19 @@ Every command response, success or error, carries: --- +## Turn Result Fields (Multi-Turn Sessions) + +When a command's response includes a `turn` object (e.g., in `bootstrap` or `turn-loop`), it carries: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `prompt` | string | Yes | User input for this turn | +| `output` | string | Yes | Assistant response | +| `stop_reason` | enum | Yes | One of: `completed`, `timeout`, `cancelled`, `max_budget_reached`, `max_turns_reached` | +| `cancel_observed` | bool | Yes | #164 Stage B: cancellation was signaled and observed (#161/#164) | + +--- + ## Error Envelope When a command fails (exit code 1), responses carry: diff --git a/src/main.py b/src/main.py index bb3cec2..622c935 100644 --- a/src/main.py +++ b/src/main.py @@ -332,6 +332,7 @@ def main(argv: list[str] | None = None) -> int: 'prompt': session.turn_result.prompt, 'output': session.turn_result.output, 'stop_reason': session.turn_result.stop_reason, + 'cancel_observed': session.turn_result.cancel_observed, }, 'persisted_session_path': session.persisted_session_path, } diff --git a/src/query_engine.py b/src/query_engine.py index 987a98a..f503aae 100644 --- a/src/query_engine.py +++ b/src/query_engine.py @@ -31,6 +31,7 @@ class TurnResult: permission_denials: tuple[PermissionDenial, ...] usage: UsageSummary stop_reason: str + cancel_observed: bool = False @dataclass diff --git a/src/runtime.py b/src/runtime.py index ebeb011..bfb47e4 100644 --- a/src/runtime.py +++ b/src/runtime.py @@ -249,7 +249,10 @@ class PortRuntime: # submitted yet this turn. assert cancel_event is not None cancel_event.set() - results.append(self._build_timeout_result(turn_prompt, command_names, tool_names)) + results.append(self._build_timeout_result( + turn_prompt, command_names, tool_names, + cancel_observed=cancel_event.is_set() + )) break assert executor is not None future = executor.submit( @@ -268,7 +271,10 @@ class PortRuntime: assert cancel_event is not None cancel_event.set() future.cancel() - results.append(self._build_timeout_result(turn_prompt, command_names, tool_names)) + results.append(self._build_timeout_result( + turn_prompt, command_names, tool_names, + cancel_observed=cancel_event.is_set() + )) break results.append(result) @@ -287,8 +293,11 @@ class PortRuntime: prompt: str, command_names: tuple[str, ...], tool_names: tuple[str, ...], + cancel_observed: bool = False, ) -> TurnResult: - """Synthesize a TurnResult representing a wall-clock timeout (#161).""" + """Synthesize a TurnResult representing a wall-clock timeout (#161). + #164 Stage B: cancel_observed signals cancellation event was set. + """ return TurnResult( prompt=prompt, output='Wall-clock timeout exceeded before turn completed.', @@ -297,6 +306,7 @@ class PortRuntime: permission_denials=(), usage=UsageSummary(), stop_reason='timeout', + cancel_observed=cancel_observed, ) def _infer_permission_denials(self, matches: list[RoutedMatch]) -> list[PermissionDenial]: