@devspecai/pi-plugin 0.0.0-stage → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +899 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -2
  4. package/extensions/.gitkeep +0 -0
  5. package/extensions/core/activation-recovery.ts +135 -0
  6. package/extensions/core/active-deliveries.ts +171 -0
  7. package/extensions/core/active-session-plans.ts +374 -0
  8. package/extensions/core/adapter-overlap.ts +247 -0
  9. package/extensions/core/assistant-speech.ts +44 -0
  10. package/extensions/core/attach-context.ts +25 -0
  11. package/extensions/core/attachment-limits.ts +20 -0
  12. package/extensions/core/attachments.ts +487 -0
  13. package/extensions/core/auth-store.ts +91 -0
  14. package/extensions/core/automation-run-retry.ts +31 -0
  15. package/extensions/core/background-journal.ts +85 -0
  16. package/extensions/core/background-observer.ts +279 -0
  17. package/extensions/core/bond-recovery.ts +65 -0
  18. package/extensions/core/bond-state.ts +80 -0
  19. package/extensions/core/clear-conversation.ts +53 -0
  20. package/extensions/core/client-versions.ts +56 -0
  21. package/extensions/core/client.ts +704 -0
  22. package/extensions/core/command-offer.ts +88 -0
  23. package/extensions/core/command-runner.ts +62 -0
  24. package/extensions/core/command-turn.ts +88 -0
  25. package/extensions/core/commit-ledger.ts +195 -0
  26. package/extensions/core/commit-stamp.ts +98 -0
  27. package/extensions/core/config.ts +63 -0
  28. package/extensions/core/connection-context.ts +28 -0
  29. package/extensions/core/context-budget.ts +175 -0
  30. package/extensions/core/control-result-outbox.ts +80 -0
  31. package/extensions/core/control.ts +156 -0
  32. package/extensions/core/ctx-guard.ts +33 -0
  33. package/extensions/core/debug.ts +136 -0
  34. package/extensions/core/delivery-queue.ts +92 -0
  35. package/extensions/core/dispatch.ts +108 -0
  36. package/extensions/core/duck.ts +128 -0
  37. package/extensions/core/git-observer.ts +382 -0
  38. package/extensions/core/github-identity.ts +26 -0
  39. package/extensions/core/implementation-contract.ts +773 -0
  40. package/extensions/core/instructions.ts +40 -0
  41. package/extensions/core/interaction-events.ts +834 -0
  42. package/extensions/core/json-schema.ts +16 -0
  43. package/extensions/core/local-activity.ts +34 -0
  44. package/extensions/core/mcp-config-guard.ts +117 -0
  45. package/extensions/core/model-catalog.ts +76 -0
  46. package/extensions/core/model-surface.ts +125 -0
  47. package/extensions/core/notice-reply.ts +215 -0
  48. package/extensions/core/owner-inject.ts +148 -0
  49. package/extensions/core/post-session-receipt.ts +70 -0
  50. package/extensions/core/preempt.ts +100 -0
  51. package/extensions/core/project-choice.ts +145 -0
  52. package/extensions/core/project-scope.ts +335 -0
  53. package/extensions/core/project-selection-ui.ts +202 -0
  54. package/extensions/core/provenance-refs.ts +158 -0
  55. package/extensions/core/push-channel.ts +281 -0
  56. package/extensions/core/question-receipt-lifecycle.ts +158 -0
  57. package/extensions/core/recall.ts +121 -0
  58. package/extensions/core/remote-payload.ts +1329 -0
  59. package/extensions/core/remote-session-arg.ts +24 -0
  60. package/extensions/core/repository-context.ts +35 -0
  61. package/extensions/core/request-chain.ts +1244 -0
  62. package/extensions/core/result-projection.ts +939 -0
  63. package/extensions/core/result.ts +100 -0
  64. package/extensions/core/review-context.ts +70 -0
  65. package/extensions/core/review-watch.ts +94 -0
  66. package/extensions/core/room-access.ts +12 -0
  67. package/extensions/core/room-context.ts +178 -0
  68. package/extensions/core/room-reader.ts +308 -0
  69. package/extensions/core/room-store.ts +381 -0
  70. package/extensions/core/room-sync.ts +79 -0
  71. package/extensions/core/room-tools.ts +86 -0
  72. package/extensions/core/room-unread.ts +173 -0
  73. package/extensions/core/session-events.ts +62 -0
  74. package/extensions/core/session-polls.ts +292 -0
  75. package/extensions/core/settings.ts +39 -0
  76. package/extensions/core/status-line.ts +432 -0
  77. package/extensions/core/still-to-discuss.ts +124 -0
  78. package/extensions/core/subagent-fleet.ts +238 -0
  79. package/extensions/core/system-notice.ts +227 -0
  80. package/extensions/core/telemetry.ts +177 -0
  81. package/extensions/core/tool-selection.ts +72 -0
  82. package/extensions/core/trail-events.ts +247 -0
  83. package/extensions/core/trail-projection.ts +373 -0
  84. package/extensions/core/trail.ts +465 -0
  85. package/extensions/core/turn-lifecycle.ts +472 -0
  86. package/extensions/core/version-mismatch.ts +165 -0
  87. package/extensions/core/work-prompt.ts +44 -0
  88. package/extensions/core/work-state.ts +179 -0
  89. package/extensions/devspec-adapter-guard.ts +106 -0
  90. package/extensions/devspec-auth.ts +191 -0
  91. package/extensions/devspec-brand.ts +140 -0
  92. package/extensions/devspec-commit-ledger.ts +477 -0
  93. package/extensions/devspec-context.ts +146 -0
  94. package/extensions/devspec-memory.ts +136 -0
  95. package/extensions/devspec-provenance-assistance.ts +233 -0
  96. package/extensions/devspec-remote.ts +5152 -0
  97. package/extensions/devspec-tools.ts +552 -0
  98. package/extensions/devspec-work.ts +244 -0
  99. package/extensions/generated/README.md +18 -0
  100. package/extensions/generated/mcp-catalog.ts +15811 -0
  101. package/package.json +71 -4
  102. package/prompts/.gitkeep +0 -0
  103. package/runtime/commit-stamp.mjs +279 -0
  104. package/runtime/git-observer-shim.mjs +390 -0
  105. package/skills/.gitkeep +0 -0
  106. package/themes/.gitkeep +0 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * DevSpec Pi — turn trail builder (pure, unit-tested).
