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