pi-agent-fleet 0.4.0 → 0.6.0
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/README.md +23 -8
- package/examples/json-number-pipeline.json +35 -0
- package/package.json +2 -2
- package/src/canvas-client.tsx +184 -66
- package/src/canvas-layout.ts +83 -0
- package/src/canvas.ts +178 -86
- package/src/command.ts +56 -42
- package/src/contracts.ts +76 -20
- package/src/controller.ts +72 -7
- package/src/dag.ts +32 -7
- package/src/edits.ts +7 -8
- package/src/fleet-recovery.ts +83 -0
- package/src/fleet-store.ts +11 -2
- package/src/insert.ts +2 -8
- package/src/model-resolution.ts +27 -2
- package/src/preferences.ts +10 -1
- package/src/prompts.ts +21 -0
- package/src/report.ts +28 -1
- package/src/scheduler.ts +51 -5
- package/src/state.ts +17 -17
- package/src/tools.ts +70 -35
- package/src/types.ts +8 -0
- package/src/worktree.ts +6 -0
package/src/canvas.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { readdir, readFile
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import type { AddressInfo } from "node:net";
|
|
6
6
|
import { basename, dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import type { ActiveFleet } from "./controller.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { listFleetRoots, readDiskFleet } from "./fleet-recovery.js";
|
|
11
|
+
|
|
12
|
+
export { listFleetRoots, readDiskFleet };
|
|
13
|
+
export type { FleetRootInfo } from "./fleet-recovery.js";
|
|
12
14
|
|
|
13
15
|
const execFileP = promisify(execFile);
|
|
14
16
|
|
|
@@ -479,31 +481,129 @@ export interface SessionEntryView {
|
|
|
479
481
|
text: string;
|
|
480
482
|
}
|
|
481
483
|
|
|
482
|
-
export
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
484
|
+
export interface ActionView {
|
|
485
|
+
type: "tool_call" | "tool_result" | "model_change" | "thinking_level_change" | "complete";
|
|
486
|
+
name?: string;
|
|
487
|
+
toolName?: string;
|
|
488
|
+
arguments?: Record<string, unknown>;
|
|
489
|
+
provider?: string;
|
|
490
|
+
modelId?: string;
|
|
491
|
+
thinkingLevel?: string;
|
|
492
|
+
stopReason?: string;
|
|
493
|
+
isError?: boolean;
|
|
494
|
+
timestamp?: string;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export type TimelineEvent =
|
|
498
|
+
| { type: "message"; role: string; text: string; timestamp?: string }
|
|
499
|
+
| { type: "tool_call"; name: string; arguments?: Record<string, unknown>; timestamp?: string }
|
|
500
|
+
| { type: "tool_result"; toolName?: string; isError?: boolean; text?: string; timestamp?: string }
|
|
501
|
+
| { type: "model_change"; provider: string; modelId: string; timestamp?: string }
|
|
502
|
+
| { type: "thinking_level_change"; thinkingLevel: string; timestamp?: string }
|
|
503
|
+
| { type: "complete"; stopReason: string; timestamp?: string };
|
|
504
|
+
|
|
505
|
+
export interface SessionTailView {
|
|
506
|
+
entries: SessionEntryView[];
|
|
507
|
+
actions: ActionView[];
|
|
508
|
+
events: TimelineEvent[];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function parseSessionTail(jsonl: string, maxEntries: number): SessionTailView {
|
|
512
|
+
const events: TimelineEvent[] = [];
|
|
513
|
+
const entries: SessionEntryView[] = [];
|
|
514
|
+
const actions: ActionView[] = [];
|
|
515
|
+
for (const raw of jsonl.split("\n")) {
|
|
516
|
+
if (!raw.trim()) continue;
|
|
517
|
+
let e: { type?: string; timestamp?: string; message?: { role?: unknown; content?: unknown; stopReason?: string; toolName?: string; isError?: boolean }; provider?: string; modelId?: string; thinkingLevel?: string } | undefined;
|
|
518
|
+
try { e = JSON.parse(raw); } catch { continue; }
|
|
519
|
+
if (!e) continue;
|
|
520
|
+
const ts = typeof e.timestamp === "string" ? e.timestamp : undefined;
|
|
521
|
+
if (e.type === "model_change" && typeof e.provider === "string" && typeof e.modelId === "string") {
|
|
522
|
+
const a: ActionView = { type: "model_change", provider: e.provider, modelId: e.modelId, timestamp: ts };
|
|
523
|
+
actions.push(a);
|
|
524
|
+
events.push({ type: "model_change", provider: e.provider, modelId: e.modelId, timestamp: ts });
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (e.type === "thinking_level_change" && typeof e.thinkingLevel === "string") {
|
|
528
|
+
const a: ActionView = { type: "thinking_level_change", thinkingLevel: e.thinkingLevel, timestamp: ts };
|
|
529
|
+
actions.push(a);
|
|
530
|
+
events.push({ type: "thinking_level_change", thinkingLevel: e.thinkingLevel, timestamp: ts });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (e.type !== "message") continue;
|
|
534
|
+
const msg = e.message;
|
|
535
|
+
if (!msg || typeof msg.role !== "string" || !Array.isArray(msg.content)) continue;
|
|
536
|
+
// tool result messages carry the result at the message level
|
|
537
|
+
if (msg.role === "toolResult" || msg.role === "tool_result") {
|
|
538
|
+
const a: ActionView = { type: "tool_result", toolName: typeof msg.toolName === "string" ? msg.toolName : undefined, isError: msg.isError, timestamp: ts };
|
|
539
|
+
actions.push(a);
|
|
540
|
+
events.push({ type: "tool_result", toolName: a.toolName, isError: a.isError, timestamp: ts });
|
|
541
|
+
}
|
|
542
|
+
const parts: string[] = [];
|
|
543
|
+
for (const p of msg.content as Array<{ type?: string; text?: string; name?: string; toolName?: string; isError?: boolean; arguments?: Record<string, unknown> }>) {
|
|
544
|
+
if (!p) continue;
|
|
545
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
546
|
+
else if ((p.type === "toolCall" || p.type === "tool_call") && typeof p.name === "string") {
|
|
547
|
+
parts.push(`[tool: ${p.name}]`);
|
|
548
|
+
const a: ActionView = { type: "tool_call", name: p.name, arguments: p.arguments, timestamp: ts };
|
|
549
|
+
actions.push(a);
|
|
550
|
+
events.push({ type: "tool_call", name: p.name, arguments: p.arguments, timestamp: ts });
|
|
551
|
+
} else if (p.type === "toolResult" || p.type === "tool_result") {
|
|
552
|
+
parts.push("[tool result]");
|
|
553
|
+
const a: ActionView = { type: "tool_result", toolName: typeof p.toolName === "string" ? p.toolName : p.name, isError: p.isError, timestamp: ts };
|
|
554
|
+
actions.push(a);
|
|
555
|
+
events.push({ type: "tool_result", toolName: a.toolName, isError: a.isError, timestamp: ts });
|
|
499
556
|
}
|
|
500
|
-
}
|
|
501
|
-
|
|
557
|
+
}
|
|
558
|
+
if (msg.stopReason && typeof msg.stopReason === "string" && msg.stopReason !== "toolUse" && msg.stopReason !== "tool_use") {
|
|
559
|
+
const a: ActionView = { type: "complete", stopReason: msg.stopReason, timestamp: ts };
|
|
560
|
+
actions.push(a);
|
|
561
|
+
events.push({ type: "complete", stopReason: msg.stopReason, timestamp: ts });
|
|
562
|
+
}
|
|
563
|
+
const text = parts.join("\n").trim();
|
|
564
|
+
if (text.length > 0) {
|
|
565
|
+
const entry: SessionEntryView = { role: msg.role as string, text: text.length > 4000 ? `${text.slice(0, 4000)}…` : text };
|
|
566
|
+
entries.push(entry);
|
|
567
|
+
events.push({ type: "message", role: entry.role, text: entry.text, timestamp: ts });
|
|
502
568
|
}
|
|
503
569
|
}
|
|
504
|
-
return
|
|
570
|
+
return { entries: entries.slice(-maxEntries), actions: actions.slice(-maxEntries), events: events.slice(-maxEntries) };
|
|
505
571
|
}
|
|
506
572
|
|
|
573
|
+
const DEMO_TASK = `You are L1 (lay-of-the-land) researcher in a 2-layer research fleet.\n\nMission context: founder built QuickCall — a daemon watching engineers' AI coding-agent sessions (Claude Code, Cursor, Codex), extracting team conventions, capturing accept/reject signals and human corrections on agent output.\n\nYour job:\n1. Read the mission brief and upstream inputs.\n2. Search for relevant precedents, code patterns, and competitive landscape.\n3. Write a concise markdown report to the required output path.\n4. Save ALL output files to the worker output directory using absolute paths.\n\nSave the report to output/l1-methods.md. The file must use markdown headings and keep each section focused. Do not modify source code.`;
|
|
574
|
+
|
|
575
|
+
export function buildDemoSession(_id: string, task?: string): SessionTailView & { task: string } {
|
|
576
|
+
const taskText = task || DEMO_TASK;
|
|
577
|
+
const baseTs = "2026-08-01T09:57:";
|
|
578
|
+
const events: TimelineEvent[] = [
|
|
579
|
+
{ type: "model_change", provider: "openai-codex", modelId: "gpt-5.4-mini", timestamp: baseTs + "04.987Z" },
|
|
580
|
+
{ type: "thinking_level_change", thinkingLevel: "high", timestamp: baseTs + "04.987Z" },
|
|
581
|
+
{ type: "message", role: "assistant", text: "I'll review the scheduler changes and write the verdict.", timestamp: baseTs + "05.000Z" },
|
|
582
|
+
{ type: "tool_call", name: "read", arguments: { path: "docs/superpowers/specs/reviewer-contract.md" }, timestamp: baseTs + "06.000Z" },
|
|
583
|
+
{ type: "tool_call", name: "read", arguments: { path: "src/scheduler.ts" }, timestamp: baseTs + "06.500Z" },
|
|
584
|
+
{ type: "message", role: "assistant", text: "The scheduler uses a priority queue. I'll run the test suite to verify behavior.", timestamp: baseTs + "08.000Z" },
|
|
585
|
+
{ type: "tool_call", name: "bash", arguments: { command: "npm run typecheck" }, timestamp: baseTs + "09.000Z" },
|
|
586
|
+
{ type: "tool_result", toolName: "bash", isError: false, text: "✓ typecheck passed", timestamp: baseTs + "12.000Z" },
|
|
587
|
+
{ type: "message", role: "assistant", text: "Typecheck passes. I'll search for fleet reviewer patterns and inspect tests.", timestamp: baseTs + "12.500Z" },
|
|
588
|
+
{ type: "tool_call", name: "web_search", arguments: { queries: ["fleet reviewer DAG pattern"] }, timestamp: baseTs + "13.000Z" },
|
|
589
|
+
{ type: "tool_call", name: "read", arguments: { path: "test/scheduler.test.ts" }, timestamp: baseTs + "15.000Z" },
|
|
590
|
+
{ type: "tool_result", toolName: "read", isError: false, text: "# 123", timestamp: baseTs + "15.500Z" },
|
|
591
|
+
{ type: "message", role: "assistant", text: "Upstream outputs look good. Tests pass and search results confirm the pattern. Writing the review verdict now.", timestamp: baseTs + "17.000Z" },
|
|
592
|
+
{ type: "tool_call", name: "write", arguments: { path: "output/review.md" }, timestamp: baseTs + "18.000Z" },
|
|
593
|
+
{ type: "tool_result", toolName: "write", isError: false, text: "Successfully wrote 45 bytes to output/review.md", timestamp: baseTs + "18.500Z" },
|
|
594
|
+
{ type: "message", role: "assistant", text: "Done.", timestamp: baseTs + "19.000Z" },
|
|
595
|
+
{ type: "complete", stopReason: "complete", timestamp: baseTs + "20.000Z" },
|
|
596
|
+
];
|
|
597
|
+
const entries: SessionEntryView[] = events.filter((e) => e.type === "message").map((e) => ({ role: (e as TimelineEvent & { type: "message" }).role, text: (e as TimelineEvent & { type: "message" }).text }));
|
|
598
|
+
const actions: ActionView[] = events.filter((e) => e.type !== "message").map((e) => {
|
|
599
|
+
if (e.type === "tool_call") return { type: e.type, name: e.name, arguments: e.arguments, timestamp: e.timestamp };
|
|
600
|
+
if (e.type === "tool_result") return { type: e.type, toolName: e.toolName, isError: e.isError, timestamp: e.timestamp };
|
|
601
|
+
if (e.type === "model_change") return { type: e.type, provider: e.provider, modelId: e.modelId, timestamp: e.timestamp };
|
|
602
|
+
if (e.type === "thinking_level_change") return { type: e.type, thinkingLevel: e.thinkingLevel, timestamp: e.timestamp };
|
|
603
|
+
return { type: e.type, stopReason: e.stopReason, timestamp: e.timestamp };
|
|
604
|
+
}) as ActionView[];
|
|
605
|
+
return { entries, actions, events, task: taskText };
|
|
606
|
+
}
|
|
507
607
|
async function latestSessionFile(workerDir: string): Promise<string | undefined> {
|
|
508
608
|
try {
|
|
509
609
|
const files = (await readdir(workerDir)).filter((f) => f.endsWith(".jsonl")).sort();
|
|
@@ -528,7 +628,7 @@ async function buildClientBundle(): Promise<string> {
|
|
|
528
628
|
platform: "browser",
|
|
529
629
|
target: "es2020",
|
|
530
630
|
jsx: "automatic",
|
|
531
|
-
minify:
|
|
631
|
+
minify: false,
|
|
532
632
|
write: false,
|
|
533
633
|
logLevel: "silent",
|
|
534
634
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
@@ -626,14 +726,52 @@ main { display:flex; flex:1 1 auto; min-height:0; }
|
|
|
626
726
|
.flags { margin-top:6px; font-size:11px; color:var(--warn); }
|
|
627
727
|
.flags span { cursor:help; }
|
|
628
728
|
.note { margin-top:6px; font-size:13px; color:var(--warn); }
|
|
629
|
-
|
|
729
|
+
.note, .fail-reason { user-select:text; cursor:text; }
|
|
730
|
+
#side { width:420px; flex:0 0 auto; border-left:1px solid var(--line); overflow:hidden; display:flex; flex-direction:column; background:var(--bg); }
|
|
630
731
|
#side:focus, #side:focus-visible { outline:none; }
|
|
631
|
-
.side-head { display:flex; justify-content:space-between; align-items:center;
|
|
632
|
-
.side-head .meta {
|
|
732
|
+
.side-head { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; flex-shrink:0; gap:6px; padding:12px 12px 8px; border-bottom:1px solid var(--line); background:var(--bg); }
|
|
733
|
+
.side-head .side-meta { display:flex; gap:6px; width:100%; }
|
|
734
|
+
.side-head .side-meta-chip { font-size:11px; color:var(--muted); border:1px solid var(--line); border-radius:4px; padding:2px 6px; white-space:nowrap; }
|
|
735
|
+
.side-head .meta { color:var(--muted); font-size:12px; display:flex; align-items:center; gap:4px; }
|
|
736
|
+
.side-head .side-hash { color:var(--muted); font-weight:400; }
|
|
737
|
+
.side-head .side-id { font-weight:700; color:var(--hdr); font-size:15px; letter-spacing:-0.01em; }
|
|
738
|
+
.side-body { flex:1; overflow:auto; padding:8px 12px 12px; }
|
|
633
739
|
.taskbox-side { border:1px solid var(--line); border-radius:6px; padding:8px; margin-bottom:10px; white-space:pre-wrap; word-break:break-word; font-size:13px; }
|
|
634
740
|
.msg { margin-bottom:10px; padding:8px; border-radius:6px; background:var(--panel); white-space:pre-wrap; word-break:break-word; }
|
|
635
741
|
.msg .role { font-weight:700; margin-bottom:4px; }
|
|
636
742
|
.role-user { color:var(--accent); } .role-assistant { color:var(--ok); } .role-tool { color:var(--warn); }
|
|
743
|
+
.collapsible { position:sticky; top:8px; z-index:1; border:1px solid var(--line); border-radius:6px; margin-bottom:10px; background:var(--bg); }
|
|
744
|
+
.collapsible-head { display:flex; align-items:center; gap:6px; width:100%; padding:8px; background:transparent; border:none; font:inherit; font-size:12px; color:var(--muted); cursor:pointer; text-align:left; }
|
|
745
|
+
.collapsible-head:hover { background:var(--panel); }
|
|
746
|
+
.collapsible-body { padding:8px; border-top:1px solid var(--line); white-space:pre-wrap; word-break:break-word; font-size:13px; max-height:260px; overflow:auto; }
|
|
747
|
+
.collapsible.collapsed .collapsible-body { display:none; }
|
|
748
|
+
.collapsible .chevron { display:inline-block; transition:transform .15s; }
|
|
749
|
+
.collapsible.collapsed .chevron { transform:rotate(-90deg); }
|
|
750
|
+
.timeline { margin-bottom:12px; }
|
|
751
|
+
.timeline-action { background:var(--panel); border-radius:5px; margin-bottom:5px; }
|
|
752
|
+
.timeline-action.open { background:var(--panel-2); }
|
|
753
|
+
.timeline-action.action-error { border-left:3px solid var(--bad); background:color-mix(in srgb, var(--bad) 9%, var(--panel)); }
|
|
754
|
+
.timeline-row { display:flex; align-items:center; gap:8px; padding:5px 8px; font-size:12px; }
|
|
755
|
+
.timeline-row .action-icon { flex-shrink:0; width:34px; text-align:center; color:var(--accent); font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:0.02em; }
|
|
756
|
+
.timeline-row .action-name { flex-shrink:0; font-weight:600; min-width:48px; font-size:12px; white-space:nowrap; }
|
|
757
|
+
.timeline-row .action-detail { flex:1; min-width:0; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }
|
|
758
|
+
.timeline-row .action-error { color:var(--bad); }
|
|
759
|
+
.timeline-row .ts { flex-shrink:0; color:var(--wire); font-size:11px; font-variant-numeric:tabular-nums; }
|
|
760
|
+
.timeline-msg { padding:4px 8px; margin-bottom:6px; border-radius:5px; background:var(--panel); font-size:13px; }
|
|
761
|
+
.timeline-msg.user { border-left:1px solid var(--accent); background:color-mix(in srgb, var(--accent) 5%, var(--panel)); }
|
|
762
|
+
.timeline-msg.assistant { border-left:1px solid var(--ok); background:color-mix(in srgb, var(--ok) 5%, var(--panel)); }
|
|
763
|
+
.timeline-msg .role { font-weight:700; margin-bottom:4px; }
|
|
764
|
+
.timeline-msg .timeline-meta { display:flex; justify-content:space-between; align-items:center; margin-bottom:3px; }
|
|
765
|
+
.timeline-msg .timeline-meta .ts { color:var(--wire); font-size:11px; }
|
|
766
|
+
.timeline-msg .timeline-text { font-size:12px; line-height:1.45; white-space:pre-wrap; word-break:break-word; user-select:text; }
|
|
767
|
+
.timeline-loading { display:flex; align-items:center; justify-content:center; gap:8px; padding:16px 8px; color:var(--muted); font-size:13px; }
|
|
768
|
+
.timeline-empty { padding:16px 8px; color:var(--muted); font-size:13px; text-align:center; }
|
|
769
|
+
.activity-toggle { flex-shrink:0; width:18px; height:18px; padding:0; background:transparent; border:1px solid var(--line); border-radius:4px; color:var(--muted); cursor:pointer; font-size:11px; line-height:1; }
|
|
770
|
+
.activity-toggle:hover { background:var(--bg); color:var(--fg); }
|
|
771
|
+
.activity-body { padding:8px; border-top:1px solid var(--line); font-size:12px; color:var(--muted); font-family:var(--mono); white-space:pre-wrap; word-break:break-word; user-select:text; }
|
|
772
|
+
.activity-body pre { margin:0; background:var(--bg); padding:8px; border-radius:4px; overflow:auto; font-size:11px; }
|
|
773
|
+
.timeline-more { margin-top:4px; padding:0; min-height:auto; border:none; background:none; color:var(--accent); font-size:11px; }
|
|
774
|
+
.timeline-more:hover { text-decoration:underline; }
|
|
637
775
|
.react-flow__minimap-node.st-completed { fill:var(--ok); }
|
|
638
776
|
.react-flow__minimap-node.st-running { fill:var(--accent); }
|
|
639
777
|
.react-flow__minimap-node.st-failed, .react-flow__minimap-node.st-contract_failed { fill:var(--bad); }
|
|
@@ -677,6 +815,7 @@ button.toggled { border-color:var(--accent); color:var(--accent); }
|
|
|
677
815
|
.empty-cta:hover { background:var(--panel); }
|
|
678
816
|
/* failure reason surfaced on failed cards */
|
|
679
817
|
.fail-reason { margin-top:6px; font-size:13px; color:var(--bad); }
|
|
818
|
+
.action-detail { user-select:text; }
|
|
680
819
|
/* respect reduced-motion: keep the state, drop the perpetual movement */
|
|
681
820
|
@media (prefers-reduced-motion: reduce) {
|
|
682
821
|
.spinner { animation:none; border-top-color:var(--accent); opacity:0.6; }
|
|
@@ -713,63 +852,6 @@ export async function renderCanvasPage(): Promise<string> {
|
|
|
713
852
|
</html>`;
|
|
714
853
|
}
|
|
715
854
|
|
|
716
|
-
export interface FleetRootInfo {
|
|
717
|
-
name: string;
|
|
718
|
-
root: string;
|
|
719
|
-
status: string;
|
|
720
|
-
created_at: string;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
export async function readDiskFleet(fleetRoot: string): Promise<ActiveFleet> {
|
|
724
|
-
const spec = JSON.parse(await readFile(join(fleetRoot, "fleet.json"), "utf-8")) as FleetSpec;
|
|
725
|
-
const state = await readState(fleetRoot);
|
|
726
|
-
return {
|
|
727
|
-
spec,
|
|
728
|
-
fleetRoot,
|
|
729
|
-
state,
|
|
730
|
-
killSwitch: { killed: false },
|
|
731
|
-
pauseSwitch: { paused: false },
|
|
732
|
-
running: false,
|
|
733
|
-
sessions: new Map(),
|
|
734
|
-
killedNodes: new Set(),
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
export async function listFleetRoots(cwd: string): Promise<FleetRootInfo[]> {
|
|
739
|
-
const base = join(cwd, ".fleet");
|
|
740
|
-
let entries: string[];
|
|
741
|
-
try {
|
|
742
|
-
entries = await readdir(base);
|
|
743
|
-
} catch {
|
|
744
|
-
return [];
|
|
745
|
-
}
|
|
746
|
-
const out: FleetRootInfo[] = [];
|
|
747
|
-
for (const name of entries) {
|
|
748
|
-
const root = join(base, name);
|
|
749
|
-
try {
|
|
750
|
-
const s = await stat(join(root, "fleet.json"));
|
|
751
|
-
if (!s.isFile()) continue;
|
|
752
|
-
const state = JSON.parse(await readFile(join(root, "state.json"), "utf-8")) as Partial<FleetState>;
|
|
753
|
-
out.push({
|
|
754
|
-
name,
|
|
755
|
-
root,
|
|
756
|
-
status: typeof state.status === "string" ? state.status : "unknown",
|
|
757
|
-
created_at: typeof state.created_at === "string" ? state.created_at : new Date(s.mtimeMs).toISOString(),
|
|
758
|
-
});
|
|
759
|
-
} catch {
|
|
760
|
-
// not a fleet root (no fleet.json) or unreadable state — skip or mark unknown
|
|
761
|
-
try {
|
|
762
|
-
await stat(join(root, "fleet.json"));
|
|
763
|
-
out.push({ name, root, status: "unknown", created_at: "" });
|
|
764
|
-
} catch {
|
|
765
|
-
// not a fleet root
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
out.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
770
|
-
return out;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
855
|
export interface CanvasServer {
|
|
774
856
|
url: string;
|
|
775
857
|
port: number;
|
|
@@ -824,6 +906,14 @@ export async function startCanvasServer(opts: {
|
|
|
824
906
|
}
|
|
825
907
|
const m = url.pathname.match(/^\/api\/session\/([a-z0-9][a-z0-9-]*)$/);
|
|
826
908
|
if (m) {
|
|
909
|
+
const isDemo = url.searchParams.get("demo") === "1";
|
|
910
|
+
let workerTask: string | undefined;
|
|
911
|
+
if (isDemo) {
|
|
912
|
+
const demo = buildDemoSession(m[1]);
|
|
913
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
914
|
+
res.end(JSON.stringify(demo));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
827
917
|
const f = await resolveFleet(url.searchParams.get("fleet"));
|
|
828
918
|
if (!f || f === "unknown" || !f.spec.workers.some((w) => w.id === m[1])) {
|
|
829
919
|
res.writeHead(404);
|
|
@@ -835,12 +925,14 @@ export async function startCanvasServer(opts: {
|
|
|
835
925
|
const file = await latestSessionFile(join(f.fleetRoot, "workers", m[1]));
|
|
836
926
|
res.writeHead(200, { "content-type": "application/json" });
|
|
837
927
|
const worker = f.spec.workers.find((w) => w.id === m[1]);
|
|
928
|
+
workerTask = worker?.task;
|
|
838
929
|
if (!file) {
|
|
839
|
-
res.end(JSON.stringify({ entries: [],
|
|
930
|
+
res.end(JSON.stringify({ entries: [], actions: [], events: [], task: workerTask }));
|
|
840
931
|
return;
|
|
841
932
|
}
|
|
842
933
|
const content = await readFile(file, "utf-8");
|
|
843
|
-
|
|
934
|
+
const { entries, actions, events } = parseSessionTail(content, tail);
|
|
935
|
+
res.end(JSON.stringify({ entries, actions, events, task: workerTask }));
|
|
844
936
|
return;
|
|
845
937
|
}
|
|
846
938
|
res.writeHead(404);
|
package/src/command.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { activeFleet, currentState, ensureCanvas, killFleet,
|
|
2
|
+
import { activeFleet, currentState, ensureCanvas, killFleet, requestRelaunch, startLoop, statusText, stopCanvas, updateWidget } from "./controller.js";
|
|
3
|
+
import { writeWorkerPrompts } from "./fleet-store.js";
|
|
5
4
|
import { openInBrowser, listFleetRoots } from "./canvas.js";
|
|
6
5
|
import { insertWorkers } from "./insert.js";
|
|
7
6
|
import { editConfig, editNode, type ConfigEditKey, type NodeEditKey } from "./edits.js";
|
|
8
|
-
import {
|
|
7
|
+
import { listModelRefs } from "./model-resolution.js";
|
|
9
8
|
import { clearPreference, loadPreferences, PREFERENCE_KEYS, savePreferences, setPreference } from "./preferences.js";
|
|
10
|
-
import {
|
|
9
|
+
import { recoverLatestFleet } from "./fleet-recovery.js";
|
|
10
|
+
import { writeState } from "./state.js";
|
|
11
11
|
import { buildWidgetLines } from "./ui.js";
|
|
12
12
|
import { renderDag } from "./viz.js";
|
|
13
13
|
|
|
14
14
|
export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
15
15
|
pi.registerCommand("fleet", {
|
|
16
|
-
description: "Fleet commands: /fleet viz, /fleet status, /fleet canvas [stop], /fleet configure [show|set k v], /fleet add <json>, /fleet edit <node_id>|config ..., /fleet clear, /fleet kill all|<node_id>, /fleet pause, /fleet resume, /fleet relaunch <node_id> [model]",
|
|
16
|
+
description: "Fleet commands: /fleet viz, /fleet status, /fleet models, /fleet canvas [stop], /fleet configure [show|set k v], /fleet add <json>, /fleet edit <node_id>|config ..., /fleet clear, /fleet kill all|<node_id>, /fleet pause, /fleet resume, /fleet continue, /fleet relaunch <node_id> [model]",
|
|
17
17
|
handler: async (args, ctx) => {
|
|
18
18
|
const [cmd, target] = args.trim().split(/\s+/);
|
|
19
19
|
if (cmd === "configure") {
|
|
@@ -32,7 +32,7 @@ export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
const prefs = await loadPreferences();
|
|
35
|
-
const r = setPreference(prefs, key, value);
|
|
35
|
+
const r = setPreference(prefs, key, value, ctx.modelRegistry);
|
|
36
36
|
if (!r.ok) {
|
|
37
37
|
ctx.ui.notify(r.error, "error");
|
|
38
38
|
return;
|
|
@@ -50,15 +50,19 @@ export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
|
50
50
|
const key = field.split(":")[0] as (typeof PREFERENCE_KEYS)[number];
|
|
51
51
|
const input = await ctx.ui.input(`${key} (current: ${prefs[key] ?? "—"}):`, "empty clears");
|
|
52
52
|
if (input === undefined) break;
|
|
53
|
+
let err: string | undefined;
|
|
53
54
|
const next = input.trim().length === 0
|
|
54
55
|
? clearPreference(prefs, key)
|
|
55
56
|
: (() => {
|
|
56
|
-
const r = setPreference(prefs, key, input.trim());
|
|
57
|
-
if (!r.ok)
|
|
57
|
+
const r = setPreference(prefs, key, input.trim(), ctx.modelRegistry);
|
|
58
|
+
if (!r.ok) {
|
|
59
|
+
err = r.error;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
58
62
|
return r.prefs;
|
|
59
63
|
})();
|
|
60
64
|
if (next === undefined) {
|
|
61
|
-
ctx.ui.notify(`invalid value for ${key}`, "error");
|
|
65
|
+
ctx.ui.notify(err ?? `invalid value for ${key}`, "error");
|
|
62
66
|
continue;
|
|
63
67
|
}
|
|
64
68
|
await savePreferences(next);
|
|
@@ -87,26 +91,36 @@ export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
|
87
91
|
ctx.ui.notify(`fleet canvas: ${url}`, "info");
|
|
88
92
|
return;
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
if (cmd === "models") {
|
|
95
|
+
ctx.ui.notify(`available models:\n${listModelRefs(ctx.modelRegistry).join("\n")}`, "info");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const active = activeFleet.current ?? await recoverLatestFleet(ctx.cwd);
|
|
99
|
+
if (active) activeFleet.current ??= active;
|
|
91
100
|
if (!active) {
|
|
92
101
|
ctx.ui.notify("no fleet planned yet", "warning");
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
95
104
|
if (cmd === "viz") {
|
|
105
|
+
active.widgetVisible = true;
|
|
96
106
|
const lines = renderDag(active.spec, active.state).split("\n");
|
|
97
107
|
ctx.ui.setWidget("fleet", lines);
|
|
108
|
+
ctx.ui.notify("fleet widget visible; fleet canvas link: " + (await ensureCanvas(ctx)).url, "info");
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
111
|
if (cmd === "status" || cmd === "") {
|
|
101
|
-
|
|
112
|
+
const server = await ensureCanvas(ctx);
|
|
113
|
+
ctx.ui.notify(`${await statusText(active)}\n\nfleet canvas: ${server.url}`, "info");
|
|
102
114
|
return;
|
|
103
115
|
}
|
|
104
116
|
if (cmd === "clear") {
|
|
117
|
+
active.widgetVisible = false;
|
|
105
118
|
ctx.ui.setWidget("fleet", []);
|
|
119
|
+
ctx.ui.notify("fleet widget hidden", "info");
|
|
106
120
|
return;
|
|
107
121
|
}
|
|
108
122
|
if (cmd === "kill") {
|
|
109
|
-
const text = await killFleet(target ?? "");
|
|
123
|
+
const text = await killFleet(target ?? "", ctx.cwd);
|
|
110
124
|
const severity = text.includes("kill") && !text.startsWith("unknown") && !text.includes("already") ? "warning" : "error";
|
|
111
125
|
ctx.ui.notify(text, severity);
|
|
112
126
|
return;
|
|
@@ -141,47 +155,47 @@ export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
|
141
155
|
ctx.ui.notify("fleet resumed", "info");
|
|
142
156
|
return;
|
|
143
157
|
}
|
|
144
|
-
if (cmd === "
|
|
145
|
-
if (!target) {
|
|
146
|
-
ctx.ui.notify("usage: /fleet relaunch <node_id> [model]", "warning");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
158
|
+
if (cmd === "continue") {
|
|
149
159
|
if (active.running) {
|
|
150
|
-
ctx.ui.notify("fleet
|
|
160
|
+
ctx.ui.notify("fleet already running", "warning");
|
|
151
161
|
return;
|
|
152
162
|
}
|
|
153
163
|
await currentState(active);
|
|
154
164
|
if (active.state.status === "completed") {
|
|
155
|
-
ctx.ui.notify("fleet completed, nothing to
|
|
165
|
+
ctx.ui.notify("fleet completed, nothing to continue", "warning");
|
|
156
166
|
return;
|
|
157
167
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
ctx.ui.notify(`unknown node "${target}"`, "warning");
|
|
168
|
+
if (active.state.status === "paused") {
|
|
169
|
+
ctx.ui.notify("fleet is paused; use /fleet resume for paused loop fleets", "warning");
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!node || !relaunchable.has(node.status)) {
|
|
166
|
-
ctx.ui.notify(`node "${target}" status ${node?.status ?? "missing"} cannot be relaunched; must be failed, contract_failed, or killed`, "warning");
|
|
172
|
+
if (active.state.status === "planned" && Object.values(active.state.nodes).every((n) => n.status === "pending")) {
|
|
173
|
+
ctx.ui.notify("fleet has not started; use /fleet launch", "warning");
|
|
167
174
|
return;
|
|
168
175
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!resolved.ok) {
|
|
173
|
-
ctx.ui.notify(resolved.error, "error");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const canonical = `${resolved.model.provider}/${resolved.model.id}`;
|
|
177
|
-
active.spec.workers = active.spec.workers.map((w) => w.id === target ? { ...w, model: canonical } : w);
|
|
178
|
-
await writeFile(join(active.fleetRoot, "fleet.json"), `${JSON.stringify(active.spec, null, 2)}\n`, "utf-8");
|
|
179
|
-
}
|
|
180
|
-
active.state = resetForRelaunch(active.state, active.spec, target);
|
|
176
|
+
active.killSwitch.killed = false;
|
|
177
|
+
active.pauseSwitch.paused = false;
|
|
178
|
+
active.state = { ...active.state, status: "running", paused: false };
|
|
181
179
|
await writeState(active.fleetRoot, active.state);
|
|
182
|
-
|
|
180
|
+
await writeWorkerPrompts(active);
|
|
183
181
|
void startLoop(active, ctx, false, true);
|
|
184
|
-
ctx.ui.notify(
|
|
182
|
+
ctx.ui.notify("fleet continue requested", "info");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (cmd === "relaunch") {
|
|
186
|
+
if (!target) {
|
|
187
|
+
ctx.ui.notify("usage: /fleet relaunch <node_id> [model]", "warning");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await currentState(active);
|
|
191
|
+
if (active.state.status === "completed") {
|
|
192
|
+
ctx.ui.notify("fleet completed, nothing to relaunch", "warning");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const model = args.trim().split(/\s+/).slice(2).join(" ") || undefined;
|
|
196
|
+
const result = await requestRelaunch(active, target, model, ctx.modelRegistry);
|
|
197
|
+
if (result.startNow) void startLoop(active, ctx, false, true);
|
|
198
|
+
ctx.ui.notify(result.message, result.startNow ? "info" : result.message.startsWith("relaunch queued") ? "info" : "warning");
|
|
185
199
|
return;
|
|
186
200
|
}
|
|
187
201
|
if (cmd === "add") {
|
|
@@ -241,7 +255,7 @@ export function registerFleetCommand(pi: ExtensionAPI): void {
|
|
|
241
255
|
if (r.ok) updateWidget(ctx, active);
|
|
242
256
|
return;
|
|
243
257
|
}
|
|
244
|
-
ctx.ui.notify("usage: /fleet viz | /fleet status | /fleet canvas [stop] | /fleet configure [show|set k v] | /fleet add <json> | /fleet edit <node_id>|config ... | /fleet clear | /fleet kill all|<node_id> | /fleet pause | /fleet resume | /fleet relaunch <node_id> [model]", "warning");
|
|
258
|
+
ctx.ui.notify("usage: /fleet viz | /fleet status | /fleet models | /fleet canvas [stop] | /fleet configure [show|set k v] | /fleet add <json> | /fleet edit <node_id>|config ... | /fleet clear | /fleet kill all|<node_id> | /fleet pause | /fleet resume | /fleet continue | /fleet relaunch <node_id> [model]", "warning");
|
|
245
259
|
},
|
|
246
260
|
});
|
|
247
261
|
}
|