| 1 | // Package event defines the typed event stream the agent emits as it runs a |
| 2 | // turn, and the Sink it emits to. It decouples "what happened" (the model |
| 3 | // produced reasoning, a tool was dispatched, a turn used N tokens) from "how to |
| 4 | // show it" (ANSI scrollback in a terminal, a card in a webview). |
| 5 | // |
| 6 | // The agent depends only on Sink; each frontend implements one. The chat TUI |
| 7 | // renders events to its scrollback; a headless run renders them to plain ANSI |
| 8 | // on stdout; a future GUI/serve transport forwards them to a webview or |
| 9 | // websocket. This replaces the old io.Writer contract, where the agent wrote |
| 10 | // pre-formatted ANSI and the consumer had to re-derive structure by matching |
| 11 | // line prefixes — fragile, and lossy for any frontend richer than a terminal. |
| 12 | package event |
| 13 | |
| 14 | import ( |
| 15 | "encoding/json" |
| 16 | |
| 17 | "reasonix/internal/billing" |
| 18 | "reasonix/internal/evidence" |
| 19 | "reasonix/internal/nilutil" |
| 20 | "reasonix/internal/provider" |
| 21 | ) |
| 22 | |
| 23 | // Kind tags an Event. Read the field(s) documented for that kind. |
| 24 | type Kind int |
| 25 | |
| 26 | const ( |
| 27 | // TurnStarted marks the start of one top-level Run (one user turn). Sinks |
| 28 | // reset any per-turn rendering state on it. Carries no payload. |
| 29 | TurnStarted Kind = iota |
| 30 | // Reasoning is a thinking-mode reasoning delta (Text). Streamed before the |
| 31 | // visible answer; sinks typically render it muted under a "thinking" header. |
| 32 | Reasoning |
| 33 | // Text is an answer-text delta (Text). |
| 34 | Text |
| 35 | // Message marks the assistant turn's text as complete: Text holds the full |
| 36 | // answer and Reasoning the full chain-of-thought (both already streamed via |
| 37 | // the deltas above). A sink may use it to re-render the streamed raw text as |
| 38 | // styled markdown; a plain sink can ignore it. |
| 39 | Message |
| 40 | // ToolDispatch announces a tool call is about to run (Tool: ID/Name/Args/ReadOnly). |
| 41 | ToolDispatch |
| 42 | // ToolResult reports a finished tool call (Tool: Output/Err/Truncated set). |
| 43 | ToolResult |
| 44 | // Usage carries per-turn token telemetry (Usage; Pricing optional, for cost). |
| 45 | Usage |
| 46 | // Notice is an out-of-band message — a warning, truncation, block, or |
| 47 | // compaction notice (Level + Text). |
| 48 | Notice |
| 49 | // Phase marks a coordinator boundary, e.g. planner→executor handoff (Text = |
| 50 | // label such as "deepseek · planning"). |
| 51 | Phase |
| 52 | // ApprovalRequest asks the frontend to approve a pending tool call |
| 53 | // (Approval: ID/Tool/Subject). The run blocks until the controller's |
| 54 | // Approve(ID, …) resolves it; a frontend shows a prompt and answers. |
| 55 | ApprovalRequest |
| 56 | // AskRequest asks the frontend to put one or more structured multiple-choice |
| 57 | // questions to the user (Ask: ID + Questions). The run blocks until the |
| 58 | // controller's AnswerQuestion(ID, …) resolves it. Powers the `ask` tool. |
| 59 | AskRequest |
| 60 | // TurnDone marks the end of one top-level Run (Err non-nil on failure; |
| 61 | // nil also for a user cancellation, which is not an error). Always the |
| 62 | // last event of a turn. |
| 63 | TurnDone |
| 64 | // CompactionStarted marks the start of a context-compaction pass (Compaction |
| 65 | // payload: Trigger). A frontend shows a "compacting…" placeholder while the |
| 66 | // summarizer runs; CompactionDone replaces it. Mirrors ToolDispatch/ToolResult. |
| 67 | CompactionStarted |
| 68 | // CompactionDone reports a finished compaction pass (Compaction payload: |
| 69 | // Trigger/Messages/Summary/Archive). An aborted pass emits this with an empty |
| 70 | // Summary so the placeholder still resolves. Replaces the older plain Notice |
| 71 | // so a sink can render a distinct, expandable card. |
| 72 | CompactionDone |
| 73 | // ToolProgress streams a chunk of a still-running tool's combined output |
| 74 | // (Tool: ID + Output = the new chunk). Emitted between ToolDispatch and |
| 75 | // ToolResult for long tools like bash so a frontend can show live progress. |
| 76 | // Appended last to keep the Kind values before it wire-stable. |
| 77 | ToolProgress |
| 78 | // MCPSurfaceReady fires once per server when its background-loaded surface |
| 79 | // (prompts or resources) finishes after startup. Lets UIs refresh /mcp |
| 80 | // status without polling. Text carries "<server>: <surface> ready (<count> |
| 81 | // items)". Appended last to keep the Kind values before it wire-stable. |
| 82 | MCPSurfaceReady |
| 83 | // Retrying fires before each backoff sleep while the provider re-attempts the |
| 84 | // connection+header phase after a transient failure (RetryAttempt of RetryMax). |
| 85 | // A frontend shows a transient "retrying (n/m)" indicator that the next stream |
| 86 | // event — or TurnDone — clears. Appended last to keep the Kind values before |
| 87 | // it wire-stable. |
| 88 | Retrying |
| 89 | // Steer fires when a mid-turn steer message is consumed from the queue and |
| 90 | // injected as a user message. Text carries the raw steer content (without the |
| 91 | // wrapper prefix), so a frontend can display it to the user as confirmation. |
| 92 | // Frontends use Steer to know a queued message has been delivered. |
| 93 | Steer |
| 94 | // GuardianAssessment reports the outcome of a guardian sub-agent safety review. |
| 95 | // Carries GuardianResult payload (Outcome, RiskLevel, Rationale, etc.). |
| 96 | GuardianAssessment |
| 97 | // ExtensionSurface carries a structured UI surface published by an extension |
| 98 | // sidecar (Extension payload with one of the Card/Form/Notification |
| 99 | // sub-structs set). Appended last to keep the Kind values before it |
| 100 | // wire-stable. |
| 101 | ExtensionSurface |
| 102 | // ExtensionStatus carries a one-line status contribution published by an |
| 103 | // extension sidecar (Extension payload with Status set). Appended last to |
| 104 | // keep the Kind values before it wire-stable. |
| 105 | ExtensionStatus |
| 106 | // StreamAttempt marks the local lifecycle of one sampling attempt within a |
| 107 | // model round (StreamAttempt payload: begin | discard | commit). IDs are |
| 108 | // local transcript identities — persisted in the event ledger, never sent |
| 109 | // to the model. Appended last to |
| 110 | // keep earlier Kind values wire-stable; older clients ignore unknown kinds. |
| 111 | StreamAttempt |
| 112 | // ContextMaintenance reports a free tool-result maintenance or a durable |
| 113 | // blocked/noop outcome. It is separate from CompactionStarted/Done so UIs do |
| 114 | // not render a paid-summary card for a cache-preserving view update. |
| 115 | ContextMaintenanceEvent |
| 116 | // WorkspaceChanged reports a debounced host-side workspace mutation. |
| 117 | WorkspaceChanged |
| 118 | // TurnPhase reports a host-side work phase for the active turn (working | |
| 119 | // checking | verifying | reviewing). Content-free; Text holds the phase. |
| 120 | TurnPhase |
| 121 | // CompletionSummary reports a content-free end-of-turn quality summary for |
| 122 | // role-setting strategies (preset, verdict, check counts, review status). |
| 123 | CompletionSummary |
| 124 | // ToolResultPreview reports that a tool has finished locally before its |
| 125 | // provider-ordered ToolResult can be emitted. Upsert-capable frontends may |
| 126 | // render the successful state early; append-only consumers should ignore it. |
| 127 | // The later ToolResult remains the call's only terminal event. |
| 128 | ToolResultPreview |
| 129 | // TurnStatusChanged is a content-free lifecycle transition such as |
| 130 | // waiting_user, cancelling, or returning to in_progress after an answer. |
| 131 | TurnStatusChanged |
| 132 | // PromptAnswered records that a durable Ask/approval item was answered and the same turn resumed; ItemID carries the stable prompt id and answer content remains in its purpose-built decision receipt. |
| 133 | PromptAnswered |
| 134 | // MCPInteractionRequest carries a server-initiated MCP elicitation (form |
| 135 | // or URL) for the frontend to answer via the MCP interaction resolve call. |
| 136 | // Appended last to keep the Kind values before it wire-stable. |
| 137 | MCPInteractionRequest |
| 138 | // SessionChanged is a content-free Serve routing barrier for all-session clients. |
| 139 | SessionChanged |
| 140 | // ReadStatus upserts one logical read's delivery state instead of per page. |
| 141 | ReadStatus |
| 142 | ToolStarted // Persisted after policy/validation and before execution. |
| 143 | // UserMessage binds an admitted user bubble to its persisted message ID. |
| 144 | // Text is display text; provider-only framing must never be emitted here. |
| 145 | UserMessage |
| 146 | // KindCount is a sentinel one past the last real Kind. New event kinds must |
| 147 | // be inserted above it so completeness tests cover them automatically. |
| 148 | KindCount |
| 149 | ) |
| 150 | |
| 151 | // TurnPhaseName is the machine-readable phase on TurnPhase events. |
| 152 | type TurnPhaseName string |
| 153 | |
| 154 | const ( |
| 155 | TurnPhaseWorking TurnPhaseName = "working" |
| 156 | TurnPhaseChecking TurnPhaseName = "checking" |
| 157 | TurnPhaseVerifying TurnPhaseName = "verifying" |
| 158 | TurnPhaseReviewing TurnPhaseName = "reviewing" |
| 159 | ) |
| 160 | |
| 161 | // CompletionSummaryInfo is the content-free quality summary on CompletionSummary |
| 162 | // events. It never carries user prompts, file contents, command args, or |
| 163 | // reviewer reasoning. |
| 164 | type CompletionSummaryInfo struct { |
| 165 | Preset string // deprecated wire-compat label; pinned to "balanced" |
| 166 | Verdict string // complete | partial | blocked | continue |
| 167 | Mutations int |
| 168 | ChangedFiles int |
| 169 | ChecksPassed int |
| 170 | ChecksFailed int |
| 171 | ChecksSuppressed int |
| 172 | Review string // none | passed | warned | failed | unavailable |
| 173 | GapKinds []string |
| 174 | ConstraintDegraded bool |
| 175 | Floor string // standard | delivery; empty on legacy events |
| 176 | Attention bool // authoritative when Floor is non-empty |
| 177 | } |
| 178 | |
| 179 | // StreamAttemptAction is the lifecycle phase of a local sampling attempt. |
| 180 | type StreamAttemptAction string |
| 181 | |
| 182 | const ( |
| 183 | StreamAttemptBegin StreamAttemptAction = "begin" |
| 184 | StreamAttemptDiscard StreamAttemptAction = "discard" |
| 185 | StreamAttemptCommit StreamAttemptAction = "commit" |
| 186 | ) |
| 187 | |
| 188 | // StreamAttemptInfo carries host-local bookkeeping for one sampling attempt. |
| 189 | // Reason is a fixed enum (connection_reset | premature_eof | idle_timeout). |
| 190 | type StreamAttemptInfo struct { |
| 191 | ID string |
| 192 | Action StreamAttemptAction |
| 193 | Attempt int // 1-based attempt number |
| 194 | Max int // total attempts including the first (typically 6) |
| 195 | Reason string |
| 196 | } |
| 197 | |
| 198 | // Level classifies a Notice so sinks can style or filter it. |
| 199 | type Level int |
| 200 | |
| 201 | const ( |
| 202 | LevelInfo Level = iota |
| 203 | LevelWarn |
| 204 | ) |
| 205 | |
| 206 | // NoticeAudience separates a notice's recipient from its severity. The empty |
| 207 | // default preserves the existing contract: ordinary notices are eligible for |
| 208 | // every frontend. Operator notices describe local runtime maintenance and must |
| 209 | // not be forwarded as end-user chat messages. Local frontends and diagnostics |
| 210 | // remain free to surface or quietly record them under their own policy. |
| 211 | type NoticeAudience string |
| 212 | |
| 213 | const ( |
| 214 | NoticeAudienceDefault NoticeAudience = "" |
| 215 | NoticeAudienceOperator NoticeAudience = "operator" |
| 216 | ) |
| 217 | |
| 218 | // Profile carries the subagent model/effort resolved for this call. |
| 219 | type Profile struct { |
| 220 | Model string |
| 221 | Effort string |
| 222 | } |
| 223 | |
| 224 | // Tool describes a tool call for ToolDispatch / ToolResult events. On dispatch |
| 225 | // ID/Name/Args/ReadOnly and optional preview metadata are set; on result |
| 226 | // Output/Err/Truncated are filled in. Args is the raw JSON arguments — a sink |
| 227 | // compacts it for display. |
| 228 | type Tool struct { |
| 229 | RunState provider.ToolRunState |
| 230 | Diagnostic json.RawMessage `json:"diagnostic,omitempty"` |
| 231 | // Verifying is emitted only once an authorized check actually enters execution. |
| 232 | Verifying bool |
| 233 | ID string |
| 234 | Name string |
| 235 | Args string |
| 236 | // Todos is the complete semantic todo replacement committed with a |
| 237 | // successful todo_write result. TodoWritten distinguishes an empty list |
| 238 | // from an older event with no semantic payload. |
| 239 | Todos []Todo |
| 240 | TodoWritten bool |
| 241 | // ResolvedName/CapabilityID describe the real target behind a stable proxy |
| 242 | // while Name/Args remain the provider-visible call. They are optional local |
| 243 | // display metadata and never enter provider requests. |
| 244 | ResolvedName string |
| 245 | CapabilityID string |
| 246 | Output string // ToolResult: the result text fed to the model |
| 247 | Err string // ToolResult: non-empty when the call failed or was blocked |
| 248 | PresentedFiles []provider.PresentedFile |
| 249 | ReadOnly bool |
| 250 | Truncated bool // ToolResult: Output was head+tailed before display/model |
| 251 | DurationMs int64 // ToolResult: wall-clock execution time in milliseconds |
| 252 | // StartedAt/EndedAt are unix-millisecond execution bounds (ToolResult). |
| 253 | // Zero when the call never ran (dependency-skipped, cancelled, synthetic). |
| 254 | StartedAt int64 |
| 255 | EndedAt int64 |
| 256 | // Partial marks an early ToolDispatch emitted when a call begins (ID/Name set, |
| 257 | // Args still streaming) so a frontend can show the card immediately; a second, |
| 258 | // full ToolDispatch (Partial false, Args set) follows when the call completes. |
| 259 | Partial bool |
| 260 | // ArgChars is the cumulative argument characters received so far for a |
| 261 | // Partial dispatch — a liveness signal while a large payload streams. Zero |
| 262 | // on the initial start dispatch and on full dispatches. |
| 263 | ArgChars int |
| 264 | // Refreshed marks a repeated full ToolDispatch for the same ID whose file |
| 265 | // preview or resolved proxy metadata changed after the initial dispatch. |
| 266 | // Frontends that can upsert by ID should replace the existing card; |
| 267 | // append-only sinks should ignore it to avoid duplicate tool cards. |
| 268 | Refreshed bool |
| 269 | // ParentID, when set, is the ID of the tool call that spawned this one — a |
| 270 | // sub-agent's calls carry the parent `task` call's ID so a frontend can nest |
| 271 | // them under it. Empty for top-level calls. |
| 272 | ParentID string |
| 273 | // AttemptID is the host-local stream_attempt id that produced a speculative |
| 274 | // partial ToolDispatch. Empty for committed/full dispatches and for nested |
| 275 | // sub-agent tools. Frontends must only journal partial events whose |
| 276 | // AttemptID matches the active stream_attempt begin. |
| 277 | AttemptID string |
| 278 | FileDiff |
| 279 | Profile *Profile // ToolDispatch: subagent model/effort (set for task/skill calls) |
| 280 | // Subagent outcome metadata is host/UI-only and never enters provider requests. |
| 281 | SubagentRef string |
| 282 | SubagentStatus string |
| 283 | SubagentErrorCode string |
| 284 | SubagentRetryable bool |
| 285 | // Execution is optional local shell metadata (ToolResult). Never sent to |
| 286 | // model providers; omitempty keeps old wire readers compatible. |
| 287 | Execution *ShellExecution |
| 288 | // Workspace mutation metadata is host-only and is omitted from eventwire. |
| 289 | WorkspaceMutation bool |
| 290 | WorkspacePaths []string |
| 291 | WorkspaceAllPaths bool |
| 292 | } |
| 293 | |
| 294 | // ShellExecution mirrors tool.ShellExecution for event sinks without importing |
| 295 | // the tool package (event is a lower-level dependency of tool consumers). |
| 296 | type ShellExecution struct { |
| 297 | Kind string `json:"kind,omitempty"` |
| 298 | Shell string `json:"shell,omitempty"` |
| 299 | ShellVersion string `json:"shellVersion,omitempty"` |
| 300 | Platform string `json:"platform,omitempty"` |
| 301 | SupportsAndAnd bool `json:"supportsAndAnd"` |
| 302 | State string `json:"state,omitempty"` |
| 303 | FailurePhase string `json:"failurePhase,omitempty"` |
| 304 | ExitCode *int `json:"exitCode,omitempty"` |
| 305 | OutputTail string `json:"outputTail,omitempty"` |
| 306 | MutationRisk string `json:"mutationRisk,omitempty"` |
| 307 | Verification string `json:"verification,omitempty"` |
| 308 | DurationMs int64 `json:"durationMs,omitempty"` |
| 309 | } |
| 310 | |
| 311 | // FileDiff is a previewed change carried on a writer tool's full ToolDispatch |
| 312 | // and on its ApprovalRequest, so a frontend can render +/- lines before the |
| 313 | // call runs. Diff is the unified diff (empty for read-only tools, binary files, |
| 314 | // or no-op changes); Added/Removed are its line tallies. |
| 315 | type FileDiff struct { |
| 316 | Diff string |
| 317 | Added int |
| 318 | Removed int |
| 319 | } |
| 320 | |
| 321 | // AskOption is one choice the user can pick for an AskQuestion. |
| 322 | type AskOption struct { |
| 323 | Label string |
| 324 | Description string // optional one-line explanation shown under the label |
| 325 | } |
| 326 | |
| 327 | // AskQuestion is one structured question the `ask` tool puts to the user. |
| 328 | type AskQuestion struct { |
| 329 | ID string // stable per-question id, so answers correlate back |
| 330 | Header string // short label (the tab title) |
| 331 | Prompt string // the question text |
| 332 | Options []AskOption |
| 333 | Multi bool // allow selecting more than one option |
| 334 | } |
| 335 | |
| 336 | // Ask carries an AskRequest: a batch of questions and the ID that correlates the |
| 337 | // controller's AnswerQuestion(ID, …) reply. |
| 338 | type Ask struct { |
| 339 | ID string |
| 340 | Questions []AskQuestion |
| 341 | TurnID string |
| 342 | } |
| 343 | |
| 344 | // MCPInteraction carries one MCPInteractionRequest: a server-initiated |
| 345 | // elicitation the frontend must answer with accept/decline/cancel. Mode is |
| 346 | // "form" (RequestedSchema is a flat primitive JSON schema) or "url" (URL is a |
| 347 | // credential-free HTTP(S) target the user opens explicitly). |
| 348 | type MCPInteraction struct { |
| 349 | ID string |
| 350 | Server string |
| 351 | Mode string |
| 352 | Message string |
| 353 | RequestedSchema json.RawMessage |
| 354 | URL string |
| 355 | ElicitationID string |
| 356 | TurnID string |
| 357 | } |
| 358 | |
| 359 | // Compaction carries a context-compaction pass for the CompactionStarted / |
| 360 | // CompactionDone events. On CompactionStarted only Trigger is set. On |
| 361 | // CompactionDone, Messages/Summary/Archive are filled in (an aborted pass leaves |
| 362 | // Summary empty). Trigger is "auto" (the prompt reached the window threshold) or |
| 363 | // "manual" (the user ran /compact). |
| 364 | type Compaction struct { |
| 365 | Trigger string // "auto" | "manual" |
| 366 | Messages int // Done: how many messages were folded into the summary |
| 367 | Summary string // Done: the briefing the agent keeps relying on |
| 368 | Archive string // Done: path the dropped originals were archived to ("" if none) |
| 369 | } |
| 370 | |
| 371 | // ContextMaintenance is the typed wire-safe receipt for snip/prune/noop/ |
| 372 | // blocked operations. Transcript bytes are represented by hashes and counts. |
| 373 | type ContextMaintenance struct { |
| 374 | Status string `json:"status,omitempty"` |
| 375 | Action string `json:"action,omitempty"` |
| 376 | Trigger string `json:"trigger,omitempty"` |
| 377 | OperationID string `json:"operationId,omitempty"` |
| 378 | InputTokens int `json:"inputTokens,omitempty"` |
| 379 | ResultTokens int `json:"resultTokens,omitempty"` |
| 380 | SavedTokens int `json:"savedTokens,omitempty"` |
| 381 | AffectedToolResults int `json:"affectedToolResults,omitempty"` |
| 382 | ProjectionVersion uint64 `json:"projectionVersion,omitempty"` |
| 383 | CacheBreak bool `json:"cacheBreak,omitempty"` |
| 384 | Reason string `json:"reason,omitempty"` |
| 385 | } |
| 386 | |
| 387 | // GuardianResult carries the outcome of a guardian sub-agent safety review. |
| 388 | // Emitted with Kind=GuardianAssessment after each review completes. |
| 389 | type GuardianResult struct { |
| 390 | ID string // unique review id |
| 391 | Tool string // tool being reviewed (e.g. "bash") |
| 392 | Subject string // call subject (e.g. "rm -rf /tmp/build") |
| 393 | Outcome string // "allow" | "deny" |
| 394 | RiskLevel string // "low" | "medium" | "high" | "critical" |
| 395 | UserAuthorization string // "unknown" | "low" | "medium" | "high" |
| 396 | Rationale string // one-sentence reason |
| 397 | DurationMs int64 // wall-clock review time |
| 398 | Usage *provider.Usage // guardian review token telemetry |
| 399 | Pricing *provider.Pricing // for cost display (nil = omit cost) |
| 400 | } |
| 401 | |
| 402 | // AskAnswer is the user's reply to one AskQuestion: the chosen option label(s) |
| 403 | // (a free-typed answer is carried as a single Selected entry). |
| 404 | type AskAnswer struct { |
| 405 | QuestionID string |
| 406 | Selected []string |
| 407 | } |
| 408 | |
| 409 | // FinalReadiness carries machine-readable recovery requirements on TurnDone. |
| 410 | // Missing values are stable category ids; user-facing detail stays localized in |
| 411 | // the frontend instead of scraping the diagnostic error string. |
| 412 | type FinalReadiness struct { |
| 413 | Attempts int `json:"attempts,omitempty"` |
| 414 | Missing []string `json:"missing,omitempty"` |
| 415 | } |
| 416 | |
| 417 | const ( |
| 418 | UsageSourceExecutor = "executor" |
| 419 | UsageSourcePlanner = "planner" |
| 420 | UsageSourceSubagent = "subagent" |
| 421 | UsageSourceCompaction = "compaction" |
| 422 | UsageSourceClassifier = "classifier" |
| 423 | UsageSourceTitle = "title" |
| 424 | UsageSourceCapabilityRouter = "capability-router" |
| 425 | UsageSourceRecoveryReviewer = "recovery-reviewer" |
| 426 | UsageSourceGoalEvaluator = "goal-evaluator" |
| 427 | ) |
| 428 | |
| 429 | // Event is one increment in a turn's event stream. Read the field(s) documented |
| 430 | // for Kind; the others are zero. |
| 431 | type Event struct { |
| 432 | Kind Kind |
| 433 | MessageID string // local identity shared by streaming and persisted messages |
| 434 | AttemptID string // owning sampling attempt; never provider-visible |
| 435 | SessionID string // durable display routing, stamped after append |
| 436 | RuntimeEpoch string // originating controller incarnation |
| 437 | SubmissionID string // exact optimistic submit correlation |
| 438 | PromptKind string // interactive prompt kind for lifecycle events |
| 439 | InteractionState string // PromptAnswered: answered | rejected | cancelled | unavailable |
| 440 | DomainKind string // host-internal state event committed atomically with this lifecycle event |
| 441 | DomainPayload json.RawMessage // host-internal payload for DomainKind |
| 442 | TurnID string // stable id of the owning top-level turn |
| 443 | Sequence uint64 // monotonic session-local event sequence |
| 444 | Status TurnStatus // lifecycle state after this event |
| 445 | Text string // Reasoning / Text / Message / Notice / Phase |
| 446 | ModelRef string // Usage: canonical "provider/model" ref that produced this usage |
| 447 | Detail string // Notice: optional diagnostic text for expandable details |
| 448 | Code string // Notice: stable id for frontend localization; empty = unmapped |
| 449 | Reasoning string // Message: the full reasoning chain |
| 450 | MemoryCitations []provider.MemoryCitation // Message: local memory references displayed by rich frontends |
| 451 | Tool Tool // ToolDispatch / ToolResult |
| 452 | Usage *provider.Usage // Usage |
| 453 | Pricing *provider.Pricing // Usage: rate card for quote middleware (nil = omit cost) |
| 454 | CostQuote *billing.CostQuote // Usage: host-side quote; sinks must not reprice |
| 455 | Source string // optional display/event source (executor, planner, subagent, ...) |
| 456 | UsageSource string // Usage: billable call source; empty means executor for compatibility |
| 457 | CacheDiagnostics *CacheDiagnostics // Usage: cache-churn attribution (nil = N/A) |
| 458 | // SessionHit/SessionMiss carry cumulative cache tokens across the whole |
| 459 | // session (Usage events only), so a frontend can show the aggregate hit-rate |
| 460 | // — which doesn't crater on a short turn or after compaction — alongside |
| 461 | // Usage's single-turn numbers. |
| 462 | SessionHit int // Usage: cumulative cache-hit prompt tokens this session |
| 463 | SessionMiss int // Usage: cumulative cache-miss prompt tokens this session |
| 464 | Level Level // Notice |
| 465 | Audience NoticeAudience // Notice: empty = ordinary frontend delivery; operator = no end-user chat forwarding |
| 466 | Approval Approval // ApprovalRequest |
| 467 | Ask Ask // AskRequest |
| 468 | MCPInteraction MCPInteraction // MCPInteractionRequest |
| 469 | Extension *ExtensionSurfacePayload // ExtensionSurface / ExtensionStatus (nil for every other kind) |
| 470 | Err error // TurnDone: non-nil on failure |
| 471 | Cancelled bool // TurnDone: Cancel was requested while the turn was active |
| 472 | Outcome string // TurnDone: optional machine-readable recoverable outcome |
| 473 | Readiness *FinalReadiness // TurnDone: structured final-readiness recovery state |
| 474 | ProtocolRecovery *provider.ProtocolRecoveryAction |
| 475 | Diagnostic *provider.FailureDiagnostic |
| 476 | RecoveryCheckpoint bool // local durable recovery checkpoint, not a notice |
| 477 | Receipt *CompletionReceipt // TurnDone: what the host verified, and what it could not |
| 478 | CheckpointTurn *int // TurnDone: authoritative checkpoint for this turn's visible user message |
| 479 | Compaction Compaction // Compaction |
| 480 | Maintenance *ContextMaintenance // ContextMaintenanceEvent |
| 481 | Guardian GuardianResult |
| 482 | DecisionReceipt *provider.DecisionReceipt // Notice: durable user decision receipt |
| 483 | WriteIntent bool // local write-ahead checkpoint, not a user notice |
| 484 | Recovery *RecoveryStatus // optional local recovery details |
| 485 | RetryAttempt int // Retrying: 1-based attempt about to be made |
| 486 | RetryMax int // Retrying: total attempts before giving up |
| 487 | RetryScope RetryScope // Retrying: optional "headers" | "stream"; empty for older emitters |
| 488 | StreamAttempt StreamAttemptInfo // StreamAttempt lifecycle |
| 489 | ReadStatus *ReadStatusPayload // ReadStatus: one logical read's delivery state |
| 490 | ReadPause *provider.ReadPause // TurnDone: durable display-only pause receipt |
| 491 | ReadCompletion *provider.ReadCompletion // TurnDone: accepted partial coverage, display-only |
| 492 | ItemID string // correlates durable inbox events |
| 493 | SessionPath string // routes Serve frames |
| 494 | SessionReset bool // SessionChanged came from /new or /clear, not resume/recovery |
| 495 | Workspace *WorkspaceChangedPayload // WorkspaceChanged (host-local) |
| 496 | // PhaseName is set on TurnPhase events (working|checking|verifying|reviewing). |
| 497 | PhaseName TurnPhaseName |
| 498 | // Completion is set on CompletionSummary events. |
| 499 | Completion *CompletionSummaryInfo |
| 500 | // CommittedMessage is the exact provider transcript record paired with a |
| 501 | // terminal tool result. It is host-internal and omitted from frontend wire |
| 502 | // payloads; the session event store commits it atomically with tool/result |
| 503 | // and any todo/write state transition. |
| 504 | CommittedMessage *provider.Message |
| 505 | } |
| 506 | |
| 507 | type WorkspaceWatchState string |
| 508 | |
| 509 | const ( |
| 510 | WorkspaceWatchActive WorkspaceWatchState = "active" |
| 511 | WorkspaceWatchDegraded WorkspaceWatchState = "degraded" |
| 512 | WorkspaceWatchUnavailable WorkspaceWatchState = "unavailable" |
| 513 | ) |
| 514 | |
| 515 | type WorkspaceRevision struct { |
| 516 | Content uint64 `json:"content"` |
| 517 | Tree uint64 `json:"tree"` |
| 518 | WorkingTree uint64 `json:"workingTree"` |
| 519 | GitMeta uint64 `json:"gitMeta"` |
| 520 | Session uint64 `json:"session"` |
| 521 | } |
| 522 | |
| 523 | type WorkspacePathChange struct { |
| 524 | Path string `json:"path"` |
| 525 | OldPath string `json:"oldPath,omitempty"` |
| 526 | Op string `json:"op"` |
| 527 | } |
| 528 | |
| 529 | type WorkspaceChangedPayload struct { |
| 530 | Revisions WorkspaceRevision |
| 531 | Changes []WorkspacePathChange |
| 532 | AllPaths bool |
| 533 | Source string |
| 534 | WatchState WorkspaceWatchState |
| 535 | } |
| 536 | |
| 537 | // ReadinessAuditSink is an optional sink capability. Sinks that do not care |
| 538 | // about readiness audit receipts can implement only Sink and will ignore them. |
| 539 | type ReadinessAuditSink interface { |
| 540 | RecordReadinessAudit(evidence.ReadinessAudit) |
| 541 | } |
| 542 | |
| 543 | // AnchorSafetyAudit is a content-free shadow decision for an anchor-based |
| 544 | // writer. It contains only bounded enums/counts; paths, anchors, source text, |
| 545 | // and digests never leave the host-side observation ledger. |
| 546 | type AnchorSafetyAudit struct { |
| 547 | Mode string |
| 548 | TaskMode string |
| 549 | RangeLines int |
| 550 | ObservationAge int |
| 551 | LegacyAllowed bool |
| 552 | ShadowAllowed bool |
| 553 | Reason string |
| 554 | SameBatchReadRejected bool |
| 555 | } |
| 556 | |
| 557 | type AnchorSafetyAuditSink interface { |
| 558 | RecordAnchorSafetyAudit(AnchorSafetyAudit) |
| 559 | } |
| 560 | |
| 561 | func RecordAnchorSafetyAudit(s Sink, a AnchorSafetyAudit) { |
| 562 | if nilutil.IsNil(s) { |
| 563 | return |
| 564 | } |
| 565 | if as, ok := s.(AnchorSafetyAuditSink); ok { |
| 566 | as.RecordAnchorSafetyAudit(a) |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | // TurnCompletionSink is an optional sink capability for synchronous controller |
| 571 | // entry points that do not publish a TurnDone UI event. It keeps accounting |
| 572 | // independent from frontend event lifecycles without synthesizing an event that |
| 573 | // transports may mistake for an interactive completion. |
| 574 | type TurnCompletionSink interface { |
| 575 | RecordTurnCompletion() |
| 576 | } |
| 577 | |
| 578 | // RecordTurnCompletion records one successfully admitted top-level controller |
| 579 | // run on sinks that opt into completion accounting. |
| 580 | func RecordTurnCompletion(s Sink) { |
| 581 | if nilutil.IsNil(s) { |
| 582 | return |
| 583 | } |
| 584 | if ts, ok := s.(TurnCompletionSink); ok { |
| 585 | ts.RecordTurnCompletion() |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | // RecordReadinessAudit forwards a readiness audit receipt to sinks that opt in. |
| 590 | func RecordReadinessAudit(s Sink, a evidence.ReadinessAudit) { |
| 591 | if nilutil.IsNil(s) { |
| 592 | return |
| 593 | } |
| 594 | if rs, ok := s.(ReadinessAuditSink); ok { |
| 595 | rs.RecordReadinessAudit(a) |
| 596 | } |
| 597 | } |
| 598 | |
| 599 | // ProtocolRecoveryKind is a content-free internal observation about a provider |
| 600 | // protocol repair. It is deliberately separate from Event/Notice so recovery |
| 601 | // stays invisible in chat transcripts and frontends do not need to understand |
| 602 | // provider implementation details. |
| 603 | type ProtocolRecoveryKind string |
| 604 | |
| 605 | const ( |
| 606 | ProtocolRecoveryMissingReasoningDetected ProtocolRecoveryKind = "missing_reasoning_detected" |
| 607 | ProtocolRecoveryMissingReasoningRetryAttempted ProtocolRecoveryKind = "missing_reasoning_retry_attempted" |
| 608 | ProtocolRecoveryMissingReasoningRetryRecovered ProtocolRecoveryKind = "missing_reasoning_retry_recovered" |
| 609 | ProtocolRecoveryMissingReasoningRetryReplaced ProtocolRecoveryKind = "missing_reasoning_retry_replaced_response" |
| 610 | ProtocolRecoveryMissingReasoningRetrySuppressed ProtocolRecoveryKind = "missing_reasoning_retry_suppressed" |
| 611 | ProtocolRecoveryMissingReasoningFallback ProtocolRecoveryKind = "missing_reasoning_fallback_used" |
| 612 | ProtocolRecoveryReasoningOverflowDetected ProtocolRecoveryKind = "reasoning_overflow_detected" |
| 613 | ProtocolRecoveryClientToolRejected ProtocolRecoveryKind = "client_tool_rejected_unreplayable_reasoning" |
| 614 | ProtocolRecoveryServerSearchSalvaged ProtocolRecoveryKind = "server_search_history_salvaged" |
| 615 | ProtocolRecoveryHistoryRepaired ProtocolRecoveryKind = "unreplayable_history_repaired" |
| 616 | ProtocolRecoveryReasoningReplay400Detected ProtocolRecoveryKind = "reasoning_replay_400_detected" |
| 617 | ProtocolRecoveryReasoningReplay400Recovered ProtocolRecoveryKind = "reasoning_replay_400_recovered" |
| 618 | ) |
| 619 | |
| 620 | type ProtocolRecoveryAudit struct { |
| 621 | Kind ProtocolRecoveryKind |
| 622 | } |
| 623 | |
| 624 | // ContractShadowAudit is the shadow task-contract's end-of-turn summary: |
| 625 | // counts and enums only, never requirement text. Shadow means observed, not |
| 626 | // enforced — the old control logic still decides behavior. |
| 627 | type ContractShadowAudit struct { |
| 628 | Intent string |
| 629 | Requirements int |
| 630 | RequirementsSatisfied int |
| 631 | Checks int |
| 632 | ChecksSatisfied int |
| 633 | Epoch uint64 |
| 634 | Verdict string |
| 635 | Complete bool |
| 636 | ReadyToFinalize bool |
| 637 | } |
| 638 | |
| 639 | // ContractShadowAuditSink is an optional sink capability; implementations |
| 640 | // must keep it content-free, like every other audit channel. |
| 641 | type ContractShadowAuditSink interface { |
| 642 | RecordContractShadow(ContractShadowAudit) |
| 643 | } |
| 644 | |
| 645 | // RecordContractShadow forwards the shadow contract summary only to sinks |
| 646 | // that explicitly opt in. Ordinary UI sinks receive nothing. |
| 647 | func RecordContractShadow(s Sink, a ContractShadowAudit) { |
| 648 | if nilutil.IsNil(s) { |
| 649 | return |
| 650 | } |
| 651 | if cs, ok := s.(ContractShadowAuditSink); ok { |
| 652 | cs.RecordContractShadow(a) |
| 653 | } |
| 654 | } |
| 655 | |
| 656 | // CompletionReportAudit is the host-authored completion report's end-of-turn |
| 657 | // summary: counts, enums, and gap kinds only, never paths or command text. |
| 658 | // The gap counters carry the point — what the turn left unproven. |
| 659 | type CompletionReportAudit struct { |
| 660 | Verdict string |
| 661 | Risk string |
| 662 | Criteria int |
| 663 | CriteriaSatisfied int |
| 664 | Changes int |
| 665 | ChangesUnreviewed int |
| 666 | Verifications int |
| 667 | VerificationsFailed int |
| 668 | VerificationsStale int |
| 669 | Gaps int |
| 670 | GapKinds []string |
| 671 | // ClaimsVerified counts the turn's own asserted verifications; |
| 672 | // ClaimsUnbacked is how many of them the ledger did not support. |
| 673 | ClaimsVerified int |
| 674 | ClaimsUnbacked int |
| 675 | } |
| 676 | |
| 677 | // CompletionReportAuditSink is an optional sink capability; implementations |
| 678 | // must keep it content-free, like every other audit channel. |
| 679 | type CompletionReportAuditSink interface { |
| 680 | RecordCompletionReport(CompletionReportAudit) |
| 681 | } |
| 682 | |
| 683 | // RecordCompletionReport forwards the completion summary only to sinks that |
| 684 | // explicitly opt in. Ordinary UI sinks receive nothing. |
| 685 | func RecordCompletionReport(s Sink, a CompletionReportAudit) { |
| 686 | if nilutil.IsNil(s) { |
| 687 | return |
| 688 | } |
| 689 | if cs, ok := s.(CompletionReportAuditSink); ok { |
| 690 | cs.RecordCompletionReport(a) |
| 691 | } |
| 692 | } |
| 693 | |
| 694 | // MemoryRecallAudit summarizes one automatic-recall decision: identifiers, |
| 695 | // scores, and budget numbers only — never the query or fact text. |
| 696 | type MemoryRecallAudit struct { |
| 697 | Hits []MemoryRecallHit |
| 698 | UsedChars int |
| 699 | Omitted int |
| 700 | Suppressed string // reason recall stayed silent; "" when hits were injected |
| 701 | // Shadow is the Retrieval V2 ranking (telemetry only, never served). |
| 702 | Shadow []MemoryRecallHit |
| 703 | } |
| 704 | |
| 705 | // MemoryRecallHit is one recalled fact's content-free fingerprint. |
| 706 | type MemoryRecallHit struct { |
| 707 | ID string |
| 708 | Revision int |
| 709 | Scope string |
| 710 | Type string |
| 711 | Freshness string |
| 712 | Score float64 |
| 713 | } |
| 714 | |
| 715 | // MemoryRecallSink is an optional sink capability; implementations must keep |
| 716 | // it content-free, like every other audit channel. |
| 717 | type MemoryRecallSink interface { |
| 718 | RecordMemoryRecall(MemoryRecallAudit) |
| 719 | } |
| 720 | |
| 721 | // RecordMemoryRecall forwards a recall decision only to sinks that explicitly |
| 722 | // opt in. Ordinary UI sinks receive nothing. |
| 723 | func RecordMemoryRecall(s Sink, a MemoryRecallAudit) { |
| 724 | if nilutil.IsNil(s) { |
| 725 | return |
| 726 | } |
| 727 | if mr, ok := s.(MemoryRecallSink); ok { |
| 728 | mr.RecordMemoryRecall(a) |
| 729 | } |
| 730 | } |
| 731 | |
| 732 | // DelegationAdmissionAudit is the shadow admission verdict for one expensive |
| 733 | // delegation call: tool name and enums only, never the query or prompt text. |
| 734 | // Shadow means observed, not enforced — no call is blocked. |
| 735 | type DelegationAdmissionAudit struct { |
| 736 | Tool string |
| 737 | Verdict string // "allow" | "deny" |
| 738 | Reason string // e.g. "local_fix_no_external_need" |
| 739 | Intent string // compatibility field; no longer classified from prompt text |
| 740 | } |
| 741 | |
| 742 | // DelegationAdmissionSink is an optional sink capability; implementations |
| 743 | // must keep it content-free, like every other audit channel. |
| 744 | type DelegationAdmissionSink interface { |
| 745 | RecordDelegationAdmission(DelegationAdmissionAudit) |
| 746 | } |
| 747 | |
| 748 | // RecordDelegationAdmission forwards a shadow admission verdict only to sinks |
| 749 | // that explicitly opt in. Ordinary UI sinks receive nothing. |
| 750 | func RecordDelegationAdmission(s Sink, a DelegationAdmissionAudit) { |
| 751 | if nilutil.IsNil(s) { |
| 752 | return |
| 753 | } |
| 754 | if da, ok := s.(DelegationAdmissionSink); ok { |
| 755 | da.RecordDelegationAdmission(a) |
| 756 | } |
| 757 | } |
| 758 | |
| 759 | // OutcomeProgressSink is an optional sink capability for the shadow outcome |
| 760 | // scorer's per-round samples: counts only, never paths or commands. Shadow |
| 761 | // means observed, not enforced — the novelty guard still decides behavior. |
| 762 | type OutcomeProgressSink interface { |
| 763 | RecordOutcomeProgress(evidence.OutcomeSample) |
| 764 | } |
| 765 | |
| 766 | // RecordOutcomeProgress forwards a shadow outcome sample only to sinks that |
| 767 | // explicitly opt in. Ordinary UI sinks receive nothing. |
| 768 | func RecordOutcomeProgress(s Sink, sample evidence.OutcomeSample) { |
| 769 | if nilutil.IsNil(s) { |
| 770 | return |
| 771 | } |
| 772 | if op, ok := s.(OutcomeProgressSink); ok { |
| 773 | op.RecordOutcomeProgress(sample) |
| 774 | } |
| 775 | } |
| 776 | |
| 777 | // ProtocolRecoveryAuditSink is an optional sink capability. Implementations |
| 778 | // must keep it content-free; prompts, responses, endpoints, model names, and |
| 779 | // tool arguments do not belong in this audit channel. |
| 780 | type ProtocolRecoveryAuditSink interface { |
| 781 | RecordProtocolRecovery(ProtocolRecoveryAudit) |
| 782 | } |
| 783 | |
| 784 | // RecordProtocolRecovery forwards a content-free recovery observation only to |
| 785 | // sinks that explicitly opt in. Ordinary UI sinks receive nothing. |
| 786 | func RecordProtocolRecovery(s Sink, a ProtocolRecoveryAudit) { |
| 787 | if nilutil.IsNil(s) { |
| 788 | return |
| 789 | } |
| 790 | if rs, ok := s.(ProtocolRecoveryAuditSink); ok { |
| 791 | rs.RecordProtocolRecovery(a) |
| 792 | } |
| 793 | } |
| 794 |