| 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/event" |
| 8 | "reasonix/internal/provider" |
| 9 | ) |
| 10 | |
| 11 | // Event is the JSON-friendly form shared by event frontends. |
| 12 | // externalizable:"true" marks large string payloads the Remote protocol may |
| 13 | // offload via content refs without changing provider-visible semantics. |
| 14 | type Event struct { |
| 15 | Kind string `json:"kind"` |
| 16 | Text string `json:"text,omitempty" externalizable:"true"` |
| 17 | Detail string `json:"detail,omitempty" externalizable:"true"` |
| 18 | Code string `json:"code,omitempty"` |
| 19 | Reasoning string `json:"reasoning,omitempty" externalizable:"true"` |
| 20 | MemoryCitations []MemoryCitation `json:"memoryCitations,omitempty"` |
| 21 | Level string `json:"level,omitempty"` |
| 22 | Tool *Tool `json:"tool,omitempty"` |
| 23 | Usage *Usage `json:"usage,omitempty"` |
| 24 | Approval *Approval `json:"approval,omitempty"` |
| 25 | Ask *Ask `json:"ask,omitempty"` |
| 26 | Compaction *Compaction `json:"compaction,omitempty"` |
| 27 | Guardian *Guardian `json:"guardian,omitempty"` |
| 28 | DecisionReceipt *DecisionReceipt `json:"decisionReceipt,omitempty"` |
| 29 | Extension *ExtensionSurface `json:"extension,omitempty"` |
| 30 | Err string `json:"err,omitempty" externalizable:"true"` |
| 31 | Outcome string `json:"outcome,omitempty"` |
| 32 | Readiness *FinalReadiness `json:"readiness,omitempty"` |
| 33 | RetryAttempt int `json:"retryAttempt,omitempty"` |
| 34 | RetryMax int `json:"retryMax,omitempty"` |
| 35 | RetryScope string `json:"retryScope,omitempty"` // "headers" | "stream"; omit for older clients |
| 36 | StreamAttempt *StreamAttempt `json:"streamAttempt,omitempty"` |
| 37 | } |
| 38 | |
| 39 | // StreamAttempt is the JSON form of event.StreamAttemptInfo. |
| 40 | type StreamAttempt struct { |
| 41 | ID string `json:"id"` |
| 42 | Action string `json:"action"` // begin | discard | commit |
| 43 | Attempt int `json:"attempt,omitempty"` |
| 44 | Max int `json:"max,omitempty"` |
| 45 | Reason string `json:"reason,omitempty"` // connection_reset | premature_eof | idle_timeout |
| 46 | } |
| 47 | |
| 48 | // ToWire converts a typed runtime event into the shared frontend JSON contract. |
| 49 | func ToWire(e event.Event) Event { |
| 50 | w := Event{Kind: kindNames[e.Kind], Text: e.Text, Detail: e.Detail, Reasoning: e.Reasoning} |
| 51 | if len(e.MemoryCitations) > 0 { |
| 52 | w.MemoryCitations = ToWireMemoryCitations(e.MemoryCitations) |
| 53 | } |
| 54 | switch e.Kind { |
| 55 | case event.Notice: |
| 56 | w.Code = e.Code |
| 57 | if e.DecisionReceipt != nil { |
| 58 | w.DecisionReceipt = ToWireDecisionReceipt(e.DecisionReceipt) |
| 59 | } |
| 60 | if e.Level == event.LevelWarn { |
| 61 | w.Level = "warn" |
| 62 | } else { |
| 63 | w.Level = "info" |
| 64 | } |
| 65 | case event.ToolDispatch, event.ToolResult, event.ToolProgress: |
| 66 | wt := &Tool{ |
| 67 | ID: e.Tool.ID, Name: e.Tool.Name, Args: e.Tool.Args, |
| 68 | ResolvedName: e.Tool.ResolvedName, CapabilityID: e.Tool.CapabilityID, |
| 69 | Output: e.Tool.Output, Err: e.Tool.Err, |
| 70 | ReadOnly: e.Tool.ReadOnly, Truncated: e.Tool.Truncated, |
| 71 | DurationMs: e.Tool.DurationMs, Partial: e.Tool.Partial, |
| 72 | ArgChars: e.Tool.ArgChars, Refreshed: e.Tool.Refreshed, |
| 73 | ParentID: e.Tool.ParentID, AttemptID: e.Tool.AttemptID, |
| 74 | Diff: e.Tool.Diff, Added: e.Tool.Added, Removed: e.Tool.Removed, |
| 75 | } |
| 76 | if e.Tool.Profile != nil { |
| 77 | wt.Profile = &Profile{Model: e.Tool.Profile.Model, Effort: e.Tool.Profile.Effort} |
| 78 | } |
| 79 | if e.Tool.Execution != nil { |
| 80 | wt.Execution = toWireShellExecution(e.Tool.Execution) |
| 81 | } |
| 82 | w.Tool = wt |
| 83 | case event.Usage: |
| 84 | if u := e.Usage; u != nil { |
| 85 | w.Usage = &Usage{ |
| 86 | PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens, |
| 87 | TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens, |
| 88 | CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens, |
| 89 | Estimated: u.Estimated, |
| 90 | Source: e.UsageSource, |
| 91 | ContextPromptTokens: u.ContextPromptTokens, |
| 92 | ContextCompletionTokens: u.ContextCompletionTokens, |
| 93 | ContextReasoningTokens: u.ContextReasoningTokens, |
| 94 | ContextCacheHitTokens: u.ContextCacheHitTokens, |
| 95 | ContextCacheMissTokens: u.ContextCacheMissTokens, |
| 96 | SessionCacheHitTokens: e.SessionHit, SessionCacheMissTokens: e.SessionMiss, |
| 97 | } |
| 98 | if e.CacheDiagnostics != nil { |
| 99 | w.Usage.CacheDiagnostics = ToWireCacheDiagnostics(e.CacheDiagnostics) |
| 100 | } |
| 101 | if e.Pricing != nil { |
| 102 | cost := e.Pricing.Cost(u) |
| 103 | w.Usage.Cost = cost |
| 104 | w.Usage.Currency = e.Pricing.Symbol() |
| 105 | w.Usage.CostUSD = cost |
| 106 | } |
| 107 | } |
| 108 | case event.ApprovalRequest: |
| 109 | w.Approval = &Approval{ |
| 110 | ID: e.Approval.ID, Tool: e.Approval.Tool, Subject: e.Approval.Subject, |
| 111 | Reason: e.Approval.Reason, Fresh: e.Approval.Fresh, Kind: e.Approval.Kind, |
| 112 | } |
| 113 | if e.Approval.Recovery != nil { |
| 114 | r := e.Approval.Recovery |
| 115 | w.Approval.Recovery = &RecoveryApproval{ |
| 116 | SourceAgent: r.SourceAgent, |
| 117 | FailedTool: r.FailedTool, |
| 118 | FailedSummary: r.FailedSummary, |
| 119 | Diagnosis: r.Diagnosis, |
| 120 | NextTool: r.NextTool, |
| 121 | NextAction: r.NextAction, |
| 122 | ChangeKind: r.ChangeKind, |
| 123 | ChangeRationale: r.ChangeRationale, |
| 124 | ReviewRationale: r.ReviewRationale, |
| 125 | PlanBefore: r.PlanBefore, |
| 126 | PlanAfter: r.PlanAfter, |
| 127 | CanGrantTask: r.CanGrantTask, |
| 128 | TaskGrantScope: r.TaskGrantScope, |
| 129 | } |
| 130 | } |
| 131 | case event.AskRequest: |
| 132 | w.Ask = ToWireAsk(e.Ask) |
| 133 | case event.CompactionStarted, event.CompactionDone: |
| 134 | w.Compaction = &Compaction{ |
| 135 | Trigger: e.Compaction.Trigger, Messages: e.Compaction.Messages, |
| 136 | Summary: e.Compaction.Summary, Archive: e.Compaction.Archive, |
| 137 | } |
| 138 | case event.GuardianAssessment: |
| 139 | w.Guardian = ToWireGuardian(e.Guardian) |
| 140 | case event.ExtensionSurface, event.ExtensionStatus: |
| 141 | w.Extension = ToWireExtensionSurface(e.Extension) |
| 142 | case event.TurnDone: |
| 143 | w.Outcome = e.Outcome |
| 144 | if e.Readiness != nil { |
| 145 | w.Readiness = &FinalReadiness{Attempts: e.Readiness.Attempts, Missing: append([]string(nil), e.Readiness.Missing...)} |
| 146 | } |
| 147 | if e.Err != nil { |
| 148 | w.Err = e.Err.Error() |
| 149 | } |
| 150 | case event.Retrying: |
| 151 | w.RetryAttempt = e.RetryAttempt |
| 152 | w.RetryMax = e.RetryMax |
| 153 | if e.RetryScope != "" { |
| 154 | w.RetryScope = string(e.RetryScope) |
| 155 | } |
| 156 | case event.StreamAttempt: |
| 157 | w.StreamAttempt = &StreamAttempt{ |
| 158 | ID: e.StreamAttempt.ID, |
| 159 | Action: string(e.StreamAttempt.Action), |
| 160 | Attempt: e.StreamAttempt.Attempt, |
| 161 | Max: e.StreamAttempt.Max, |
| 162 | Reason: e.StreamAttempt.Reason, |
| 163 | } |
| 164 | } |
| 165 | return w |
| 166 | } |
| 167 | |
| 168 | // DecisionReceipt is the JSON form of a provider-excluded user decision. |
| 169 | type DecisionReceipt struct { |
| 170 | ID string `json:"id"` |
| 171 | Kind string `json:"kind"` |
| 172 | Tool string `json:"tool,omitempty"` |
| 173 | Subject string `json:"subject,omitempty"` |
| 174 | Outcome string `json:"outcome"` |
| 175 | } |
| 176 | |
| 177 | func ToWireDecisionReceipt(in *provider.DecisionReceipt) *DecisionReceipt { |
| 178 | if in == nil { |
| 179 | return nil |
| 180 | } |
| 181 | return &DecisionReceipt{ID: in.ID, Kind: in.Kind, Tool: in.Tool, Subject: in.Subject, Outcome: in.Outcome} |
| 182 | } |
| 183 | |
| 184 | type FinalReadiness struct { |
| 185 | Attempts int `json:"attempts,omitempty"` |
| 186 | Missing []string `json:"missing,omitempty"` |
| 187 | } |
| 188 | |
| 189 | // MemoryCitation is the JSON form of provider.MemoryCitation. |
| 190 | type MemoryCitation struct { |
| 191 | ID string `json:"id,omitempty"` |
| 192 | Source string `json:"source"` |
| 193 | LineStart int `json:"lineStart,omitempty"` |
| 194 | LineEnd int `json:"lineEnd,omitempty"` |
| 195 | Note string `json:"note,omitempty"` |
| 196 | Kind string `json:"kind,omitempty"` |
| 197 | } |
| 198 | |
| 199 | // ToWireMemoryCitations converts local memory references into frontend JSON. |
| 200 | func ToWireMemoryCitations(in []provider.MemoryCitation) []MemoryCitation { |
| 201 | out := make([]MemoryCitation, 0, len(in)) |
| 202 | for _, c := range in { |
| 203 | if c.Source == "" && c.ID == "" && c.Note == "" { |
| 204 | continue |
| 205 | } |
| 206 | out = append(out, MemoryCitation{ |
| 207 | ID: c.ID, |
| 208 | Source: c.Source, |
| 209 | LineStart: c.LineStart, |
| 210 | LineEnd: c.LineEnd, |
| 211 | Note: c.Note, |
| 212 | Kind: c.Kind, |
| 213 | }) |
| 214 | } |
| 215 | return out |
| 216 | } |
| 217 | |
| 218 | // Compaction is the JSON form of an event.Compaction. |
| 219 | type Compaction struct { |
| 220 | Trigger string `json:"trigger,omitempty"` |
| 221 | Messages int `json:"messages,omitempty"` |
| 222 | Summary string `json:"summary,omitempty" externalizable:"true"` |
| 223 | Archive string `json:"archive,omitempty" externalizable:"true"` |
| 224 | } |
| 225 | |
| 226 | // AskOption is one JSON-formatted choice in a structured ask request. |
| 227 | type AskOption struct { |
| 228 | Label string `json:"label"` |
| 229 | Description string `json:"description,omitempty" externalizable:"true"` |
| 230 | } |
| 231 | |
| 232 | // AskQuestion is one JSON-formatted structured ask question. |
| 233 | type AskQuestion struct { |
| 234 | ID string `json:"id"` |
| 235 | Header string `json:"header,omitempty"` |
| 236 | Prompt string `json:"prompt" externalizable:"true"` |
| 237 | Options []AskOption `json:"options"` |
| 238 | Multi bool `json:"multi,omitempty"` |
| 239 | } |
| 240 | |
| 241 | // Ask is the JSON form of an event.Ask. |
| 242 | type Ask struct { |
| 243 | ID string `json:"id"` |
| 244 | Questions []AskQuestion `json:"questions"` |
| 245 | } |
| 246 | |
| 247 | // Profile carries the subagent model/effort resolved for a tool call. |
| 248 | type Profile struct { |
| 249 | Model string `json:"model,omitempty"` |
| 250 | Effort string `json:"effort,omitempty"` |
| 251 | } |
| 252 | |
| 253 | // Tool is the JSON form of an event.Tool. |
| 254 | type Tool struct { |
| 255 | ID string `json:"id,omitempty"` |
| 256 | Name string `json:"name"` |
| 257 | Args string `json:"args,omitempty" externalizable:"true"` |
| 258 | ResolvedName string `json:"resolvedName,omitempty"` |
| 259 | CapabilityID string `json:"capabilityId,omitempty"` |
| 260 | Output string `json:"output,omitempty" externalizable:"true"` |
| 261 | Err string `json:"err,omitempty" externalizable:"true"` |
| 262 | ReadOnly bool `json:"readOnly"` |
| 263 | Truncated bool `json:"truncated,omitempty"` |
| 264 | DurationMs int64 `json:"durationMs,omitempty"` |
| 265 | Partial bool `json:"partial,omitempty"` |
| 266 | ArgChars int `json:"argChars,omitempty"` |
| 267 | Refreshed bool `json:"refreshed,omitempty"` |
| 268 | ParentID string `json:"parentId,omitempty"` |
| 269 | AttemptID string `json:"attemptId,omitempty"` // host-local stream_attempt id for speculative partials |
| 270 | Diff string `json:"diff,omitempty" externalizable:"true"` |
| 271 | Added int `json:"added,omitempty"` |
| 272 | Removed int `json:"removed,omitempty"` |
| 273 | Profile *Profile `json:"profile,omitempty"` |
| 274 | Execution *ShellExecution `json:"execution,omitempty"` |
| 275 | } |
| 276 | |
| 277 | // ShellExecution is the JSON form of event.ShellExecution (local UI metadata). |
| 278 | type ShellExecution struct { |
| 279 | Kind string `json:"kind,omitempty"` |
| 280 | Shell string `json:"shell,omitempty"` |
| 281 | ShellVersion string `json:"shellVersion,omitempty"` |
| 282 | Platform string `json:"platform,omitempty"` |
| 283 | SupportsAndAnd bool `json:"supportsAndAnd"` |
| 284 | State string `json:"state,omitempty"` |
| 285 | FailurePhase string `json:"failurePhase,omitempty"` |
| 286 | ExitCode *int `json:"exitCode,omitempty"` |
| 287 | OutputTail string `json:"outputTail,omitempty"` |
| 288 | MutationRisk string `json:"mutationRisk,omitempty"` |
| 289 | Verification string `json:"verification,omitempty"` |
| 290 | DurationMs int64 `json:"durationMs,omitempty"` |
| 291 | } |
| 292 | |
| 293 | func toWireShellExecution(in *event.ShellExecution) *ShellExecution { |
| 294 | if in == nil { |
| 295 | return nil |
| 296 | } |
| 297 | out := &ShellExecution{ |
| 298 | Kind: in.Kind, Shell: in.Shell, ShellVersion: in.ShellVersion, |
| 299 | Platform: in.Platform, SupportsAndAnd: in.SupportsAndAnd, |
| 300 | State: in.State, FailurePhase: in.FailurePhase, |
| 301 | OutputTail: in.OutputTail, MutationRisk: in.MutationRisk, |
| 302 | Verification: in.Verification, DurationMs: in.DurationMs, |
| 303 | } |
| 304 | if in.ExitCode != nil { |
| 305 | code := *in.ExitCode |
| 306 | out.ExitCode = &code |
| 307 | } |
| 308 | return out |
| 309 | } |
| 310 | |
| 311 | // Usage is the JSON form of provider usage telemetry. |
| 312 | type Usage struct { |
| 313 | PromptTokens int `json:"promptTokens"` |
| 314 | CompletionTokens int `json:"completionTokens"` |
| 315 | TotalTokens int `json:"totalTokens"` |
| 316 | CacheHitTokens int `json:"cacheHitTokens"` |
| 317 | CacheMissTokens int `json:"cacheMissTokens"` |
| 318 | ReasoningTokens int `json:"reasoningTokens,omitempty"` |
| 319 | Estimated bool `json:"estimated,omitempty"` |
| 320 | Source string `json:"source,omitempty"` |
| 321 | CacheDiagnostics *CacheDiagnostics `json:"cacheDiagnostics,omitempty"` |
| 322 | // Session-cumulative cache tokens keep status displays steadier than one-turn values. |
| 323 | SessionCacheHitTokens int `json:"sessionCacheHitTokens"` |
| 324 | SessionCacheMissTokens int `json:"sessionCacheMissTokens"` |
| 325 | // Context* fields are the latest single-request shape for gauges/rebind. |
| 326 | // When omitted, clients fall back to the billable prompt/completion totals. |
| 327 | ContextPromptTokens int `json:"contextPromptTokens,omitempty"` |
| 328 | ContextCompletionTokens int `json:"contextCompletionTokens,omitempty"` |
| 329 | ContextReasoningTokens int `json:"contextReasoningTokens,omitempty"` |
| 330 | ContextCacheHitTokens int `json:"contextCacheHitTokens,omitempty"` |
| 331 | ContextCacheMissTokens int `json:"contextCacheMissTokens,omitempty"` |
| 332 | Cost float64 `json:"cost,omitempty"` |
| 333 | Currency string `json:"currency,omitempty"` |
| 334 | // CostUSD is a compatibility alias for older consumers; it mirrors Cost. |
| 335 | CostUSD float64 `json:"costUsd,omitempty"` |
| 336 | } |
| 337 | |
| 338 | // CacheDiagnostics is the JSON form of cache prefix diagnostics. |
| 339 | type CacheDiagnostics struct { |
| 340 | PrefixHash string `json:"prefixHash"` |
| 341 | PrefixChanged bool `json:"prefixChanged"` |
| 342 | PrefixChangeReasons []string `json:"prefixChangeReasons,omitempty"` |
| 343 | SystemHash string `json:"systemHash"` |
| 344 | ToolsHash string `json:"toolsHash"` |
| 345 | LogRewriteVersion int `json:"logRewriteVersion"` |
| 346 | ToolSchemaTokens int `json:"toolSchemaTokens"` |
| 347 | CacheMissTokens int `json:"cacheMissTokens"` |
| 348 | CacheHitTokens int `json:"cacheHitTokens"` |
| 349 | } |
| 350 | |
| 351 | // Approval is the JSON form of an event.Approval. |
| 352 | type Approval struct { |
| 353 | ID string `json:"id"` |
| 354 | Tool string `json:"tool"` |
| 355 | Subject string `json:"subject" externalizable:"true"` |
| 356 | Reason string `json:"reason,omitempty" externalizable:"true"` |
| 357 | Fresh bool `json:"fresh,omitempty"` |
| 358 | Kind string `json:"kind,omitempty"` // tool | plan | recovery |
| 359 | Recovery *RecoveryApproval `json:"recovery,omitempty"` |
| 360 | } |
| 361 | |
| 362 | // RecoveryApproval is the JSON form of an event.RecoveryApproval. |
| 363 | type RecoveryApproval struct { |
| 364 | SourceAgent string `json:"source_agent,omitempty"` |
| 365 | FailedTool string `json:"failed_tool,omitempty"` |
| 366 | FailedSummary string `json:"failed_summary,omitempty"` |
| 367 | Diagnosis string `json:"diagnosis,omitempty"` |
| 368 | NextTool string `json:"next_tool,omitempty"` |
| 369 | NextAction string `json:"next_action,omitempty"` |
| 370 | ChangeKind string `json:"change_kind,omitempty"` |
| 371 | ChangeRationale string `json:"change_rationale,omitempty"` |
| 372 | ReviewRationale string `json:"review_rationale,omitempty"` |
| 373 | PlanBefore string `json:"plan_before,omitempty"` |
| 374 | PlanAfter string `json:"plan_after,omitempty"` |
| 375 | CanGrantTask bool `json:"can_grant_task,omitempty"` |
| 376 | TaskGrantScope string `json:"task_grant_scope,omitempty"` |
| 377 | } |
| 378 | |
| 379 | // Guardian is the JSON form of an event.GuardianResult. |
| 380 | type Guardian struct { |
| 381 | ID string `json:"id"` |
| 382 | Tool string `json:"tool"` |
| 383 | Subject string `json:"subject"` |
| 384 | Outcome string `json:"outcome"` |
| 385 | RiskLevel string `json:"risk_level,omitempty"` |
| 386 | UserAuthorization string `json:"user_authorization,omitempty"` |
| 387 | Rationale string `json:"rationale,omitempty" externalizable:"true"` |
| 388 | DurationMs int64 `json:"duration_ms,omitempty"` |
| 389 | Usage *Usage `json:"usage,omitempty"` |
| 390 | } |
| 391 | |
| 392 | // ToWireGuardian converts an event.GuardianResult into its JSON wire form. |
| 393 | func ToWireGuardian(g event.GuardianResult) *Guardian { |
| 394 | out := &Guardian{ |
| 395 | ID: g.ID, |
| 396 | Tool: g.Tool, |
| 397 | Subject: g.Subject, |
| 398 | Outcome: g.Outcome, |
| 399 | RiskLevel: g.RiskLevel, |
| 400 | UserAuthorization: g.UserAuthorization, |
| 401 | Rationale: g.Rationale, |
| 402 | DurationMs: g.DurationMs, |
| 403 | } |
| 404 | if u := g.Usage; u != nil { |
| 405 | out.Usage = &Usage{ |
| 406 | PromptTokens: u.PromptTokens, CompletionTokens: u.CompletionTokens, |
| 407 | TotalTokens: u.TotalTokens, CacheHitTokens: u.CacheHitTokens, |
| 408 | CacheMissTokens: u.CacheMissTokens, ReasoningTokens: u.ReasoningTokens, |
| 409 | Estimated: u.Estimated, |
| 410 | } |
| 411 | if g.Pricing != nil { |
| 412 | cost := g.Pricing.Cost(u) |
| 413 | out.Usage.Cost = cost |
| 414 | out.Usage.Currency = g.Pricing.Symbol() |
| 415 | out.Usage.CostUSD = cost |
| 416 | } |
| 417 | } |
| 418 | return out |
| 419 | } |
| 420 | |
| 421 | // ToWireAsk converts an event.Ask into its JSON wire form. |
| 422 | func ToWireAsk(a event.Ask) *Ask { |
| 423 | qs := make([]AskQuestion, len(a.Questions)) |
| 424 | for i, q := range a.Questions { |
| 425 | opts := make([]AskOption, len(q.Options)) |
| 426 | for j, o := range q.Options { |
| 427 | opts[j] = AskOption{Label: o.Label, Description: o.Description} |
| 428 | } |
| 429 | qs[i] = AskQuestion{ID: q.ID, Header: q.Header, Prompt: q.Prompt, Options: opts, Multi: q.Multi} |
| 430 | } |
| 431 | return &Ask{ID: a.ID, Questions: qs} |
| 432 | } |
| 433 | |
| 434 | // ToWireCacheDiagnostics converts cache diagnostics into their JSON wire form. |
| 435 | func ToWireCacheDiagnostics(d *event.CacheDiagnostics) *CacheDiagnostics { |
| 436 | return &CacheDiagnostics{ |
| 437 | PrefixHash: d.PrefixHash, |
| 438 | PrefixChanged: d.PrefixChanged, |
| 439 | PrefixChangeReasons: append([]string(nil), d.PrefixChangeReasons...), |
| 440 | SystemHash: d.SystemHash, |
| 441 | ToolsHash: d.ToolsHash, |
| 442 | LogRewriteVersion: d.LogRewriteVersion, |
| 443 | ToolSchemaTokens: d.ToolSchemaTokens, |
| 444 | CacheMissTokens: d.CacheMissTokens, |
| 445 | CacheHitTokens: d.CacheHitTokens, |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | // KindNames returns every stable frontend event kind in event.Kind order. It is |
| 450 | // the protocol-neutral source used by consumers such as the Remote schema |
| 451 | // generator; callers receive a copy and may sort it without mutating eventwire. |
| 452 | func KindNames() []string { |
| 453 | names := make([]string, 0, int(event.KindCount)) |
| 454 | for kind := event.Kind(0); kind < event.KindCount; kind++ { |
| 455 | if name, ok := kindNames[kind]; ok { |
| 456 | names = append(names, name) |
| 457 | } |
| 458 | } |
| 459 | return names |
| 460 | } |
| 461 | |
| 462 | // KindName returns the stable wire name of one event kind, or false for a |
| 463 | // kind outside the known set. |
| 464 | func KindName(kind event.Kind) (string, bool) { |
| 465 | name, ok := kindNames[kind] |
| 466 | return name, ok |
| 467 | } |
| 468 | |
| 469 | var kindNames = map[event.Kind]string{ |
| 470 | event.TurnStarted: "turn_started", |
| 471 | event.Reasoning: "reasoning", |
| 472 | event.Text: "text", |
| 473 | event.Message: "message", |
| 474 | event.ToolDispatch: "tool_dispatch", |
| 475 | event.ToolResult: "tool_result", |
| 476 | event.Usage: "usage", |
| 477 | event.Notice: "notice", |
| 478 | event.Phase: "phase", |
| 479 | event.ApprovalRequest: "approval_request", |
| 480 | event.AskRequest: "ask_request", |
| 481 | event.TurnDone: "turn_done", |
| 482 | event.CompactionStarted: "compaction_started", |
| 483 | event.CompactionDone: "compaction_done", |
| 484 | event.ToolProgress: "tool_progress", |
| 485 | event.MCPSurfaceReady: "mcp_surface_ready", |
| 486 | event.Retrying: "retrying", |
| 487 | event.Steer: "steer", |
| 488 | event.GuardianAssessment: "guardian_assessment", |
| 489 | event.ExtensionSurface: "extension_surface", |
| 490 | event.ExtensionStatus: "extension_status", |
| 491 | event.StreamAttempt: "stream_attempt", |
| 492 | } |
| 493 | |
| 494 | // ExtensionSurface is the JSON form of an event.ExtensionSurfacePayload. |
| 495 | type ExtensionSurface struct { |
| 496 | PluginID string `json:"pluginId"` |
| 497 | SurfaceID string `json:"surfaceId"` |
| 498 | SessionID string `json:"sessionId,omitempty"` |
| 499 | Generation uint64 `json:"generation,omitempty"` |
| 500 | Kind string `json:"kind"` |
| 501 | Status *ExtensionStatus `json:"status,omitempty"` |
| 502 | Card *ExtensionCard `json:"card,omitempty"` |
| 503 | Form *ExtensionForm `json:"form,omitempty"` |
| 504 | Notification *ExtensionNotification `json:"notification,omitempty"` |
| 505 | } |
| 506 | |
| 507 | // ExtensionStatus is the JSON form of an event.ExtensionStatusView. |
| 508 | type ExtensionStatus struct { |
| 509 | Label string `json:"label"` |
| 510 | Detail string `json:"detail,omitempty"` |
| 511 | Severity string `json:"severity,omitempty"` |
| 512 | Progress *float64 `json:"progress,omitempty"` |
| 513 | } |
| 514 | |
| 515 | // ExtensionKeyValue is the JSON form of an event.ExtensionKeyValue. |
| 516 | type ExtensionKeyValue struct { |
| 517 | Key string `json:"key"` |
| 518 | Value string `json:"value"` |
| 519 | } |
| 520 | |
| 521 | // ExtensionActionRef is the JSON form of an event.ExtensionActionRef. |
| 522 | type ExtensionActionRef struct { |
| 523 | ActionID string `json:"actionId"` |
| 524 | Label string `json:"label"` |
| 525 | } |
| 526 | |
| 527 | // ExtensionCard is the JSON form of an event.ExtensionCardView. |
| 528 | type ExtensionCard struct { |
| 529 | Title string `json:"title,omitempty"` |
| 530 | Markdown string `json:"markdown,omitempty" externalizable:"true"` |
| 531 | Text string `json:"text,omitempty" externalizable:"true"` |
| 532 | Fields []ExtensionKeyValue `json:"fields,omitempty"` |
| 533 | Progress *float64 `json:"progress,omitempty"` |
| 534 | Actions []ExtensionActionRef `json:"actions,omitempty"` |
| 535 | } |
| 536 | |
| 537 | // ExtensionFormField is the JSON form of an event.ExtensionFormField. Default |
| 538 | // travels as raw JSON: the remote schema has no "any" type, and the field is |
| 539 | // already protocol-validated JSON on arrival. |
| 540 | type ExtensionFormField struct { |
| 541 | Key string `json:"key"` |
| 542 | Label string `json:"label,omitempty"` |
| 543 | Kind string `json:"kind,omitempty"` |
| 544 | Options []string `json:"options,omitempty"` |
| 545 | Default json.RawMessage `json:"default,omitempty"` |
| 546 | Required bool `json:"required,omitempty"` |
| 547 | } |
| 548 | |
| 549 | // ExtensionForm is the JSON form of an event.ExtensionFormView. |
| 550 | type ExtensionForm struct { |
| 551 | Title string `json:"title,omitempty"` |
| 552 | Message string `json:"message,omitempty" externalizable:"true"` |
| 553 | Fields []ExtensionFormField `json:"fields"` |
| 554 | } |
| 555 | |
| 556 | // ExtensionNotification is the JSON form of an event.ExtensionNotificationView. |
| 557 | type ExtensionNotification struct { |
| 558 | Title string `json:"title"` |
| 559 | Body string `json:"body,omitempty" externalizable:"true"` |
| 560 | Severity string `json:"severity,omitempty"` |
| 561 | } |
| 562 | |
| 563 | // ToWireExtensionSurface converts an event.ExtensionSurfacePayload into its |
| 564 | // JSON wire form. A nil payload yields nil so a malformed event never |
| 565 | // marshals a half-filled extension object. |
| 566 | func ToWireExtensionSurface(p *event.ExtensionSurfacePayload) *ExtensionSurface { |
| 567 | if p == nil { |
| 568 | return nil |
| 569 | } |
| 570 | out := &ExtensionSurface{ |
| 571 | PluginID: p.PluginID, SurfaceID: p.SurfaceID, SessionID: p.SessionID, |
| 572 | Generation: p.Generation, Kind: p.Kind, |
| 573 | } |
| 574 | if s := p.Status; s != nil { |
| 575 | out.Status = &ExtensionStatus{Label: s.Label, Detail: s.Detail, Severity: s.Severity, Progress: s.Progress} |
| 576 | } |
| 577 | if c := p.Card; c != nil { |
| 578 | card := &ExtensionCard{ |
| 579 | Title: c.Title, Markdown: c.Markdown, Text: c.Text, Progress: c.Progress, |
| 580 | } |
| 581 | if len(c.Fields) > 0 { |
| 582 | card.Fields = make([]ExtensionKeyValue, len(c.Fields)) |
| 583 | for i, f := range c.Fields { |
| 584 | card.Fields[i] = ExtensionKeyValue{Key: f.Key, Value: f.Value} |
| 585 | } |
| 586 | } |
| 587 | if len(c.Actions) > 0 { |
| 588 | card.Actions = make([]ExtensionActionRef, len(c.Actions)) |
| 589 | for i, a := range c.Actions { |
| 590 | card.Actions[i] = ExtensionActionRef{ActionID: a.ActionID, Label: a.Label} |
| 591 | } |
| 592 | } |
| 593 | out.Card = card |
| 594 | } |
| 595 | if f := p.Form; f != nil { |
| 596 | form := &ExtensionForm{Title: f.Title, Message: f.Message} |
| 597 | if len(f.Fields) > 0 { |
| 598 | form.Fields = make([]ExtensionFormField, len(f.Fields)) |
| 599 | for i, field := range f.Fields { |
| 600 | wireField := ExtensionFormField{ |
| 601 | Key: field.Key, Label: field.Label, Kind: field.Kind, |
| 602 | Options: append([]string(nil), field.Options...), |
| 603 | Required: field.Required, |
| 604 | } |
| 605 | if field.Default != nil { |
| 606 | // The value arrived as protocol-validated JSON, so a |
| 607 | // marshal failure here is unreachable in practice; a |
| 608 | // pathological in-memory value simply drops the default. |
| 609 | if raw, err := json.Marshal(field.Default); err == nil { |
| 610 | wireField.Default = raw |
| 611 | } |
| 612 | } |
| 613 | form.Fields[i] = wireField |
| 614 | } |
| 615 | } |
| 616 | out.Form = form |
| 617 | } |
| 618 | if n := p.Notification; n != nil { |
| 619 | out.Notification = &ExtensionNotification{Title: n.Title, Body: n.Body, Severity: n.Severity} |
| 620 | } |
| 621 | return out |
| 622 | } |
| 623 |