From 3f1b96f43b1e87693d93410febc48897d65584f4 Mon Sep 17 00:00:00 2001 From: asto Date: Sun, 18 Oct 2026 06:08:56 +0800 Subject: [PATCH] fix(engine): keep turn and submission attribution on TurnComplete Terminal `TurnComplete` events used to carry no operation identity of their own: preflight rejections (route install failure, edit_last_turn rejection), manual compaction terminals and model-turn terminals all reached embedded hosts bare, so hosts closed whichever request happened to be busy. The parity projection hardcoded `turn_id: None`, discarding the attribution the engine already knew, and the TUI hooks lifecycle emitted `turn_end` with the stale-surviving `runtime_turn_id`, letting a later rejection or compaction close the preceding model turn. The internal `Event::TurnComplete`, the wire `EventMsg::TurnComplete` and the engine's terminal emissions now preserve each operation's original identity: - `turn_id` is the actually started turn (absent when a submitted operation fails before starting, e.g. reject_edit_last_turn or `handle_purge` failure). - `submission_id` is the host submission correlation, echoed including preflight rejection; runtime self-starts carry `None`. - `compaction_id` is the manual compaction operation identity, never the surrounding host submission; auto compaction keeps closing its owning model turn, and purge carries none of the identities. `protocol_parity` projects all three instead of hardcoding `turn_id: None`, and `tui/ui/event_loop.rs` captures a `TurnEndLifecycleReceipt` from the terminal event itself, using the carried attribution for the loading-ownership check and the `turn_end` lifecycle receipt; the U02-07 heuristic stays only for events with absent attribution. Wire changes are strictly additive and serde-default, so older producers round-trip unchanged. Engine, remote_control, runtime_threads and runtime_api tests pin the contract, and docs/ARCHITECTURE.md documents the wire addition. Co-authored-by: zhuowp <9426526+zhuowp@users.noreply.github.com> Signed-off-by: asto --- crates/protocol/src/event_msg.rs | 60 +++++- crates/tui/src/acp_server/runtime.rs | 3 + crates/tui/src/core/engine.rs | 144 ++++++++++--- crates/tui/src/core/engine/compaction.rs | 39 +++- .../tui/src/core/engine/tests/compaction.rs | 204 +++++++++++++++++- .../src/core/engine/tests/test_cases_04.rs | 3 + .../src/core/engine/tests/test_cases_05.rs | 117 +++++++++- .../src/core/engine/tests/test_cases_06.rs | 13 ++ .../src/core/engine/tests/test_cases_14.rs | 17 +- crates/tui/src/core/events.rs | 11 + crates/tui/src/core/protocol_parity.rs | 37 +++- crates/tui/src/exec_agent.rs | 3 + crates/tui/src/remote_control.rs | 6 + crates/tui/src/runtime_api/tests.rs | 33 +++ crates/tui/src/runtime_threads/tests.rs | 198 +++++++++++++++++ crates/tui/src/tui/pet_watch/mod.rs | 9 + crates/tui/src/tui/ui/event_loop.rs | 105 ++++++--- crates/tui/src/tui/ui/tests.rs | 79 +++++++ .../events/approval_denied.golden.jsonl | 2 +- .../events/cancel_midstream.golden.jsonl | 2 +- .../events/one_tool_call.golden.jsonl | 2 +- .../events/parallel_tool_calls.golden.jsonl | 2 +- .../events/plain_answer.golden.jsonl | 2 +- ...rovider_error_after_tool_call.golden.jsonl | 2 +- docs/ARCHITECTURE.md | 22 ++ 25 files changed, 1035 insertions(+), 80 deletions(-) diff --git a/crates/protocol/src/event_msg.rs b/crates/protocol/src/event_msg.rs index 6876a1c35f..f3c8d80a70 100644 --- a/crates/protocol/src/event_msg.rs +++ b/crates/protocol/src/event_msg.rs @@ -450,10 +450,19 @@ pub enum EventMsg { TurnComplete { thread_id: ThreadId, session_id: SessionId, - /// The engine's `TurnComplete` carries no turn id; the emitter fills - /// it from the envelope when it knows it. + /// Actual started turn; absent when a submitted operation fails + /// before starting. #[serde(default, skip_serializing_if = "Option::is_none")] turn_id: Option, + /// Host submission correlation, including preflight rejection. Runtime + /// self-starts do not carry a host token. Older producers default to + /// absent. + #[serde(default, skip_serializing_if = "Option::is_none")] + submission_id: Option, + /// Manual compaction operation identity, never the surrounding host + /// submission. + #[serde(default, skip_serializing_if = "Option::is_none")] + compaction_id: Option, status: TurnOutcomeStatus, #[serde(default, skip_serializing_if = "Option::is_none")] error: Option, @@ -1195,6 +1204,8 @@ mod tests { route, }, EventMsg::TurnComplete { + submission_id: None, + compaction_id: None, thread_id: t.clone(), session_id: s.clone(), turn_id: None, @@ -1584,6 +1595,8 @@ mod tests { #[test] fn event_msg_roundtrip() { let msg = EventMsg::TurnComplete { + submission_id: None, + compaction_id: None, thread_id: ThreadId::new(), session_id: SessionId::new(), turn_id: Some("turn-1".into()), @@ -1601,6 +1614,49 @@ mod tests { assert!(json.contains(r#""status":"completed""#)); } + #[test] + fn turn_complete_ownership_is_additive_and_round_trips() { + for (turn_id, submission_id, compaction_id) in [ + (Some("turn-1"), Some("sub-host-1"), None), + (None, Some("sub-rejected"), None), + (Some("autonomous-turn"), None, None), + (None, None, Some("compact-1")), + (None, None, None), + ] { + let msg = EventMsg::TurnComplete { + thread_id: ThreadId::new(), + session_id: SessionId::new(), + turn_id: turn_id.map(str::to_string), + submission_id: submission_id.map(str::to_string), + compaction_id: compaction_id.map(str::to_string), + status: TurnOutcomeStatus::Failed, + error: Some("fixture rejection".into()), + usage: TokenUsage::default(), + parent_route_usage: None, + routed_usage_dropped_records: 0, + tool_catalog: None, + base_url: None, + }; + let value = serde_json::to_value(&msg).unwrap(); + for (key, expected) in [ + ("turn_id", turn_id), + ("submission_id", submission_id), + ("compaction_id", compaction_id), + ] { + if let Some(expected) = expected { + assert_eq!(value[key], expected); + } else { + assert!(value.get(key).is_none(), "{key} must stay absent"); + } + } + let back: EventMsg = serde_json::from_value(value).unwrap(); + assert_eq!( + back, msg, + "legacy absent fields and owned receipts round-trip" + ); + } + } + #[test] fn text_channel_is_elided_on_the_wire() { let msg = EventMsg::ResponseDelta { diff --git a/crates/tui/src/acp_server/runtime.rs b/crates/tui/src/acp_server/runtime.rs index e7914b7cdc..968bfda1e6 100644 --- a/crates/tui/src/acp_server/runtime.rs +++ b/crates/tui/src/acp_server/runtime.rs @@ -665,6 +665,9 @@ mod tests { engine .tx_event .send(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: codewhale_models::Usage::default(), parent_route_usage: codewhale_models::Usage::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index ab2fb32234..ce6b65e478 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -2719,6 +2719,11 @@ impl Engine { let pending_post_turn = self.reserve_post_turn_snapshot(); let status = terminal_turn_status_at_settlement(status, self.cancel_token.is_cancelled()); terminal_permit.send(Event::TurnComplete { + // The composer shell turn started above; it never carries a host + // submission envelope or compaction id. + turn_id: Some(turn_id.clone()), + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -4311,15 +4316,18 @@ impl Engine { let route = match self.current_runtime_route() { Ok(route) => route, Err(err) => { - self.reject_edit_last_turn(ErrorEnvelope::new( - ErrorCategory::Authentication, - ErrorSeverity::Critical, - false, - "edit_last_turn_invalid_route", - format!( - "Cannot edit the last turn because its provider route is no longer valid: {err}" + self.reject_edit_last_turn( + submission_id.clone(), + ErrorEnvelope::new( + ErrorCategory::Authentication, + ErrorSeverity::Critical, + false, + "edit_last_turn_invalid_route", + format!( + "Cannot edit the last turn because its provider route is no longer valid: {err}" + ), ), - )) + ) .await; continue; } @@ -4336,24 +4344,30 @@ impl Engine { ) { crate::runtime_handoff::EditLastTurnTarget::Editable(idx) => idx, crate::runtime_handoff::EditLastTurnTarget::Unsupported => { - self.reject_edit_last_turn(ErrorEnvelope::new( - ErrorCategory::InvalidInput, - ErrorSeverity::Error, - false, - "edit_last_turn_unsupported_user_content", - "Cannot edit the last turn because the latest user message has no editable text content.", - )) + self.reject_edit_last_turn( + submission_id.clone(), + ErrorEnvelope::new( + ErrorCategory::InvalidInput, + ErrorSeverity::Error, + false, + "edit_last_turn_unsupported_user_content", + "Cannot edit the last turn because the latest user message has no editable text content.", + ), + ) .await; continue; } crate::runtime_handoff::EditLastTurnTarget::Missing => { - self.reject_edit_last_turn(ErrorEnvelope::new( - ErrorCategory::State, - ErrorSeverity::Error, - false, - "edit_last_turn_no_user_prompt", - "Cannot edit the last turn because the session history has no user message to replace.", - )) + self.reject_edit_last_turn( + submission_id.clone(), + ErrorEnvelope::new( + ErrorCategory::State, + ErrorSeverity::Error, + false, + "edit_last_turn_no_user_prompt", + "Cannot edit the last turn because the session history has no user message to replace.", + ), + ) .await; continue; } @@ -5244,24 +5258,47 @@ impl Engine { } } - /// Reject an edit operation before model dispatch while still completing - /// the submitted host lifecycle. `Event::Error` is advisory to embedded - /// hosts; `TurnComplete(Failed)` is the authoritative terminal signal that - /// releases their busy state and closes the admitted operation. - async fn reject_edit_last_turn(&mut self, envelope: ErrorEnvelope) { + /// Reject a submitted operation before model dispatch while still + /// completing the submitted host lifecycle. `Event::Error` is advisory to + /// embedded hosts; `TurnComplete(Failed)` is the authoritative terminal + /// signal that releases their busy state and closes the admitted + /// operation. The terminal carries the rejected operation's + /// `submission_id` and no `turn_id`: nothing started, so no turn may be + /// claimed — and no earlier turn may be inherited either. + async fn reject_unstarted_submission( + &mut self, + submission_id: Option, + envelope: ErrorEnvelope, + ) { let message = envelope.message.clone(); let _ = self.send_event(Event::error(envelope)).await; let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, status: TurnOutcomeStatus::Failed, - error: Some(message.clone()), + error: Some(message), tool_catalog: None, base_url: None, }) .await; + } + + /// Editing an existing conversation preserves its established goal-failure + /// reconciliation. Other callers only emit the rejected operation's + /// receipt. + async fn reject_edit_last_turn( + &mut self, + submission_id: Option, + envelope: ErrorEnvelope, + ) { + let message = envelope.message.clone(); + self.reject_unstarted_submission(submission_id, envelope) + .await; let outcome = SendMessageOutcome::NotStarted { error: Some(message), }; @@ -6320,16 +6357,30 @@ impl Engine { initial_usage_owner.as_deref(), &initial_routed_usage, ); - let _ = self - .send_event(Event::error(ErrorEnvelope::fatal_auth(format!( - "Cannot start the turn because its provider route is not ready: {err}" - )))) - .await; self.sync_unstarted_goal_for_terminal_projection( goal_objective.as_deref(), goal_token_budget, goal_status, ); + // The turn never started, so the terminal names only the rejected + // host submission — no `turn_id`, and no inherited turn either. + let message = + format!("Cannot start the turn because its provider route is not ready: {err}"); + let _ = self + .send_event(Event::error(ErrorEnvelope::fatal_auth(message.clone()))) + .await; + terminal_permit.send(Event::TurnComplete { + turn_id: None, + submission_id, + compaction_id: None, + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: TurnOutcomeStatus::Failed, + error: Some(message), + tool_catalog: None, + base_url: None, + }); let outcome = SendMessageOutcome::NotStarted { error: Some(err) }; self.reconcile_non_completed_goal_turn(&outcome).await; return outcome; @@ -6520,8 +6571,11 @@ impl Engine { turn_id: turn.id.clone(), created_at: turn_started_at, route: Some(turn_route), - // Bind submit-window actions to the turn admitted by this permit. - submission_id, + // Echo the host's correlation token (`None` when this turn was + // self-started without one) so the host can bind its submit-window + // actions to the turn that actually started. The terminal + // `TurnComplete` echoes the same token again. + submission_id: submission_id.clone(), }); // Auto's classifier completed before this parent turn was admitted. @@ -6622,6 +6676,11 @@ impl Engine { ); let error = (status == TurnOutcomeStatus::Failed).then_some(message.clone()); terminal_permit.send(Event::TurnComplete { + // `TurnStarted` already named this turn; the failed terminal + // echoes it and the submission that started it. + turn_id: Some(turn.id.clone()), + submission_id: submission_id.clone(), + compaction_id: None, usage: turn.usage.clone(), parent_route_usage: turn.parent_route_usage.clone(), routed_usage_dropped_records: turn.routed_usage_dropped_records, @@ -7006,6 +7065,11 @@ impl Engine { // name was read as user-visible delivery on Interrupted turns that // rendered nothing.) let completion_sender = terminal_permit.send(Event::TurnComplete { + // The authoritative terminal names the turn that actually ran and + // the host submission that started it (`None` for self-starts). + turn_id: Some(turn.id.clone()), + submission_id, + compaction_id: None, usage: turn.usage, parent_route_usage: turn.parent_route_usage, routed_usage_dropped_records: turn.routed_usage_dropped_records, @@ -7148,6 +7212,11 @@ impl Engine { .await; let _ = self .send_event(Event::TurnComplete { + // Purge carries none of the turn, submission or compaction + // identities: it never starts a model turn. + turn_id: None, + submission_id: None, + compaction_id: None, usage: zero_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -7207,6 +7276,11 @@ impl Engine { let _ = self .send_event(Event::TurnComplete { + // Purge carries none of the turn, submission or compaction + // identities: it never starts a model turn. + turn_id: None, + submission_id: None, + compaction_id: None, usage: zero_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/core/engine/compaction.rs b/crates/tui/src/core/engine/compaction.rs index c441a34600..82848214b4 100644 --- a/crates/tui/src/core/engine/compaction.rs +++ b/crates/tui/src/core/engine/compaction.rs @@ -282,6 +282,9 @@ impl Engine { route: ResolvedRuntimeRoute, compaction: CompactionConfig, ) { + // Manual compaction terminals identify the compaction operation, never + // a surrounding host submission or a model turn. + let compaction_id = Some(id.clone()); self.emit_compaction_started(id.clone(), false, "Making room…".to_string()) .await; let Some(cancel_token) = self.claim_compaction(&id) else { @@ -289,6 +292,9 @@ impl Engine { self.emit_compaction_cancelled(id, false, message).await; let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -307,7 +313,23 @@ impl Engine { self.emit_compaction_failed(id, false, message.clone()) .await; let _ = self - .send_event(Event::error(ErrorEnvelope::fatal_auth(message))) + .send_event(Event::error(ErrorEnvelope::fatal_auth(message.clone()))) + .await; + // Route rejection is still a terminal for the compaction + // operation: hosts waiting on its lifecycle must be released. + let _ = self + .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: TurnOutcomeStatus::Failed, + error: Some(message), + tool_catalog: None, + base_url: None, + }) .await; return; } @@ -334,6 +356,9 @@ impl Engine { id: String, cancel_token: CancellationToken, ) { + // Manual compaction terminals identify the compaction operation, never + // a surrounding host submission or a model turn. + let compaction_id = Some(id.clone()); let zero_usage = Usage { input_tokens: 0, output_tokens: 0, @@ -349,6 +374,9 @@ impl Engine { .await; let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, usage: zero_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -399,6 +427,9 @@ impl Engine { .await; let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, usage: compaction_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -426,6 +457,9 @@ impl Engine { .await; let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, usage: compaction_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -510,6 +544,9 @@ impl Engine { let _ = self .send_event(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id, usage: compaction_usage, parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/core/engine/tests/compaction.rs b/crates/tui/src/core/engine/tests/compaction.rs index 273089b6fe..9808e8115e 100644 --- a/crates/tui/src/core/engine/tests/compaction.rs +++ b/crates/tui/src/core/engine/tests/compaction.rs @@ -48,10 +48,23 @@ async fn rejected_manual_compaction_route_closes_typed_lifecycle() { order.push("failed"); } Event::Error { .. } => order.push("error"), + Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + .. + } => { + assert!(turn_id.is_none()); + assert!(submission_id.is_none()); + assert_eq!(compaction_id.as_deref(), Some("compact-route-invalid")); + assert_eq!(status, TurnOutcomeStatus::Failed); + order.push("terminal"); + } _ => {} } } - assert_eq!(order, ["started", "failed", "error"]); + assert_eq!(order, ["started", "failed", "error", "terminal"]); assert_eq!(started_id, failed_id); } @@ -89,8 +102,9 @@ async fn queued_manual_compaction_cancellation_is_idempotent_and_skips_route_act [ Event::CompactionStarted { id: started, auto: false, .. }, Event::CompactionCancelled { id: cancelled, auto: false, .. }, - Event::TurnComplete { status: TurnOutcomeStatus::Interrupted, .. } - ] if started == id && cancelled == id + Event::TurnComplete { turn_id: None, submission_id: None, + compaction_id: Some(terminal), status: TurnOutcomeStatus::Interrupted, .. } + ] if started == id && cancelled == id && terminal == id )); assert!( !drained @@ -113,6 +127,190 @@ async fn queued_manual_compaction_cancellation_is_idempotent_and_skips_route_act engine.finish_compaction(id); } +#[tokio::test] +#[allow(clippy::await_holding_lock)] +async fn terminal_correlation_preflight_failures_keep_each_submitted_owner() { + let _env_lock = lock_test_env(); + let _api_key = EnvVarGuard::remove("DEEPSEEK_API_KEY"); + let workspace = tempdir().expect("isolated workspace"); + let route_config = Config { + provider: Some("deepseek".to_string()), + default_text_model: Some(crate::config::DEFAULT_TEXT_MODEL.to_string()), + ..Config::default() + } + .with_legacy_root(Some(String::new()), None); + let invalid_route = resolve_runtime_route( + &route_config, + ProviderKind::Deepseek, + Some(crate::config::DEFAULT_TEXT_MODEL), + ) + .expect("structurally resolved route"); + assert!( + invalid_route.clone().validate().is_err(), + "fixture must fail at engine route installation" + ); + let (engine, handle) = + Engine::new(deterministic_engine_config(workspace.path()), &route_config); + let task = tokio::spawn(engine.run()); + for submission in [None, Some("sub-route-rejected")] { + let mut op = external_user_message_op("invalid route", AppMode::Agent, &Config::default()); + if let Op::SendMessage(TurnSpec { + route, + provenance, + submission_id, + .. + }) = &mut op + { + **route = invalid_route.clone(); + *submission_id = submission.map(str::to_string); + if submission.is_none() { + *provenance = UserInputProvenance::Runtime; + } + } + handle.send(op).await.expect("queue route rejection"); + } + handle + .send(Op::EditLastTurn { + new_message: "rejected edit".into(), + submission_id: Some("sub-edit-rejected".into()), + }) + .await + .expect("queue rejected edit"); + handle + .send(Op::CompactContext { + id: "compact-after-rejections".into(), + route: Box::new(invalid_route), + compaction: Box::new(CompactionConfig::default()), + }) + .await + .expect("queue compaction rejection"); + let expected = [ + (None, None), + (Some("sub-route-rejected"), None), + (Some("sub-edit-rejected"), None), + (None, Some("compact-after-rejections")), + ]; + let mut terminal_count = 0; + { + let mut rx = handle.rx_event.write().await; + while terminal_count < expected.len() { + let event = tokio::time::timeout(model_turn_event_timeout(), rx.recv()) + .await + .expect("every rejected operation must settle") + .expect("engine event"); + match event { + Event::TurnStarted { .. } => panic!("preflight rejections must not start turns"), + Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } => { + assert!(turn_id.is_none()); + assert_eq!( + (submission_id.as_deref(), compaction_id.as_deref()), + expected[terminal_count] + ); + assert_eq!(status, TurnOutcomeStatus::Failed); + assert!(error.is_some()); + terminal_count += 1; + } + _ => {} + } + } + } + handle.send(Op::Shutdown).await.expect("shutdown"); + task.await.expect("engine task"); + let mut rx = handle.rx_event.write().await; + assert!( + !std::iter::from_fn(|| rx.try_recv().ok()).any(|event| matches!( + event, + Event::TurnStarted { .. } | Event::TurnComplete { .. } + )), + "each rejected operation has exactly one terminal and no provider turn" + ); +} + +#[tokio::test] +async fn terminal_correlation_started_failure_matches_its_original_submission() { + let workspace = tempdir().expect("isolated workspace"); + let model = std::sync::Arc::new(FailingGoalModelClient { + calls: std::sync::atomic::AtomicUsize::new(0), + message: "deterministic terminal correlation failure".into(), + }); + let client: crate::core::model_client::SharedModelClient = model.clone(); + let (engine, handle) = Engine::new_with_model_client( + deterministic_engine_config(workspace.path()), + &Config::default(), + client, + ); + let task = tokio::spawn(engine.run()); + handle + .send(external_user_message_op_with_submission( + "fail this turn", + AppMode::Agent, + &Config::default(), + Some("sub-started-failure".into()), + )) + .await + .expect("submit"); + let mut started = None; + { + let mut rx = handle.rx_event.write().await; + loop { + let event = tokio::time::timeout(model_turn_event_timeout(), rx.recv()) + .await + .expect("failed model turn must settle") + .expect("engine event"); + match event { + Event::TurnStarted { + turn_id, + submission_id, + .. + } => { + assert!(started.is_none(), "exactly one start"); + assert_eq!(submission_id.as_deref(), Some("sub-started-failure")); + started = Some(turn_id); + } + Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } => { + assert!(started.is_some()); + assert_eq!(turn_id, started); + assert_eq!(submission_id.as_deref(), Some("sub-started-failure")); + assert!(compaction_id.is_none()); + assert_eq!(status, TurnOutcomeStatus::Failed); + assert!( + error + .as_deref() + .is_some_and(|s| s.contains("terminal correlation failure")) + ); + break; + } + _ => {} + } + } + } + handle.send(Op::Shutdown).await.expect("shutdown"); + task.await.expect("engine task"); + assert_eq!(model.calls.load(std::sync::atomic::Ordering::SeqCst), 1); + let mut rx = handle.rx_event.write().await; + assert!( + !std::iter::from_fn(|| rx.try_recv().ok()).any(|event| matches!( + event, + Event::TurnStarted { .. } | Event::TurnComplete { .. } + )), + "a model failure must not produce a second terminal" + ); +} + struct BlockingEmergencyCompactionModelClient { entered: std::sync::Arc, request_dropped: std::sync::Arc, diff --git a/crates/tui/src/core/engine/tests/test_cases_04.rs b/crates/tui/src/core/engine/tests/test_cases_04.rs index 180cccc759..62e530271f 100644 --- a/crates/tui/src/core/engine/tests/test_cases_04.rs +++ b/crates/tui/src/core/engine/tests/test_cases_04.rs @@ -1167,6 +1167,9 @@ async fn headless_host_drains_existing_engine_completion_inbox_before_exit() { engine .tx_event .send(Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/core/engine/tests/test_cases_05.rs b/crates/tui/src/core/engine/tests/test_cases_05.rs index 297efe5bb7..9f914d5187 100644 --- a/crates/tui/src/core/engine/tests/test_cases_05.rs +++ b/crates/tui/src/core/engine/tests/test_cases_05.rs @@ -183,7 +183,22 @@ async fn turn_started_echoes_submission_id_and_self_starts_stay_none() { .await .expect("timed out waiting for the submitted turn to complete") .expect("engine event"); - if let Event::TurnComplete { status, error, .. } = event { + if let Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } = event + { + assert_eq!(turn_id.as_deref(), Some(submitted_turn_id.as_str())); + assert_eq!( + submission_id.as_deref(), + Some("sub-host-1"), + "the submitted turn's terminal must echo its correlation token" + ); + assert!(compaction_id.is_none()); assert_eq!(status, TurnOutcomeStatus::Completed, "{error:?}"); break; } @@ -199,7 +214,7 @@ async fn turn_started_echoes_submission_id_and_self_starts_stay_none() { payload: "child finished its work".to_string(), }) .expect("inject idle sub-agent completion"); - { + let self_started_turn_id = { let mut rx = handle.rx_event.write().await; loop { let event = tokio::time::timeout(model_turn_event_timeout(), rx.recv()) @@ -220,10 +235,10 @@ async fn turn_started_echoes_submission_id_and_self_starts_stay_none() { submission_id.is_none(), "a runtime self-started turn must not present a submission id" ); - break; + break turn_id; } } - } + }; { let mut rx = handle.rx_event.write().await; loop { @@ -231,7 +246,21 @@ async fn turn_started_echoes_submission_id_and_self_starts_stay_none() { .await .expect("timed out waiting for the self-started turn to complete") .expect("engine event"); - if let Event::TurnComplete { status, error, .. } = event { + if let Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } = event + { + assert_eq!(turn_id.as_deref(), Some(self_started_turn_id.as_str())); + assert!( + submission_id.is_none(), + "autonomous terminal must not inherit the preceding host token" + ); + assert!(compaction_id.is_none()); assert_eq!(status, TurnOutcomeStatus::Completed, "{error:?}"); break; } @@ -243,6 +272,84 @@ async fn turn_started_echoes_submission_id_and_self_starts_stay_none() { assert_eq!(mock.call_count(), 2); } +/// A composer shell turn never reaches the model; its terminal must still +/// carry its own turn id and nothing else — no host submission and no +/// compaction id. +#[tokio::test] +async fn composer_shell_terminal_carries_its_own_turn_and_nothing_else() { + use crate::llm_client::mock::MockLlmClient; + + let workspace = tempdir().unwrap(); + // The composer shell turn never reaches the model; the mock has no canned + // responses so any accidental dispatch fails loudly instead of passing. + let mock = Arc::new(MockLlmClient::new(vec![])); + let (engine, handle) = Engine::new_with_model_client( + deterministic_engine_config(workspace.path()), + &Config::default(), + mock.clone(), + ); + let task = tokio::spawn(engine.run()); + handle + .send(Op::RunShellCommand { + command: "echo composer-shell-terminal-attribution".to_string(), + mode: AppMode::Agent, + allow_shell: true, + trust_mode: false, + auto_approve: true, + approval_mode: ApprovalMode::Bypass, + }) + .await + .expect("queue composer shell command"); + { + let mut rx = handle.rx_event.write().await; + let started_turn_id = loop { + let event = tokio::time::timeout(model_turn_event_timeout(), rx.recv()) + .await + .expect("timed out waiting for the composer shell turn") + .expect("engine event"); + if let Event::TurnStarted { + turn_id, + submission_id, + .. + } = event + { + assert!( + submission_id.is_none(), + "a composer shell turn has no host submission to echo" + ); + break turn_id; + } + }; + loop { + let event = tokio::time::timeout(model_turn_event_timeout(), rx.recv()) + .await + .expect("timed out waiting for the composer shell completion") + .expect("engine event"); + if let Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } = event + { + assert_eq!(turn_id.as_deref(), Some(started_turn_id.as_str())); + assert!( + submission_id.is_none(), + "a composer shell terminal must not present a submission id" + ); + assert!(compaction_id.is_none()); + assert_eq!(status, TurnOutcomeStatus::Completed, "{error:?}"); + break; + } + } + } + handle.send(Op::Shutdown).await.expect("shutdown engine"); + task.await.expect("engine task"); + assert_eq!(mock.call_count(), 0); +} + #[test] fn idle_and_in_turn_subagent_delivery_claim_each_completion_once() { use crate::tools::subagent::SubAgentCompletion; diff --git a/crates/tui/src/core/engine/tests/test_cases_06.rs b/crates/tui/src/core/engine/tests/test_cases_06.rs index 26e664b53c..8978149d7c 100644 --- a/crates/tui/src/core/engine/tests/test_cases_06.rs +++ b/crates/tui/src/core/engine/tests/test_cases_06.rs @@ -745,6 +745,19 @@ fn external_user_message_op(content: &str, mode: AppMode, config: &Config) -> Op }) } +fn external_user_message_op_with_submission( + content: &str, + mode: AppMode, + config: &Config, + submission_id: Option, +) -> Op { + let mut op = external_user_message_op(content, mode, config); + if let Op::SendMessage(TurnSpec { submission_id: token, .. }) = &mut op { + *token = submission_id; + } + op +} + fn auto_review_message_op(content: &str, config: &Config) -> Op { Op::SendMessage(TurnSpec { profile_constitution: None, diff --git a/crates/tui/src/core/engine/tests/test_cases_14.rs b/crates/tui/src/core/engine/tests/test_cases_14.rs index d23ecdcc4e..ef3beb858a 100644 --- a/crates/tui/src/core/engine/tests/test_cases_14.rs +++ b/crates/tui/src/core/engine/tests/test_cases_14.rs @@ -449,7 +449,7 @@ async fn edit_last_turn_without_user_prompt_errors_and_sends_nothing() { handle .send(Op::EditLastTurn { new_message: "edited prompt".to_string(), - submission_id: None, + submission_id: Some("sub-edit-rejected".to_string()), }) .await .expect("send edit"); @@ -461,6 +461,9 @@ async fn edit_last_turn_without_user_prompt_errors_and_sends_nothing() { let mut events = handle.rx_event.write().await; while let Ok(Some(event)) = tokio::time::timeout_at(deadline, events.recv()).await { match event { + Event::TurnStarted { .. } => { + panic!("rejected edit must not start a turn"); + } Event::Error { envelope, .. } => { assert_eq!(envelope.code, "edit_last_turn_no_user_prompt"); assert!(!envelope.recoverable); @@ -471,7 +474,17 @@ async fn edit_last_turn_without_user_prompt_errors_and_sends_nothing() { ); saw_edit_error = true; } - Event::TurnComplete { status, error, .. } => { + Event::TurnComplete { + turn_id, + submission_id, + compaction_id, + status, + error, + .. + } => { + assert!(turn_id.is_none()); + assert_eq!(submission_id.as_deref(), Some("sub-edit-rejected")); + assert!(compaction_id.is_none()); assert_eq!(status, TurnOutcomeStatus::Failed); assert!( error diff --git a/crates/tui/src/core/events.rs b/crates/tui/src/core/events.rs index 47a170d6ae..5c385f863a 100644 --- a/crates/tui/src/core/events.rs +++ b/crates/tui/src/core/events.rs @@ -334,6 +334,17 @@ pub enum Event { /// The turn is complete (no more tool calls) TurnComplete { + /// Actual started turn, captured by its producer; absent on preflight + /// rejection. + turn_id: Option, + /// Original host submission, also echoed when it fails before + /// `TurnStarted`. Runtime self-starts and non-message operations never + /// borrow a pending host id. + submission_id: Option, + /// Manual compaction operation id. Mutually exclusive with the two + /// fields above; auto compaction closes its enclosing model turn + /// instead. + compaction_id: Option, /// Total usage for session/goal/token metrics, including programmatic /// child calls performed inline during this turn. usage: Usage, diff --git a/crates/tui/src/core/protocol_parity.rs b/crates/tui/src/core/protocol_parity.rs index e87fbaa992..dd58ddf327 100644 --- a/crates/tui/src/core/protocol_parity.rs +++ b/crates/tui/src/core/protocol_parity.rs @@ -609,6 +609,9 @@ pub fn event_to_protocol(event: &Event, ids: &ProtocolIds) -> wire::EventMsg { route: route_to_wire(route), }, Event::TurnComplete { + turn_id, + submission_id, + compaction_id, usage, parent_route_usage, routed_usage_dropped_records, @@ -619,7 +622,9 @@ pub fn event_to_protocol(event: &Event, ids: &ProtocolIds) -> wire::EventMsg { } => wire::EventMsg::TurnComplete { thread_id, session_id, - turn_id: None, + turn_id: turn_id.clone(), + submission_id: submission_id.clone(), + compaction_id: compaction_id.clone(), status: outcome_status_to_wire(*status), error: error.clone(), usage: usage_to_wire(usage), @@ -1390,6 +1395,9 @@ mod tests { }; let complete = event_to_protocol( &Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: total.clone(), parent_route_usage: parent.clone(), routed_usage_dropped_records: 3, @@ -1459,6 +1467,26 @@ mod tests { } } + #[test] + fn manual_compaction_terminal_keeps_its_operation_id_in_projection() { + let event = Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: Some("compact-1".into()), + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: TurnOutcomeStatus::Failed, + error: Some("route rejected".into()), + tool_catalog: None, + base_url: None, + }; + let value = serde_json::to_value(event.to_protocol(&ids())).unwrap(); + assert_eq!(value["compaction_id"], "compact-1"); + assert!(value.get("turn_id").is_none()); + assert!(value.get("submission_id").is_none()); + } + /// The guard is the exhaustive `match` in `event_to_protocol`: this test /// exists so the guard has a name in the test log and so the projection /// is proven to agree with the protocol's wire-tag table. @@ -1508,6 +1536,9 @@ mod tests { submission_id: Some("sub-host-1".into()), }, Event::TurnComplete { + turn_id: Some("turn-1".into()), + submission_id: Some("sub-host-1".into()), + compaction_id: None, usage: usage.clone(), parent_route_usage: usage.clone(), routed_usage_dropped_records: 0, @@ -1615,6 +1646,10 @@ mod tests { serde_json::to_value(events[8].to_protocol(&ids)).unwrap()["status"], "interrupted" ); + let terminal = serde_json::to_value(events[8].to_protocol(&ids)).unwrap(); + assert_eq!(terminal["turn_id"], "turn-1"); + assert_eq!(terminal["submission_id"], "sub-host-1"); + assert!(terminal.get("compaction_id").is_none()); let error = events .iter() .find(|event| matches!(event, Event::Error { .. })) diff --git a/crates/tui/src/exec_agent.rs b/crates/tui/src/exec_agent.rs index fcc021b2ab..b61a4f769f 100644 --- a/crates/tui/src/exec_agent.rs +++ b/crates/tui/src/exec_agent.rs @@ -1657,6 +1657,9 @@ mod tests { ..Usage::default() }; Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: usage.clone(), parent_route_usage: usage, routed_usage_dropped_records: 0, diff --git a/crates/tui/src/remote_control.rs b/crates/tui/src/remote_control.rs index 2e2b32b7b8..8f6ba61d81 100644 --- a/crates/tui/src/remote_control.rs +++ b/crates/tui/src/remote_control.rs @@ -7699,6 +7699,9 @@ mod tests { fn turn_complete_event() -> EngineEvent { EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: codewhale_models::Usage::default(), parent_route_usage: codewhale_models::Usage::default(), routed_usage_dropped_records: 0, @@ -8263,6 +8266,9 @@ mod tests { ..codewhale_models::Usage::default() }; controller.observe_engine_event(&EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: usage.clone(), parent_route_usage: usage.clone(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/runtime_api/tests.rs b/crates/tui/src/runtime_api/tests.rs index c9fe793e2a..159aedbbc6 100644 --- a/crates/tui/src/runtime_api/tests.rs +++ b/crates/tui/src/runtime_api/tests.rs @@ -3445,6 +3445,9 @@ async fn compatibility_stream_closes_losslessly_across_replay_live_handoff() -> .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 3, output_tokens: 1, @@ -3710,6 +3713,9 @@ async fn compatibility_stream_exposes_and_resolves_user_input_without_answer_ech harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -4069,6 +4075,9 @@ async fn thread_endpoints_expose_lifecycle_contract() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 5, @@ -4090,6 +4099,9 @@ async fn thread_endpoints_expose_lifecycle_contract() -> Result<()> { Op::CompactContext { .. } => { let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 0, output_tokens: 0, @@ -4230,6 +4242,9 @@ async fn turn_endpoint_operation_key_returns_original_and_conflicts_on_mismatch( sleep(Duration::from_millis(20)).await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -4452,6 +4467,9 @@ async fn turn_operation_lookup_is_authenticated_read_only_and_survives_restart() engine .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Default::default(), parent_route_usage: Default::default(), routed_usage_dropped_records: 0, @@ -4581,6 +4599,9 @@ async fn events_endpoint_respects_since_seq_cursor() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 5, output_tokens: 3, @@ -5520,6 +5541,9 @@ async fn steer_and_interrupt_endpoints_work_on_active_turn() -> Result<()> { sleep(Duration::from_millis(60)).await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 2, output_tokens: 1, @@ -6071,6 +6095,9 @@ async fn stream_endpoint_remains_backward_compatible() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 4, output_tokens: 2, @@ -8636,6 +8663,9 @@ async fn session_create_from_thread_rejects_active_turn() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 2, output_tokens: 1, @@ -19759,6 +19789,9 @@ async fn runtime_image_http_rejects_before_dispatch_and_accepts_large_canonical_ harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Default::default(), parent_route_usage: Default::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/runtime_threads/tests.rs b/crates/tui/src/runtime_threads/tests.rs index 4a412da577..2a3aeedde0 100644 --- a/crates/tui/src/runtime_threads/tests.rs +++ b/crates/tui/src/runtime_threads/tests.rs @@ -3541,6 +3541,9 @@ async fn caller_cancellation_after_engine_acceptance_keeps_owned_turn_lifecycle( harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -3655,6 +3658,9 @@ async fn operation_key_replays_torn_response_survives_restart_and_rejects_mismat harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -4173,6 +4179,9 @@ async fn thread_updates_while_start_waits_for_capacity_survive_latest_turn_write harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -4336,6 +4345,9 @@ async fn compact_lifecycle_outlives_caller_and_preserves_concurrent_thread_updat harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -8393,6 +8405,9 @@ async fn thread_lifecycle_persists_across_restart() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 12, @@ -8550,6 +8565,9 @@ async fn initial_classifier_usage_is_persisted_before_terminal_and_merged_exactl harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 18, output_tokens: 8, @@ -8988,6 +9006,9 @@ async fn terminal_settlement_preserves_late_sink_receipts_during_pending_request harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 8, output_tokens: 5, @@ -9144,6 +9165,9 @@ async fn monitor_deduplicates_sink_and_metadata_and_persists_metadata_only_missi harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: batch.records[0].usage.usage.clone(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -9299,6 +9323,9 @@ async fn monitor_separates_lifecycle_start_from_billing_dispatch_and_child_usage harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 4, @@ -9420,6 +9447,9 @@ async fn monitor_separates_lifecycle_start_from_billing_dispatch_and_child_usage harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -9630,6 +9660,9 @@ async fn monitor_persists_request_snapshots_and_matching_terminal_diagnostics() harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -9812,6 +9845,9 @@ async fn monitor_persists_request_snapshots_and_matching_terminal_diagnostics() harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -9884,6 +9920,9 @@ async fn completed_turn_without_engine_output_fails() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 0, @@ -10035,6 +10074,9 @@ async fn worker_lifecycle_receipts_preserve_owner_outcome_and_durable_replay() - } let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -10106,6 +10148,9 @@ async fn worker_lifecycle_receipts_preserve_owner_outcome_and_durable_replay() - let _ = tx_event.send(completed).await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -10350,6 +10395,9 @@ async fn preturn_control_status_does_not_make_empty_turn_succeed() -> Result<()> .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -10413,6 +10461,9 @@ async fn engine_error_remains_failed_after_nominal_turn_complete() -> Result<()> .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -10996,6 +11047,9 @@ async fn compact_interrupt_persists_canceled_item_for_the_exact_request() -> Res harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -11234,6 +11288,9 @@ async fn multi_turn_continuity_same_thread() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 5, output_tokens: 5, @@ -11476,6 +11533,9 @@ async fn host_goal_loop_kickoff_arms_one_continuation_and_parks_at_engine_cap() .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 10, @@ -11595,6 +11655,9 @@ async fn host_goal_loop_skips_rearm_without_update_goal_and_after_failed_pass() .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 10, @@ -11674,6 +11737,9 @@ async fn host_goal_loop_skips_rearm_without_update_goal_and_after_failed_pass() .await; let _ = failed_tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 5, output_tokens: 5, @@ -11772,6 +11838,9 @@ async fn host_goal_loop_mirrors_terminal_snapshot_and_does_not_rearm() -> Result .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 7, output_tokens: 3, @@ -11878,6 +11947,9 @@ async fn model_created_goal_persists_through_adopted_revision() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 10, @@ -11962,6 +12034,9 @@ async fn model_created_goal_never_overwrites_concurrent_explicit_goal() -> Resul .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 10, @@ -12064,6 +12139,9 @@ async fn live_goal_progress_is_revision_fenced_and_settled_once() -> Result<()> harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 10, output_tokens: 10, @@ -12795,6 +12873,9 @@ async fn approval_required_with_stale_active_turn_is_denied() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 0, output_tokens: 0, @@ -13026,6 +13107,9 @@ async fn approval_required_awaits_external_decision_allow() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -13143,6 +13227,9 @@ async fn user_input_snapshot_survives_reload_and_clears_after_submission() -> Re harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -13311,6 +13398,9 @@ async fn user_input_tool_timeout_and_cancel_clear_snapshot_before_turn_end() -> harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -13902,6 +13992,9 @@ async fn thread_detail_cursor_precedes_projection_reads_at_terminal_boundary() - harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14155,6 +14248,9 @@ async fn thread_detail_materializes_stream_prefixes_before_their_delta_cursor() harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14296,6 +14392,9 @@ async fn thread_detail_delta_boundary_is_replay_idempotent() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14367,6 +14466,9 @@ async fn terminal_turn_cancels_pending_user_input_and_clears_snapshot() -> Resul harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14469,6 +14571,9 @@ async fn interrupted_turn_cancels_pending_user_input_and_clears_snapshot() -> Re harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14660,6 +14765,9 @@ async fn dynamic_tool_result_settles_snapshot_and_emits_one_safe_resolution() -> harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -14847,6 +14955,9 @@ async fn dynamic_tool_result_receipt_outlives_canceled_delivery_future() -> Resu harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -15678,6 +15789,9 @@ async fn dynamic_tool_timeout_clears_snapshot_and_emits_once() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -15766,6 +15880,9 @@ async fn terminal_turn_cancels_pending_dynamic_tool_exactly_once() -> Result<()> harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -15903,6 +16020,9 @@ async fn engine_withdrawal_retires_approval_and_unblocks_queued_events() -> Resu harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -15997,6 +16117,9 @@ async fn approval_wait_heartbeat_is_never_sequenced_after_the_decision() -> Resu harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16115,6 +16238,9 @@ async fn approval_required_external_deny_is_denied() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16264,6 +16390,9 @@ async fn identical_raw_tool_call_ids_on_two_threads_stay_independently_gated() - harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16441,6 +16570,9 @@ async fn runtime_approval_disposition_auto_paths_obey_force_and_posture() -> Res harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16573,6 +16705,9 @@ async fn runtime_approval_disposition_forced_requests_require_human() -> Result< harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16670,6 +16805,9 @@ async fn auto_review_force_prompt_is_denied_without_opening_a_modal() -> Result< harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16795,6 +16933,9 @@ async fn approval_interrupt_revokes_waiter_and_rejects_late_actions() -> Result< harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16932,6 +17073,9 @@ async fn approval_timeout_denies_clears_ui_and_next_turn_can_start() -> Result<( harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -17027,6 +17171,9 @@ async fn thinking_delta_emits_agent_reasoning_item() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -17434,6 +17581,9 @@ async fn elevation_required_with_stale_active_turn_is_denied() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 0, output_tokens: 0, @@ -17506,6 +17656,9 @@ async fn steer_turn_on_active_turn_records_item_and_event() -> Result<()> { .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 8, output_tokens: 9, @@ -17699,6 +17852,9 @@ async fn steer_receipts_outlive_caller_cancellation_after_engine_acceptance() -> .await?; tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -17814,6 +17970,9 @@ async fn steer_dropped_by_the_engine_is_reported_as_undelivered() -> Result<()> .await?; tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -17905,6 +18064,9 @@ async fn steer_rejects_a_terminal_durable_turn_without_dispatch_or_item() -> Res .await?; tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -18178,6 +18340,9 @@ async fn compaction_lifecycle_emits_item_events_with_compaction_counts() -> Resu .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 3, output_tokens: 3, @@ -18221,6 +18386,9 @@ async fn compaction_lifecycle_emits_item_events_with_compaction_counts() -> Resu .await; let _ = tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage { input_tokens: 1, output_tokens: 1, @@ -19204,6 +19372,9 @@ async fn agent_mail_release_acceptance_two_task_matrix() -> Result<()> { harness_b .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -20523,6 +20694,9 @@ mod runtime_image_inputs { }, EngineEvent::MessageComplete { index: 0 }, EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -20824,6 +20998,9 @@ async fn notices_raise_from_engine_events_and_clear_on_settle_or_ack() -> Result harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -21822,6 +21999,9 @@ async fn engine_plumbing_items_are_tagged_internal_and_retry_hints_are_dropped() harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -22494,6 +22674,9 @@ async fn runtime_receipts_mask_configured_secrets() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -22740,6 +22923,9 @@ async fn tool_completion_items_carry_typed_artifact_refs() -> Result<()> { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -23001,6 +23187,9 @@ async fn run_workspace_turn( harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -23588,6 +23777,9 @@ mod execution_identity { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -23794,6 +23986,9 @@ mod execution_identity { harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -24139,6 +24334,9 @@ async fn profile_constitution_runtime_admission_binds_the_complete_snapshot_to_r harness .tx_event .send(EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/tui/pet_watch/mod.rs b/crates/tui/src/tui/pet_watch/mod.rs index 1ea971d51c..48292bae66 100644 --- a/crates/tui/src/tui/pet_watch/mod.rs +++ b/crates/tui/src/tui/pet_watch/mod.rs @@ -884,6 +884,9 @@ mod tests { observe( &mut app, &Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Default::default(), parent_route_usage: Default::default(), routed_usage_dropped_records: 0, @@ -1081,6 +1084,9 @@ mod tests { let done = metadata( &Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Default::default(), parent_route_usage: Default::default(), routed_usage_dropped_records: 0, @@ -1176,6 +1182,9 @@ mod tests { let turn = metadata( &Event::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Default::default(), parent_route_usage: Default::default(), routed_usage_dropped_records: 0, diff --git a/crates/tui/src/tui/ui/event_loop.rs b/crates/tui/src/tui/ui/event_loop.rs index 6992ddbc4e..261423707a 100644 --- a/crates/tui/src/tui/ui/event_loop.rs +++ b/crates/tui/src/tui/ui/event_loop.rs @@ -31,6 +31,72 @@ pub(super) fn event_owner_is_active( !owner_session_id.is_empty() && current_session_id == Some(owner_session_id) } +/// Terminal data captured directly from an engine completion event. +/// +/// `runtime_turn_id` deliberately survives a completed turn for saved-session +/// and remote-control projections. It is therefore not an authority for a +/// later `TurnComplete`: a preflight rejection or manual compaction has no +/// model turn id and must not inherit the preceding model turn. +pub(super) struct TurnEndLifecycleReceipt { + turn_id: Option, + status: &'static str, + error: Option, +} + +impl TurnEndLifecycleReceipt { + pub(super) fn capture(event: &EngineEvent) -> Option { + let EngineEvent::TurnComplete { + turn_id, + status, + error, + .. + } = event + else { + return None; + }; + let status = match status { + crate::core::events::TurnOutcomeStatus::Completed => "completed", + crate::core::events::TurnOutcomeStatus::Failed => "failed", + crate::core::events::TurnOutcomeStatus::Interrupted => "interrupted", + }; + Some(Self { + turn_id: turn_id.clone(), + status, + error: error.clone(), + }) + } +} + +/// Build the lifecycle receipt from terminal data captured before dispatch. +pub(super) fn turn_end_lifecycle_event( + app: &App, + receipt: TurnEndLifecycleReceipt, + turn_elapsed: Duration, +) -> codewhale_hooks::LifecycleEvent { + let kind = match receipt.status { + "completed" => "turn.completed", + "failed" => "turn.failed", + "interrupted" => "turn.interrupted", + _ => "turn.ended", + }; + codewhale_hooks::LifecycleEvent { + event: "turn_end".to_string(), + kind: kind.to_string(), + thread_id: app.hooks.session_id().to_string(), + turn_id: receipt.turn_id, + item_id: None, + payload: serde_json::json!({ + "status": receipt.status, + "duration_ms": turn_elapsed.as_millis() as u64, + "workspace": app.workspace.display().to_string(), + "error": receipt.error.as_deref().map(|message| codewhale_hooks::bounded_text( + message, + codewhale_hooks::OUTBOX_DETAIL_MAX_CHARS, + )), + }), + } +} + /// Apply only the projection owned by this host session. A delayed SetModel /// receipt from the previous session cannot replace the current transcript. pub(super) fn apply_engine_session_projection( @@ -2468,6 +2534,7 @@ pub(crate) async fn run_event_loop( crate::tui::pet_watch::observe(app, &event, Instant::now()); } record_turn_activity(app, &event, Instant::now()); + let turn_end_lifecycle = TurnEndLifecycleReceipt::capture(&event); match event { EngineEvent::MessageStarted { .. } => { // Assistant text starting after parallel tool work @@ -2996,6 +3063,7 @@ pub(crate) async fn run_event_loop( } } EngineEvent::TurnComplete { + turn_id: _, usage, parent_route_usage, routed_usage_dropped_records, @@ -3003,6 +3071,7 @@ pub(crate) async fn run_event_loop( error, tool_catalog, base_url, + .. } => { // A decision whose tool never reported completion // still gets its receipt before the turn closes. @@ -3549,33 +3618,19 @@ pub(crate) async fn run_event_loop( // projected from the turn status — `turn.failed` for // failed turns, `turn.completed` for completed ones, // `turn.interrupted` for locally cancelled ones. + // The receipt's turn identity comes from the terminal + // event itself, never from the stale-surviving + // `runtime_turn_id`: a preflight rejection or manual + // compaction must not close the preceding model turn. // No-op when the feature is disabled. { - let outbox_status = turn_status_label.as_str(); - let kind = match outbox_status { - "completed" => "turn.completed", - "failed" => "turn.failed", - "interrupted" => "turn.interrupted", - _ => "turn.ended", - }; - app.lifecycle_outbox.emit(codewhale_hooks::LifecycleEvent { - event: "turn_end".to_string(), - kind: kind.to_string(), - thread_id: app.hooks.session_id().to_string(), - turn_id: app.runtime_turn_id.clone(), - item_id: None, - payload: serde_json::json!({ - "status": outbox_status, - "duration_ms": turn_elapsed.as_millis() as u64, - "workspace": app.workspace.display().to_string(), - "error": error - .as_deref() - .map(|message| codewhale_hooks::bounded_text( - message, - codewhale_hooks::OUTBOX_DETAIL_MAX_CHARS, - )), - }), - }); + let event = turn_end_lifecycle_event( + app, + turn_end_lifecycle + .expect("TurnComplete must capture lifecycle data"), + turn_elapsed, + ); + app.lifecycle_outbox.emit(event); } // Plan hand-off freezes this successful turn's exact diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index cd71392b4f..d33ed330c2 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -16080,6 +16080,9 @@ fn user_input_completion_preserves_newer_question_and_dispatch() { observe_human_request_settlement( &mut app, &EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -16114,6 +16117,9 @@ fn user_input_turn_end_cancel_and_disconnect_retire_question_views() { _ => observe_human_request_settlement( &mut app, &EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -18693,6 +18699,9 @@ fn local_cancel_marks_late_stream_events_for_suppression() { )); assert!(!suppress_engine_event_after_local_cancel( &EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: None, usage: Usage::default(), parent_route_usage: Usage::default(), routed_usage_dropped_records: 0, @@ -18715,6 +18724,76 @@ fn local_cancel_marks_late_stream_events_for_suppression() { )); } +#[test] +fn turn_end_outbox_uses_the_terminal_events_owner() { + let mut app = create_test_app(); + app.runtime_turn_id = Some("turn-model".to_string()); + + let model_terminal = EngineEvent::TurnComplete { + turn_id: Some("turn-model".to_string()), + submission_id: None, + compaction_id: None, + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: crate::core::events::TurnOutcomeStatus::Completed, + error: None, + tool_catalog: None, + base_url: None, + }; + let model = super::event_loop::turn_end_lifecycle_event( + &app, + super::event_loop::TurnEndLifecycleReceipt::capture(&model_terminal) + .expect("terminal lifecycle data"), + Duration::from_millis(37), + ); + assert_eq!(model.kind, "turn.completed"); + assert_eq!(model.turn_id.as_deref(), Some("turn-model")); + + let rejected_terminal = EngineEvent::TurnComplete { + turn_id: None, + submission_id: Some("submission-rejected".to_string()), + compaction_id: None, + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: crate::core::events::TurnOutcomeStatus::Failed, + error: Some("route rejected".to_string()), + tool_catalog: None, + base_url: None, + }; + let rejected = super::event_loop::turn_end_lifecycle_event( + &app, + super::event_loop::TurnEndLifecycleReceipt::capture(&rejected_terminal) + .expect("terminal lifecycle data"), + Duration::ZERO, + ); + assert_eq!(rejected.kind, "turn.failed"); + assert!(rejected.turn_id.is_none()); + + let compaction_terminal = EngineEvent::TurnComplete { + turn_id: None, + submission_id: None, + compaction_id: Some("compact-manual".to_string()), + usage: Usage::default(), + parent_route_usage: Usage::default(), + routed_usage_dropped_records: 0, + status: crate::core::events::TurnOutcomeStatus::Completed, + error: None, + tool_catalog: None, + base_url: None, + }; + let manual_compaction = super::event_loop::turn_end_lifecycle_event( + &app, + super::event_loop::TurnEndLifecycleReceipt::capture(&compaction_terminal) + .expect("terminal lifecycle data"), + Duration::ZERO, + ); + assert_eq!(manual_compaction.kind, "turn.completed"); + assert!(manual_compaction.turn_id.is_none()); + assert_eq!(app.runtime_turn_id.as_deref(), Some("turn-model")); +} + /// A turn the Engine stopped itself (here its per-turn wall-clock budget) /// posts no error event. Its reason must stay in the transcript, not only the /// footer, or a later notice replaces it and the session looks hung. diff --git a/crates/tui/tests/fixtures/conformance/events/approval_denied.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/approval_denied.golden.jsonl index e61e6d1173..f1e53f06c5 100644 --- a/crates/tui/tests/fixtures/conformance/events/approval_denied.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/approval_denied.golden.jsonl @@ -19,5 +19,5 @@ {"event":"message_complete","index":0} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"\nCurrent local date: \nCurrent workspace: \nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n","type":"text"}],"role":"user"},{"content":[{"execution_id":"","id":"call_touch","input":{"command":"touch denied.txt"},"name":"Bash","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"Error: Tool 'Bash' denied by user — the call was not approved. Do not retry the same call; present what you intended and wait for the user's approval or new instructions.","execution_id":"","is_error":true,"tool_use_id":"call_touch","type":"tool_result"}],"role":"user"},{"content":[{"text":"Understood, I did not create it.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"","workspace":""} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":21},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","usage":{"input_tokens":350,"output_tokens":21}} +{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":21},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","turn_id":"","usage":{"input_tokens":350,"output_tokens":21}} {"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","README.md":"41f49bffb38de5ba"}}} diff --git a/crates/tui/tests/fixtures/conformance/events/cancel_midstream.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/cancel_midstream.golden.jsonl index 0afc9514af..7f595f8fdc 100644 --- a/crates/tui/tests/fixtures/conformance/events/cancel_midstream.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/cancel_midstream.golden.jsonl @@ -10,6 +10,6 @@ {"event":"status","message":"Request cancelled"} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Tell me a long story.","type":"text"},{"text":"\nCurrent local date: \nCurrent workspace: \nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n","type":"text"}],"role":"user"},{"content":[{"text":"Here is a partial","type":"text"}],"role":"assistant_interrupted"}],"model":"deepseek-v4-pro","system_prompt":"","workspace":""} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":null,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"interrupted","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"interrupted","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"interrupted","tool_catalog":"","usage":{"input_tokens":120,"output_tokens":0}} +{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"interrupted","tool_catalog":"","turn_id":"","usage":{"input_tokens":120,"output_tokens":0}} {"event":"status","message":"Turn interrupted."} {"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}} diff --git a/crates/tui/tests/fixtures/conformance/events/one_tool_call.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/one_tool_call.golden.jsonl index 9715839edb..78ce99dcc5 100644 --- a/crates/tui/tests/fixtures/conformance/events/one_tool_call.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/one_tool_call.golden.jsonl @@ -20,5 +20,5 @@ {"event":"message_complete","index":0} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"\nCurrent local date: \nCurrent workspace: \nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n","type":"text"}],"role":"user"},{"content":[{"execution_id":"","id":"call_read_readme","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"","tool_use_id":"call_read_readme","type":"tool_result"}],"role":"user"},{"content":[{"text":"It says: conformance fixture.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"","workspace":""} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":26},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","usage":{"input_tokens":350,"output_tokens":26}} +{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":26},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","turn_id":"","usage":{"input_tokens":350,"output_tokens":26}} {"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}} diff --git a/crates/tui/tests/fixtures/conformance/events/parallel_tool_calls.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/parallel_tool_calls.golden.jsonl index 77458caa1c..75d70a121b 100644 --- a/crates/tui/tests/fixtures/conformance/events/parallel_tool_calls.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/parallel_tool_calls.golden.jsonl @@ -26,5 +26,5 @@ {"event":"message_complete","index":0} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"\nCurrent local date: \nCurrent workspace: \nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n","type":"text"}],"role":"user"},{"content":[{"execution_id":"","id":"call_read_a","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"},{"execution_id":"","id":"call_read_b","input":{"action":"read","path":"NOTES.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"","tool_use_id":"call_read_a","type":"tool_result"}],"role":"user"},{"content":[{"content":"content_hash=\"sha256:f957b19529906961933c5c30f8713c500a9bb5d9d0695c40d48c97a26a3594ec\"\nsecond file","execution_id":"","tool_use_id":"call_read_b","type":"tool_result"}],"role":"user"},{"content":[{"text":"Both files read.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"","workspace":""} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"event":"turn_complete","parent_route_usage":{"input_tokens":360,"output_tokens":46},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","usage":{"input_tokens":360,"output_tokens":46}} +{"event":"turn_complete","parent_route_usage":{"input_tokens":360,"output_tokens":46},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","turn_id":"","usage":{"input_tokens":360,"output_tokens":46}} {"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}} diff --git a/crates/tui/tests/fixtures/conformance/events/plain_answer.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/plain_answer.golden.jsonl index c4331c628c..7c7cb9c74b 100644 --- a/crates/tui/tests/fixtures/conformance/events/plain_answer.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/plain_answer.golden.jsonl @@ -11,5 +11,5 @@ {"event":"message_complete","index":0} {"engine_session_id":"conformance-session","event":"session_updated","messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"\nCurrent local date: \nCurrent workspace: \nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n","type":"text"}],"role":"user"},{"content":[{"text":"Hello there.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"","workspace":""} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":4},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","usage":{"input_tokens":120,"output_tokens":4}} +{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":4},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"","turn_id":"","usage":{"input_tokens":120,"output_tokens":4}} {"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}} diff --git a/crates/tui/tests/fixtures/conformance/events/provider_error_after_tool_call.golden.jsonl b/crates/tui/tests/fixtures/conformance/events/provider_error_after_tool_call.golden.jsonl index f169b14ee5..80a33be279 100644 --- a/crates/tui/tests/fixtures/conformance/events/provider_error_after_tool_call.golden.jsonl +++ b/crates/tui/tests/fixtures/conformance/events/provider_error_after_tool_call.golden.jsonl @@ -9,5 +9,5 @@ {"event":"tool_call_started","input":{"action":"write","content":"side effect\n","path":"c0205.txt"},"tool_call_id":"","tool_name":"File"} {"event":"tool_call_complete","result":{"content":"Not executed: the provider stream failed before the model response completed (Model not exist.).","metadata":{"error_category":"model_stream_failed","model_output_incomplete":true,"side_effect_status":"not_started"},"outcome":"ok","success":false},"tool_call_id":"","tool_name":"File"} {"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"410aa443c5feedb22f5e0b92ec167093f56d50f1f5ac3d4d18e9c18329ef0daa","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":16057,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":39,"omitted":7,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"grep_files"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"},{"truncated":false,"value":"terminal/reset"}]}},"registry_tool_count":{"status":"known","value":62},"rendered_tool_count":13,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":1,"last_response_tool_calls_suppressed":1,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"failed","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"soft_landing_sent":false,"status":"failed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":13,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr. Output keeps the last 2000 lines or 50KB. An optional timeout is the seconds to wait in the foreground (default 30 seconds); a command still running then is never killed for time: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id, so long builds and tests need no special timeout. Call task_shell_wait directly with that task_id to read more or wait for it; do n"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) with mandatory native filesystem read-only isolation and no network. Available during peer writes. Refused when native enforcement is unavailable; no background, stdin, external backend, or sandbox escalation.\",\"type\":\"boolean\"},\"sandbox_permissions\":{\"description\":\"The wider sandbox mode this exact command needs. Use only as a one-shot retry after a sandbox denial; requires justification and user approval in Ask.\",\"enum\":[\"workspace-write\",\"danger-full-access\"],\"type\":\"string\"},\"timeout\":{\"description\":\"Optional seconds to wait in the foreground (default 30 seconds). A command still running then is not killed: it moves to the background by itself and you get its output so far, how long ago it last printed, its build phase, and a task_id. Call task_shell_wait directly with that task_id to read more or wait for it; do not sleep-poll with bash.\",\"type\":\"number\"}},\"required\":[\"command\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"bash"},"ordinal":1,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Create the session's one persistent goal: a completion objective Codewhale keeps working toward across turns until it is verified complete, blocked, or the user stops it. A goal is for a durable objective that outlasts one turn: a multi-step outcome the user wants continued and verified. A question, a greeting, or a one-shot edit completes as an ordinary turn. When the user explicitly asks to use `/goal` or to make something the goal, `create_goal` is what records it; acknowledging it in prose records nothi"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"objective\":{\"description\":\"The full objective to pursue. Keep the complete user goal, not a shortened one-turn version.\",\"type\":\"string\"},\"token_budget\":{\"description\":\"Optional soft token budget for the goal.\",\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"objective\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"create_goal"},"ordinal":2,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Edit one file with targeted text replacements. Every edits[].oldText must identify a unique, non-overlapping region of the original file. Merge nearby or overlapping changes into one edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"edits\":{\"description\":\"One or more disjoint replacements, all matched against the original file.\",\"items\":{\"additionalProperties\":false,\"properties\":{\"newText\":{\"description\":\"Replacement text; may be empty to delete the matched span.\",\"type\":\"string\"},\"oldText\":{\"description\":\"Text identifying one unique region to replace.\",\"type\":\"string\"}},\"required\":[\"newText\",\"oldText\"],\"type\":\"object\"},\"type\":\"array\"},\"path\":{\"description\":\"Path to the file to edit (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"edits\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"edit"},"ordinal":3,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Find workspace files by name, path, or glob (e.g. src/**/*.rs). Space-separated terms must all match. Literal matches rank above fuzzy matches; results are capped. Respects ignore files and skips build/dependency directories by default. Use extensions to filter suffixes."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"exclude\":{\"description\":\"Optional replacement exclusion globs (e.g. [\\\"**/target/**\\\", \\\"*.lock\\\"]); [] disables default exclusions.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"extensions\":{\"description\":\"Optional list of file extensions to include (e.g. [\\\"rs\\\", \\\"md\\\"]).\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"limit\":{\"description\":\"Maximum number of ranked results to return (default: 20, maximum: 200).\",\"type\":\"integer\"},\"path\":{\"description\":\"Optional base path to search (relative to workspace).\",\"type\":\"string\"},\"query\":{\"description\":\"File name, path fragment, space-separated terms, or glob (e.g. **/engine*.rs).\",\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"file_search"},"ordinal":4,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Inspect the current runtime goal state, including objective, status, token budget, elapsed time, evidence, and blocker."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"get_goal"},"ordinal":5,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Load a named skill's SKILL.md body and companion file list into this turn. Use when the user names a skill, or when an entry in the system prompt's `## Skills` index matches the task -- load it before starting the work, not after. Pass query=\"...\" to search names and descriptions, or name=\"list\" for the whole catalogue. Resolves global and plugin skills that `read` cannot reach."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"name\":{\"description\":\"Skill id to load. Omit or pass \\\"list\\\" to see all available skills.\",\"type\":\"string\"},\"query\":{\"description\":\"Search term matched against skill names and descriptions. Use when the index was truncated or no name is known.\",\"type\":\"string\"}},\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"load_skill"},"ordinal":6,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"ReadOnly"},{"truncated":false,"value":"Sandboxable"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Read a file. A text file comes back whole in one call when it fits this call's output budget — 100000 bytes by default, raisable to 500000 with max_bytes. There is no line cap. Use offset and limit for an exact line range; when output is budget-limited the footer names the exact offset to continue from. Every text response reports the file's byte size, line count, and whether output was truncated. A PNG, JPEG, GIF or WebP image comes back as image content you can see (large images are downscaled), including"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"limit\":{\"description\":\"Maximum number of lines to read.\",\"type\":\"number\"},\"max_bytes\":{\"description\":\"Output budget in bytes for this one call. Defaults to 100000; values above the 500000 maximum are clamped down rather than rejected, and a value below the active default leaves the default in place.\",\"type\":\"number\"},\"offset\":{\"description\":\"Line number to start reading from (1-indexed).\",\"type\":\"number\"},\"path\":{\"description\":\"Path to the file to read (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"read"},"ordinal":7,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":2,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Poll a background shell task without blocking the agent indefinitely, or stop it with cancel=true. Completion is delivered automatically; use this only for early output, explicit barriers, or gate evidence. If `gate` is supplied and the shell task has completed, records structured gate evidence on the active durable task."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"cancel\":{\"description\":\"Stop (kill) the task instead of polling; not valid with gate.\",\"type\":\"boolean\"},\"command\":{\"description\":\"Original command, used when recording gate evidence.\",\"type\":\"string\"},\"gate\":{\"enum\":[\"fmt\",\"check\",\"clippy\",\"test\",\"custom\"],\"type\":\"string\"},\"task_id\":{\"description\":\"Background shell task id.\",\"type\":\"string\"},\"timeout_ms\":{\"maximum\":600000,\"minimum\":1000,\"type\":\"integer\"},\"wait\":{\"default\":false,\"type\":\"boolean\"}},\"required\":[\"task_id\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"task_shell_wait"},"ordinal":8,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Replace the To-do list the user watches. For any task with three or more steps or more than one file, write the list before you start, keep exactly one item in_progress, and mark items done as you finish them. Skip it only for a single quick answer or edit."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"properties\":{\"todos\":{\"description\":\"The complete list of To-do items. This replaces the existing list.\",\"items\":{\"properties\":{\"content\":{\"description\":\"The task description\",\"type\":\"string\"},\"status\":{\"description\":\"Task status\",\"enum\":[\"pending\",\"in_progress\",\"completed\",\"cancelled\"],\"type\":\"string\"}},\"required\":[\"content\",\"status\"],\"type\":\"object\"},\"type\":\"array\"}},\"required\":[\"todos\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"todo_write"},"ordinal":9,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Auto"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":0,"omitted":0,"rendered":[]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Update the runtime goal completion gate by calling this tool; a prose status in your answer does not change the goal or stop continuation. Critical verification may seal one immutable completion contract. Advisory review is append-only context and never completes, blocks, or pauses the goal. Mark blocked when progress requires user input. Mark yield when you finished a stage and the next step needs the user's decision: the goal stays unfinished, the turn ends, and their next message resumes it."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":true,"value":"{\"additionalProperties\":false,\"properties\":{\"advisory\":{\"description\":\"Required when status is advisory. Appended separately from the judged completion contract.\",\"type\":\"string\"},\"blocker\":{\"description\":\"Required when status is blocked. Explain the condition preventing progress.\",\"type\":\"string\"},\"evidence\":{\"description\":\"Required when status is complete. Briefly cite the proof that the goal is done.\",\"type\":\"string\"},\"progress\":{\"additionalProperties\":false,\"description\":\"Optional with not_achieved, advisory or yield: your current best estimate of overall completion, shown to the user as reported progress. Keep percent honest — it is an estimate, never a verified fraction.\",\"properties\":{\"next\":{\"description\":\"One short line: what comes next.\",\"type\":\"string\"},\"now\":{\"description\":\"One short line: what is being worked on right now.\",\"type\":\"string\"},\"percent\":{\"description\":\"Estimated percent complete, 0-100.\",\"maximum\":100,\"minimum\":0,\"type\":\"integer\"}},\"required\":[\"percent\"],\"type\":\"object\"},\"status\":{\"description\":\"Use complete only when a critical verifier proves the goal; not_achieved to record verifier gaps; blocked when meaningful progress cannot continue; yield when a stage is finished and the next step is the user's call; advisory to append best-effort context without changing lifecycle state.\",\"enum\":[\"complete\",\"blocked\",\"not_achieved\",\"advisory\",\"yield\"],\"type\":\"string\"},\"verification\":{\"additionalProperties\":false,\"description\":\"Required when status is complete or not_achieved. A verifier-as-judge receipt from a concrete check, such as Run action=\\\"verifiers\\\" or an equivalent project-specific gate.\",\"properties\":{\"check\":{\"description\":\"The verifier/check that passed.\",\"type\":\"string\"},\"gaps\":{\"description\":\"Concrete remaining gaps. Required for critical not_achieved reviews; order and duplicate wording do not affect the stall fingerprint.\",\"items\":{\"type\":\"string\"},\"type\":\"array\"},\"role\":{\"description\":\"Critical reviews may satisfy the judged completion contract. Advisory reviews are fail-ope"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"update_goal"},"ordinal":10,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Suggest"}},"cache_control_type":{"status":"known","value":{"truncated":false,"value":"ephemeral"}},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"WritesFiles"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Write content to a file. Creates the file if it does not exist, overwrites it if it does, and creates parent directories automatically."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"content\":{\"description\":\"Content to write to the file.\",\"type\":\"string\"},\"path\":{\"description\":\"Path to the file to write (relative or absolute).\",\"type\":\"string\"}},\"required\":[\"content\",\"path\"],\"type\":\"object\"}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"status":"known","value":true},"name":{"truncated":false,"value":"write"},"ordinal":11,"provenance":{"status":"known","value":"builtin"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"reason":"request field absent","status":"unknown"},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Run a JavaScript program that composes tool calls with `await tools.call(name, args)` and returns a bounded JSON result. Prefer it whenever you would make several dependent or repetitive calls, MCP and plugin tools included: intermediate results stay in the program and only what you return reaches the conversation. Every nested call passes the same permission checks as a direct call; a call that needs approval pauses the program until the user decides, and a denied or refused call throws inside the program "},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"JavaScript program. The return value (or thrown error) becomes the result; use tools.call(name, argsObject) for tool calls.\"}},\"required\":[\"code\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"execute_tools"},"ordinal":12,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"execute_tools_20260918"}},"visibility":"active"},{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"reason":"tool is not in the registry","status":"unknown"},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"reason":"tool is not in the registry","status":"unknown"},"defer_loading":{"status":"known","value":false},"description":{"truncated":false,"value":"Search deferred tool definitions and return matching tool references."},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\",\"description\":\"Search query for tool discovery.\"},\"match\":{\"type\":\"string\",\"enum\":[\"bm25\",\"regex\"],\"default\":\"bm25\",\"description\":\"Matching algorithm: bm25 for natural-language matching, regex for a regular expression over tool names/descriptions/schema.\"},\"max_results\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":8,\"default\":8,\"description\":\"Maximum number of matching tool references to return.\"}},\"required\":[\"query\"]}"},"mcp_server":{"reason":"the MCP pool did not attribute this tool name","status":"unknown"},"model_visible":{"reason":"tool is not in the registry","status":"unknown"},"name":{"truncated":false,"value":"tool_search"},"ordinal":13,"provenance":{"status":"known","value":"synthetic"},"strict":{"reason":"request field absent","status":"unknown"},"tool_type":{"status":"known","value":{"truncated":false,"value":"tool_search_20251119"}},"visibility":"active"}],"tools_field_present":true,"turn_id":{"truncated":false,"value":""},"unavailable_for_this_request":["provider_wire_payload"]}} -{"error":"Model not exist.","event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"failed","tool_catalog":"","usage":{"input_tokens":120,"output_tokens":0}} +{"error":"Model not exist.","event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"failed","tool_catalog":"","turn_id":"","usage":{"input_tokens":120,"output_tokens":0}} {"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b622df3627..f258ff0f3d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -198,6 +198,28 @@ rather than a traced write path. - **`crates/release`** / **`crates/build-support`** - Release checks and build plumbing. +### Embedded turn terminal ownership + +`TurnComplete` is the authoritative terminal event; `Error` remains advisory. +The engine and protocol projection preserve the operation's original identity: + +- Model turns echo their actual `TurnStarted.turn_id` and the submitted + `submission_id` on completion, failure and interruption. Runtime self-starts + carry no host submission token. +- Send/edit rejection before `TurnStarted` emits a failed terminal with the + rejected `submission_id` and no `turn_id`. It does not claim a previous turn. +- Manual compaction terminals carry only their `compaction_id`, including route + rejection and cancellation. Automatic compaction remains part of its owning + model turn. Purge operations carry none of these identities. + +Wire fields are optional and default to absent for older producers. An embedded +host must match an owned receipt before closing a pending operation; it must +not infer ownership from advisory errors, an absent ID, or whichever request +happens to be busy. Hosts also need their own engine-incarnation fence: these +IDs do not authorize an event from an evicted runtime to mutate its +replacement. This contract adds no model prompt/context contributor and no +second turn loop. + ### LLM Integration - **`client.rs`** - The live HTTP client layer: OpenAI-compatible, Anthropic,