pi-agent-fleet 0.4.0 → 0.5.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 +169 -18
- package/src/canvas.ts +173 -85
- package/src/command.ts +56 -14
- package/src/contracts.ts +68 -16
- package/src/controller.ts +25 -4
- package/src/dag.ts +32 -7
- package/src/edits.ts +8 -9
- package/src/fleet-recovery.ts +73 -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 +10 -0
- package/src/report.ts +28 -1
- package/src/scheduler.ts +3 -2
- package/src/tools.ts +67 -14
- package/src/types.ts +8 -0
- package/src/worktree.ts +6 -0
package/src/tools.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { Type } from "typebox";
|
|
@@ -8,8 +7,9 @@ import { loadPreferences, mergeFleetConfig } from "./preferences.js";
|
|
|
8
7
|
import { activeFleet, currentState, dagPreview, ensureCanvas, killFleet, prepareRelaunch, startLoop, statusText, stopCanvas, updateWidget } from "./controller.js";
|
|
9
8
|
import { openInBrowser, listFleetRoots } from "./canvas.js";
|
|
10
9
|
import { editConfig, editNode, type ConfigEditKey, type NodeEditKey } from "./edits.js";
|
|
11
|
-
import { ensureFleetGitignore, fleetRootFor, isInsideGitRepo, writePlanFiles, writeWorkerPrompts } from "./fleet-store.js";
|
|
12
|
-
import {
|
|
10
|
+
import { ensureFleetGitignore, fleetRootFor, isInsideGitRepo, persistFleetJson, writePlanFiles, writeWorkerPrompts } from "./fleet-store.js";
|
|
11
|
+
import { recoverLatestFleet } from "./fleet-recovery.js";
|
|
12
|
+
import { listModelRefs, resolveModelReference, validateFleetModels } from "./model-resolution.js";
|
|
13
13
|
import { runFleetDesign, slugifyFleetName } from "./planner.js";
|
|
14
14
|
import { writeReport } from "./report.js";
|
|
15
15
|
import { initFleetState, resetForRelaunch, writeState } from "./state.js";
|
|
@@ -27,6 +27,10 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
27
27
|
Type.Literal("verdict"), Type.Literal("json"), Type.Literal("yaml"),
|
|
28
28
|
]),
|
|
29
29
|
required: Type.Optional(Type.Boolean()),
|
|
30
|
+
schema: Type.Optional(Type.Object({
|
|
31
|
+
required_keys: Type.Optional(Type.Array(Type.String(), { description: "JSON object keys that must be present" })),
|
|
32
|
+
number_keys: Type.Optional(Type.Array(Type.String(), { description: "JSON object keys that must be numbers or arrays of numbers" })),
|
|
33
|
+
}, { description: "Optional JSON object shape; only valid with kind json" })),
|
|
30
34
|
});
|
|
31
35
|
const EffortSchema = Type.Union([
|
|
32
36
|
Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"),
|
|
@@ -67,7 +71,7 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
67
71
|
name: "fleet_plan",
|
|
68
72
|
label: "Fleet Plan",
|
|
69
73
|
description:
|
|
70
|
-
"Validate a fleet DAG definition, create its fleet root, and return an ASCII preview. Does NOT launch. PREREQUISITE: if the user's request is prose requirements or a goal rather than an explicit fleet definition, you MUST call fleet_design first and pass its drafted definition here — do not hand-write the fleet JSON yourself. Present the preview to the user; call fleet_launch only after they explicitly confirm. Choose models by task difficulty: cheap/fast models for trivial writers and validators, mid-tier coding models for code-run workers, strongest reasoning models for reviewers and synthesizers. When several models fit a tier, vary providers across nodes instead of defaulting to one family. Set worker.model per node to override config.model. All model refs are validated against the live registry — planning fails if any model is unavailable.",
|
|
74
|
+
"Validate a fleet DAG definition, create its fleet root, and return an ASCII preview. Does NOT launch. PREREQUISITE: if the user's request is prose requirements or a goal rather than an explicit fleet definition, you MUST call fleet_design first and pass its drafted definition here — do not hand-write the fleet JSON yourself. Present the preview to the user; call fleet_launch only after they explicitly confirm. Choose models by task difficulty: cheap/fast models for trivial writers and validators, mid-tier coding models for code-run workers, strongest reasoning models for reviewers and synthesizers. When several models fit a tier, vary providers across nodes instead of defaulting to one family. Set worker.model per node to override config.model. All model refs are validated against the live registry — planning fails if any model is unavailable. Call fleet_models first if you do not know exact provider/model IDs.",
|
|
71
75
|
promptSnippet: "Plan a DAG-of-agents fleet from a fleet definition without launching it.",
|
|
72
76
|
parameters: Type.Object({
|
|
73
77
|
fleet: FleetSchema,
|
|
@@ -84,11 +88,12 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
84
88
|
const state = initFleetState(v.spec);
|
|
85
89
|
await ensureFleetGitignore(ctx.cwd);
|
|
86
90
|
await writePlanFiles(fleetRoot, v.spec, state);
|
|
87
|
-
const active = activeFleet.current = { spec: v.spec, fleetRoot, state, killSwitch: { killed: false }, pauseSwitch: { paused: false }, running: false, costWarned: false, sessions: new Map(), killedNodes: new Set() };
|
|
91
|
+
const active = activeFleet.current = { spec: v.spec, fleetRoot, state, killSwitch: { killed: false }, pauseSwitch: { paused: false }, running: false, costWarned: false, sessions: new Map(), killedNodes: new Set(), widgetVisible: false };
|
|
88
92
|
updateWidget(ctx, active);
|
|
89
93
|
|
|
90
94
|
const dag = await dagPreview(v.spec, undefined, fleetRoot);
|
|
91
|
-
|
|
95
|
+
const canvas = ctx.hasUI ? await ensureCanvas(ctx) : undefined;
|
|
96
|
+
return textResult(`${dag}\n\nfleet root: ${fleetRoot}${canvas ? `\nfleet canvas: ${canvas.url}` : ""}\n\nShow this preview to the user. Call fleet_launch only after they explicitly confirm.`, { fleetRoot, layers: v.layers, canvasUrl: canvas?.url });
|
|
92
97
|
},
|
|
93
98
|
});
|
|
94
99
|
|
|
@@ -117,14 +122,32 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
if (fleet.state.status !== "planned" || Object.values(fleet.state.nodes).some((n) => n.status !== "pending")) {
|
|
126
|
+
return textResult(
|
|
127
|
+
`fleet already started (${fleet.state.status}); use fleet_continue to resume pending work, fleet_relaunch <node_id> to retry a failed node, or start a new fleet with fleet_plan`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
if (ctx.hasUI && !params.skip_confirm) {
|
|
121
132
|
const ok = await ctx.ui.confirm("Launch fleet?", renderDag(fleet.spec));
|
|
122
133
|
if (!ok) return textResult("fleet launch aborted");
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
await writeWorkerPrompts(fleet);
|
|
137
|
+
const canvas = ctx.hasUI ? await ensureCanvas(ctx) : undefined;
|
|
126
138
|
void startLoop(fleet, ctx, false);
|
|
127
|
-
return textResult(
|
|
139
|
+
return textResult(`fleet launched${canvas ? `\n\nfleet canvas: ${canvas.url}` : ""}`, { canvasUrl: canvas?.url });
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerTool({
|
|
144
|
+
name: "fleet_models",
|
|
145
|
+
label: "Fleet Models",
|
|
146
|
+
description: "List available model refs (provider/id) from the live registry. Call this before fleet_plan if you do not know exact provider/model IDs.",
|
|
147
|
+
promptSnippet: "List available fleet model refs.",
|
|
148
|
+
parameters: Type.Object({}),
|
|
149
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
150
|
+
return textResult(`available models:\n${listModelRefs(ctx.modelRegistry).join("\n")}`);
|
|
128
151
|
},
|
|
129
152
|
});
|
|
130
153
|
|
|
@@ -134,8 +157,9 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
134
157
|
description: "Show the current active fleet DAG and live widget lines.",
|
|
135
158
|
promptSnippet: "Show current active fleet status.",
|
|
136
159
|
parameters: Type.Object({}),
|
|
137
|
-
async execute() {
|
|
138
|
-
const active = activeFleet.current;
|
|
160
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
161
|
+
const active = activeFleet.current ?? await recoverLatestFleet(ctx.cwd);
|
|
162
|
+
if (active) activeFleet.current ??= active;
|
|
139
163
|
if (!active) return textResult("no fleet planned yet");
|
|
140
164
|
return textResult(await statusText(active));
|
|
141
165
|
},
|
|
@@ -147,8 +171,8 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
147
171
|
description: "Request a fleet-wide kill (target \"all\") or kill a single node by worker id. Killing a running node aborts its session; killing a pending node marks it killed at the next dispatch pass. Killed nodes can be revived with fleet_relaunch.",
|
|
148
172
|
promptSnippet: "Kill the whole fleet or a single node by worker id.",
|
|
149
173
|
parameters: Type.Object({ target: Type.String({ description: "all or a worker id" }) }),
|
|
150
|
-
async execute(_id, params) {
|
|
151
|
-
return textResult(await killFleet(params.target));
|
|
174
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
175
|
+
return textResult(await killFleet(params.target, ctx.cwd));
|
|
152
176
|
},
|
|
153
177
|
});
|
|
154
178
|
|
|
@@ -188,6 +212,33 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
188
212
|
},
|
|
189
213
|
});
|
|
190
214
|
|
|
215
|
+
pi.registerTool({
|
|
216
|
+
name: "fleet_continue",
|
|
217
|
+
label: "Fleet Continue",
|
|
218
|
+
description: "Continue the active fleet from its current state without restarting completed nodes. Dispatches pending and ready nodes and unblocks downstream as dependencies complete. Use this after a failed/killed fleet to resume work safely.",
|
|
219
|
+
promptSnippet: "Continue the active fleet from current state.",
|
|
220
|
+
parameters: Type.Object({}),
|
|
221
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
222
|
+
const active = activeFleet.current ?? await recoverLatestFleet(ctx.cwd);
|
|
223
|
+
if (active) activeFleet.current ??= active;
|
|
224
|
+
if (!active) return textResult("no fleet planned yet");
|
|
225
|
+
if (active.running) return textResult("fleet already running");
|
|
226
|
+
await currentState(active);
|
|
227
|
+
if (active.state.status === "completed") return textResult("fleet completed, nothing to continue");
|
|
228
|
+
if (active.state.status === "paused") return textResult("fleet is paused; use fleet_resume for paused loop fleets");
|
|
229
|
+
if (active.state.status === "planned" && Object.values(active.state.nodes).every((n) => n.status === "pending")) {
|
|
230
|
+
return textResult("fleet has not started; use fleet_launch");
|
|
231
|
+
}
|
|
232
|
+
active.killSwitch.killed = false;
|
|
233
|
+
active.pauseSwitch.paused = false;
|
|
234
|
+
active.state = { ...active.state, status: "running", paused: false };
|
|
235
|
+
await writeState(active.fleetRoot, active.state);
|
|
236
|
+
await writeWorkerPrompts(active);
|
|
237
|
+
void startLoop(active, ctx, false, true);
|
|
238
|
+
return textResult("fleet continue requested");
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
191
242
|
pi.registerTool({
|
|
192
243
|
name: "fleet_relaunch",
|
|
193
244
|
label: "Fleet Relaunch",
|
|
@@ -216,10 +267,11 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
216
267
|
if (!resolved.ok) return textResult(resolved.error);
|
|
217
268
|
const canonical = `${resolved.model.provider}/${resolved.model.id}`;
|
|
218
269
|
fleet.spec.workers = fleet.spec.workers.map((w) => w.id === params.node_id ? { ...w, model: canonical } : w);
|
|
219
|
-
await
|
|
270
|
+
await persistFleetJson(fleet);
|
|
220
271
|
}
|
|
221
272
|
fleet.state = resetForRelaunch(fleet.state, fleet.spec, params.node_id);
|
|
222
273
|
await writeState(fleet.fleetRoot, fleet.state);
|
|
274
|
+
await writeWorkerPrompts(fleet);
|
|
223
275
|
prepareRelaunch(fleet, params.node_id);
|
|
224
276
|
void startLoop(fleet, ctx, false, true);
|
|
225
277
|
return textResult(`fleet relaunch requested for ${params.node_id}`);
|
|
@@ -289,7 +341,8 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
289
341
|
promptSnippet: "Regenerate the active fleet report.",
|
|
290
342
|
parameters: Type.Object({}),
|
|
291
343
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
292
|
-
const active = activeFleet.current;
|
|
344
|
+
const active = activeFleet.current ?? await recoverLatestFleet(ctx.cwd);
|
|
345
|
+
if (active) activeFleet.current ??= active;
|
|
293
346
|
if (!active) return textResult("no fleet planned yet");
|
|
294
347
|
const state = await currentState(active);
|
|
295
348
|
const report = await writeReport({ spec: active.spec, state, fleetRoot: active.fleetRoot, repoCwd: ctx.cwd });
|
|
@@ -300,7 +353,7 @@ export function registerFleetTools(pi: ExtensionAPI): void {
|
|
|
300
353
|
pi.registerTool({
|
|
301
354
|
name: "fleet_edit",
|
|
302
355
|
label: "Fleet Edit",
|
|
303
|
-
description: "Edit the active fleet: a pending node's model, effort, or task — or fleet config (max_concurrent, warn_cost_usd, model, effort) when node_id is omitted. Changes persist to fleet.json and apply to nodes
|
|
356
|
+
description: "Edit the active fleet: a pending or relaunchable node's model, effort, or task — or fleet config (max_concurrent, warn_cost_usd, model, effort) when node_id is omitted. Changes persist to fleet.json and apply immediately. Edits to running, completed, or blocked nodes are refused; failed, contract_failed, and killed nodes can be edited so relaunch uses the updated task.",
|
|
304
357
|
promptSnippet: "Edit a pending fleet node or fleet config.",
|
|
305
358
|
parameters: Type.Object({
|
|
306
359
|
node_id: Type.Optional(Type.String({ description: "Worker id to edit; omit for fleet config edits" })),
|
package/src/types.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export type OutputKind = "markdown" | "file-exists" | "verdict" | "json" | "yaml";
|
|
2
2
|
|
|
3
|
+
export interface JsonOutputSchema {
|
|
4
|
+
required_keys?: string[];
|
|
5
|
+
number_keys?: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export interface ContractOutput {
|
|
4
9
|
path: string;
|
|
5
10
|
kind: OutputKind;
|
|
6
11
|
required: boolean;
|
|
12
|
+
schema?: JsonOutputSchema;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export type WorkerType = "research" | "code-run" | "reviewer" | "write" | "read-only";
|
|
@@ -64,6 +70,8 @@ export interface ContractCheck {
|
|
|
64
70
|
required: boolean;
|
|
65
71
|
ok: boolean;
|
|
66
72
|
error?: string;
|
|
73
|
+
actualPath?: string;
|
|
74
|
+
firstLines?: string;
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
export interface ContractResult {
|
package/src/worktree.ts
CHANGED
|
@@ -17,6 +17,7 @@ export async function createWorktree(opts: CreateWorktreeOpts): Promise<string>
|
|
|
17
17
|
const branch = `fleet/${opts.fleetName}/${opts.nodeId}`;
|
|
18
18
|
await mkdir(path, { recursive: true });
|
|
19
19
|
await removeWorktree(path, opts.baseRepo);
|
|
20
|
+
await removeBranch(opts.baseRepo, branch);
|
|
20
21
|
await execFileP("git", ["worktree", "add", "-b", branch, path], { cwd: opts.baseRepo });
|
|
21
22
|
return path;
|
|
22
23
|
}
|
|
@@ -55,6 +56,7 @@ export async function prepareIntegratorWorktree(
|
|
|
55
56
|
): Promise<{ path: string; ok: boolean; conflict?: string }> {
|
|
56
57
|
const path = join(opts.fleetRoot, "worktrees", "fleet-integrator");
|
|
57
58
|
await removeWorktree(path, opts.baseRepo);
|
|
59
|
+
await removeBranch(opts.baseRepo, `fleet/${opts.fleetName}/fleet-integrator`);
|
|
58
60
|
await mkdir(path, { recursive: true });
|
|
59
61
|
await execFileP("git", ["worktree", "add", "-b", `fleet/${opts.fleetName}/fleet-integrator`, path], {
|
|
60
62
|
cwd: opts.baseRepo,
|
|
@@ -73,3 +75,7 @@ export async function prepareIntegratorWorktree(
|
|
|
73
75
|
export async function removeWorktree(path: string, baseRepo: string): Promise<void> {
|
|
74
76
|
await execFileP("git", ["worktree", "remove", "--force", path], { cwd: baseRepo }).catch(() => {});
|
|
75
77
|
}
|
|
78
|
+
|
|
79
|
+
export async function removeBranch(baseRepo: string, branch: string): Promise<void> {
|
|
80
|
+
await execFileP("git", ["branch", "-D", branch], { cwd: baseRepo }).catch(() => {});
|
|
81
|
+
}
|