3
+ *
4
+ * Renders the cumulative, terminal-style work trail for a turn from pi's
5
+ * streaming events. Posted by the extension as phase:'trail' — fully
6
+ * mechanical, zero tokens. The server grows ONE live bubble per turn and
7
+ * preserves it as the collapsed "Show work" under the final answer.
8
+ */
9
+
10
+ import {
11
+ classifyDomainTool,
12
+ domainAction,
13
+ domainIdentity,
14
+ writeOutcomeFromResult,
15
+ type DomainTrailEvent,
16
+ type SubagentTrailChild,
17
+ type SubagentTrailEvent,
18
+ type TrailEvent,
19
+ type ToolTrailEvent,
20
+ type TrailWriteOutcome,
21
+ } from "./trail-events.ts";
22
+ import { projectTrailArgs, projectTrailResult, safeTrailLabel } from "./trail-projection.ts";
23
+ import type { SubagentFleetSnapshot } from "./subagent-fleet.ts";
24
+
25
+ /**
26
+ * Render cap for one trail snapshot. The server folds sliding-window snapshots
27
+ * on a 24-char tail anchor (item 95db8479), so a head-trimmed window merges
28
+ * cleanly — UNLESS a single inter-post burst exceeds the whole window (the
29
+ * anchor scrolls out and one overlap region appends). 12k makes that burst
30
+ * rare while keeping each post cheap to carry.
31
+ */
32
+ const MAX_TRAIL_CHARS = 12_000;
33
+
34
+ /** Cap for the FINAL render that rides the answer post (item 9b7455df). The
35
+ * server REPLACES the folded accumulation with it, so capping at the live
36
+ * 12k would discard earlier rounds the fold had preserved — the trail is
37
+ * run-scoped now (cc9e8c43) and a long run accumulates many rounds. */
38
+ export const FINAL_TRAIL_CHARS = 48_000;
39
+
40
+ type TrailEntry =
41
+ | { kind: "thinking"; text: string; seq: number; occurredAt: string }
42
+ | { kind: "text"; text: string; seq: number }
43
+ | { kind: "tool"; line: ToolLine; seq: number };
44
+
45
+ interface ToolLine {
46
+ toolCallId: string;
47
+ name: string;
48
+ argsSummary?: string;
49
+ identity: { id?: string; title?: string };
50
+ open: boolean;
51
+ ok?: boolean;
52
+ resultSummary?: string;
53
+ /** Public disposition selected from explicit receipt fields, never inferred from prose. */
54
+ writeOutcome?: TrailWriteOutcome;
55
+ subagentChildren?: SubagentTrailChild[];
56
+ subagentSummary?: string;
57
+ startedAt: number;
58
+ startedAtIso: string;
59
+ completedAtIso?: string;
60
+ durationMs?: number;
61
+ }
62
+
63
+ /** Privacy-safe one-line metadata selected for a known tool's arguments. */
64
+ export function summarizeArgs(name: string, args: unknown): string | undefined {
65
+ return projectTrailArgs(name, args);
66
+ }
67
+
68
+ /** Privacy-safe outcome metadata selected for a known tool's result. */
69
+ export function summarizeResult(name: string, result: unknown): string | undefined {
70
+ return projectTrailResult(name, result);
71
+ }
72
+
73
+ function asRecord(value: unknown): Record<string, unknown> | null {
74
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
75
+ }
76
+
77
+ function toolDetails(value: unknown): Record<string, unknown> | null {
78
+ const record = asRecord(value);
79
+ return asRecord(record?.details) ?? record;
80
+ }
81
+
82
+ function statusForChild(value: unknown): SubagentTrailChild["status"] {
83
+ if (value === "running" || value === "completed" || value === "failed" || value === "pending" || value === "detached" || value === "stopped" || value === "paused") return value;
84
+ if (value === "complete") return "completed";
85
+ return "pending";
86
+ }
87
+
88
+ function isoFromMs(value: unknown): string | undefined {
89
+ return typeof value === "number" && Number.isFinite(value) && value > 0
90
+ ? new Date(value).toISOString()
91
+ : undefined;
92
+ }
93
+
94
+ function safeChildField(value: unknown, maxChars = 80): string | undefined {
95
+ const label = safeTrailLabel(value, maxChars);
96
+ return label && /^[A-Za-z0-9 ._:/@+-]+$/.test(label) ? label : undefined;
97
+ }
98
+
99
+ function safeThinkingLevel(value: unknown): string | undefined {
100
+ if (typeof value !== "string") return undefined;
101
+ return new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max", "inherit"]).has(value)
102
+ ? value
103
+ : undefined;
104
+ }
105
+
106
+ function subagentChildren(value: unknown): SubagentTrailChild[] {
107
+ const details = toolDetails(value);
108
+ if (!details) return [];
109
+ const progress = Array.isArray(details.progress) ? details.progress : [];
110
+ if (progress.length > 0) {
111
+ return progress.slice(0, 24).flatMap((raw, index) => {
112
+ const item = asRecord(raw);
113
+ if (!item) return [];
114
+ const agent = safeChildField(item.agent) ?? `Agent ${index + 1}`;
115
+ const model = safeChildField(item.model, 120);
116
+ const thinking = safeThinkingLevel(item.thinking);
117
+ return [{
118
+ key: typeof item.index === "number" ? String(item.index) : String(index),
119
+ agent,
120
+ status: statusForChild(item.status),
121
+ ...(model ? { model } : {}),
122
+ ...(thinking ? { thinking } : {}),
123
+ ...(isoFromMs(item.lastActivityAt) ? { lastActivityAt: isoFromMs(item.lastActivityAt) } : {}),
124
+ ...(typeof item.durationMs === "number" ? { durationMs: item.durationMs } : {}),
125
+ } satisfies SubagentTrailChild];
126
+ });
127
+ }
128
+ const completions = Array.isArray(details.completions) ? details.completions : [];
129
+ if (completions.length > 0) {
130
+ let childIndex = 0;
131
+ return completions.slice(0, 24).flatMap((rawCompletion) => {
132
+ const completion = asRecord(rawCompletion);
133
+ if (!completion) return [];
134
+ const results = Array.isArray(completion.results) ? completion.results : [];
135
+ const candidates = results.length > 0 ? results : [completion];
136
+ return candidates.flatMap((rawChild) => {
137
+ const child = asRecord(rawChild);
138
+ const agent = safeChildField(child?.agent);
139
+ if (!child || !agent) return [];
140
+ const rawState = child.success === false
141
+ ? "failed"
142
+ : child.success === true
143
+ ? "completed"
144
+ : completion.state;
145
+ const status = statusForChild(rawState) === "pending" ? "completed" : statusForChild(rawState);
146
+ const model = safeChildField(child.model, 120);
147
+ return [{
148
+ // Do not reuse completion.runId: terminal identity remains local to
149
+ // pi-subagents. Order is enough inside this one terminal snapshot.
150
+ key: `completed-${childIndex++}`,
151
+ agent,
152
+ status,
153
+ ...(model ? { model } : {}),
154
+ } satisfies SubagentTrailChild];
155
+ });
156
+ });
157
+ }
158
+ const graph = asRecord(details.workflowGraph);
159
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
160
+ return nodes.slice(0, 24).flatMap((raw, index) => {
161
+ const item = asRecord(raw);
162
+ if (!item) return [];
163
+ const agent = safeChildField(item.agent) ?? safeChildField(item.label) ?? `Agent ${index + 1}`;
164
+ const model = safeChildField(item.model, 120);
165
+ const thinking = safeThinkingLevel(item.thinking);
166
+ return [{
167
+ key: String(index),
168
+ agent,
169
+ status: statusForChild(item.status),
170
+ ...(model ? { model } : {}),
171
+ ...(thinking ? { thinking } : {}),
172
+ ...(typeof item.durationMs === "number" ? { durationMs: item.durationMs } : {}),
173
+ } satisfies SubagentTrailChild];
174
+ });
175
+ }
176
+
177
+ /** Extract text, thinking, tool-call presence, stopReason and any provider
178
+ * errorMessage from an assistant message. A terminally failed provider call
179
+ * lands as stopReason "error" + errorMessage (item e99d158b) — never speech. */
180
+ export function extractAssistantParts(message: unknown): {
181
+ text: string;
182
+ thinking: string;
183
+ hasToolCalls: boolean;
184
+ stopReason: string | null;
185
+ errorMessage: string | null;
186
+ } {
187
+ const m = message as { content?: unknown; stopReason?: unknown; errorMessage?: unknown } | null;
188
+ const blocks = Array.isArray(m?.content) ? (m.content as Array<Record<string, unknown>>) : [];
189
+ let text = "";
190
+ let thinking = "";
191
+ let hasToolCalls = false;
192
+ for (const b of blocks) {
193
+ if (b?.type === "text" && typeof b.text === "string") text += b.text;
194
+ if (b?.type === "thinking" && typeof b.thinking === "string") thinking += b.thinking;
195
+ if (b?.type === "toolCall") hasToolCalls = true;
196
+ }
197
+ const stopReason = typeof m?.stopReason === "string" ? m.stopReason : null;
198
+ const errorMessage = typeof m?.errorMessage === "string" ? m.errorMessage : null;
199
+ return { text, thinking, hasToolCalls, stopReason, errorMessage };
200
+ }
201
+
202
+ /** Extract plain text from any message (user prompts included). */
203
+ export function extractText(message: unknown): string {
204
+ const m = message as { content?: unknown } | null;
205
+ if (typeof m?.content === "string") return m.content;
206
+ const blocks = Array.isArray(m?.content) ? (m.content as Array<Record<string, unknown>>) : [];
207
+ return blocks
208
+ .filter((b) => b?.type === "text" && typeof b.text === "string")
209
+ .map((b) => b.text as string)
210
+ .join("\n");
211
+ }
212
+
213
+ /**
214
+ * Run-scoped, CHRONOLOGICAL trail (item cc9e8c43). pi fires turn_start per
215
+ * LLM round, and each round is its own assistant message — so thinking/text
216
+ * accumulate as ordered entries, one pair per round, interleaved with the
217
+ * tool lines they produced. Within a round, message_update deltas are
218
+ * cumulative snapshots: they replace the round's OPEN entry in place, which
219
+ * keeps every render a strict prefix of the next (9b7455df).
220
+ */
221
+ export class TrailBuilder {
222
+ private entries: TrailEntry[] = [];
223
+ /** Indices of the in-flight round's thinking/text entries (-1 = none yet). */
224
+ private curThinking = -1;
225
+ private curText = -1;
226
+ private nextSeq = 1;
227
+
228
+ /**
229
+ * A new assistant message begins (message_start) — or the current one is
230
+ * final (message_end, after its content is applied). Either way the next
231
+ * delta belongs to a NEW round: reset the open entries so they append
232
+ * instead of replacing the previous round's. Wiring BOTH events means one
233
+ * of them firing is sufficient; neither firing degrades to the old
234
+ * replace-in-place behaviour (last round only), never a crash.
235
+ */
236
+ beginAssistantMessage(): void {
237
+ this.curThinking = -1;
238
+ this.curText = -1;
239
+ }
240
+
241
+ /** Cumulative snapshots from message_update/message_end — replace the
242
+ * CURRENT round's entry, never append to it. */
243
+ setThinking(thinking: string): void {
244
+ const t = thinking.trim();
245
+ if (!t) return;
246
+ if (this.curThinking >= 0) {
247
+ (this.entries[this.curThinking] as { kind: "thinking"; text: string; seq: number }).text = t;
248
+ } else {
249
+ this.curThinking = this.entries.push({
250
+ kind: "thinking",
251
+ text: t,
252
+ seq: this.nextSeq++,
253
+ occurredAt: new Date().toISOString(),
254
+ }) - 1;
255
+ }
256
+ }
257
+
258
+ setText(text: string): void {
259
+ const t = text.trim();
260
+ if (!t) return;
261
+ if (this.curText >= 0) {
262
+ (this.entries[this.curText] as { kind: "text"; text: string; seq: number }).text = t;
263
+ } else {
264
+ this.curText = this.entries.push({ kind: "text", text: t, seq: this.nextSeq++ }) - 1;
265
+ }
266
+ }
267
+
268
+ toolStart(toolCallId: string, name: string, args?: unknown): void {
269
+ const startedAt = Date.now();
270
+ this.entries.push({
271
+ kind: "tool",
272
+ seq: this.nextSeq++,
273
+ line: {
274
+ toolCallId,
275
+ name,
276
+ argsSummary: summarizeArgs(name, args),
277
+ identity: domainIdentity(name, args),
278
+ open: true,
279
+ startedAt,
280
+ startedAtIso: new Date(startedAt).toISOString(),
281
+ },
282
+ });
283
+ }
284
+
285
+ toolUpdate(toolCallId: string, partialResult: unknown): void {
286
+ for (let i = this.entries.length - 1; i >= 0; i--) {
287
+ const e = this.entries[i];
288
+ if (e.kind === "tool" && e.line.toolCallId === toolCallId) {
289
+ if (e.line.name === "subagent" || e.line.name === "subagent_wait") {
290
+ const children = subagentChildren(partialResult);
291
+ // subagent_wait currently streams human-readable status with an
292
+ // empty details projection. Do not erase the independently sourced
293
+ // public fleet snapshot on every textual progress update.
294
+ if (children.length > 0) {
295
+ e.line.subagentChildren = children;
296
+ e.line.subagentSummary = undefined;
297
+ }
298
+ }
299
+ return;
300
+ }
301
+ }
302
+ }
303
+
304
+ /** Overlay pi-subagents' public active-fleet projection onto an open wait. */
305
+ applySubagentFleet(toolCallId: string, snapshot: SubagentFleetSnapshot): boolean {
306
+ for (let i = this.entries.length - 1; i >= 0; i--) {
307
+ const e = this.entries[i];
308
+ if (e.kind !== "tool" || e.line.toolCallId !== toolCallId) continue;
309
+ if (!e.line.open || e.line.name !== "subagent_wait") return false;
310
+ e.line.subagentChildren = snapshot.children;
311
+ e.line.subagentSummary = snapshot.summary;
312
+ return true;
313
+ }
314
+ return false;
315
+ }
316
+
317
+ toolEnd(toolCallId: string, name: string, ok: boolean, result?: unknown): void {
318
+ for (let i = this.entries.length - 1; i >= 0; i--) {
319
+ const e = this.entries[i];
320
+ if (e.kind !== "tool") continue;
321
+ if (e.line.open && (e.line.toolCallId === toolCallId || e.line.name === name)) {
322
+ const completedAt = Date.now();
323
+ e.line.open = false;
324
+ const domain = classifyDomainTool(name);
325
+ e.line.writeOutcome = domain ? (ok ? writeOutcomeFromResult(result) : "failed") : undefined;
326
+ e.line.ok = ok && e.line.writeOutcome !== "failed";
327
+ // Reuse only the already-safe start identity as an argument fallback.
328
+ // An empty completed receipt is distinct from a still-running call.
329
+ const resultIdentity = domainIdentity(name, e.line.identity, result ?? {});
330
+ e.line.identity = {
331
+ id: e.line.ok ? resultIdentity.id : undefined,
332
+ title: resultIdentity.title ?? e.line.identity.title,
333
+ };
334
+ const resultSummary = summarizeResult(name, result);
335
+ e.line.resultSummary = e.line.writeOutcome
336
+ ? [`outcome=${e.line.writeOutcome}`, resultSummary].filter(Boolean).join("; ")
337
+ : resultSummary;
338
+ if (name === "subagent" || name === "subagent_wait") {
339
+ const children = subagentChildren(result);
340
+ // A terminal tool result owns the terminal child projection. Clear
341
+ // a prior active-only fleet snapshot when it provides no structured
342
+ // children so a finished wait never leaves a child spinning.
343
+ e.line.subagentChildren = children;
344
+ e.line.subagentSummary = undefined;
345
+ }
346
+ e.line.durationMs = Math.max(0, completedAt - e.line.startedAt);
347
+ e.line.completedAtIso = new Date(completedAt).toISOString();
348
+ return;
349
+ }
350
+ }
351
+ }
352
+
353
+ isEmpty(): boolean {
354
+ return this.entries.length === 0;
355
+ }
356
+
357
+ /** Typed events for the structured activity record. Assistant speech is deliberately excluded. */
358
+ events(): TrailEvent[] {
359
+ const out: TrailEvent[] = [];
360
+ for (const e of this.entries) {
361
+ if (e.kind === "thinking") {
362
+ const flat = e.text.replace(/\n/g, " ");
363
+ out.push({
364
+ seq: e.seq,
365
+ kind: "thinking",
366
+ text: flat.length > 4_000 ? `${flat.slice(0, 4_000)}…` : flat,
367
+ occurredAt: e.occurredAt,
368
+ });
369
+ } else if (e.kind === "text") {
370
+ // Text is speech: progress/final messages belong only in the transcript.
371
+ continue;
372
+ } else {
373
+ const status = e.line.open ? "running" : e.line.ok ? "ok" : "error";
374
+ const timing = {
375
+ occurredAt: e.line.startedAtIso,
376
+ ...(e.line.completedAtIso ? { completedAt: e.line.completedAtIso } : {}),
377
+ };
378
+ if (e.line.name === "subagent" || e.line.name === "subagent_wait") {
379
+ const children = e.line.subagentChildren ?? [];
380
+ const running = children.filter((child) => child.status === "running").length;
381
+ const ev: SubagentTrailEvent = {
382
+ seq: e.seq,
383
+ kind: "subagent",
384
+ name: e.line.name,
385
+ status,
386
+ children,
387
+ ...timing,
388
+ ...(e.line.subagentSummary
389
+ ? { summary: e.line.subagentSummary }
390
+ : children.length > 0
391
+ ? { summary: running > 0 ? `${running} of ${children.length} agents active` : `${children.length} agents` }
392
+ : {}),
393
+ ...(e.line.durationMs != null ? { durationMs: e.line.durationMs } : {}),
394
+ };
395
+ out.push(ev);
396
+ continue;
397
+ }
398
+ const domain = classifyDomainTool(e.line.name);
399
+ if (domain) {
400
+ const ident = e.line.identity;
401
+ const ev: DomainTrailEvent = {
402
+ seq: e.seq,
403
+ kind: "domain",
404
+ correlationId: e.line.toolCallId,
405
+ domain: domain.domain,
406
+ action: domainAction(domain, e.line.writeOutcome),
407
+ name: e.line.name,
408
+ status,
409
+ ...timing,
410
+ };
411
+ if (ident.title) ev.title = ident.title;
412
+ if (ident.id) ev.id = ident.id;
413
+ if (!e.line.open && e.line.resultSummary) ev.result = e.line.resultSummary;
414
+ if (e.line.durationMs != null) ev.durationMs = e.line.durationMs;
415
+ out.push(ev);
416
+ } else {
417
+ const ev: ToolTrailEvent = {
418
+ seq: e.seq,
419
+ kind: "tool",
420
+ correlationId: e.line.toolCallId,
421
+ name: e.line.name,
422
+ status,
423
+ ...timing,
424
+ };
425
+ if (e.line.argsSummary) ev.args = e.line.argsSummary;
426
+ if (!e.line.open && e.line.resultSummary) ev.result = e.line.resultSummary;
427
+ if (e.line.durationMs != null) ev.durationMs = e.line.durationMs;
428
+ out.push(ev);
429
+ }
430
+ }
431
+ }
432
+ return out;
433
+ }
434
+
435
+ render(maxChars: number = MAX_TRAIL_CHARS, includeSpeech = true): string {
436
+ const parts: string[] = [];
437
+ for (const e of this.entries) {
438
+ if (e.kind === "thinking") {
439
+ // Prefix-stable by construction (item 9b7455df): no emphasis markers —
440
+ // a closing '*' retracts on every thinking delta, which breaks the
441
+ // cumulative-prefix merge server-side and duplicated the whole block.
442
+ // The cap keeps the FIRST 800 chars with the ellipsis trailing, so a
443
+ // growing trace is always a strict prefix of its next snapshot.
444
+ const flat = e.text.replace(/\n/g, " ");
445
+ const t = flat.length > 800 ? `${flat.slice(0, 800)}…` : flat;
446
+ parts.push(`> 💭 ${t}`);
447
+ } else if (e.kind === "text") {
448
+ if (includeSpeech) parts.push(e.text);
449
+ } else {
450
+ const t = e.line;
451
+ const status = t.open ? "…" : t.ok ? "✔" : "✖";
452
+ const safeIdentity = t.identity.title ?? t.identity.id;
453
+ const summary = t.argsSummary ?? safeIdentity;
454
+ const args = summary ? ` — \`${summary}\`` : "";
455
+ const res = !t.open && t.resultSummary ? ` → ${t.resultSummary}` : "";
456
+ parts.push(`${status} \`${t.name}\`${args}${res}`);
457
+ }
458
+ }
459
+ let out = parts.join("\n\n");
460
+ if (out.length > maxChars) {
461
+ out = `…\n\n${out.slice(-maxChars)}`;
462
+ }
463
+ return out;
464
+ }
465
+ }