返回 DeepSeek-Reasonix
wire.go
根目录 / internal / eventwire / wire.go
1 // Package eventwire defines the shared frontend JSON contract for event.Event.
2 package eventwire
3
4 import (
5 "encoding/json"
6
7 "reasonix/internal/billing"
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 )
11
12 // CompletionSummary is the JSON form of event.CompletionSummaryInfo.
13 type CompletionSummary struct {
14 Preset string `json:"preset"` // deprecated; pinned compat value
15 Verdict string `json:"verdict"`
16 Mutations int `json:"mutations"`
17 ChangedFiles int `json:"changed_files,omitempty"`
18 ChecksPassed int `json:"checks_passed"`
19 ChecksFailed int `json:"checks_failed"`
20 ChecksSuppressed int `json:"checks_suppressed"`
21 Review string `json:"review"`
22 GapKinds []string `json:"gap_kinds,omitempty"`
23 ConstraintDegraded bool `json:"constraint_degraded"`
24 Floor string `json:"floor,omitempty"`
25 Attention bool `json:"attention"`
26 }
27
28 func toWireCompletionSummary(c *event.CompletionSummaryInfo) *CompletionSummary {
29 if c == nil {
30 return nil
31 }
32 return &CompletionSummary{
33 Preset: c.Preset,
34 Verdict: c.Verdict,
35 Mutations: c.Mutations,
36 ChangedFiles: c.ChangedFiles,
37 ChecksPassed: c.ChecksPassed,
38 ChecksFailed: c.ChecksFailed,
39 ChecksSuppressed: c.ChecksSuppressed,
40 Review: c.Review,
41 GapKinds: append([]string(nil), c.GapKinds...),
42 ConstraintDegraded: c.ConstraintDegraded,
43 Floor: c.Floor,
44 Attention: c.Attention,
45 }
46 }
47
48 type WorkspaceChanged struct {
49 Revisions WorkspaceRevision `json:"revisions"`
50 Changes []WorkspacePathChange `json:"changes"`
51 AllPaths bool `json:"allPaths"`
52 Source string `json:"source"`
53 WatchState string `json:"watchState"`
54 }
55
56 type WorkspaceRevision struct {
57 Content uint64 `json:"content"`
58 Tree uint64 `json:"tree"`
59 WorkingTree uint64 `json:"workingTree"`
60 GitMeta uint64 `json:"gitMeta"`
61 Session uint64 `json:"session"`
62 }
63
64 type WorkspacePathChange struct {
65 Path string `json:"path"`
66 OldPath string `json:"oldPath,omitempty"`
67 Op string `json:"op"`
68 }
69
70 // StreamAttempt is the JSON form of event.StreamAttemptInfo.
71 type StreamAttempt struct {
72 ID string `json:"id"`
73 Action string `json:"action"` // begin | discard | commit
74 Attempt int `json:"attempt,omitempty"`
75 Max int `json:"max,omitempty"`
76 Reason string `json:"reason,omitempty"` // connection_reset | premature_eof | idle_timeout
77 }
78
79 // ToWire converts a typed runtime event into the shared frontend JSON contract.
80 func ToWire(e event.Event) Event {
81 w := Event{Kind: kindNames[e.Kind], MessageID: e.MessageID, AttemptID: e.AttemptID, Source: e.Source, PromptKind: e.PromptKind, TurnID: e.TurnID, Sequence: e.Sequence, Status: string(e.Status), Text: e.Text, Detail: e.Detail, Reasoning: e.Reasoning, ItemID: e.ItemID, SessionPath: e.SessionPath, SessionReset: e.SessionReset}
82 w.SessionID, w.RuntimeEpoch, w.SubmissionID = e.SessionID, e.RuntimeEpoch, e.SubmissionID
83 w.applyPromptIdentity(e)
84 if len(e.MemoryCitations) > 0 {
85 w.MemoryCitations = ToWireMemoryCitations(e.MemoryCitations)
86 }
87 switch e.Kind {
88 case event.Notice:
89 w.applyNotice(e)
90 case event.ReadStatus:
91 w.ReadStatus = toWireReadStatus(e.ReadStatus)
92 case event.ToolDispatch, event.ToolStarted, event.ToolResult, event.ToolProgress, event.ToolResultPreview:
93 w.Tool = toWireTool(e.Tool)
94 case event.WorkspaceChanged:
95 ws := e.Workspace
96 if ws == nil {
97 ws = &event.WorkspaceChangedPayload{}
98 }
99 changes := make([]WorkspacePathChange, 0, len(ws.Changes))
100 for _, c := range ws.Changes {
101 changes = append(changes, WorkspacePathChange{Path: c.Path, OldPath: c.OldPath, Op: c.Op})
102 }
103 w.Workspace = &WorkspaceChanged{
104 Revisions: WorkspaceRevision{Content: ws.Revisions.Content, Tree: ws.Revisions.Tree, WorkingTree: ws.Revisions.WorkingTree, GitMeta: ws.Revisions.GitMeta, Session: ws.Revisions.Session},
105 Changes: changes, AllPaths: ws.AllPaths, Source: ws.Source, WatchState: string(ws.WatchState),
106 }
107 case event.Usage:
108 w.Usage = toWireUsage(e)
109 case event.ApprovalRequest:
110 w.Approval = toWireApproval(e.Approval)
111 case event.AskRequest:
112 w.Ask = ToWireAsk(e.Ask)
113 case event.MCPInteractionRequest:
114 w.MCPInteraction = ToWireMCPInteraction(e.MCPInteraction)
115 case event.CompactionStarted, event.CompactionDone:
116 w.Compaction = &Compaction{
117 Trigger: e.Compaction.Trigger, Messages: e.Compaction.Messages,
118 Summary: e.Compaction.Summary, Archive: e.Compaction.Archive,
119 }
120 case event.ContextMaintenanceEvent:
121 if m := e.Maintenance; m != nil {
122 w.Maintenance = &ContextMaintenance{
123 Status: m.Status, Action: m.Action, Trigger: m.Trigger,
124 OperationID: m.OperationID, InputTokens: m.InputTokens,
125 ResultTokens: m.ResultTokens, SavedTokens: m.SavedTokens,
126 AffectedToolResults: m.AffectedToolResults,
127 ProjectionVersion: m.ProjectionVersion, CacheBreak: m.CacheBreak,
128 Reason: m.Reason,
129 }
130 }
131 case event.SessionOperation:
132 if op := e.SessionOperation; op != nil {
133 copy := *op
134 w.SessionOperation = &copy
135 }
136 case event.GuardianAssessment:
137 w.Guardian = ToWireGuardian(e.Guardian)
138 case event.ExtensionSurface, event.ExtensionStatus:
139 w.Extension = ToWireExtensionSurface(e.Extension)
140 case event.TurnDone:
141 w.Recovery = e.Recovery
142 w.Outcome = e.Outcome
143 w.ReadPause = e.ReadPause
144 w.ReadCompletion = e.ReadCompletion
145 w.CheckpointTurn = e.CheckpointTurn
146 w.Receipt = completionReceiptWire(e.Receipt)
147 w.ProtocolRecovery = e.ProtocolRecovery
148 w.Diagnostic = e.Diagnostic
149 if e.Readiness != nil {
150 w.Readiness = &FinalReadiness{Attempts: e.Readiness.Attempts, Missing: append([]string(nil), e.Readiness.Missing...)}
151 }
152 if e.Err != nil {
153 w.Err = e.Err.Error()
154 }
155 case event.Retrying:
156 w.Recovery = e.Recovery
157 w.RetryAttempt = e.RetryAttempt
158 w.RetryMax = e.RetryMax
159 if e.RetryScope != "" {
160 w.RetryScope = string(e.RetryScope)
161 }
162 case event.StreamAttempt:
163 w.StreamAttempt = &StreamAttempt{
164 ID: e.StreamAttempt.ID,
165 Action: string(e.StreamAttempt.Action),
166 Attempt: e.StreamAttempt.Attempt,
167 Max: e.StreamAttempt.Max,
168 Reason: e.StreamAttempt.Reason,
169 }
170 case event.TurnPhase:
171 w.Phase = string(e.PhaseName)
172 if w.Phase == "" {
173 w.Phase = e.Text
174 }
175 case event.CompletionSummary:
176 w.Completion = toWireCompletionSummary(e.Completion)
177 }
178 return w
179 }
180
181 func toWireUsage(e event.Event) *Usage {
182 u := e.Usage
183 if u == nil {
184 return nil
185 }
186 wire := &Usage{
187 PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens,
188 TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens,
189 CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens,
190 Estimated: u.Estimated,
191 Source: e.UsageSource,
192 ContextPromptTokens: u.ContextPromptTokens,
193 ContextCompletionTokens: u.ContextCompletionTokens,
194 ContextReasoningTokens: u.ContextReasoningTokens,
195 ContextCacheHitTokens: u.ContextCacheHitTokens,
196 ContextCacheMissTokens: u.ContextCacheMissTokens,
197 SessionCacheHitTokens: e.SessionHit, SessionCacheMissTokens: e.SessionMiss,
198 }
199 if e.CacheDiagnostics != nil {
200 wire.CacheDiagnostics = ToWireCacheDiagnostics(e.CacheDiagnostics)
201 }
202 quote := e.CostQuote
203 if quote == nil && e.Pricing != nil {
204 quote = event.EnsureCostQuote(e, nil)
205 }
206 if quote != nil {
207 wire.CostQuote = quote
208 wire.CostComplete = quote.CostComplete
209 wire.DisplayComplete = quote.DisplayComplete
210 wire.DisplayStatus = quote.DisplayStatus
211 wire.AggregateMode = quote.AggregateMode
212 wire.OriginalTotals = append([]billing.Money(nil), quote.OriginalTotals...)
213 if quote.Selected != nil {
214 wire.Cost = quote.Selected.Float64()
215 wire.Currency = quote.LegacyCurrencySymbol()
216 wire.CostUSD = wire.Cost
217 wire.CurrencyCode = quote.LegacyCurrencyCode()
218 }
219 }
220 return wire
221 }
222
223 // DecisionReceipt is the JSON form of a provider-excluded user decision.
224 type DecisionReceipt struct {
225 ID string `json:"id"`
226 Kind string `json:"kind"`
227 Tool string `json:"tool,omitempty"`
228 Subject string `json:"subject,omitempty"`
229 Outcome string `json:"outcome"`
230 }
231
232 func ToWireDecisionReceipt(in *provider.DecisionReceipt) *DecisionReceipt {
233 if in == nil {
234 return nil
235 }
236 return &DecisionReceipt{ID: in.ID, Kind: in.Kind, Tool: in.Tool, Subject: in.Subject, Outcome: in.Outcome}
237 }
238
239 type FinalReadiness struct {
240 Attempts int `json:"attempts,omitempty"`
241 Missing []string `json:"missing,omitempty"`
242 }
243
244 // MemoryCitation is the JSON form of provider.MemoryCitation.
245 type MemoryCitation struct {
246 ID string `json:"id,omitempty"`
247 Source string `json:"source"`
248 LineStart int `json:"lineStart,omitempty"`
249 LineEnd int `json:"lineEnd,omitempty"`
250 Note string `json:"note,omitempty"`
251 Kind string `json:"kind,omitempty"`
252 }
253
254 // ToWireMemoryCitations converts local memory references into frontend JSON.
255 func ToWireMemoryCitations(in []provider.MemoryCitation) []MemoryCitation {
256 out := make([]MemoryCitation, 0, len(in))
257 for _, c := range in {
258 if c.Source == "" && c.ID == "" && c.Note == "" {
259 continue
260 }
261 out = append(out, MemoryCitation{
262 ID: c.ID,
263 Source: c.Source,
264 LineStart: c.LineStart,
265 LineEnd: c.LineEnd,
266 Note: c.Note,
267 Kind: c.Kind,
268 })
269 }
270 return out
271 }
272
273 func (w *Event) applyPromptIdentity(e event.Event) {
274 if e.ItemID != "" {
275 promptEvent := false
276 switch e.Kind {
277 case event.AskRequest:
278 promptEvent = true
279 w.PromptKind = "ask"
280 case event.ApprovalRequest:
281 promptEvent = true
282 w.PromptKind = e.Approval.Kind
283 if w.PromptKind == "" {
284 w.PromptKind = "approval"
285 }
286 case event.MCPInteractionRequest:
287 promptEvent = true
288 w.PromptKind = "mcp"
289 case event.PromptAnswered:
290 promptEvent = true
291 }
292 if promptEvent {
293 w.PromptID = e.ItemID
294 w.PromptLegacy = e.TurnID == ""
295 }
296 }
297 }
298
299 // Compaction is the JSON form of an event.Compaction.
300 type Compaction struct {
301 Trigger string `json:"trigger,omitempty"`
302 Messages int `json:"messages,omitempty"`
303 Summary string `json:"summary,omitempty" externalizable:"true"`
304 Archive string `json:"archive,omitempty" externalizable:"true"`
305 }
306
307 // AskOption is one JSON-formatted choice in a structured ask request.
308 type AskOption struct {
309 Label string `json:"label"`
310 Description string `json:"description,omitempty" externalizable:"true"`
311 }
312
313 // AskQuestion is one JSON-formatted structured ask question.
314 type AskQuestion struct {
315 ID string `json:"id"`
316 Header string `json:"header,omitempty"`
317 Prompt string `json:"prompt" externalizable:"true"`
318 Options []AskOption `json:"options"`
319 Multi bool `json:"multi,omitempty"`
320 }
321
322 // Ask is the JSON form of an event.Ask.
323 type Ask struct {
324 ID string `json:"id"`
325 Questions []AskQuestion `json:"questions"`
326 TurnID string `json:"turnId,omitempty"`
327 }
328
329 // MCPInteraction is the JSON form of an event.MCPInteraction: one
330 // server-initiated elicitation awaiting the user's accept/decline/cancel.
331 // Schema and URL come from the MCP server; form answers travel only in the
332 // resolve call, never on this event.
333 type MCPInteraction struct {
334 ID string `json:"id"`
335 Server string `json:"server"`
336 Mode string `json:"mode"`
337 Message string `json:"message" externalizable:"true"`
338 RequestedSchema json.RawMessage `json:"requestedSchema,omitempty"`
339 URL string `json:"url,omitempty"`
340 ElicitationID string `json:"elicitationId,omitempty"`
341 TurnID string `json:"turnId,omitempty"`
342 }
343
344 // applyNotice fills the Notice-specific wire fields.
345 func (w *Event) applyNotice(e event.Event) {
346 w.Code = e.Code
347 if e.DecisionReceipt != nil {
348 w.DecisionReceipt = ToWireDecisionReceipt(e.DecisionReceipt)
349 }
350 if e.Level == event.LevelWarn {
351 w.Level = "warn"
352 } else {
353 w.Level = "info"
354 }
355 }
356
357 // ToWireMCPInteraction converts event.MCPInteraction to its wire form.
358 func ToWireMCPInteraction(i event.MCPInteraction) *MCPInteraction {
359 return &MCPInteraction{
360 ID: i.ID, Server: i.Server, Mode: i.Mode, Message: i.Message,
361 RequestedSchema: i.RequestedSchema, URL: i.URL, ElicitationID: i.ElicitationID, TurnID: i.TurnID,
362 }
363 }
364
365 // Profile carries the subagent model/effort resolved for a tool call.
366 type Profile struct {
367 Model string `json:"model,omitempty"`
368 Effort string `json:"effort,omitempty"`
369 }
370
371 // ShellExecution is the JSON form of event.ShellExecution (local UI metadata).
372 type ShellExecution struct {
373 Kind string `json:"kind,omitempty"`
374 Shell string `json:"shell,omitempty"`
375 ShellVersion string `json:"shellVersion,omitempty"`
376 Platform string `json:"platform,omitempty"`
377 SupportsAndAnd bool `json:"supportsAndAnd"`
378 State string `json:"state,omitempty"`
379 FailurePhase string `json:"failurePhase,omitempty"`
380 ExitCode *int `json:"exitCode,omitempty"`
381 OutputTail string `json:"outputTail,omitempty"`
382 MutationRisk string `json:"mutationRisk,omitempty"`
383 Verification string `json:"verification,omitempty"`
384 DurationMs int64 `json:"durationMs,omitempty"`
385 }
386
387 func toWireShellExecution(in *event.ShellExecution) *ShellExecution {
388 if in == nil {
389 return nil
390 }
391 out := &ShellExecution{
392 Kind: in.Kind, Shell: in.Shell, ShellVersion: in.ShellVersion,
393 Platform: in.Platform, SupportsAndAnd: in.SupportsAndAnd,
394 State: in.State, FailurePhase: in.FailurePhase,
395 OutputTail: in.OutputTail, MutationRisk: in.MutationRisk,
396 Verification: in.Verification, DurationMs: in.DurationMs,
397 }
398 if in.ExitCode != nil {
399 code := *in.ExitCode
400 out.ExitCode = &code
401 }
402 return out
403 }
404
405 // Usage is the JSON form of provider usage telemetry.
406 type Usage struct {
407 PromptTokens int `json:"promptTokens"`
408 CompletionTokens int `json:"completionTokens"`
409 TotalTokens int `json:"totalTokens"`
410 CacheHitTokens int `json:"cacheHitTokens"`
411 CacheMissTokens int `json:"cacheMissTokens"`
412 ReasoningTokens int `json:"reasoningTokens,omitempty"`
413 Estimated bool `json:"estimated,omitempty"`
414 Source string `json:"source,omitempty"`
415 CacheDiagnostics *CacheDiagnostics `json:"cacheDiagnostics,omitempty"`
416 // Session-cumulative cache tokens keep status displays steadier than one-turn values.
417 SessionCacheHitTokens int `json:"sessionCacheHitTokens"`
418 SessionCacheMissTokens int `json:"sessionCacheMissTokens"`
419 // Context* fields are the latest single-request shape for gauges/rebind.
420 // When omitted, clients fall back to the billable prompt/completion totals.
421 ContextPromptTokens int `json:"contextPromptTokens,omitempty"`
422 ContextCompletionTokens int `json:"contextCompletionTokens,omitempty"`
423 ContextReasoningTokens int `json:"contextReasoningTokens,omitempty"`
424 ContextCacheHitTokens int `json:"contextCacheHitTokens,omitempty"`
425 ContextCacheMissTokens int `json:"contextCacheMissTokens,omitempty"`
426 Cost float64 `json:"cost,omitempty"`
427 Currency string `json:"currency,omitempty"`
428 // CurrencyCode is the ISO code for Cost (preferred over symbol Currency).
429 CurrencyCode string `json:"currencyCode,omitempty"`
430 // CostUSD is a compatibility alias for older consumers; it mirrors Cost
431 // (selected display valuation) and does not imply USD.
432 CostUSD float64 `json:"costUsd,omitempty"`
433 // CostQuote is the structured host-side quote. New consumers must prefer it
434 // over cost/currency aliases. Never sent to model providers.
435 CostQuote *billing.CostQuote `json:"costQuote,omitempty"`
436 CostComplete bool `json:"costComplete,omitempty"`
437 DisplayComplete bool `json:"displayComplete,omitempty"`
438 DisplayStatus string `json:"displayStatus,omitempty"`
439 AggregateMode string `json:"aggregateMode,omitempty"`
440 OriginalTotals []billing.Money `json:"originalTotals,omitempty"`
441 }
442
443 // CacheDiagnostics is the JSON form of cache prefix diagnostics.
444 type CacheDiagnostics struct {
445 PrefixHash string `json:"prefixHash"`
446 PrefixChanged bool `json:"prefixChanged"`
447 PrefixChangeReasons []string `json:"prefixChangeReasons,omitempty"`
448 SystemHash string `json:"systemHash"`
449 ToolsHash string `json:"toolsHash"`
450 LogRewriteVersion int `json:"logRewriteVersion"`
451 ToolSchemaTokens int `json:"toolSchemaTokens"`
452 CacheMissTokens int `json:"cacheMissTokens"`
453 CacheHitTokens int `json:"cacheHitTokens"`
454 SessionContext *SessionContextDiagnostics `json:"sessionContext,omitempty"`
455 }
456
457 // Guardian is the JSON form of an event.GuardianResult.
458 type Guardian struct {
459 ID string `json:"id"`
460 Tool string `json:"tool"`
461 Subject string `json:"subject"`
462 Outcome string `json:"outcome"`
463 RiskLevel string `json:"risk_level,omitempty"`
464 UserAuthorization string `json:"user_authorization,omitempty"`
465 Rationale string `json:"rationale,omitempty" externalizable:"true"`
466 DurationMs int64 `json:"duration_ms,omitempty"`
467 Usage *Usage `json:"usage,omitempty"`
468 }
469
470 // ToWireGuardian converts an event.GuardianResult into its JSON wire form.
471 func ToWireGuardian(g event.GuardianResult) *Guardian {
472 out := &Guardian{
473 ID: g.ID,
474 Tool: g.Tool,
475 Subject: g.Subject,
476 Outcome: g.Outcome,
477 RiskLevel: g.RiskLevel,
478 UserAuthorization: g.UserAuthorization,
479 Rationale: g.Rationale,
480 DurationMs: g.DurationMs,
481 }
482 if u := g.Usage; u != nil {
483 out.Usage = &Usage{
484 PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens,
485 TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens,
486 CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens,
487 Estimated: u.Estimated,
488 }
489 if g.Pricing != nil {
490 q := event.EnsureCostQuote(event.Event{Kind: event.Usage, Usage: u, Pricing: g.Pricing}, nil)
491 if q != nil {
492 out.Usage.CostQuote = q
493 out.Usage.Cost = q.LegacyCostFloat()
494 out.Usage.Currency = q.LegacyCurrencySymbol()
495 out.Usage.CostUSD = out.Usage.Cost
496 out.Usage.CurrencyCode = q.LegacyCurrencyCode()
497 }
498 }
499 }
500 return out
501 }
502
503 // ToWireAsk converts an event.Ask into its JSON wire form.
504 func ToWireAsk(a event.Ask) *Ask {
505 qs := make([]AskQuestion, len(a.Questions))
506 for i, q := range a.Questions {
507 opts := make([]AskOption, len(q.Options))
508 for j, o := range q.Options {
509 opts[j] = AskOption{Label: o.Label, Description: o.Description}
510 }
511 qs[i] = AskQuestion{ID: q.ID, Header: q.Header, Prompt: q.Prompt, Options: opts, Multi: q.Multi}
512 }
513 return &Ask{ID: a.ID, Questions: qs, TurnID: a.TurnID}
514 }
515
516 // ToWireCacheDiagnostics converts cache diagnostics into their JSON wire form.
517 func ToWireCacheDiagnostics(d *event.CacheDiagnostics) *CacheDiagnostics {
518 out := &CacheDiagnostics{
519 PrefixHash: d.PrefixHash,
520 PrefixChanged: d.PrefixChanged,
521 PrefixChangeReasons: append([]string(nil), d.PrefixChangeReasons...),
522 SystemHash: d.SystemHash,
523 ToolsHash: d.ToolsHash,
524 LogRewriteVersion: d.LogRewriteVersion,
525 ToolSchemaTokens: d.ToolSchemaTokens,
526 CacheMissTokens: d.CacheMissTokens,
527 CacheHitTokens: d.CacheHitTokens,
528 }
529 if sc := d.SessionContext; sc != nil {
530 section := func(in event.SessionContextSectionDiagnostics) SessionContextSectionDiagnostics {
531 return SessionContextSectionDiagnostics{Digest: in.Digest, Chars: in.Chars}
532 }
533 out.SessionContext = &SessionContextDiagnostics{
534 Version: sc.Version, Digest: sc.Digest, TargetRole: sc.TargetRole,
535 Reasons: append([]string(nil), sc.Reasons...),
536 Environment: section(sc.Environment), Workspace: section(sc.Workspace),
537 BackgroundMemory: section(sc.BackgroundMemory), SkillsCatalog: section(sc.SkillsCatalog),
538 }
539 }
540 return out
541 }
542
543 // KindNames returns every stable frontend event kind in event.Kind order. It is
544 // the protocol-neutral source used by consumers such as the Remote schema
545 // generator; callers receive a copy and may sort it without mutating eventwire.
546 func KindNames() []string {
547 names := make([]string, 0, int(event.KindCount))
548 for kind := range event.KindCount {
549 if name, ok := kindNames[kind]; ok {
550 names = append(names, name)
551 }
552 }
553 return names
554 }
555
556 // KindName returns the stable wire name of one event kind, or false for a
557 // kind outside the known set.
558 func KindName(kind event.Kind) (string, bool) {
559 name, ok := kindNames[kind]
560 return name, ok
561 }
562
563 var kindNames = map[event.Kind]string{
564 event.UserMessage: "user_message",
565 event.TurnStarted: "turn_started",
566 event.Reasoning: "reasoning",
567 event.Text: "text",
568 event.Message: "message",
569 event.ToolDispatch: "tool_dispatch",
570 event.ToolStarted: "tool_started",
571 event.ToolResult: "tool_result",
572 event.Usage: "usage",
573 event.Notice: "notice",
574 event.Phase: "phase",
575 event.ApprovalRequest: "approval_request",
576 event.AskRequest: "ask_request",
577 event.TurnDone: "turn_done",
578 event.CompactionStarted: "compaction_started",
579 event.CompactionDone: "compaction_done",
580 event.ToolProgress: "tool_progress",
581 event.MCPSurfaceReady: "mcp_surface_ready",
582 event.Retrying: "retrying",
583 event.Steer: "steer",
584 event.GuardianAssessment: "guardian_assessment",
585 event.ExtensionSurface: "extension_surface",
586 event.ExtensionStatus: "extension_status",
587 event.StreamAttempt: "stream_attempt",
588 event.ContextMaintenanceEvent: "context_maintenance",
589 event.WorkspaceChanged: "workspace_changed",
590 event.TurnPhase: "turn_phase",
591 event.CompletionSummary: "completion_summary",
592 event.ToolResultPreview: "tool_result_preview",
593 event.TurnStatusChanged: "turn_status",
594 event.MCPInteractionRequest: "mcp_interaction",
595 event.PromptAnswered: "prompt_answered",
596 event.SessionChanged: "session_changed",
597 event.ReadStatus: "read_status",
598 event.SessionOperation: "session_operation",
599 }
600
601 // ContextMaintenance is the JSON form of event.ContextMaintenance.
602 type ContextMaintenance struct {
603 Status string `json:"status,omitempty"`
604 Action string `json:"action,omitempty"`
605 Trigger string `json:"trigger,omitempty"`
606 OperationID string `json:"operationId,omitempty"`
607 InputTokens int `json:"inputTokens,omitempty"`
608 ResultTokens int `json:"resultTokens,omitempty"`
609 SavedTokens int `json:"savedTokens,omitempty"`
610 AffectedToolResults int `json:"affectedToolResults,omitempty"`
611 ProjectionVersion uint64 `json:"projectionVersion,omitempty"`
612 CacheBreak bool `json:"cacheBreak,omitempty"`
613 Reason string `json:"reason,omitempty"`
614 }
615
616 // ExtensionSurface is the JSON form of an event.ExtensionSurfacePayload.
617 type ExtensionSurface struct {
618 PluginID string `json:"pluginId"`
619 SurfaceID string `json:"surfaceId"`
620 SessionID string `json:"sessionId,omitempty"`
621 Generation uint64 `json:"generation,omitempty"`
622 FormInstanceID string `json:"formInstanceId,omitempty"`
623 Kind string `json:"kind"`
624 Status *ExtensionStatus `json:"status,omitempty"`
625 Card *ExtensionCard `json:"card,omitempty"`
626 Form *ExtensionForm `json:"form,omitempty"`
627 Notification *ExtensionNotification `json:"notification,omitempty"`
628 }
629
630 // ExtensionStatus is the JSON form of an event.ExtensionStatusView.
631 type ExtensionStatus struct {
632 Label string `json:"label"`
633 Detail string `json:"detail,omitempty"`
634 Severity string `json:"severity,omitempty"`
635 Progress *float64 `json:"progress,omitempty"`
636 }
637
638 // ExtensionKeyValue is the JSON form of an event.ExtensionKeyValue.
639 type ExtensionKeyValue struct {
640 Key string `json:"key"`
641 Value string `json:"value"`
642 }
643
644 // ExtensionActionRef is the JSON form of an event.ExtensionActionRef.
645 type ExtensionActionRef struct {
646 ActionID string `json:"actionId"`
647 Label string `json:"label"`
648 }
649
650 // ExtensionCard is the JSON form of an event.ExtensionCardView.
651 type ExtensionCard struct {
652 Title string `json:"title,omitempty"`
653 Markdown string `json:"markdown,omitempty" externalizable:"true"`
654 Text string `json:"text,omitempty" externalizable:"true"`
655 Fields []ExtensionKeyValue `json:"fields,omitempty"`
656 Progress *float64 `json:"progress,omitempty"`
657 Actions []ExtensionActionRef `json:"actions,omitempty"`
658 }
659
660 // ExtensionFormField is the JSON form of an event.ExtensionFormField. Default
661 // travels as raw JSON: the remote schema has no "any" type, and the field is
662 // already protocol-validated JSON on arrival.
663 type ExtensionFormField struct {
664 Key string `json:"key"`
665 Label string `json:"label,omitempty"`
666 Kind string `json:"kind,omitempty"`
667 Options []string `json:"options,omitempty"`
668 Default json.RawMessage `json:"default,omitempty"`
669 Required bool `json:"required,omitempty"`
670 }
671
672 // ExtensionForm is the JSON form of an event.ExtensionFormView.
673 type ExtensionForm struct {
674 Title string `json:"title,omitempty"`
675 Message string `json:"message,omitempty" externalizable:"true"`
676 Fields []ExtensionFormField `json:"fields"`
677 }
678
679 // ExtensionNotification is the JSON form of an event.ExtensionNotificationView.
680 type ExtensionNotification struct {
681 Title string `json:"title"`
682 Body string `json:"body,omitempty" externalizable:"true"`
683 Severity string `json:"severity,omitempty"`
684 }
685
686 // ToWireExtensionSurface converts an event.ExtensionSurfacePayload into its
687 // JSON wire form. A nil payload yields nil so a malformed event never
688 // marshals a half-filled extension object.
689 func ToWireExtensionSurface(p *event.ExtensionSurfacePayload) *ExtensionSurface {
690 if p == nil {
691 return nil
692 }
693 out := &ExtensionSurface{
694 PluginID: p.PluginID, SurfaceID: p.SurfaceID, SessionID: p.SessionID,
695 Generation: p.Generation, FormInstanceID: p.FormInstanceID, Kind: p.Kind,
696 }
697 if s := p.Status; s != nil {
698 out.Status = &ExtensionStatus{Label: s.Label, Detail: s.Detail, Severity: s.Severity, Progress: s.Progress}
699 }
700 if c := p.Card; c != nil {
701 card := &ExtensionCard{
702 Title: c.Title, Markdown: c.Markdown, Text: c.Text, Progress: c.Progress,
703 }
704 if len(c.Fields) > 0 {
705 card.Fields = make([]ExtensionKeyValue, len(c.Fields))
706 for i, f := range c.Fields {
707 card.Fields[i] = ExtensionKeyValue{Key: f.Key, Value: f.Value}
708 }
709 }
710 if len(c.Actions) > 0 {
711 card.Actions = make([]ExtensionActionRef, len(c.Actions))
712 for i, a := range c.Actions {
713 card.Actions[i] = ExtensionActionRef{ActionID: a.ActionID, Label: a.Label}
714 }
715 }
716 out.Card = card
717 }
718 if f := p.Form; f != nil {
719 form := &ExtensionForm{Title: f.Title, Message: f.Message}
720 if len(f.Fields) > 0 {
721 form.Fields = make([]ExtensionFormField, len(f.Fields))
722 for i, field := range f.Fields {
723 wireField := ExtensionFormField{
724 Key: field.Key, Label: field.Label, Kind: field.Kind,
725 Options: append([]string(nil), field.Options...),
726 Required: field.Required,
727 }
728 if field.Default != nil {
729 // The value arrived as protocol-validated JSON, so a
730 // marshal failure here is unreachable in practice; a
731 // pathological in-memory value simply drops the default.
732 if raw, err := json.Marshal(field.Default); err == nil {
733 wireField.Default = raw
734 }
735 }
736 form.Fields[i] = wireField
737 }
738 }
739 out.Form = form
740 }
741 if n := p.Notification; n != nil {
742 out.Notification = &ExtensionNotification{Title: n.Title, Body: n.Body, Severity: n.Severity}
743 }
744 return out
745 }
746
746 lines GO