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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -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: false,
353
+ async: spawnOptions.background,
342
354
  config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
343
- }, spawnOptions.background ? { ...ctx, signal: childSignal, startForegroundRun: (run, runId) => startForegroundRun(ctx, run, runId), onRunStarted: (runId) => openLiveSidebar(ctx, runId) } : { ...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, _signal, _onUpdate, ctx) {
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 record = subagentManager.getRecord(p.agent_id);
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
- current = await subagentManager.waitForRecord(current.id) ?? current;
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") current.resultConsumed = true;
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 team = allTeams(discoverTeams(ctx.cwd)).find((candidate) => candidate.name === loaded.manifest.team);
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: allAgents(discoverAgents(ctx.cwd)), 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 });
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 team = allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
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 === "child-process";
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));
@@ -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,
@@ -142,6 +142,7 @@ export interface TeamTaskState {
142
142
  startedAt?: string;
143
143
  finishedAt?: string;
144
144
  exitCode?: number | null;
145
+ model?: string;
145
146
  modelAttempts?: ModelAttemptState[];
146
147
  usage?: UsageState;
147
148
  jsonEvents?: number;
@@ -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 unless the user explicitly asks for a large crew.
38
+ - Do not include more than 12 total subagents; split or summarize oversized plans instead.