@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 5.19.1-fork.1
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 +68 -0
- package/dist/src/agent-execution.js +81 -39
- package/dist/src/background-widget.d.ts +5 -0
- package/dist/src/background-widget.js +1 -1
- package/dist/src/decoders.js +26 -0
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +5 -0
- package/dist/src/host-view.js +29 -5
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +190 -37
- package/dist/src/index.js +495 -93
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +1 -1
- package/dist/src/roles.js +4 -3
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +27 -3
- package/dist/src/trajectory.d.ts +12 -2
- package/dist/src/trajectory.js +30 -8
- package/dist/src/types.d.ts +30 -2
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +19 -0
- package/dist/src/utils.js +30 -0
- package/dist/src/validation.d.ts +6 -1
- package/dist/src/validation.js +21 -1
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +267 -159
- package/dist/subagents/index.js.map +2 -2
- package/dist/subagents/src/contracts.d.ts +45 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +13 -6
- package/dist/subagents/src/manager.js +3 -3
- package/dist/subagents/src/view.d.ts +1 -0
- package/dist/subagents/src/view.js +13 -52
- package/dist/trajectory/assets/index.html +27 -5
- package/dist/trajectory/index.js +76 -28
- package/dist/trajectory/index.js.map +2 -2
- package/dist/trajectory/src/assets/index.html +27 -5
- package/dist/trajectory/src/index.js +8 -18
- package/dist/trajectory/src/server.js +4 -2
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +6 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +77 -41
- package/src/background-widget.ts +1 -1
- package/src/decoders.ts +20 -0
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +23 -5
- package/src/host.ts +173 -35
- package/src/paths.ts +3 -0
- package/src/registry.ts +1 -1
- package/src/roles.ts +4 -3
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +22 -4
- package/src/trajectory.ts +30 -11
- package/src/types.ts +11 -4
- package/src/utils.ts +28 -0
- package/src/validation.ts +19 -1
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +2 -2
- package/subagents/src/contracts.ts +18 -7
- package/subagents/src/index.ts +16 -6
- package/subagents/src/manager.ts +3 -3
- package/subagents/src/view.ts +13 -50
- package/trajectory/src/assets/index.html +27 -5
- package/trajectory/src/index.ts +6 -9
- package/trajectory/src/server.ts +2 -2
package/subagents/src/view.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { formatCost, formatStalledDuration, sanitizeDisplayText, WORKFLOW_AGENT_STALL_THRESHOLD_MS } from "../../src/index.js";
|
|
4
|
-
import {
|
|
3
|
+
import { formatCost, formatStalledDuration, formatWorkflowRuntime, sanitizeDisplayText, WORKFLOW_AGENT_STALL_THRESHOLD_MS } from "../../src/index.js";
|
|
4
|
+
import { agentActivityLabel, stalledDuration, textBlock } from "../../src/host-view.js";
|
|
5
|
+
import { drawFrame, formatElapsed, formatTokens, mark, QUIET_MS, REPAINT_MS, type Paint } from "../../src/background-widget.js";
|
|
5
6
|
import type { SubagentRunRequest, SubagentStatus } from "./contracts.js";
|
|
6
7
|
|
|
7
8
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
@@ -19,13 +20,6 @@ export type SubagentRenderState = {
|
|
|
19
20
|
subagentProgressFrozenAt?: number;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
function textBlock(text: string) {
|
|
23
|
-
return {
|
|
24
|
-
render(width: number): string[] { return text.split("\n").map((line) => truncateToWidth(line, Math.max(1, width), "…")); },
|
|
25
|
-
invalidate() {},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
function roleName(value: unknown): string | undefined {
|
|
30
24
|
if (typeof value === "string") return value.trim() || undefined;
|
|
31
25
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
@@ -73,18 +67,6 @@ function statusValue(value: unknown): SubagentStatus | undefined {
|
|
|
73
67
|
return value as SubagentStatus;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
function runtime(startedAt: number | undefined, finishedAt: number | undefined, now: number): string {
|
|
77
|
-
if (startedAt === undefined) return "";
|
|
78
|
-
const seconds = Math.max(0, Math.floor(((finishedAt ?? now) - startedAt) / 1000));
|
|
79
|
-
if (seconds < 60) return `${String(seconds)}s`;
|
|
80
|
-
const minutes = Math.floor(seconds / 60);
|
|
81
|
-
const remainingSeconds = seconds % 60;
|
|
82
|
-
if (minutes < 60) return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
|
|
83
|
-
const hours = Math.floor(minutes / 60);
|
|
84
|
-
const remainingMinutes = minutes % 60;
|
|
85
|
-
return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
70
|
function stateGlyph(state: SubagentStatus["state"], spinner: string): string {
|
|
89
71
|
if (state === "running") return spinner;
|
|
90
72
|
return state === "completed" ? "✓" : "✗";
|
|
@@ -95,25 +77,6 @@ function stateColor(state: SubagentStatus["state"]): "accent" | "success" | "err
|
|
|
95
77
|
return state === "completed" ? "success" : "error";
|
|
96
78
|
}
|
|
97
79
|
|
|
98
|
-
function activity(status: SubagentStatus): string | undefined {
|
|
99
|
-
if (status.state !== "running") return undefined;
|
|
100
|
-
const current = status.progress?.activity;
|
|
101
|
-
if (current?.kind === "reasoning" || current?.kind === "text") {
|
|
102
|
-
const name = current.kind === "reasoning" ? "reasoning" : "responding";
|
|
103
|
-
const text = sanitizeDisplayText(current.text);
|
|
104
|
-
return text && text !== name ? `${name} · ${text}` : name;
|
|
105
|
-
}
|
|
106
|
-
if (current?.kind === "tool") return sanitizeDisplayText(current.text);
|
|
107
|
-
return sanitizeDisplayText([...(status.progress?.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function stalledDuration(status: SubagentStatus, now: number): number | undefined {
|
|
111
|
-
const lastEventAt = status.progress?.lastEventAt;
|
|
112
|
-
if (status.state !== "running" || lastEventAt === undefined || !Number.isFinite(lastEventAt)) return undefined;
|
|
113
|
-
const duration = now - lastEventAt;
|
|
114
|
-
return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
80
|
/** Tokens and cost as the workflow header shows them. */
|
|
118
81
|
function usageStats(status: SubagentStatus): string {
|
|
119
82
|
const value = status.progress?.accounting;
|
|
@@ -143,15 +106,15 @@ function boundedValue(value: unknown): string {
|
|
|
143
106
|
|
|
144
107
|
function formatSubagentProgress(status: SubagentStatus, args: SubagentRenderArgs, theme: Theme, spinner: string, now: number, expanded: boolean): string {
|
|
145
108
|
const color = stateColor(status.state);
|
|
146
|
-
const elapsed =
|
|
109
|
+
const elapsed = status.startedAt === undefined ? "" : formatWorkflowRuntime((status.finishedAt ?? now) - status.startedAt);
|
|
147
110
|
const metadata = requestMetadata(args, args.id === undefined);
|
|
148
111
|
const stats = usageStats(status);
|
|
149
112
|
const lines = [
|
|
150
|
-
`${theme.fg(color, stateGlyph(status.state,
|
|
113
|
+
`${theme.fg(color, stateGlyph(status.state, "●"))} ${theme.bold(theme.fg("accent", `Subagent: ${label({ ...args, id: status.id })}`))} ${theme.fg(color, `[${status.state}]`)}${metadata ? ` ${theme.fg("dim", metadata)}` : ""}${stats ? ` ${stats}` : ""}${elapsed ? ` runtime=${elapsed}` : ""}`,
|
|
151
114
|
];
|
|
152
|
-
const current =
|
|
153
|
-
const stalled = stalledDuration(status, now);
|
|
154
|
-
if (current) lines.push(` ${theme.fg("accent", spinner)} ${theme.fg("dim", current)}${stalled === undefined ? "" : ` ${theme.fg("warning", `- stalled? ${formatStalledDuration(stalled)}`)}`}`);
|
|
115
|
+
const current = status.state === "running" ? agentActivityLabel(status.progress ?? {}) : undefined;
|
|
116
|
+
const stalled = stalledDuration({ ...status.progress, state: status.state }, now);
|
|
117
|
+
if (current || status.state === "running") lines.push(` ${theme.fg("accent", spinner)}${current ? ` ${theme.fg("dim", current)}` : ""}${stalled === undefined ? "" : ` ${theme.fg("warning", `- stalled? ${formatStalledDuration(stalled)}`)}`}`);
|
|
155
118
|
else if (stalled !== undefined) lines.push(` ${theme.fg("warning", `stalled? ${formatStalledDuration(stalled)}`)}`);
|
|
156
119
|
if (status.error) lines.push(` ${theme.fg("error", `${status.error.code}: ${status.error.message}`)}`);
|
|
157
120
|
if (expanded) {
|
|
@@ -218,7 +181,7 @@ export function subagentProgressBlock(status: SubagentStatus, args: SubagentRend
|
|
|
218
181
|
render(width: number): string[] {
|
|
219
182
|
const now = TERMINAL_STATES.has(current.state) ? frozenAt : Date.now();
|
|
220
183
|
if (!TERMINAL_STATES.has(current.state)) frozenAt = now;
|
|
221
|
-
const frame = SPINNER[Math.floor(now /
|
|
184
|
+
const frame = SPINNER[Math.floor(now / REPAINT_MS) % SPINNER.length] ?? "◇";
|
|
222
185
|
return formatSubagentProgress(current, args, currentTheme, frame, now, expanded).split("\n").map((line) => truncateToWidth(line, Math.max(1, width), "…"));
|
|
223
186
|
},
|
|
224
187
|
invalidate() {},
|
|
@@ -236,7 +199,7 @@ export function renderSubagentResult(result: AgentToolResult<unknown>, options:
|
|
|
236
199
|
const status = state.subagentStatus;
|
|
237
200
|
|
|
238
201
|
if (status?.state === "running" && options.isPartial && !state.subagentSpinner) {
|
|
239
|
-
state.subagentSpinner = setInterval(() => { context.invalidate(); },
|
|
202
|
+
state.subagentSpinner = setInterval(() => { context.invalidate(); }, REPAINT_MS);
|
|
240
203
|
state.subagentSpinner.unref();
|
|
241
204
|
} else if ((!options.isPartial || status?.state !== "running") && state.subagentSpinner) {
|
|
242
205
|
clearInterval(state.subagentSpinner);
|
|
@@ -373,7 +336,7 @@ export function createSubagentBackgroundWidget(host: ReceiptHost = {}) {
|
|
|
373
336
|
};
|
|
374
337
|
}, { placement: "belowEditor" });
|
|
375
338
|
showing = true;
|
|
376
|
-
timer = setInterval(() => { requestRender?.(); },
|
|
339
|
+
timer = setInterval(() => { requestRender?.(); }, REPAINT_MS);
|
|
377
340
|
timer.unref();
|
|
378
341
|
} catch {
|
|
379
342
|
hide();
|
|
@@ -408,9 +371,9 @@ export function createSubagentBackgroundWidget(host: ReceiptHost = {}) {
|
|
|
408
371
|
}
|
|
409
372
|
|
|
410
373
|
|
|
411
|
-
export function renderSubagentInspectCall(args: { id?: string }, theme: Theme) {
|
|
374
|
+
export function renderSubagentInspectCall(args: { id?: string; scope?: string }, theme: Theme) {
|
|
412
375
|
const title = theme.fg("toolTitle", theme.bold("subagents_inspect"));
|
|
413
|
-
return textBlock(args.id ? `${title} ${theme.fg("accent", args.id)}` : `${title} ${theme.fg("muted", "all")}`);
|
|
376
|
+
return textBlock(args.id ? `${title} ${theme.fg("accent", args.id)}` : `${title} ${theme.fg("muted", args.scope === "all" ? "all" : "session")}`);
|
|
414
377
|
}
|
|
415
378
|
|
|
416
379
|
export function renderSubagentInspectResult(result: AgentToolResult<unknown>, options: { expanded: boolean }, theme: Theme, args: { id?: string }) {
|
|
@@ -157,6 +157,14 @@
|
|
|
157
157
|
.log-row { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 10px; padding: 5px 8px; border-top: 1px solid var(--line); font: 11px ui-monospace, Menlo, Consolas, monospace; }
|
|
158
158
|
.log-time { color: var(--mute); white-space: nowrap; }
|
|
159
159
|
.log-message { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
160
|
+
.calls-list { max-height: 480px; overflow: auto; }
|
|
161
|
+
.call-row { display: grid; grid-template-columns: 16px 7.5rem minmax(80px, 160px) minmax(0, 1fr) 64px; gap: 10px; align-items: center; padding: 5px 8px; border-top: 1px solid var(--line); font: 11px ui-monospace, Menlo, Consolas, monospace; cursor: pointer; list-style: none; }
|
|
162
|
+
.call-row::-webkit-details-marker { display: none; }
|
|
163
|
+
.call-row:hover { background: var(--bg-hover); }
|
|
164
|
+
.call-row b, .call-preview { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
165
|
+
.call-preview { color: var(--dim); }
|
|
166
|
+
.call-row .num { color: var(--dim); text-align: right; }
|
|
167
|
+
.call-body { padding: 4px 8px 12px 34px; }
|
|
160
168
|
.row { display: grid; grid-template-columns: 16px 1fr; gap: 8px; align-items: center; padding: 3px 12px; cursor: pointer; }
|
|
161
169
|
.row.child { padding-left: 28px; }
|
|
162
170
|
.row:hover { background: var(--bg-hover); }
|
|
@@ -341,7 +349,7 @@
|
|
|
341
349
|
<script src="./morphdom.min.js"></script>
|
|
342
350
|
<script src="./prism.min.js"></script>
|
|
343
351
|
<script>
|
|
344
|
-
const defaultRunLayout = () => ({ swimHeight: 220, ganttCollapsed: false, agentsCollapsed: false, logsCollapsed: false });
|
|
352
|
+
const defaultRunLayout = () => ({ swimHeight: 220, ganttCollapsed: false, agentsCollapsed: false, callsCollapsed: false, logsCollapsed: false });
|
|
345
353
|
const maxSwimHeight = () => Math.max(72, window.innerHeight * 0.7);
|
|
346
354
|
function loadRunLayout() {
|
|
347
355
|
const fallback = defaultRunLayout();
|
|
@@ -354,6 +362,7 @@
|
|
|
354
362
|
swimHeight: Number.isFinite(value.swimHeight) && value.swimHeight >= 72 ? value.swimHeight : fallback.swimHeight,
|
|
355
363
|
ganttCollapsed: typeof value.ganttCollapsed === "boolean" ? value.ganttCollapsed : fallback.ganttCollapsed,
|
|
356
364
|
agentsCollapsed: typeof value.agentsCollapsed === "boolean" ? value.agentsCollapsed : fallback.agentsCollapsed,
|
|
365
|
+
callsCollapsed: typeof value.callsCollapsed === "boolean" ? value.callsCollapsed : fallback.callsCollapsed,
|
|
357
366
|
logsCollapsed: typeof value.logsCollapsed === "boolean" ? value.logsCollapsed : fallback.logsCollapsed,
|
|
358
367
|
};
|
|
359
368
|
} catch {
|
|
@@ -372,7 +381,7 @@
|
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
383
|
function saveSidebarCollapsed() { try { localStorage.setItem("traj-sidebar-collapsed", JSON.stringify([...state.sidebarCollapsed])); } catch { /* Preferences are optional. */ } }
|
|
375
|
-
const state = { publishers: [], currentPub: null, currentRun: null, currentTarget: null, currentAgent: null, pendingTarget: null, agentRange: null, agentRangeAgent: null, selectedEvent: null, timelineHighlight: null, eventsSig: null, toolPane: "summary", sysPane: "prompt", inspMode: "event", inspSig: null, socket: null, transcripts: {}, transcriptPending: {}, transcriptSources: {}, transcriptRefresh: {}, transcriptRevision: {}, transcriptStatus: {}, transcriptCacheOrder: [], actionTargets: new Map(), shareUrls: {}, runLayout: loadRunLayout(), sidebarCollapsed: loadSidebarCollapsed(), workflowArgumentsOpen: new Set() };
|
|
384
|
+
const state = { publishers: [], currentPub: null, currentRun: null, currentTarget: null, currentAgent: null, pendingTarget: null, agentRange: null, agentRangeAgent: null, selectedEvent: null, timelineHighlight: null, eventsSig: null, toolPane: "summary", sysPane: "prompt", inspMode: "event", inspSig: null, socket: null, transcripts: {}, transcriptPending: {}, transcriptSources: {}, transcriptRefresh: {}, transcriptRevision: {}, transcriptStatus: {}, transcriptCacheOrder: [], actionTargets: new Map(), shareUrls: {}, runLayout: loadRunLayout(), sidebarCollapsed: loadSidebarCollapsed(), workflowArgumentsOpen: new Set(), callsOpen: new Set() };
|
|
376
385
|
let hasAcceptedState = false;
|
|
377
386
|
let socketGeneration = 0;
|
|
378
387
|
let socketReconnect;
|
|
@@ -676,6 +685,8 @@
|
|
|
676
685
|
const style = running ? `left:${Math.max(0, left / span * 100)}%;width:0%` : barStyle(Math.max(0, left), agent.durationMs || 0, 0, span);
|
|
677
686
|
lanes.push({ key: `agent:${agent.id}`, html: `<div class="lane${agent.parentId ? " child" : ""}" data-agent="${esc(agent.id)}">${glyph(agent.state)}<div class="lab">${esc(agent.label || agent.name)}</div><div class="track"><i class="bar ${stateClass(agent.state)}" style="${style}"${running ? ` data-clock-bar-start="${agent.startedAt || start}" data-clock-bar-min="2"` : ""}></i>${toolBars}</div></div>` });
|
|
678
687
|
}
|
|
688
|
+
const calls = scriptCalls(record);
|
|
689
|
+
if (calls.length) lanes.push({ key: "calls", html: `<div class="lane crumb"><span></span><div class="lab">script</div><div class="track">${calls.map((call) => { const status = callState(record, call); const running = status === "running"; const title = `${callLabel(call)} · ${status}${running ? "" : ` · ${callDuration(call)}`}`; return `<i class="bar tool ${stateClass(status)}" style="${running ? `left:${Math.max(0, (call.startedAt - start) / span * 100)}%;width:0.5%` : barStyle(call.startedAt - start, (call.finishedAt ?? call.startedAt) - call.startedAt, 0.5, span)}"${running ? ` data-clock-bar-start="${call.startedAt}" data-clock-bar-min="0.5"` : ""} title="${esc(title)}"></i>`; }).join("")}</div></div>` });
|
|
679
690
|
return [{ key: "axis", html: `<div class="axis"><div></div><div class="ticks"><span>${fmtClock(start)}</span><span data-clock-axis="mid">${live ? "" : `+${fmtRuntime(span / 2)}`}</span><span data-clock-axis="end">${live ? "" : "end"}</span></div></div>` }, ...lanes];
|
|
680
691
|
}
|
|
681
692
|
// Only the lanes whose markup changed are re-parsed; the quantised span keeps the others byte-identical between frames.
|
|
@@ -726,8 +737,19 @@
|
|
|
726
737
|
return `<div class="agent-grid"><div class="agent-grid-head"><span>AGENT</span><span>MODEL</span><span>ROLE</span><span>TOOL TIMING</span><span>DURATION</span><span>TOKENS</span><span>COST</span></div>${body}</div>`;
|
|
727
738
|
}
|
|
728
739
|
function renderLogs(record) { const events = record.run.events || []; const logs = events.filter((event) => event.type === "log"); const omitted = events.find((event) => event.type === "trajectory:truncated"); if (!logs.length && !omitted) return ""; const collapsed = state.runLayout.logsCollapsed; return `<section class="logs"><button type="button" class="sec section-toggle" data-toggle="logs" aria-expanded="${!collapsed}">${collapsed ? "▸" : "▾"} LOGS</button>${collapsed ? "" : `<div class="logs-list">${logs.map((event) => `<div class="log-row"><span class="log-time">${esc(fmtClock(event.timestamp))}</span><span class="log-message">${esc(event.message)}</span></div>`).join("")}${omitted ? `<div class="log-row"><span class="log-time">—</span><span class="log-message">${esc(omitted.omitted)} events omitted from live state</span></div>` : ""}</div>`}</section>`; }
|
|
740
|
+
const scriptCalls = (record) => (record.run.scriptCalls || []).filter((call) => call?.type !== "trajectory:truncated");
|
|
741
|
+
// The journal proves completion even if a crash prevented the finish metadata write.
|
|
742
|
+
const callState = (record, call) => call.output?.status === "completed" || call.output?.status === "available" || call.output?.status === "truncated" && call.output.kind === "result" ? "completed" : call.error ? (call.error.code === "CANCELLED" ? "stopped" : "failed") : call.finishedAt !== undefined ? "completed" : record.run.state === "running" || record.run.state === "pausing" ? "running" : "interrupted";
|
|
743
|
+
const callDuration = (call) => { if (call.finishedAt === undefined) return "—"; const ms = Math.max(0, call.finishedAt - call.startedAt); return ms < 1000 ? `${ms}ms` : fmtRuntime(ms); };
|
|
744
|
+
const callLabel = (call) => call.kind === "shell" ? "shell" : `tools.${call.name}`;
|
|
745
|
+
const callPreview = (call) => call.kind === "tool" ? call.error?.code || "" : typeof call.input === "string" ? call.input : JSON.stringify(call.input);
|
|
746
|
+
function renderCalls(record) {
|
|
747
|
+
const calls = scriptCalls(record); const omitted = (record.run.scriptCalls || []).find((call) => call?.type === "trajectory:truncated"); if (!calls.length && !omitted) return ""; const collapsed = state.runLayout.callsCollapsed;
|
|
748
|
+
const rows = calls.map((call) => { const key = `${state.currentRun}\t${call.path}`; const status = callState(record, call); const duration = status === "running" ? `<span data-clock-kind="runtime" data-clock-start="${call.startedAt}" data-clock-end="live"></span>` : call.finishedAt === undefined ? "—" : esc(callDuration(call)); const input = call.kind !== "shell" ? "" : call.inputBytes !== undefined ? `<div class="hint">Input truncated (${esc(call.inputBytes)} bytes).</div><div class="code">${esc(call.input)}</div>` : `<div class="code">${highlightCode(json(call.input), "json")}</div>`; return `<details class="call"${state.callsOpen.has(key) ? " open" : ""} data-call="${esc(key)}"><summary class="call-row">${glyph(status)}<span class="log-time">${esc(fmtClock(call.startedAt))}</span><b>${esc(callLabel(call))}</b><span class="call-preview">${esc(callPreview(call))}</span><span class="num">${duration}</span></summary><div class="call-body">${call.phase ? `<div class="kv"><div class="k">Phase</div><div>${esc(call.phase)}</div></div>` : ""}${call.kind === "shell" ? `<div class="sec">INPUT</div>${input}<div class="sec">OUTPUT</div>${renderOutputPane(call)}` : `<div class="kv"><div class="k">Status</div><div>${esc(status)}</div>${call.error ? `<div class="k">Error</div><div>${esc(call.error.code)}</div>` : ""}</div>`}</div></details>`; }).join("");
|
|
749
|
+
return `<section class="logs"><button type="button" class="sec section-toggle" data-toggle="calls" aria-expanded="${!collapsed}">${collapsed ? "▸" : "▾"} CALLS</button>${collapsed ? "" : `<div class="calls-list">${rows}${omitted ? `<div class="log-row"><span class="log-time">—</span><span class="log-message">${esc(omitted.omitted)} calls omitted from live state</span></div>` : ""}</div>`}</section>`;
|
|
750
|
+
}
|
|
729
751
|
function renderAgentSection(record, timingsByAgent) { const collapsed = state.runLayout.agentsCollapsed; return `<button type="button" class="sec section-toggle" data-toggle="agents" aria-expanded="${!collapsed}">${collapsed ? "▸" : "▾"} AGENTS</button>${collapsed ? "" : renderAgentGrid(record, timingsByAgent)}`; }
|
|
730
|
-
function renderList(record, timingsByAgent) { const run = record.run; const completed = run.agents.filter((agent) => agent.state === "completed").length; return `<section class="workflow-card"><button type="button" class="workflow-head on" data-run="${esc(state.currentRun)}">${glyph(run.state)}<span class="workflow-name">${esc(run.workflowName)}</span><span class="workflow-meta">${completed}/${run.agents.length} agents · ${fmtCost(runCost(record))} · <span data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${run.state === "running" ? "live" : endOf(record)}"></span></span></button>${renderAgentSection(record, timingsByAgent)}${renderLogs(record)}</section>`; }
|
|
752
|
+
function renderList(record, timingsByAgent) { const run = record.run; const completed = run.agents.filter((agent) => agent.state === "completed").length; return `<section class="workflow-card"><button type="button" class="workflow-head on" data-run="${esc(state.currentRun)}">${glyph(run.state)}<span class="workflow-name">${esc(run.workflowName)}</span><span class="workflow-meta">${completed}/${run.agents.length} agents · ${fmtCost(runCost(record))} · <span data-clock-kind="runtime" data-clock-start="${startOf(record).getTime()}" data-clock-end="${run.state === "running" ? "live" : endOf(record)}"></span></span></button>${renderAgentSection(record, timingsByAgent)}${renderCalls(record)}${renderLogs(record)}</section>`; }
|
|
731
753
|
function actionButton(label, action, disabled) { return `<button class="btn-alt" data-act="${action}"${disabled ? " disabled" : ""}>${label}</button>`; }
|
|
732
754
|
function subagentAgent(subagent) { return { id: subagent.id, name: subagentLabel(subagent), label: subagentLabel(subagent), role: subagent.role || "subagent", state: subagent.state, model: subagent.model, requestedModel: subagent.request.model, tools: subagent.tools || [], toolDefinitions: subagent.toolDefinitions || [], skills: subagent.request.skills || [], extensions: subagent.request.extensions || [], contextFiles: subagent.request.contextFiles || [], prompt: subagent.request.prompt, systemPrompt: subagent.attempt?.setup?.systemPrompt || subagent.request.prompt, attempts: subagent.attempts || 0, mode: subagent.mode, cwd: subagent.cwd, worktree: subagent.worktree, output: subagent.output || subagentOutputFallback(subagent), outcome: subagent.result !== undefined ? { kind: "result", value: subagent.result } : subagent.failure !== undefined ? { kind: "failure", value: subagent.failure } : subagent.attempt?.error !== undefined ? { kind: "failure", value: subagent.attempt.error } : undefined, finishedAt: subagent.finishedAt, attemptDetails: subagent.attempt ? [subagent.attempt] : [] }; }
|
|
733
755
|
function subagentOutputFallback(subagent) { if (subagent.result !== undefined) return { status: "available", value: subagent.result, bytes: 0 }; if (subagent.failure !== undefined) return { status: "failed", code: subagent.failure.code || "AGENT_FAILED", message: subagent.failure.message || "Subagent failed" }; if (subagent.state === "running") return { status: "pending" }; if (subagent.state === "completed") return { status: "unavailable" }; if (subagent.state === "stopped") return { status: "cancelled", code: subagent.error?.code || "CANCELLED", message: subagent.error?.message || "Subagent stopped" }; if (subagent.state === "failed") return { status: "failed", code: subagent.error?.code || "AGENT_FAILED", message: subagent.error?.message || "Subagent failed" }; return { status: "unavailable" }; }
|
|
@@ -862,8 +884,8 @@
|
|
|
862
884
|
});
|
|
863
885
|
}
|
|
864
886
|
$("script-close").addEventListener("click", closeScript); $("script-bg").addEventListener("click", closeScript); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeScript(); setAgentRange(null); if (document.body.dataset.view === "run") renderRun(); else renderAgent(); } });
|
|
865
|
-
document.addEventListener("toggle", (event) => { const target = event.target; if (!(target instanceof HTMLDetailsElement)
|
|
866
|
-
document.querySelector(".app").addEventListener("click", (event) => { const target = event.target.closest("[data-sidebar-group], [data-sidebar-section], [data-copy-share], [data-act], [data-run], [data-subagent], [data-agent], [data-agent-details], [data-script], [data-pane], [data-toggle]"); if (!target) return; if (target.dataset.copyShare) { const input = target.previousElementSibling; navigator.clipboard?.writeText(target.dataset.copyShare).then(() => showToast("copied"), () => { input?.select(); showToast("press Ctrl+C to copy"); }); return; } if (target.dataset.sidebarGroup !== undefined || target.dataset.sidebarSection !== undefined) { const key = target.dataset.sidebarGroup ?? target.dataset.sidebarSection; if (state.sidebarCollapsed.has(key)) state.sidebarCollapsed.delete(key); else state.sidebarCollapsed.add(key); saveSidebarCollapsed(); renderSidebar(); return; } if (target.dataset.toggle) { if (document.body.dataset.view !== "run") return; const key = target.dataset.toggle === "gantt" ? "ganttCollapsed" : target.dataset.toggle === "agents" ? "agentsCollapsed" : target.dataset.toggle === "logs" ? "logsCollapsed" : ""; if (!key) return; state.runLayout[key] = !state.runLayout[key]; saveRunLayout(); renderRun(); return; } if (target.dataset.act) { if (target.dataset.act === "steer") { const input = $("subagent-steer"); const message = input?.value.trim(); if (!message) return; sendAction("steer", { payload: { message } }); if (input) input.value = ""; return; } const name = target.closest("[data-name]")?.dataset.name; sendAction(target.dataset.act, name ? { name } : {}); return; } if (target.dataset.agentDetails !== undefined) { state.inspMode = "agent"; state.inspSig = null; renderAgent(); return; } if (target.dataset.script) { openScript(); return; } if (target.dataset.pane) { const found = selected(); const agent = found ? found.target.kind === "subagent" ? subagentAgent(found.record) : found.record.run.agents.find((candidate) => candidate.id === state.currentAgent) : undefined; const entries = found && agent ? agentEntries(found.record, agent) : []; if (target.closest("#tool-tabs")) { state.toolPane = target.dataset.pane; state.inspMode = "event"; renderInspector(entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined, agent, entries); } else { state.sysPane = target.dataset.pane; state.inspMode = "agent"; if (agent) renderSystemPane(agent); } return; } if (target.dataset.run) { selectRun(target.dataset.run); return; } if (target.dataset.subagent) { selectSubagent(target.dataset.subagent); return; } if (target.dataset.agent && document.body.dataset.view === "run") { state.currentAgent = target.dataset.agent; state.selectedEvent = null; state.inspMode = "event"; setView("agent"); } });
|
|
887
|
+
document.addEventListener("toggle", (event) => { const target = event.target; if (!(target instanceof HTMLDetailsElement)) return; const key = target.dataset.workflowArgs ?? target.dataset.call; const open = target.dataset.workflowArgs !== undefined ? state.workflowArgumentsOpen : target.dataset.call !== undefined ? state.callsOpen : undefined; if (!open) return; if (target.open) open.add(key); else open.delete(key); }, true);
|
|
888
|
+
document.querySelector(".app").addEventListener("click", (event) => { const target = event.target.closest("[data-sidebar-group], [data-sidebar-section], [data-copy-share], [data-act], [data-run], [data-subagent], [data-agent], [data-agent-details], [data-script], [data-pane], [data-toggle]"); if (!target) return; if (target.dataset.copyShare) { const input = target.previousElementSibling; navigator.clipboard?.writeText(target.dataset.copyShare).then(() => showToast("copied"), () => { input?.select(); showToast("press Ctrl+C to copy"); }); return; } if (target.dataset.sidebarGroup !== undefined || target.dataset.sidebarSection !== undefined) { const key = target.dataset.sidebarGroup ?? target.dataset.sidebarSection; if (state.sidebarCollapsed.has(key)) state.sidebarCollapsed.delete(key); else state.sidebarCollapsed.add(key); saveSidebarCollapsed(); renderSidebar(); return; } if (target.dataset.toggle) { if (document.body.dataset.view !== "run") return; const key = target.dataset.toggle === "gantt" ? "ganttCollapsed" : target.dataset.toggle === "agents" ? "agentsCollapsed" : target.dataset.toggle === "calls" ? "callsCollapsed" : target.dataset.toggle === "logs" ? "logsCollapsed" : ""; if (!key) return; state.runLayout[key] = !state.runLayout[key]; saveRunLayout(); renderRun(); return; } if (target.dataset.act) { if (target.dataset.act === "steer") { const input = $("subagent-steer"); const message = input?.value.trim(); if (!message) return; sendAction("steer", { payload: { message } }); if (input) input.value = ""; return; } const name = target.closest("[data-name]")?.dataset.name; sendAction(target.dataset.act, name ? { name } : {}); return; } if (target.dataset.agentDetails !== undefined) { state.inspMode = "agent"; state.inspSig = null; renderAgent(); return; } if (target.dataset.script) { openScript(); return; } if (target.dataset.pane) { const found = selected(); const agent = found ? found.target.kind === "subagent" ? subagentAgent(found.record) : found.record.run.agents.find((candidate) => candidate.id === state.currentAgent) : undefined; const entries = found && agent ? agentEntries(found.record, agent) : []; if (target.closest("#tool-tabs")) { state.toolPane = target.dataset.pane; state.inspMode = "event"; renderInspector(entries[state.selectedEvent || 0] ? entryDetails(entries[state.selectedEvent || 0], agent, entries) : undefined, agent, entries); } else { state.sysPane = target.dataset.pane; state.inspMode = "agent"; if (agent) renderSystemPane(agent); } return; } if (target.dataset.run) { selectRun(target.dataset.run); return; } if (target.dataset.subagent) { selectSubagent(target.dataset.subagent); return; } if (target.dataset.agent && document.body.dataset.view === "run") { state.currentAgent = target.dataset.agent; state.selectedEvent = null; state.inspMode = "event"; setView("agent"); } });
|
|
867
889
|
document.addEventListener("visibilitychange", () => { sendFocus(); if (!document.hidden) scheduleRender(); });
|
|
868
890
|
// Phones show one sheet at a time, so picking something moves to the sheet that shows its result.
|
|
869
891
|
const phone = matchMedia("(max-width: 900px)");
|
package/trajectory/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
|
8
8
|
import { clearTrajectoryHost, setTrajectoryHost, type TrajectoryHost, type TrajectoryPublisherProvider } from "../../src/trajectory-host-handle.js";
|
|
9
9
|
import { processAlive } from "../../src/session-lease.js";
|
|
10
10
|
import { errorText, isNodeError, object, positiveInteger } from "../../src/utils.js";
|
|
11
|
-
import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError, TRAJECTORY_MAX_TRANSCRIPT_BYTES, type TrajectoryPublisherInput, type TrajectoryPublisherMetadata, type TrajectoryTranscriptRequest, type TrajectoryTranscriptResult } from "../../src/trajectory.js";
|
|
11
|
+
import { isTimingTranscriptEntry, isTrajectoryAction, isTrajectoryTarget, trajectoryActionError, TRAJECTORY_MAX_TRANSCRIPT_BYTES, type TrajectoryPublisherInput, type TrajectoryPublisherMetadata, type TrajectoryTranscriptRequest, type TrajectoryTranscriptResult } from "../../src/trajectory.js";
|
|
12
12
|
import { shareTrajectoryRun } from "./export.js";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_TRAJECTORY_PORT = 7432;
|
|
@@ -202,7 +202,6 @@ const RECONNECT_MAX_DELAY_MS = 5_000;
|
|
|
202
202
|
type LiveStateRecord = Record<string, unknown>;
|
|
203
203
|
type TranscriptRevision = { signature: string; revision: number };
|
|
204
204
|
type PendingTranscript = { generation: number; timer: ReturnType<typeof setTimeout>; publisherId: string; runId?: string; agentId?: string; subagentId?: string; revision?: number };
|
|
205
|
-
function isTimingEntry(value: unknown): boolean { return object(value) && value.type === "custom" && value.customType === "pi-workflows:tool-timing"; }
|
|
206
205
|
function boundedString(value: unknown, maxLength = 1024): unknown {
|
|
207
206
|
if (typeof value !== "string") return value;
|
|
208
207
|
const bytes = Buffer.from(value);
|
|
@@ -212,7 +211,7 @@ const MAX_LIVE_STRING_BYTES = 64 * 1024;
|
|
|
212
211
|
const MAX_LIVE_ARRAY_ENTRIES = 256;
|
|
213
212
|
const MAX_LIVE_OBJECT_KEYS = 64;
|
|
214
213
|
const LIVE_METADATA_ARRAY_KEYS = new Set(["agents", "runs", "subagents"]);
|
|
215
|
-
const LIVE_TRUNCATABLE_ARRAY_KEYS = new Set(["events", "phaseHistory"]);
|
|
214
|
+
const LIVE_TRUNCATABLE_ARRAY_KEYS = new Set(["events", "phaseHistory", "scriptCalls"]);
|
|
216
215
|
const LIVE_METADATA_OBJECT_KEYS = new Set(["transcripts"]);
|
|
217
216
|
function liveValueWillBeBounded(value: unknown, key = "", depth = 0): boolean {
|
|
218
217
|
if (typeof value === "string") return Buffer.byteLength(value) > MAX_LIVE_STRING_BYTES;
|
|
@@ -231,7 +230,7 @@ function liveValueWillBeBounded(value: unknown, key = "", depth = 0): boolean {
|
|
|
231
230
|
const MAX_LIVE_TIMING_BYTES = 64 * 1024;
|
|
232
231
|
/** Keeps the newest timing entries: dropping the oldest leaves a live agent's gantt growing instead of frozen. */
|
|
233
232
|
function boundedTiming(value: unknown): unknown[] {
|
|
234
|
-
const entries = Array.isArray(value) ? value.filter(
|
|
233
|
+
const entries = Array.isArray(value) ? value.filter(isTimingTranscriptEntry) : [];
|
|
235
234
|
const retained: unknown[] = [];
|
|
236
235
|
let bytes = 2;
|
|
237
236
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
@@ -446,11 +445,9 @@ export function createTrajectoryController(agentDir: string): TrajectoryControll
|
|
|
446
445
|
}
|
|
447
446
|
};
|
|
448
447
|
const transcriptFromFallback = async (input: TrajectoryPublisherInput, request: TrajectoryTranscriptRequest): Promise<TrajectoryTranscriptResult> => {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
};
|
|
453
|
-
const boundedResult = (entries: readonly unknown[], revision: number): TrajectoryTranscriptResult => { try { if (Buffer.byteLength(JSON.stringify(entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES) return { status: "oversized", revision, entries: [], error: "Transcript is too large" }; } catch { return { status: "failed", revision, entries: [], error: "Transcript failed" }; } return { status: entries.length ? "available" : "empty", revision, entries }; };
|
|
448
|
+
const oversized = (entries: readonly unknown[], revision: number): TrajectoryTranscriptResult | undefined => { try { return Buffer.byteLength(JSON.stringify(entries)) > TRAJECTORY_MAX_TRANSCRIPT_BYTES ? { status: "oversized", revision, entries: [], error: "Transcript is too large" } : undefined; } catch { return { status: "failed", revision, entries: [], error: "Transcript failed" }; } };
|
|
449
|
+
const boundedTranscriptResult = (result: TrajectoryTranscriptResult): TrajectoryTranscriptResult => (result.status === "available" ? oversized(result.entries, result.revision) : undefined) ?? result;
|
|
450
|
+
const boundedResult = (entries: readonly unknown[], revision: number): TrajectoryTranscriptResult => oversized(entries, revision) ?? { status: entries.length ? "available" : "empty", revision, entries };
|
|
454
451
|
if (input.loadTranscript) {
|
|
455
452
|
const result = await input.loadTranscript(request);
|
|
456
453
|
const revisionChanged = request.revision !== undefined && result.revision !== request.revision && result.status !== "missing" && result.status !== "failed" && result.status !== "oversized" && result.status !== "disconnected";
|
package/trajectory/src/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { URL } from "node:url";
|
|
6
6
|
import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError } from "../../src/trajectory-contracts.js";
|
|
7
7
|
import { sameFilesystemPath } from "../../src/paths.js";
|
|
8
|
+
import { TOOL_TIMING_ENTRY_TYPE } from "../../src/tool-timing.js";
|
|
8
9
|
const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
|
|
9
10
|
|
|
10
11
|
type Socket = import("node:stream").Duplex;
|
|
@@ -17,9 +18,8 @@ const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
|
17
18
|
const MAX_PENDING_REQUESTS = 128;
|
|
18
19
|
const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
|
|
19
20
|
const ACTION_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
20
|
-
const TIMING_ENTRY_TYPE = "pi-workflows:tool-timing";
|
|
21
21
|
function isTimingEntry(value: unknown): boolean {
|
|
22
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType ===
|
|
22
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType === TOOL_TIMING_ENTRY_TYPE);
|
|
23
23
|
}
|
|
24
24
|
function compactTranscript(value: unknown): unknown {
|
|
25
25
|
if (Array.isArray(value)) return value.filter(isTimingEntry);
|