Synced from monorepo
Synced from monorepo Changes: - Temporarily disable session share link creation in the TUI - Do not approve plan on empty Enter from the revise prompt - Expose chat product Skills via ACP available_commands_update - Return immediately from a blocking wait on an already-completed ACP task - Split headless pager module for clearer structure - Stop git worktree prune from removing user registrations on resume - Use compaction sampler tokenizer for item token counts - Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE - Cancel all session subagents when the user stops - Let the session persistence actor exit when its session ends - Make fullscreen terminal resize much cheaper on long sessions - Report honestly from kill_task when an ACP task does not exist - Hide /usage for external-auth deployments - Forward the history-load trailer’s computer_reason to the client - Remove ineffective no-op tool reminder - Declare slash-command screen-mode support in one place - Keep settings enum picker on the committed value until Enter - Reap a PTY’s full process tree - Stream tool calls from headless mode over ACP - Bridge gateway task lifecycle to ACP for chat session background tasks - Don’t warn about truncated history on a suppressed replay - Fit full-replace summarizer input and recover on context-length errors - Stop dropping agents over an unrecognized frontmatter color - Add /undo as a slash alias for /rewind - Harden sleep/wake token-refresh paths against forced re-login - Add session/list ACP method - Give each sampling backend its own conversion module - Treat an unenrolled child process as a lint error - Suppress the cancelled marker on send-now wake turns - Stop tearing down Roslyn on every edit, and read C# diagnostics Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
This commit is contained in:
parent
500129c714
commit
dd04f397b1
367 changed files with 29489 additions and 10051 deletions
|
|
@ -91,7 +91,7 @@ impl std::fmt::Debug for EventWriter {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::events::types::{
|
||||
EVENT_SCHEMA_VERSION, Event, SessionRelationship, TurnOutcomeLabel,
|
||||
EVENT_SCHEMA_VERSION, Event, SessionRelationship, ToolOutcome, TurnOutcomeLabel,
|
||||
};
|
||||
|
||||
fn _assert_event_writer_is_send_sync_clone()
|
||||
|
|
@ -116,6 +116,13 @@ mod tests {
|
|||
redirect_kind: None,
|
||||
});
|
||||
writer.emit(Event::FirstToken);
|
||||
writer.emit(Event::ToolCompleted {
|
||||
tool_name: "bash".into(),
|
||||
duration_ms: 1500,
|
||||
outcome: ToolOutcome::Success,
|
||||
tool_call_id: "call_xyz".into(),
|
||||
source: crate::events::types::ToolCompletedSource::Shell,
|
||||
});
|
||||
writer.emit(Event::TurnEnded {
|
||||
outcome: TurnOutcomeLabel::Completed,
|
||||
cancellation_category: None,
|
||||
|
|
@ -124,7 +131,7 @@ mod tests {
|
|||
|
||||
let text = std::fs::read_to_string(dir.path().join("events.jsonl")).unwrap();
|
||||
let lines: Vec<&str> = text.trim().split('\n').collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
assert_eq!(lines.len(), 4);
|
||||
|
||||
let first: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
|
||||
assert_eq!(first["type"], "turn_started");
|
||||
|
|
@ -135,9 +142,19 @@ mod tests {
|
|||
assert_eq!(second["type"], "first_token");
|
||||
|
||||
let third: serde_json::Value = serde_json::from_str(lines[2]).unwrap();
|
||||
assert_eq!(third["type"], "turn_ended");
|
||||
assert_eq!(third["outcome"], "completed");
|
||||
assert!(third.get("cancellation_category").is_none());
|
||||
assert_eq!(third["type"], "tool_completed");
|
||||
assert_eq!(third["tool_name"], "bash");
|
||||
assert_eq!(third["duration_ms"], 1500);
|
||||
assert_eq!(third["tool_call_id"], "call_xyz");
|
||||
assert!(
|
||||
third.get("source").is_none(),
|
||||
"shell ToolCompleted must omit source"
|
||||
);
|
||||
|
||||
let fourth: serde_json::Value = serde_json::from_str(lines[3]).unwrap();
|
||||
assert_eq!(fourth["type"], "turn_ended");
|
||||
assert_eq!(fourth["outcome"], "completed");
|
||||
assert!(fourth.get("cancellation_category").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ pub use log::EventWriter;
|
|||
pub use tracker::EventTracker;
|
||||
pub use types::{
|
||||
CancellationCategory, EVENT_SCHEMA_VERSION, Event, McpConfigServer, McpErrorCategory,
|
||||
PermissionDecision, Phase, SessionRelationship, ToolOutcome, TurnOutcomeLabel,
|
||||
PermissionDecision, Phase, SessionRelationship, ToolCompletedSource, ToolOutcome,
|
||||
TurnOutcomeLabel,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,21 @@ use std::time::Instant;
|
|||
use super::log::EventWriter;
|
||||
use super::types::{CancellationCategory, Event, RedirectKind, TurnOutcomeLabel};
|
||||
|
||||
/// In-flight tool for cancel telemetry. Duration is the dispatch wall already
|
||||
/// measured, so cancel can reuse it instead of re-timing post-flight.
|
||||
#[derive(Debug, Clone)]
|
||||
struct ActiveTool {
|
||||
tool_name: String,
|
||||
tool_call_id: String,
|
||||
dispatch_duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Per-session event state. `!Send` — lives on the session actor.
|
||||
/// Background tasks use `tracker.writer()` to get a `Clone + Send + Sync` handle.
|
||||
pub struct EventTracker {
|
||||
writer: EventWriter,
|
||||
turn_ended_emitted: Cell<bool>,
|
||||
active_tool: RefCell<Option<(String, Instant)>>,
|
||||
active_tool: RefCell<Option<ActiveTool>>,
|
||||
turn_tool_count: Cell<u32>,
|
||||
/// Cross-turn one-shot: the *fatal* user-interrupt cause that cancelled the
|
||||
/// most recent turn (set by the cancel paths), consumed by the *next* real
|
||||
|
|
@ -42,7 +51,7 @@ impl std::fmt::Debug for EventTracker {
|
|||
.field("writer", &self.writer)
|
||||
.field("turn_ended_emitted", &self.turn_ended_emitted.get())
|
||||
.field("turn_tool_count", &self.turn_tool_count.get())
|
||||
.field("active_tool", &active_tool.as_ref().map(|(name, _)| name))
|
||||
.field("active_tool", &*active_tool)
|
||||
.field(
|
||||
"prior_interrupt_category",
|
||||
&self.prior_interrupt_category.get(),
|
||||
|
|
@ -101,12 +110,21 @@ impl EventTracker {
|
|||
});
|
||||
}
|
||||
|
||||
/// Set the active tool for cancellation tracking and return the start instant.
|
||||
pub fn tool_started(&self, tool_name: String) -> Instant {
|
||||
let now = Instant::now();
|
||||
*self.active_tool.borrow_mut() = Some((tool_name, now));
|
||||
self.turn_tool_count.set(self.turn_tool_count.get() + 1);
|
||||
now
|
||||
/// Mark a tool as active for cancellation tracking.
|
||||
///
|
||||
/// `dispatch_duration_ms` is the wall time already measured for this call, so
|
||||
/// a cancel can report it rather than re-measure from post-flight.
|
||||
pub fn tool_started(&self, tool_name: String, tool_call_id: String, dispatch_duration_ms: u64) {
|
||||
let is_new = self.active_tool.borrow().is_none();
|
||||
*self.active_tool.borrow_mut() = Some(ActiveTool {
|
||||
tool_name,
|
||||
tool_call_id,
|
||||
dispatch_duration_ms,
|
||||
});
|
||||
// Re-entry (e.g. after reauth adds retry wall time) only refreshes duration.
|
||||
if is_new {
|
||||
self.turn_tool_count.set(self.turn_tool_count.get() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_count_this_turn(&self) -> u32 {
|
||||
|
|
@ -123,12 +141,17 @@ impl EventTracker {
|
|||
|
||||
/// Cancel in-flight tool and emit `ToolCompleted(cancelled)`.
|
||||
/// Called from `cancel_running_task()` before `turn_ended`.
|
||||
///
|
||||
/// A tool cancelled while still dispatching was never marked active, so it
|
||||
/// gets no `tool_completed` row at all.
|
||||
pub fn cancel_active_tool(&self) {
|
||||
if let Some((tool_name, start)) = self.active_tool.borrow_mut().take() {
|
||||
if let Some(tool) = self.active_tool.borrow_mut().take() {
|
||||
self.emit(Event::ToolCompleted {
|
||||
tool_name,
|
||||
duration_ms: start.elapsed().as_millis() as u64,
|
||||
tool_name: tool.tool_name,
|
||||
duration_ms: tool.dispatch_duration_ms,
|
||||
outcome: super::types::ToolOutcome::Cancelled,
|
||||
tool_call_id: tool.tool_call_id,
|
||||
source: super::types::ToolCompletedSource::Shell,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,18 @@ pub enum Event {
|
|||
},
|
||||
ToolCompleted {
|
||||
tool_name: String,
|
||||
/// Dispatch wall time; a cancel row reuses the duration measured at dispatch.
|
||||
duration_ms: u64,
|
||||
outcome: ToolOutcome,
|
||||
/// Model/ACP tool call id; matches the conversation's `tool_result`.
|
||||
/// Omitted on write when empty.
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
tool_call_id: String,
|
||||
/// Which emitter wrote this row. Shell (default) is omitted on the wire
|
||||
/// and is what package joins should use; workspace rows time the
|
||||
/// hub/proxy hop for the same call.
|
||||
#[serde(skip_serializing_if = "ToolCompletedSource::is_shell")]
|
||||
source: ToolCompletedSource,
|
||||
},
|
||||
PermissionRequested {
|
||||
tool_name: String,
|
||||
|
|
@ -457,6 +467,26 @@ pub enum Event {
|
|||
},
|
||||
}
|
||||
|
||||
/// Who emitted a [`Event::ToolCompleted`] row.
|
||||
///
|
||||
/// Wire: shell is omitted (legacy empty/`source` absent); workspace is
|
||||
/// `"workspace"`.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolCompletedSource {
|
||||
/// Shell dispatch clock — join against these.
|
||||
#[default]
|
||||
Shell,
|
||||
/// Workspace hub/proxy hop clock.
|
||||
Workspace,
|
||||
}
|
||||
|
||||
impl ToolCompletedSource {
|
||||
pub fn is_shell(&self) -> bool {
|
||||
matches!(self, Self::Shell)
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a mid-turn interjection originated. Drives the `source` field on
|
||||
/// [`Event::Interjected`].
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
|
|
@ -654,6 +684,29 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_completed_source_omits_shell_writes_workspace() {
|
||||
let shell = serde_json::to_value(Event::ToolCompleted {
|
||||
tool_name: "bash".into(),
|
||||
duration_ms: 10,
|
||||
outcome: ToolOutcome::Success,
|
||||
tool_call_id: "c1".into(),
|
||||
source: ToolCompletedSource::Shell,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(shell.get("source").is_none());
|
||||
|
||||
let workspace = serde_json::to_value(Event::ToolCompleted {
|
||||
tool_name: "bash".into(),
|
||||
duration_ms: 10,
|
||||
outcome: ToolOutcome::Success,
|
||||
tool_call_id: "c1".into(),
|
||||
source: ToolCompletedSource::Workspace,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(workspace["source"], "workspace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interjected_event_serializes_tag_source_and_count() {
|
||||
let ev = Event::Interjected {
|
||||
|
|
|
|||
Loading…
Reference in a new issue