@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.
- package/CHANGELOG.md +899 -0
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/extensions/.gitkeep +0 -0
- package/extensions/core/activation-recovery.ts +135 -0
- package/extensions/core/active-deliveries.ts +171 -0
- package/extensions/core/active-session-plans.ts +374 -0
- package/extensions/core/adapter-overlap.ts +247 -0
- package/extensions/core/assistant-speech.ts +44 -0
- package/extensions/core/attach-context.ts +25 -0
- package/extensions/core/attachment-limits.ts +20 -0
- package/extensions/core/attachments.ts +487 -0
- package/extensions/core/auth-store.ts +91 -0
- package/extensions/core/automation-run-retry.ts +31 -0
- package/extensions/core/background-journal.ts +85 -0
- package/extensions/core/background-observer.ts +279 -0
- package/extensions/core/bond-recovery.ts +65 -0
- package/extensions/core/bond-state.ts +80 -0
- package/extensions/core/clear-conversation.ts +53 -0
- package/extensions/core/client-versions.ts +56 -0
- package/extensions/core/client.ts +704 -0
- package/extensions/core/command-offer.ts +88 -0
- package/extensions/core/command-runner.ts +62 -0
- package/extensions/core/command-turn.ts +88 -0
- package/extensions/core/commit-ledger.ts +195 -0
- package/extensions/core/commit-stamp.ts +98 -0
- package/extensions/core/config.ts +63 -0
- package/extensions/core/connection-context.ts +28 -0
- package/extensions/core/context-budget.ts +175 -0
- package/extensions/core/control-result-outbox.ts +80 -0
- package/extensions/core/control.ts +156 -0
- package/extensions/core/ctx-guard.ts +33 -0
- package/extensions/core/debug.ts +136 -0
- package/extensions/core/delivery-queue.ts +92 -0
- package/extensions/core/dispatch.ts +108 -0
- package/extensions/core/duck.ts +128 -0
- package/extensions/core/git-observer.ts +382 -0
- package/extensions/core/github-identity.ts +26 -0
- package/extensions/core/implementation-contract.ts +773 -0
- package/extensions/core/instructions.ts +40 -0
- package/extensions/core/interaction-events.ts +834 -0
- package/extensions/core/json-schema.ts +16 -0
- package/extensions/core/local-activity.ts +34 -0
- package/extensions/core/mcp-config-guard.ts +117 -0
- package/extensions/core/model-catalog.ts +76 -0
- package/extensions/core/model-surface.ts +125 -0
- package/extensions/core/notice-reply.ts +215 -0
- package/extensions/core/owner-inject.ts +148 -0
- package/extensions/core/post-session-receipt.ts +70 -0
- package/extensions/core/preempt.ts +100 -0
- package/extensions/core/project-choice.ts +145 -0
- package/extensions/core/project-scope.ts +335 -0
- package/extensions/core/project-selection-ui.ts +202 -0
- package/extensions/core/provenance-refs.ts +158 -0
- package/extensions/core/push-channel.ts +281 -0
- package/extensions/core/question-receipt-lifecycle.ts +158 -0
- package/extensions/core/recall.ts +121 -0
- package/extensions/core/remote-payload.ts +1329 -0
- package/extensions/core/remote-session-arg.ts +24 -0
- package/extensions/core/repository-context.ts +35 -0
- package/extensions/core/request-chain.ts +1244 -0
- package/extensions/core/result-projection.ts +939 -0
- package/extensions/core/result.ts +100 -0
- package/extensions/core/review-context.ts +70 -0
- package/extensions/core/review-watch.ts +94 -0
- package/extensions/core/room-access.ts +12 -0
- package/extensions/core/room-context.ts +178 -0
- package/extensions/core/room-reader.ts +308 -0
- package/extensions/core/room-store.ts +381 -0
- package/extensions/core/room-sync.ts +79 -0
- package/extensions/core/room-tools.ts +86 -0
- package/extensions/core/room-unread.ts +173 -0
- package/extensions/core/session-events.ts +62 -0
- package/extensions/core/session-polls.ts +292 -0
- package/extensions/core/settings.ts +39 -0
- package/extensions/core/status-line.ts +432 -0
- package/extensions/core/still-to-discuss.ts +124 -0
- package/extensions/core/subagent-fleet.ts +238 -0
- package/extensions/core/system-notice.ts +227 -0
- package/extensions/core/telemetry.ts +177 -0
- package/extensions/core/tool-selection.ts +72 -0
- package/extensions/core/trail-events.ts +247 -0
- package/extensions/core/trail-projection.ts +373 -0
- package/extensions/core/trail.ts +465 -0
- package/extensions/core/turn-lifecycle.ts +472 -0
- package/extensions/core/version-mismatch.ts +165 -0
- package/extensions/core/work-prompt.ts +44 -0
- package/extensions/core/work-state.ts +179 -0
- package/extensions/devspec-adapter-guard.ts +106 -0
- package/extensions/devspec-auth.ts +191 -0
- package/extensions/devspec-brand.ts +140 -0
- package/extensions/devspec-commit-ledger.ts +477 -0
- package/extensions/devspec-context.ts +146 -0
- package/extensions/devspec-memory.ts +136 -0
- package/extensions/devspec-provenance-assistance.ts +233 -0
- package/extensions/devspec-remote.ts +5152 -0
- package/extensions/devspec-tools.ts +552 -0
- package/extensions/devspec-work.ts +244 -0
- package/extensions/generated/README.md +18 -0
- package/extensions/generated/mcp-catalog.ts +15811 -0
- package/package.json +71 -4
- package/prompts/.gitkeep +0 -0
- package/runtime/commit-stamp.mjs +279 -0
- package/runtime/git-observer-shim.mjs +390 -0
- package/skills/.gitkeep +0 -0
- 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
|
+
}
|