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