返回 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/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
624 lines GO