| 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/evidence" |
| 18 | "reasonix/internal/nilutil" |
| 19 | "reasonix/internal/provider" |
| 20 | ) |
| 21 | |
| 22 | // Kind tags an Event. Read the field(s) documented for that kind. |
| 23 | type Kind int |
| 24 | |
| 25 | const ( |
| 26 | // TurnStarted marks the start of one top-level Run (one user turn). Sinks |
| 27 | // reset any per-turn rendering state on it. Carries no payload. |
| 28 | TurnStarted Kind = iota |
| 29 | // Reasoning is a thinking-mode reasoning delta (Text). Streamed before the |
| 30 | // visible answer; sinks typically render it muted under a "thinking" header. |
| 31 | Reasoning |
| 32 | // Text is an answer-text delta (Text). |
| 33 | Text |
| 34 | // Message marks the assistant turn's text as complete: Text holds the full |
| 35 | // answer and Reasoning the full chain-of-thought (both already streamed via |
| 36 | // the deltas above). A sink may use it to re-render the streamed raw text as |
| 37 | // styled markdown; a plain sink can ignore it. |
| 38 | Message |
| 39 | // ToolDispatch announces a tool call is about to run (Tool: ID/Name/Args/ReadOnly). |
| 40 | ToolDispatch |
| 41 | // ToolResult reports a finished tool call (Tool: Output/Err/Truncated set). |
| 42 | ToolResult |
| 43 | // Usage carries per-turn token telemetry (Usage; Pricing optional, for cost). |
| 44 | Usage |
| 45 | // Notice is an out-of-band message — a warning, truncation, block, or |
| 46 | // compaction notice (Level + Text). |
| 47 | Notice |
| 48 | // Phase marks a coordinator boundary, e.g. planner→executor handoff (Text = |
| 49 | // label such as "deepseek · planning"). |
| 50 | Phase |
| 51 | // ApprovalRequest asks the frontend to approve a pending tool call |
| 52 | // (Approval: ID/Tool/Subject). The run blocks until the controller's |
| 53 | // Approve(ID, …) resolves it; a frontend shows a prompt and answers. |
| 54 | ApprovalRequest |
| 55 | // AskRequest asks the frontend to put one or more structured multiple-choice |
| 56 | // questions to the user (Ask: ID + Questions). The run blocks until the |
| 57 | // controller's AnswerQuestion(ID, …) resolves it. Powers the `ask` tool. |
| 58 | AskRequest |
| 59 | // TurnDone marks the end of one top-level Run (Err non-nil on failure; |
| 60 | // nil also for a user cancellation, which is not an error). Always the |
| 61 | // last event of a turn. |
| 62 | TurnDone |
| 63 | // CompactionStarted marks the start of a context-compaction pass (Compaction |
| 64 | // payload: Trigger). A frontend shows a "compacting…" placeholder while the |
| 65 | // summarizer runs; CompactionDone replaces it. Mirrors ToolDispatch/ToolResult. |
| 66 | CompactionStarted |
| 67 | // CompactionDone reports a finished compaction pass (Compaction payload: |
| 68 | // Trigger/Messages/Summary/Archive). An aborted pass emits this with an empty |
| 69 | // Summary so the placeholder still resolves. Replaces the older plain Notice |
| 70 | // so a sink can render a distinct, expandable card. |
| 71 | CompactionDone |
| 72 | // ToolProgress streams a chunk of a still-running tool's combined output |
| 73 | // (Tool: ID + Output = the new chunk). Emitted between ToolDispatch and |
| 74 | // ToolResult for long tools like bash so a frontend can show live progress. |
| 75 | // Appended last to keep the Kind values before it wire-stable. |
| 76 | ToolProgress |
| 77 | // MCPSurfaceReady fires once per server when its background-loaded surface |
| 78 | // (prompts or resources) finishes after startup. Lets UIs refresh /mcp |
| 79 | // status without polling. Text carries "<server>: <surface> ready (<count> |
| 80 | // items)". Appended last to keep the Kind values before it wire-stable. |
| 81 | MCPSurfaceReady |
| 82 | // Retrying fires before each backoff sleep while the provider re-attempts the |
| 83 | // connection+header phase after a transient failure (RetryAttempt of RetryMax). |
| 84 | // A frontend shows a transient "retrying (n/m)" indicator that the next stream |
| 85 | // event — or TurnDone — clears. Appended last to keep the Kind values before |
| 86 | // it wire-stable. |
| 87 | Retrying |
| 88 | // Steer fires when a mid-turn steer message is consumed from the queue and |
| 89 | // injected as a user message. Text carries the raw steer content (without the |
| 90 | // wrapper prefix), so a frontend can display it to the user as confirmation. |
| 91 | // Frontends use Steer to know a queued message has been delivered. |
| 92 | Steer |
| 93 | // GuardianAssessment reports the outcome of a guardian sub-agent safety review. |
| 94 | // Carries GuardianResult payload (Outcome, RiskLevel, Rationale, etc.). |
| 95 | GuardianAssessment |
| 96 | // ExtensionSurface carries a structured UI surface published by an extension |
| 97 | // sidecar (Extension payload with one of the Card/Form/Notification |
| 98 | // sub-structs set). Appended last to keep the Kind values before it |
| 99 | // wire-stable. |
| 100 | ExtensionSurface |
| 101 | // ExtensionStatus carries a one-line status contribution published by an |
| 102 | // extension sidecar (Extension payload with Status set). Appended last to |
| 103 | // keep the Kind values before it wire-stable. |
| 104 | ExtensionStatus |
| 105 | // StreamAttempt marks the local lifecycle of one sampling attempt within a |
| 106 | // model round (StreamAttempt payload: begin | discard | commit). IDs are |
| 107 | // host-local only — never persisted or sent to the model. Appended last to |
| 108 | // keep earlier Kind values wire-stable; older clients ignore unknown kinds. |
| 109 | StreamAttempt |
| 110 | // KindCount is a sentinel one past the last real Kind. New event kinds must |
| 111 | // be inserted above it so completeness tests cover them automatically. |
| 112 | KindCount |
| 113 | ) |
| 114 | |
| 115 | // StreamAttemptAction is the lifecycle phase of a local sampling attempt. |
| 116 | type StreamAttemptAction string |
| 117 | |
| 118 | const ( |
| 119 | StreamAttemptBegin StreamAttemptAction = "begin" |
| 120 | StreamAttemptDiscard StreamAttemptAction = "discard" |
| 121 | StreamAttemptCommit StreamAttemptAction = "commit" |
| 122 | ) |
| 123 | |
| 124 | // RetryScope distinguishes connection+header retries from body-phase stream |
| 125 | // retries. Older clients ignore the empty/unknown value. |
| 126 | type RetryScope string |
| 127 | |
| 128 | const ( |
| 129 | RetryScopeHeaders RetryScope = "headers" |
| 130 | RetryScopeStream RetryScope = "stream" |
| 131 | ) |
| 132 | |
| 133 | // StreamAttemptInfo carries host-local bookkeeping for one sampling attempt. |
| 134 | // Reason is a fixed enum (connection_reset | premature_eof | idle_timeout). |
| 135 | type StreamAttemptInfo struct { |
| 136 | ID string |
| 137 | Action StreamAttemptAction |
| 138 | Attempt int // 1-based attempt number |
| 139 | Max int // total attempts including the first (typically 6) |
| 140 | Reason string |
| 141 | } |
| 142 | |
| 143 | const TurnOutcomeFinalReadiness = "final_readiness" |
| 144 | |
| 145 | // TurnOutcomeRecoveryPaused marks an Auto recovery Episode budget stop. New |
| 146 | // clients show an informational status (not send-failed); older clients still |
| 147 | // read Err text and ignore the unknown outcome. |
| 148 | const TurnOutcomeRecoveryPaused = "recovery_paused" |
| 149 | |
| 150 | // Level classifies a Notice so sinks can style or filter it. |
| 151 | type Level int |
| 152 | |
| 153 | const ( |
| 154 | LevelInfo Level = iota |
| 155 | LevelWarn |
| 156 | ) |
| 157 | |
| 158 | // NoticeAudience separates a notice's recipient from its severity. The empty |
| 159 | // default preserves the existing contract: ordinary notices are eligible for |
| 160 | // every frontend. Operator notices describe local runtime maintenance and must |
| 161 | // not be forwarded as end-user chat messages. Local frontends and diagnostics |
| 162 | // remain free to surface or quietly record them under their own policy. |
| 163 | type NoticeAudience string |
| 164 | |
| 165 | const ( |
| 166 | NoticeAudienceDefault NoticeAudience = "" |
| 167 | NoticeAudienceOperator NoticeAudience = "operator" |
| 168 | ) |
| 169 | |
| 170 | // Profile carries the subagent model/effort resolved for this call. |
| 171 | type Profile struct { |
| 172 | Model string |
| 173 | Effort string |
| 174 | } |
| 175 | |
| 176 | // Tool describes a tool call for ToolDispatch / ToolResult events. On dispatch |
| 177 | // ID/Name/Args/ReadOnly and optional preview metadata are set; on result |
| 178 | // Output/Err/Truncated are filled in. Args is the raw JSON arguments — a sink |
| 179 | // compacts it for display. |
| 180 | type Tool struct { |
| 181 | ID string |
| 182 | Name string |
| 183 | Args string |
| 184 | // ResolvedName/CapabilityID describe the real target behind a stable proxy |
| 185 | // while Name/Args remain the provider-visible call. They are optional local |
| 186 | // display metadata and never enter provider requests. |
| 187 | ResolvedName string |
| 188 | CapabilityID string |
| 189 | Output string // ToolResult: the result text fed to the model |
| 190 | Err string // ToolResult: non-empty when the call failed or was blocked |
| 191 | ReadOnly bool |
| 192 | Truncated bool // ToolResult: Output was head+tailed before display/model |
| 193 | DurationMs int64 // ToolResult: wall-clock execution time in milliseconds |
| 194 | // Partial marks an early ToolDispatch emitted when a call begins (ID/Name set, |
| 195 | // Args still streaming) so a frontend can show the card immediately; a second, |
| 196 | // full ToolDispatch (Partial false, Args set) follows when the call completes. |
| 197 | Partial bool |
| 198 | // ArgChars is the cumulative argument characters received so far for a |
| 199 | // Partial dispatch — a liveness signal while a large payload streams. Zero |
| 200 | // on the initial start dispatch and on full dispatches. |
| 201 | ArgChars int |
| 202 | // Refreshed marks a repeated full ToolDispatch for the same ID whose file |
| 203 | // preview or resolved proxy metadata changed after the initial dispatch. |
| 204 | // Frontends that can upsert by ID should replace the existing card; |
| 205 | // append-only sinks should ignore it to avoid duplicate tool cards. |
| 206 | Refreshed bool |
| 207 | // ParentID, when set, is the ID of the tool call that spawned this one — a |
| 208 | // sub-agent's calls carry the parent `task` call's ID so a frontend can nest |
| 209 | // them under it. Empty for top-level calls. |
| 210 | ParentID string |
| 211 | // AttemptID is the host-local stream_attempt id that produced a speculative |
| 212 | // partial ToolDispatch. Empty for committed/full dispatches and for nested |
| 213 | // sub-agent tools. Frontends must only journal partial events whose |
| 214 | // AttemptID matches the active stream_attempt begin. |
| 215 | AttemptID string |
| 216 | FileDiff |
| 217 | Profile *Profile // ToolDispatch: subagent model/effort (set for task/skill calls) |
| 218 | // Execution is optional local shell metadata (ToolResult). Never sent to |
| 219 | // model providers; omitempty keeps old wire readers compatible. |
| 220 | Execution *ShellExecution |
| 221 | } |
| 222 | |
| 223 | // ShellExecution mirrors tool.ShellExecution for event sinks without importing |
| 224 | // the tool package (event is a lower-level dependency of tool consumers). |
| 225 | type ShellExecution struct { |
| 226 | Kind string `json:"kind,omitempty"` |
| 227 | Shell string `json:"shell,omitempty"` |
| 228 | ShellVersion string `json:"shellVersion,omitempty"` |
| 229 | Platform string `json:"platform,omitempty"` |
| 230 | SupportsAndAnd bool `json:"supportsAndAnd"` |
| 231 | State string `json:"state,omitempty"` |
| 232 | FailurePhase string `json:"failurePhase,omitempty"` |
| 233 | ExitCode *int `json:"exitCode,omitempty"` |
| 234 | OutputTail string `json:"outputTail,omitempty"` |
| 235 | MutationRisk string `json:"mutationRisk,omitempty"` |
| 236 | Verification string `json:"verification,omitempty"` |
| 237 | DurationMs int64 `json:"durationMs,omitempty"` |
| 238 | } |
| 239 | |
| 240 | // FileDiff is a previewed change carried on a writer tool's full ToolDispatch |
| 241 | // and on its ApprovalRequest, so a frontend can render +/- lines before the |
| 242 | // call runs. Diff is the unified diff (empty for read-only tools, binary files, |
| 243 | // or no-op changes); Added/Removed are its line tallies. |
| 244 | type FileDiff struct { |
| 245 | Diff string |
| 246 | Added int |
| 247 | Removed int |
| 248 | } |
| 249 | |
| 250 | // Approval identifies a pending tool-call approval for an ApprovalRequest |
| 251 | // event. ID correlates the request with the controller's Approve(ID, …) reply. |
| 252 | type Approval struct { |
| 253 | ID string |
| 254 | Tool string |
| 255 | Subject string |
| 256 | Reason string // optional annotation explaining why approval is needed |
| 257 | // RawInput is the exact structured tool input. ACP permission clients use it |
| 258 | // together with locations/reason instead of parsing a human title. |
| 259 | RawInput json.RawMessage |
| 260 | Fresh bool // current human decision required; do not offer remembered grants |
| 261 | // Kind classifies the approval surface: "tool" (default), "plan", or |
| 262 | // "recovery". Empty means ordinary tool permission for backward compat. |
| 263 | Kind string |
| 264 | // Recovery carries Auto Guard card fields when Kind is "recovery". |
| 265 | // Old frontends ignore it and still render a one-shot fresh approval. |
| 266 | Recovery *RecoveryApproval |
| 267 | } |
| 268 | |
| 269 | // RecoveryApproval is the backward-compatible structured payload for Auto |
| 270 | // Guard decisions. All fields are plain strings/bools so wire JSON stays simple |
| 271 | // and old clients can ignore unknown nested objects safely. |
| 272 | type RecoveryApproval struct { |
| 273 | SourceAgent string // agent that proposed the next mutation |
| 274 | FailedTool string // tool that failed; empty for pre-action boundaries |
| 275 | FailedSummary string // short failure/error summary; optional |
| 276 | Diagnosis string // agent/host diagnosis when failure recovery is active |
| 277 | NextTool string // tool about to run |
| 278 | NextAction string // concrete next command/file change/MCP action |
| 279 | ChangeKind string // same_strategy | strategy | scope | risk | uncertain |
| 280 | ChangeRationale string // what changed vs the original approach |
| 281 | ReviewRationale string // why the host/reviewer needs confirmation |
| 282 | PlanBefore string // active structured plan before a material transition |
| 283 | PlanAfter string // proposed structured plan after a material transition |
| 284 | CanGrantTask bool // offer a semantic grant scoped to the current task |
| 285 | TaskGrantScope string // concise host-classified operation + exact target |
| 286 | } |
| 287 | |
| 288 | // AskOption is one choice the user can pick for an AskQuestion. |
| 289 | type AskOption struct { |
| 290 | Label string |
| 291 | Description string // optional one-line explanation shown under the label |
| 292 | } |
| 293 | |
| 294 | // AskQuestion is one structured question the `ask` tool puts to the user. |
| 295 | type AskQuestion struct { |
| 296 | ID string // stable per-question id, so answers correlate back |
| 297 | Header string // short label (the tab title) |
| 298 | Prompt string // the question text |
| 299 | Options []AskOption |
| 300 | Multi bool // allow selecting more than one option |
| 301 | } |
| 302 | |
| 303 | // Ask carries an AskRequest: a batch of questions and the ID that correlates the |
| 304 | // controller's AnswerQuestion(ID, …) reply. |
| 305 | type Ask struct { |
| 306 | ID string |
| 307 | Questions []AskQuestion |
| 308 | } |
| 309 | |
| 310 | // Extension surface kind values carried by ExtensionSurfacePayload.Kind. They |
| 311 | // mirror the extension protocol's structured surface kinds; "request" is |
| 312 | // reserved for stage-8b request surfaces (stage 8a routes blocking prompts |
| 313 | // through the ordinary AskRequest channel instead). |
| 314 | const ( |
| 315 | ExtensionSurfaceStatus = "status" |
| 316 | ExtensionSurfaceCard = "card" |
| 317 | ExtensionSurfaceForm = "form" |
| 318 | ExtensionSurfaceNotification = "notification" |
| 319 | ExtensionSurfaceRequest = "request" |
| 320 | ) |
| 321 | |
| 322 | // ExtensionSurfacePayload carries one extension sidecar's structured UI |
| 323 | // contribution for the ExtensionSurface / ExtensionStatus kinds. The structs |
| 324 | // mirror the Extension Protocol v1 UI payload DTOs field-for-field so any |
| 325 | // frontend can render them with native widgets; the protocol stays |
| 326 | // structured-only (no HTML/CSS/JS/URLs). All user-visible strings are already |
| 327 | // credential-redacted by the host UI hub before the event is emitted. Exactly |
| 328 | // one sub-struct is set, selected by Kind. |
| 329 | type ExtensionSurfacePayload struct { |
| 330 | PluginID string |
| 331 | SurfaceID string |
| 332 | SessionID string |
| 333 | Generation uint64 |
| 334 | Kind string // status | card | form | notification (request reserved) |
| 335 | Status *ExtensionStatusView |
| 336 | Card *ExtensionCardView |
| 337 | Form *ExtensionFormView |
| 338 | Notification *ExtensionNotificationView |
| 339 | } |
| 340 | |
| 341 | // ExtensionStatusView is a one-line status contribution (mirrors the |
| 342 | // protocol's UIStatusPayload). |
| 343 | type ExtensionStatusView struct { |
| 344 | Label string |
| 345 | Detail string |
| 346 | Severity string // info | warn | error |
| 347 | Progress *float64 |
| 348 | } |
| 349 | |
| 350 | // ExtensionKeyValue is one labelled value row in a card (mirrors UIKeyValue). |
| 351 | type ExtensionKeyValue struct { |
| 352 | Key string |
| 353 | Value string |
| 354 | } |
| 355 | |
| 356 | // ExtensionActionRef renders a button invoking a declared extension action |
| 357 | // (mirrors UIActionRef). |
| 358 | type ExtensionActionRef struct { |
| 359 | ActionID string |
| 360 | Label string |
| 361 | } |
| 362 | |
| 363 | // ExtensionCardView is a rich read-only surface (mirrors UICardPayload). |
| 364 | type ExtensionCardView struct { |
| 365 | Title string |
| 366 | Markdown string |
| 367 | Text string |
| 368 | Fields []ExtensionKeyValue |
| 369 | Progress *float64 |
| 370 | Actions []ExtensionActionRef |
| 371 | } |
| 372 | |
| 373 | // ExtensionFormField is one input row of a form surface (mirrors UIFormField). |
| 374 | type ExtensionFormField struct { |
| 375 | Key string |
| 376 | Label string |
| 377 | Kind string // confirm | input | select | multiselect |
| 378 | Options []string |
| 379 | Default any |
| 380 | Required bool |
| 381 | } |
| 382 | |
| 383 | // ExtensionFormView is an editable surface; submissions return to the |
| 384 | // extension through the UI hub (mirrors UIFormPayload). |
| 385 | type ExtensionFormView struct { |
| 386 | Title string |
| 387 | Message string |
| 388 | Fields []ExtensionFormField |
| 389 | } |
| 390 | |
| 391 | // ExtensionNotificationView is a transient toast-style message (mirrors |
| 392 | // UINotificationPayload). |
| 393 | type ExtensionNotificationView struct { |
| 394 | Title string |
| 395 | Body string |
| 396 | Severity string // info | warn | error |
| 397 | } |
| 398 | |
| 399 | // Compaction carries a context-compaction pass for the CompactionStarted / |
| 400 | // CompactionDone events. On CompactionStarted only Trigger is set. On |
| 401 | // CompactionDone, Messages/Summary/Archive are filled in (an aborted pass leaves |
| 402 | // Summary empty). Trigger is "auto" (the prompt reached the window threshold) or |
| 403 | // "manual" (the user ran /compact). |
| 404 | type Compaction struct { |
| 405 | Trigger string // "auto" | "manual" |
| 406 | Messages int // Done: how many messages were folded into the summary |
| 407 | Summary string // Done: the briefing the agent keeps relying on |
| 408 | Archive string // Done: path the dropped originals were archived to ("" if none) |
| 409 | } |
| 410 | |
| 411 | // GuardianResult carries the outcome of a guardian sub-agent safety review. |
| 412 | // Emitted with Kind=GuardianAssessment after each review completes. |
| 413 | type GuardianResult struct { |
| 414 | ID string // unique review id |
| 415 | Tool string // tool being reviewed (e.g. "bash") |
| 416 | Subject string // call subject (e.g. "rm -rf /tmp/build") |
| 417 | Outcome string // "allow" | "deny" |
| 418 | RiskLevel string // "low" | "medium" | "high" | "critical" |
| 419 | UserAuthorization string // "unknown" | "low" | "medium" | "high" |
| 420 | Rationale string // one-sentence reason |
| 421 | DurationMs int64 // wall-clock review time |
| 422 | Usage *provider.Usage // guardian review token telemetry |
| 423 | Pricing *provider.Pricing // for cost display (nil = omit cost) |
| 424 | } |
| 425 | |
| 426 | // AskAnswer is the user's reply to one AskQuestion: the chosen option label(s) |
| 427 | // (a free-typed answer is carried as a single Selected entry). |
| 428 | type AskAnswer struct { |
| 429 | QuestionID string |
| 430 | Selected []string |
| 431 | } |
| 432 | |
| 433 | // CacheDiagnostics describes whether and why the cacheable prefix changed since |
| 434 | // the last turn. It rides on the Usage event so every frontend can show |
| 435 | // cache-churn attribution. |
| 436 | type CacheDiagnostics struct { |
| 437 | PrefixHash string |
| 438 | PrefixChanged bool |
| 439 | PrefixChangeReasons []string // "system", "tools", "log_rewrite" |
| 440 | SystemHash string |
| 441 | ToolsHash string |
| 442 | LogRewriteVersion int |
| 443 | ToolSchemaTokens int |
| 444 | CacheMissTokens int |
| 445 | CacheHitTokens int |
| 446 | } |
| 447 | |
| 448 | // FinalReadiness carries machine-readable recovery requirements on TurnDone. |
| 449 | // Missing values are stable category ids; user-facing detail stays localized in |
| 450 | // the frontend instead of scraping the diagnostic error string. |
| 451 | type FinalReadiness struct { |
| 452 | Attempts int |
| 453 | Missing []string |
| 454 | } |
| 455 | |
| 456 | const ( |
| 457 | UsageSourceExecutor = "executor" |
| 458 | UsageSourcePlanner = "planner" |
| 459 | UsageSourceSubagent = "subagent" |
| 460 | UsageSourceCompaction = "compaction" |
| 461 | UsageSourceClassifier = "classifier" |
| 462 | UsageSourceTitle = "title" |
| 463 | UsageSourceCapabilityRouter = "capability-router" |
| 464 | UsageSourceRecoveryReviewer = "recovery-reviewer" |
| 465 | UsageSourceGoalEvaluator = "goal-evaluator" |
| 466 | ) |
| 467 | |
| 468 | // Event is one increment in a turn's event stream. Read the field(s) documented |
| 469 | // for Kind; the others are zero. |
| 470 | // Notice codes are stable machine-readable identifiers for known notices. |
| 471 | // Frontends localize a notice's main copy by Code and fall back to matching |
| 472 | // the English Text (or showing it raw) when Code is empty or unknown, so |
| 473 | // wording edits in Go no longer silently break localization. Values are |
| 474 | // wire-stable: never rename or reuse one once shipped. |
| 475 | const ( |
| 476 | NoticeCodeFinalReadiness = "final_readiness" |
| 477 | NoticeCodeEmptyFinal = "empty_final" |
| 478 | NoticeCodeExecutorHandoff = "executor_handoff" |
| 479 | NoticeCodeToolBudget = "tool_budget" |
| 480 | NoticeCodeLoopGuard = "loop_guard" |
| 481 | NoticeCodeWorkspaceLease = "workspace_lease" |
| 482 | NoticeCodeCancelledTurn = "cancelled_turn_display" |
| 483 | NoticeCodeUnappliedSteer = "unapplied_steer" |
| 484 | NoticeCodeSessionRecoveryForked = "session_recovery_forked" |
| 485 | NoticeCodeSessionRecoveryAdopted = "session_recovery_adopted" |
| 486 | NoticeCodeSessionRecoveryAdoptedCovered = "session_recovery_adopted_covered" |
| 487 | NoticeCodeSessionRecoveryDepthCap = "session_recovery_depth_cap" |
| 488 | NoticeCodeSessionShutdownRecoveryForked = "session_shutdown_recovery_forked" |
| 489 | NoticeCodeDecisionReceipt = "decision_receipt" |
| 490 | ) |
| 491 | |
| 492 | type Event struct { |
| 493 | Kind Kind |
| 494 | Text string // Reasoning / Text / Message / Notice / Phase |
| 495 | ModelRef string // Usage: canonical "provider/model" ref that produced this usage |
| 496 | Detail string // Notice: optional diagnostic text for expandable details |
| 497 | Code string // Notice: stable id for frontend localization; empty = unmapped |
| 498 | Reasoning string // Message: the full reasoning chain |
| 499 | MemoryCitations []provider.MemoryCitation // Message: local memory references displayed by rich frontends |
| 500 | Tool Tool // ToolDispatch / ToolResult |
| 501 | Usage *provider.Usage // Usage |
| 502 | Pricing *provider.Pricing // Usage: for cost display (nil = omit cost) |
| 503 | Source string // optional display/event source (executor, planner, subagent, ...) |
| 504 | UsageSource string // Usage: billable call source; empty means executor for compatibility |
| 505 | CacheDiagnostics *CacheDiagnostics // Usage: cache-churn attribution (nil = N/A) |
| 506 | // SessionHit/SessionMiss carry cumulative cache tokens across the whole |
| 507 | // session (Usage events only), so a frontend can show the aggregate hit-rate |
| 508 | // — which doesn't crater on a short turn or after compaction — alongside |
| 509 | // Usage's single-turn numbers. |
| 510 | SessionHit int // Usage: cumulative cache-hit prompt tokens this session |
| 511 | SessionMiss int // Usage: cumulative cache-miss prompt tokens this session |
| 512 | Level Level // Notice |
| 513 | Audience NoticeAudience // Notice: empty = ordinary frontend delivery; operator = no end-user chat forwarding |
| 514 | Approval Approval // ApprovalRequest |
| 515 | Ask Ask // AskRequest |
| 516 | Extension *ExtensionSurfacePayload // ExtensionSurface / ExtensionStatus (nil for every other kind) |
| 517 | Err error // TurnDone: non-nil on failure |
| 518 | Cancelled bool // TurnDone: Cancel was requested while the turn was active |
| 519 | Outcome string // TurnDone: optional machine-readable recoverable outcome |
| 520 | Readiness *FinalReadiness // TurnDone: structured final-readiness recovery state |
| 521 | Compaction Compaction // Compaction |
| 522 | Guardian GuardianResult |
| 523 | DecisionReceipt *provider.DecisionReceipt // Notice: durable user decision receipt |
| 524 | RetryAttempt int // Retrying: 1-based attempt about to be made |
| 525 | RetryMax int // Retrying: total attempts before giving up |
| 526 | RetryScope RetryScope // Retrying: optional "headers" | "stream"; empty for older emitters |
| 527 | StreamAttempt StreamAttemptInfo // StreamAttempt lifecycle |
| 528 | } |
| 529 | |
| 530 | // ReadinessAuditSink is an optional sink capability. Sinks that do not care |
| 531 | // about readiness audit receipts can implement only Sink and will ignore them. |
| 532 | type ReadinessAuditSink interface { |
| 533 | RecordReadinessAudit(evidence.ReadinessAudit) |
| 534 | } |
| 535 | |
| 536 | // TurnCompletionSink is an optional sink capability for synchronous controller |
| 537 | // entry points that do not publish a TurnDone UI event. It keeps accounting |
| 538 | // independent from frontend event lifecycles without synthesizing an event that |
| 539 | // transports may mistake for an interactive completion. |
| 540 | type TurnCompletionSink interface { |
| 541 | RecordTurnCompletion() |
| 542 | } |
| 543 | |
| 544 | // RecordTurnCompletion records one successfully admitted top-level controller |
| 545 | // run on sinks that opt into completion accounting. |
| 546 | func RecordTurnCompletion(s Sink) { |
| 547 | if nilutil.IsNil(s) { |
| 548 | return |
| 549 | } |
| 550 | if ts, ok := s.(TurnCompletionSink); ok { |
| 551 | ts.RecordTurnCompletion() |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | // RecordReadinessAudit forwards a readiness audit receipt to sinks that opt in. |
| 556 | func RecordReadinessAudit(s Sink, a evidence.ReadinessAudit) { |
| 557 | if nilutil.IsNil(s) { |
| 558 | return |
| 559 | } |
| 560 | if rs, ok := s.(ReadinessAuditSink); ok { |
| 561 | rs.RecordReadinessAudit(a) |
| 562 | } |
| 563 | } |
| 564 | |
| 565 | // ProtocolRecoveryKind is a content-free internal observation about a provider |
| 566 | // protocol repair. It is deliberately separate from Event/Notice so recovery |
| 567 | // stays invisible in chat transcripts and frontends do not need to understand |
| 568 | // provider implementation details. |
| 569 | type ProtocolRecoveryKind string |
| 570 | |
| 571 | const ( |
| 572 | ProtocolRecoveryMissingReasoningDetected ProtocolRecoveryKind = "missing_reasoning_detected" |
| 573 | ProtocolRecoveryMissingReasoningRetryAttempted ProtocolRecoveryKind = "missing_reasoning_retry_attempted" |
| 574 | ProtocolRecoveryMissingReasoningRetryRecovered ProtocolRecoveryKind = "missing_reasoning_retry_recovered" |
| 575 | ProtocolRecoveryMissingReasoningRetryReplaced ProtocolRecoveryKind = "missing_reasoning_retry_replaced_response" |
| 576 | ProtocolRecoveryMissingReasoningRetrySuppressed ProtocolRecoveryKind = "missing_reasoning_retry_suppressed" |
| 577 | ProtocolRecoveryMissingReasoningFallback ProtocolRecoveryKind = "missing_reasoning_fallback_used" |
| 578 | ) |
| 579 | |
| 580 | type ProtocolRecoveryAudit struct { |
| 581 | Kind ProtocolRecoveryKind |
| 582 | } |
| 583 | |
| 584 | // ProtocolRecoveryAuditSink is an optional sink capability. Implementations |
| 585 | // must keep it content-free; prompts, responses, endpoints, model names, and |
| 586 | // tool arguments do not belong in this audit channel. |
| 587 | type ProtocolRecoveryAuditSink interface { |
| 588 | RecordProtocolRecovery(ProtocolRecoveryAudit) |
| 589 | } |
| 590 | |
| 591 | // RecordProtocolRecovery forwards a content-free recovery observation only to |
| 592 | // sinks that explicitly opt in. Ordinary UI sinks receive nothing. |
| 593 | func RecordProtocolRecovery(s Sink, a ProtocolRecoveryAudit) { |
| 594 | if nilutil.IsNil(s) { |
| 595 | return |
| 596 | } |
| 597 | if rs, ok := s.(ProtocolRecoveryAuditSink); ok { |
| 598 | rs.RecordProtocolRecovery(a) |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | // Sink consumes a turn's events. The agent calls Emit serially from its run |
| 603 | // loop (tool execution may fan out across goroutines, but emission does not), |
| 604 | // so an implementation need not be safe for concurrent Emit. Emit must not |
| 605 | // block indefinitely — a channel-backed sink should be buffered or drained by |
| 606 | // a live reader. |
| 607 | type Sink interface { |
| 608 | Emit(Event) |
| 609 | } |
| 610 | |
| 611 | // FuncSink adapts a plain function to a Sink. |
| 612 | type FuncSink func(Event) |
| 613 | |
| 614 | // Emit calls the wrapped function. |
| 615 | func (f FuncSink) Emit(e Event) { |
| 616 | if f != nil { |
| 617 | f(e) |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | // Discard is a Sink that drops every event. Useful in tests and for runs that |
| 622 | // only care about the final session state. |
| 623 | var Discard Sink = FuncSink(func(Event) {}) |
| 624 |