pi-crew 0.1.20 → 0.1.21
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/package.json +1 -1
- package/src/extension/register.ts +44 -10
- package/src/extension/team-tool.ts +6 -3
- package/src/runtime/background-runner.ts +6 -4
- package/src/runtime/direct-run.ts +35 -0
- package/src/runtime/subagent-manager.ts +34 -0
- package/src/state/state-store.ts +1 -0
- package/src/state/types.ts +1 -0
- package/workflows/implementation.workflow.md +1 -1
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@ import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-vie
|
|
|
19
19
|
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
20
20
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
21
21
|
import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
|
|
22
|
-
import { SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
|
|
22
|
+
import { readPersistedSubagentRecord, savePersistedSubagentRecord, SubagentManager, type SubagentRecord, type SubagentSpawnOptions } from "../runtime/subagent-manager.ts";
|
|
23
23
|
|
|
24
24
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
25
25
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -109,6 +109,18 @@ function sendFollowUp(pi: ExtensionAPI, content: string): void {
|
|
|
109
109
|
sender.call(pi, { customType: "pi-crew-subagent-notification", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function refreshPersistedSubagentRecord(ctx: ExtensionContext | ExtensionCommandContext, record: SubagentRecord): SubagentRecord {
|
|
113
|
+
if (!record.runId) return record;
|
|
114
|
+
const loaded = loadRunManifestById(ctx.cwd, record.runId);
|
|
115
|
+
if (!loaded) return record;
|
|
116
|
+
if (loaded.manifest.status === "completed" || loaded.manifest.status === "failed" || loaded.manifest.status === "cancelled" || loaded.manifest.status === "blocked") {
|
|
117
|
+
const refreshed = { ...record, status: loaded.manifest.status === "completed" ? "completed" as const : loaded.manifest.status === "cancelled" ? "cancelled" as const : "failed" as const, error: loaded.manifest.status === "completed" ? undefined : loaded.manifest.summary, completedAt: record.completedAt ?? Date.now() };
|
|
118
|
+
savePersistedSubagentRecord(ctx.cwd, refreshed);
|
|
119
|
+
return refreshed;
|
|
120
|
+
}
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
function formatSubagentRecord(record: SubagentRecord): string {
|
|
113
125
|
const duration = record.completedAt ? `${Math.round((record.completedAt - record.startedAt) / 1000)}s` : "running";
|
|
114
126
|
return [
|
|
@@ -338,9 +350,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
338
350
|
agent: spawnOptions.type,
|
|
339
351
|
goal: spawnOptions.prompt,
|
|
340
352
|
model: spawnOptions.model,
|
|
341
|
-
async:
|
|
353
|
+
async: spawnOptions.background,
|
|
342
354
|
config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
|
|
343
|
-
}, spawnOptions.background ? { ...ctx, signal: childSignal
|
|
355
|
+
}, spawnOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
344
356
|
const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
|
|
345
357
|
if (options.background || record.status === "queued") {
|
|
346
358
|
return subagentToolResult([`Agent ${record.status === "queued" ? "queued" : "started"}.`, `Agent ID: ${record.id}`, `Type: ${record.type}`, `Description: ${record.description}`, "Use get_subagent_result to retrieve output. Do not duplicate this agent's work."].join("\n"), { agentId: record.id, status: record.status });
|
|
@@ -360,18 +372,40 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
360
372
|
wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
|
|
361
373
|
verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
|
|
362
374
|
}) as never,
|
|
363
|
-
async execute(_id, params,
|
|
375
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
364
376
|
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
365
377
|
if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
|
|
366
|
-
const
|
|
378
|
+
const inMemory = subagentManager.getRecord(p.agent_id);
|
|
379
|
+
const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
|
|
367
380
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
|
|
368
|
-
let current = record;
|
|
381
|
+
let current = refreshPersistedSubagentRecord(ctx, record);
|
|
382
|
+
if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
|
|
383
|
+
current = { ...current, status: "error", error: "Subagent was interrupted before its durable run id was recorded; it cannot be recovered after restart.", completedAt: current.completedAt ?? Date.now() };
|
|
384
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
385
|
+
}
|
|
369
386
|
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
370
387
|
current.resultConsumed = true;
|
|
371
|
-
|
|
388
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
389
|
+
const waited = await subagentManager.waitForRecord(current.id);
|
|
390
|
+
if (waited) current = waited;
|
|
391
|
+
else {
|
|
392
|
+
while (current.status === "running" || current.status === "queued") {
|
|
393
|
+
if (signal?.aborted) {
|
|
394
|
+
current = { ...current, status: "error", error: "Waiting for subagent result was aborted.", completedAt: Date.now() };
|
|
395
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
399
|
+
current = refreshPersistedSubagentRecord(ctx, current);
|
|
400
|
+
if (!current.runId) break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
372
403
|
}
|
|
373
404
|
const output = readSubagentRunResult(ctx, current);
|
|
374
|
-
if (current.status !== "running" && current.status !== "queued")
|
|
405
|
+
if (current.status !== "running" && current.status !== "queued") {
|
|
406
|
+
current.resultConsumed = true;
|
|
407
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
408
|
+
}
|
|
375
409
|
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "Agent is still running. Use wait=true or check again later." : current.error ?? "No output."].filter((line): line is string => Boolean(line)).join("\n");
|
|
376
410
|
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
377
411
|
},
|
|
@@ -382,9 +416,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
382
416
|
label: "Steer Agent",
|
|
383
417
|
description: "Send a steering note to a running pi-crew subagent. Live-session steering is planned; child-process runs expose durable status and can be cancelled if needed.",
|
|
384
418
|
parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
|
|
385
|
-
async execute(_id, params) {
|
|
419
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
386
420
|
const p = params as { agent_id?: string; message?: string };
|
|
387
|
-
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) : undefined;
|
|
421
|
+
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
|
|
388
422
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
|
|
389
423
|
return subagentToolResult([`Steering request noted for ${record.id}.`, "Current default pi-crew backend is child-process, so mid-turn session.steer is not available yet.", record.runId ? `Use team cancel runId=${record.runId} if the agent must be interrupted.` : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
|
|
390
424
|
},
|
|
@@ -46,6 +46,7 @@ import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "
|
|
|
46
46
|
import { appendLiveAgentControlRequest } from "../runtime/live-agent-control.ts";
|
|
47
47
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
48
48
|
import { formatTaskGraphLines, waitingReason } from "../runtime/task-display.ts";
|
|
49
|
+
import { directTeamAndWorkflowFromRun } from "../runtime/direct-run.ts";
|
|
49
50
|
|
|
50
51
|
export interface TeamToolDetails {
|
|
51
52
|
action: string;
|
|
@@ -513,9 +514,11 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
513
514
|
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
514
515
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "resume", status: "error" }, true);
|
|
515
516
|
if (!loaded.manifest.workflow) return result(`Run '${params.runId}' has no workflow to resume.`, { action: "resume", status: "error" }, true);
|
|
516
|
-
const
|
|
517
|
+
const agents = allAgents(discoverAgents(ctx.cwd));
|
|
518
|
+
const direct = directTeamAndWorkflowFromRun(loaded.manifest, loaded.tasks, agents);
|
|
519
|
+
const team = direct?.team ?? allTeams(discoverTeams(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.team);
|
|
517
520
|
if (!team) return result(`Team '${loaded.manifest.team}' not found.`, { action: "resume", status: "error" }, true);
|
|
518
|
-
const workflow = allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
|
|
521
|
+
const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.workflow);
|
|
519
522
|
if (!workflow) return result(`Workflow '${loaded.manifest.workflow}' not found.`, { action: "resume", status: "error" }, true);
|
|
520
523
|
return await withRunLock(loaded.manifest, async () => {
|
|
521
524
|
const resetTasks = loaded.tasks.map((task) => task.status === "failed" || task.status === "cancelled" || task.status === "skipped" || task.status === "running" ? { ...task, status: "queued" as const, error: undefined, startedAt: undefined, finishedAt: undefined, claim: undefined } : task);
|
|
@@ -524,7 +527,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
524
527
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
525
528
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
526
529
|
const executeWorkers = runtime.kind !== "scaffold";
|
|
527
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents
|
|
530
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
|
|
528
531
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
529
532
|
});
|
|
530
533
|
}
|
|
@@ -6,6 +6,7 @@ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows
|
|
|
6
6
|
import { loadConfig } from "../config/config.ts";
|
|
7
7
|
import { executeTeamRun } from "./team-runner.ts";
|
|
8
8
|
import { resolveCrewRuntime } from "./runtime-resolver.ts";
|
|
9
|
+
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
9
10
|
|
|
10
11
|
function argValue(name: string): string | undefined {
|
|
11
12
|
const index = process.argv.indexOf(name);
|
|
@@ -24,14 +25,15 @@ async function main(): Promise<void> {
|
|
|
24
25
|
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const agents = allAgents(discoverAgents(cwd));
|
|
29
|
+
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|
|
30
|
+
const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
28
31
|
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
29
|
-
const workflow = allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
32
|
+
const workflow = direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
30
33
|
if (!workflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
31
|
-
const agents = allAgents(discoverAgents(cwd));
|
|
32
34
|
const loadedConfig = loadConfig(cwd);
|
|
33
35
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
34
|
-
const executeWorkers = runtime.kind
|
|
36
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
35
37
|
const result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime });
|
|
36
38
|
manifest = result.manifest;
|
|
37
39
|
tasks = result.tasks;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import type { TeamConfig } from "../teams/team-config.ts";
|
|
4
|
+
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
5
|
+
|
|
6
|
+
export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
|
|
7
|
+
return manifest.workflow === "direct-agent";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
|
|
11
|
+
if (!isDirectRun(manifest)) return undefined;
|
|
12
|
+
const firstTask = tasks[0];
|
|
13
|
+
const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
|
|
14
|
+
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
15
|
+
const role = firstTask?.role ?? "agent";
|
|
16
|
+
const stepId = firstTask?.stepId ?? "01_agent";
|
|
17
|
+
return {
|
|
18
|
+
team: {
|
|
19
|
+
name: manifest.team,
|
|
20
|
+
description: `Direct subagent run for ${agentName}`,
|
|
21
|
+
source: "builtin",
|
|
22
|
+
filePath: "<generated>",
|
|
23
|
+
roles: [{ name: role, agent: agentName, description: agent?.description }],
|
|
24
|
+
defaultWorkflow: "direct-agent",
|
|
25
|
+
workspaceMode: manifest.workspaceMode,
|
|
26
|
+
},
|
|
27
|
+
workflow: {
|
|
28
|
+
name: manifest.workflow ?? "direct-agent",
|
|
29
|
+
description: `Direct task for ${agentName}`,
|
|
30
|
+
source: "builtin",
|
|
31
|
+
filePath: "<generated>",
|
|
32
|
+
steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
2
4
|
import type { PiTeamsToolResult } from "../extension/tool-result.ts";
|
|
5
|
+
import { projectPiRoot } from "../utils/paths.ts";
|
|
3
6
|
|
|
4
7
|
export type SubagentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "error" | "stopped";
|
|
5
8
|
|
|
@@ -42,6 +45,32 @@ interface QueuedSpawn {
|
|
|
42
45
|
|
|
43
46
|
const TERMINAL_RUN_STATUS = new Set(["completed", "failed", "cancelled", "blocked"]);
|
|
44
47
|
|
|
48
|
+
function persistedSubagentPath(cwd: string, id: string): string {
|
|
49
|
+
return path.join(projectPiRoot(cwd), "teams", "state", "subagents", `${id}.json`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function serializableRecord(record: SubagentRecord): SubagentRecord {
|
|
53
|
+
const { promise: _promise, ...rest } = record;
|
|
54
|
+
return rest;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function savePersistedSubagentRecord(cwd: string, record: SubagentRecord): void {
|
|
58
|
+
try {
|
|
59
|
+
const filePath = persistedSubagentPath(cwd, record.id);
|
|
60
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
61
|
+
fs.writeFileSync(filePath, `${JSON.stringify(serializableRecord(record), null, 2)}\n`, "utf-8");
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readPersistedSubagentRecord(cwd: string, id: string): SubagentRecord | undefined {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(fs.readFileSync(persistedSubagentPath(cwd, id), "utf-8"));
|
|
68
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as SubagentRecord : undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
function resultText(result: PiTeamsToolResult): string {
|
|
46
75
|
return result.content?.map((item) => item.type === "text" ? item.text : "").filter(Boolean).join("\n") ?? "";
|
|
47
76
|
}
|
|
@@ -78,6 +107,7 @@ export class SubagentManager {
|
|
|
78
107
|
background: options.background,
|
|
79
108
|
};
|
|
80
109
|
this.records.set(record.id, record);
|
|
110
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
81
111
|
if (record.status === "queued") {
|
|
82
112
|
this.queue.push({ record, options, runner, signal });
|
|
83
113
|
return record;
|
|
@@ -155,11 +185,13 @@ export class SubagentManager {
|
|
|
155
185
|
if (options.background) this.runningBackground++;
|
|
156
186
|
record.status = "running";
|
|
157
187
|
record.startedAt = Date.now();
|
|
188
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
158
189
|
record.promise = (async () => {
|
|
159
190
|
try {
|
|
160
191
|
const result = await runner(options, signal);
|
|
161
192
|
record.runId = detailsRunId(result);
|
|
162
193
|
record.result = resultText(result);
|
|
194
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
163
195
|
if (result.isError) {
|
|
164
196
|
record.status = "error";
|
|
165
197
|
record.error = record.result;
|
|
@@ -173,6 +205,7 @@ export class SubagentManager {
|
|
|
173
205
|
} finally {
|
|
174
206
|
if (options.background) this.runningBackground = Math.max(0, this.runningBackground - 1);
|
|
175
207
|
record.completedAt = record.completedAt ?? Date.now();
|
|
208
|
+
savePersistedSubagentRecord(options.cwd, record);
|
|
176
209
|
this.onComplete?.(record);
|
|
177
210
|
this.drainQueue();
|
|
178
211
|
}
|
|
@@ -194,6 +227,7 @@ export class SubagentManager {
|
|
|
194
227
|
record.status = loaded.manifest.status === "completed" ? "completed" : loaded.manifest.status === "cancelled" ? "cancelled" : "failed";
|
|
195
228
|
record.error = record.status === "completed" ? undefined : loaded.manifest.summary;
|
|
196
229
|
record.completedAt = Date.now();
|
|
230
|
+
savePersistedSubagentRecord(cwd, record);
|
|
197
231
|
return;
|
|
198
232
|
}
|
|
199
233
|
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
package/src/state/state-store.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function createTasksFromWorkflow(runId: string, workflow: WorkflowConfig,
|
|
|
55
55
|
status: "queued",
|
|
56
56
|
dependsOn: dependencies,
|
|
57
57
|
cwd,
|
|
58
|
+
model: step.model,
|
|
58
59
|
graph: {
|
|
59
60
|
taskId: id,
|
|
60
61
|
parentId: dependencies[0] ? stepToTaskId.get(dependencies[0]) : undefined,
|
package/src/state/types.ts
CHANGED
|
@@ -35,4 +35,4 @@ Rules:
|
|
|
35
35
|
- Use parallel tasks in the same phase only when their work is independent.
|
|
36
36
|
- Later phases depend on all tasks in the previous phase.
|
|
37
37
|
- Include verification/review tasks when implementation is requested.
|
|
38
|
-
- Do not include more than 12 total subagents
|
|
38
|
+
- Do not include more than 12 total subagents; split or summarize oversized plans instead.
|