pi-crew 0.1.19 → 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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/extension/register.ts +55 -13
- package/src/extension/team-tool.ts +6 -3
- package/src/runtime/background-runner.ts +6 -4
- package/src/runtime/child-pi.ts +5 -2
- package/src/runtime/direct-run.ts +35 -0
- package/src/runtime/pi-args.ts +4 -2
- package/src/runtime/subagent-manager.ts +34 -0
- package/src/runtime/team-runner.ts +170 -2
- package/src/state/state-store.ts +1 -0
- package/src/state/types.ts +5 -0
- package/teams/implementation.team.md +5 -2
- package/workflows/implementation.workflow.md +33 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.20
|
|
4
|
+
|
|
5
|
+
- Reworked the implementation workflow into an adaptive planner-led orchestration flow that decides the number, roles, and phases of subagents from the task instead of using a fixed fanout template.
|
|
6
|
+
- Added dynamic adaptive task injection, persisted adaptive task metadata, and resume reconstruction for planner-selected subagent steps.
|
|
7
|
+
- Block implementation runs when the planner does not produce a valid adaptive plan, including missing/unreadable planner artifacts and malformed/oversized plans.
|
|
8
|
+
- Added tests for adaptive plan parsing, dynamic batch fanout, invalid-plan blocking, writer-role support, and adaptive resume recovery.
|
|
9
|
+
- Hardened subagent/runtime fixes from post-0.1.19 review: env-isolated depth tests, foreground failure status updates, generic tool conflict aliases, and max_turns propagation.
|
|
10
|
+
|
|
3
11
|
## 0.1.19
|
|
4
12
|
|
|
5
13
|
- Added Claude-style `Agent`, `get_subagent_result`, and `steer_subagent` tools backed by pi-crew's durable worker runtime, plus conflict-safe `crew_agent`, `crew_agent_result`, and `crew_agent_steer` aliases.
|
package/package.json
CHANGED
|
@@ -16,10 +16,10 @@ import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.t
|
|
|
16
16
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
17
17
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
18
18
|
import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
|
|
19
|
-
import { loadRunManifestById } from "../state/state-store.ts";
|
|
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 [
|
|
@@ -215,6 +227,12 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
215
227
|
void runner(controller.signal)
|
|
216
228
|
.catch((error) => {
|
|
217
229
|
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
if (runId) {
|
|
231
|
+
try {
|
|
232
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
233
|
+
if (loaded && loaded.manifest.status !== "completed" && loaded.manifest.status !== "failed" && loaded.manifest.status !== "cancelled" && loaded.manifest.status !== "blocked") updateRunStatus(loaded.manifest, "failed", message);
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
218
236
|
ctx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
|
|
219
237
|
})
|
|
220
238
|
.finally(() => {
|
|
@@ -332,8 +350,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
332
350
|
agent: spawnOptions.type,
|
|
333
351
|
goal: spawnOptions.prompt,
|
|
334
352
|
model: spawnOptions.model,
|
|
335
|
-
async:
|
|
336
|
-
|
|
353
|
+
async: spawnOptions.background,
|
|
354
|
+
config: spawnOptions.maxTurns ? { runtime: { maxTurns: spawnOptions.maxTurns } } : undefined,
|
|
355
|
+
}, spawnOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
337
356
|
const record = subagentManager.spawn(options, runner, options.background ? undefined : signal);
|
|
338
357
|
if (options.background || record.status === "queued") {
|
|
339
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 });
|
|
@@ -353,18 +372,40 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
353
372
|
wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })),
|
|
354
373
|
verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })),
|
|
355
374
|
}) as never,
|
|
356
|
-
async execute(_id, params,
|
|
375
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
357
376
|
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
358
377
|
if (!p.agent_id) return subagentToolResult("get_subagent_result requires agent_id.", {}, true);
|
|
359
|
-
const
|
|
378
|
+
const inMemory = subagentManager.getRecord(p.agent_id);
|
|
379
|
+
const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
|
|
360
380
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id}`, {}, true);
|
|
361
|
-
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
|
+
}
|
|
362
386
|
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
363
387
|
current.resultConsumed = true;
|
|
364
|
-
|
|
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
|
+
}
|
|
365
403
|
}
|
|
366
404
|
const output = readSubagentRunResult(ctx, current);
|
|
367
|
-
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
|
+
}
|
|
368
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");
|
|
369
410
|
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
370
411
|
},
|
|
@@ -375,9 +416,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
375
416
|
label: "Steer Agent",
|
|
376
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.",
|
|
377
418
|
parameters: Type.Object({ agent_id: Type.String(), message: Type.String() }) as never,
|
|
378
|
-
async execute(_id, params) {
|
|
419
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
379
420
|
const p = params as { agent_id?: string; message?: string };
|
|
380
|
-
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;
|
|
381
422
|
if (!record) return subagentToolResult(`Agent not found: ${p.agent_id ?? ""}`, {}, true);
|
|
382
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 });
|
|
383
424
|
},
|
|
@@ -402,8 +443,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
402
443
|
label: "Steer Crew Agent",
|
|
403
444
|
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
|
|
404
445
|
};
|
|
405
|
-
for (const extraTool of [
|
|
406
|
-
|
|
446
|
+
for (const extraTool of [crewAgentTool, crewAgentResultTool, crewAgentSteerTool]) pi.registerTool(extraTool);
|
|
447
|
+
for (const extraTool of [agentTool, getSubagentResultTool, steerSubagentTool]) {
|
|
448
|
+
try { pi.registerTool(extraTool); } catch {}
|
|
407
449
|
}
|
|
408
450
|
|
|
409
451
|
pi.registerCommand("teams", {
|
|
@@ -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;
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -210,8 +210,11 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
210
210
|
observeStdoutChunk(input, stdout);
|
|
211
211
|
return { exitCode: 0, stdout, stderr: "" };
|
|
212
212
|
}
|
|
213
|
-
if (mock === "json-success") {
|
|
214
|
-
const
|
|
213
|
+
if (mock === "json-success" || mock === "adaptive-plan") {
|
|
214
|
+
const text = mock === "adaptive-plan" && input.task.includes("ADAPTIVE_PLAN_JSON_START")
|
|
215
|
+
? `Adaptive mock plan\nADAPTIVE_PLAN_JSON_START\n${JSON.stringify({ phases: [{ name: "research", tasks: [{ role: "explorer", task: "Explore adaptive target" }, { role: "analyst", task: "Analyze adaptive target" }, { role: "planner", task: "Plan adaptive target" }] }, { name: "build", tasks: [{ role: "executor", task: "Implement adaptive target" }] }, { name: "check", tasks: [{ role: "reviewer", task: "Review adaptive target" }, { role: "test-engineer", task: "Test adaptive target" }, { role: "writer", task: "Summarize adaptive target" }] }] })}\nADAPTIVE_PLAN_JSON_END`
|
|
216
|
+
: `Mock JSON success for ${input.agent.name}`;
|
|
217
|
+
const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
|
|
215
218
|
observeStdoutChunk(input, stdout);
|
|
216
219
|
return { exitCode: 0, stdout, stderr: "" };
|
|
217
220
|
}
|
|
@@ -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
|
+
}
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface BuildPiWorkerArgsInput {
|
|
|
15
15
|
model?: string;
|
|
16
16
|
sessionEnabled?: boolean;
|
|
17
17
|
maxDepth?: number;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface BuildPiWorkerArgsResult {
|
|
@@ -82,8 +83,9 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
82
83
|
args.push(`Task: ${input.task}`);
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
86
|
+
const env = input.env ?? process.env;
|
|
87
|
+
const parentDepth = currentCrewDepth(env);
|
|
88
|
+
const maxDepth = resolveCrewMaxDepth(input.maxDepth, env);
|
|
87
89
|
return {
|
|
88
90
|
args,
|
|
89
91
|
env: {
|
|
@@ -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));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
3
|
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
3
4
|
import type { CrewRuntimeCapabilities } from "./runtime-resolver.ts";
|
|
@@ -76,6 +77,129 @@ function mergeTaskUpdates(base: TeamTaskState[], results: Array<{ tasks: TeamTas
|
|
|
76
77
|
return refreshTaskGraphQueues(merged);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
interface AdaptivePlanTask {
|
|
81
|
+
role: string;
|
|
82
|
+
title?: string;
|
|
83
|
+
task: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface AdaptivePlanPhase {
|
|
87
|
+
name: string;
|
|
88
|
+
tasks: AdaptivePlanTask[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface AdaptivePlan {
|
|
92
|
+
phases: AdaptivePlanPhase[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MAX_ADAPTIVE_TASKS = 12;
|
|
96
|
+
|
|
97
|
+
function slug(value: string): string {
|
|
98
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "task";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function __test__parseAdaptivePlan(text: string, allowedRoles: string[]): AdaptivePlan | undefined {
|
|
102
|
+
const markerMatch = text.match(/ADAPTIVE_PLAN_JSON_START\s*([\s\S]*?)\s*ADAPTIVE_PLAN_JSON_END/);
|
|
103
|
+
const fencedMatch = markerMatch ? undefined : text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
104
|
+
const raw = markerMatch?.[1] ?? fencedMatch?.[1];
|
|
105
|
+
if (!raw) return undefined;
|
|
106
|
+
let parsed: unknown;
|
|
107
|
+
try { parsed = JSON.parse(raw); } catch { return undefined; }
|
|
108
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
109
|
+
const phasesRaw = Array.isArray((parsed as { phases?: unknown }).phases) ? (parsed as { phases: unknown[] }).phases : Array.isArray((parsed as { tasks?: unknown }).tasks) ? [{ name: "adaptive", tasks: (parsed as { tasks: unknown[] }).tasks }] : undefined;
|
|
110
|
+
if (!phasesRaw) return undefined;
|
|
111
|
+
const allowed = new Set(allowedRoles);
|
|
112
|
+
const phases: AdaptivePlanPhase[] = [];
|
|
113
|
+
let total = 0;
|
|
114
|
+
for (const [phaseIndex, phaseRaw] of phasesRaw.entries()) {
|
|
115
|
+
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
|
|
116
|
+
const phaseObj = phaseRaw as { name?: unknown; tasks?: unknown };
|
|
117
|
+
if (!Array.isArray(phaseObj.tasks) || phaseObj.tasks.length === 0) return undefined;
|
|
118
|
+
const tasks: AdaptivePlanTask[] = [];
|
|
119
|
+
for (const taskRaw of phaseObj.tasks) {
|
|
120
|
+
if (!taskRaw || typeof taskRaw !== "object" || Array.isArray(taskRaw)) return undefined;
|
|
121
|
+
const taskObj = taskRaw as { role?: unknown; title?: unknown; task?: unknown };
|
|
122
|
+
if (typeof taskObj.role !== "string" || !allowed.has(taskObj.role)) return undefined;
|
|
123
|
+
if (typeof taskObj.task !== "string" || !taskObj.task.trim()) return undefined;
|
|
124
|
+
if (total >= MAX_ADAPTIVE_TASKS) return undefined;
|
|
125
|
+
tasks.push({ role: taskObj.role, title: typeof taskObj.title === "string" ? taskObj.title : undefined, task: taskObj.task.trim() });
|
|
126
|
+
total++;
|
|
127
|
+
}
|
|
128
|
+
phases.push({ name: typeof phaseObj.name === "string" && phaseObj.name.trim() ? phaseObj.name.trim() : `phase-${phaseIndex + 1}`, tasks });
|
|
129
|
+
}
|
|
130
|
+
return phases.length ? { phases } : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reconstructAdaptiveWorkflow(workflow: WorkflowConfig, tasks: TeamTaskState[]): WorkflowConfig {
|
|
134
|
+
const existing = new Set(workflow.steps.map((step) => step.id));
|
|
135
|
+
const steps: WorkflowStep[] = [];
|
|
136
|
+
for (const task of tasks) {
|
|
137
|
+
if (!task.stepId?.startsWith("adaptive-") || !task.adaptive?.task || existing.has(task.stepId)) continue;
|
|
138
|
+
steps.push({ id: task.stepId, role: task.role, dependsOn: task.graph?.dependencies ?? task.dependsOn, parallelGroup: `adaptive-${slug(task.adaptive.phase)}`, task: task.adaptive.task });
|
|
139
|
+
}
|
|
140
|
+
return steps.length ? { ...workflow, steps: [...workflow.steps, ...steps] } : workflow;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function injectAdaptivePlanIfReady(input: { manifest: TeamRunManifest; tasks: TeamTaskState[]; workflow: WorkflowConfig; team: TeamConfig }): { tasks: TeamTaskState[]; workflow: WorkflowConfig; injected: boolean; missingPlan: boolean } {
|
|
144
|
+
if (input.workflow.name !== "implementation") return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: false };
|
|
145
|
+
if (input.tasks.some((task) => task.stepId?.startsWith("adaptive-"))) return { tasks: input.tasks, workflow: reconstructAdaptiveWorkflow(input.workflow, input.tasks), injected: false, missingPlan: false };
|
|
146
|
+
const completedAssess = input.tasks.find((task) => task.stepId === "assess" && task.status === "completed");
|
|
147
|
+
if (!completedAssess) return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: false };
|
|
148
|
+
if (!completedAssess.resultArtifact?.path) {
|
|
149
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: completedAssess.id, message: "Adaptive planner result artifact is missing." });
|
|
150
|
+
return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
|
|
151
|
+
}
|
|
152
|
+
const assessTask = completedAssess;
|
|
153
|
+
const resultPath = completedAssess.resultArtifact.path;
|
|
154
|
+
let text = "";
|
|
155
|
+
try { text = fs.readFileSync(resultPath, "utf-8"); } catch {
|
|
156
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner result artifact could not be read." });
|
|
157
|
+
return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
|
|
158
|
+
}
|
|
159
|
+
const plan = __test__parseAdaptivePlan(text, input.team.roles.map((role) => role.name));
|
|
160
|
+
if (!plan) {
|
|
161
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_missing", runId: input.manifest.runId, taskId: assessTask.id, message: "Adaptive planner did not produce a valid plan; no dynamic subagents were spawned." });
|
|
162
|
+
return { tasks: input.tasks, workflow: input.workflow, injected: false, missingPlan: true };
|
|
163
|
+
}
|
|
164
|
+
const steps: WorkflowStep[] = [];
|
|
165
|
+
const tasks: TeamTaskState[] = [];
|
|
166
|
+
let previousStepIds = ["assess"];
|
|
167
|
+
let counter = 0;
|
|
168
|
+
for (const [phaseIndex, phase] of plan.phases.entries()) {
|
|
169
|
+
const currentStepIds: string[] = [];
|
|
170
|
+
for (const [taskIndex, planned] of phase.tasks.entries()) {
|
|
171
|
+
counter++;
|
|
172
|
+
const stepId = `adaptive-${phaseIndex + 1}-${taskIndex + 1}-${slug(planned.role)}`;
|
|
173
|
+
const taskId = `adaptive-${String(counter).padStart(2, "0")}-${slug(planned.role)}`;
|
|
174
|
+
steps.push({ id: stepId, role: planned.role, dependsOn: previousStepIds, parallelGroup: `adaptive-${slug(phase.name)}`, task: planned.task });
|
|
175
|
+
tasks.push({
|
|
176
|
+
id: taskId,
|
|
177
|
+
runId: input.manifest.runId,
|
|
178
|
+
stepId,
|
|
179
|
+
role: planned.role,
|
|
180
|
+
agent: input.team.roles.find((role) => role.name === planned.role)?.agent ?? planned.role,
|
|
181
|
+
title: planned.title ?? stepId,
|
|
182
|
+
status: "queued",
|
|
183
|
+
dependsOn: previousStepIds,
|
|
184
|
+
cwd: input.manifest.cwd,
|
|
185
|
+
adaptive: { phase: phase.name, task: planned.task },
|
|
186
|
+
graph: { taskId, dependencies: previousStepIds, children: [], queue: "blocked" },
|
|
187
|
+
});
|
|
188
|
+
currentStepIds.push(stepId);
|
|
189
|
+
}
|
|
190
|
+
previousStepIds = currentStepIds;
|
|
191
|
+
}
|
|
192
|
+
const dependencyTaskIdByStep = new Map<string, string>([["assess", assessTask.id], ...tasks.map((task) => [task.stepId ?? task.id, task.id] as const)]);
|
|
193
|
+
const withGraph = tasks.map((task) => ({
|
|
194
|
+
...task,
|
|
195
|
+
dependsOn: task.dependsOn.map((dep) => dependencyTaskIdByStep.get(dep) ?? dep),
|
|
196
|
+
graph: task.graph ? { ...task.graph, dependencies: task.dependsOn.map((dep) => dependencyTaskIdByStep.get(dep) ?? dep), queue: "blocked" as const } : task.graph,
|
|
197
|
+
}));
|
|
198
|
+
const allTasks = refreshTaskGraphQueues([...input.tasks, ...withGraph]);
|
|
199
|
+
appendEvent(input.manifest.eventsPath, { type: "adaptive.plan_injected", runId: input.manifest.runId, taskId: assessTask.id, message: `Injected ${withGraph.length} adaptive subagent task(s) across ${plan.phases.length} phase(s).`, data: { phases: plan.phases.map((phase) => ({ name: phase.name, count: phase.tasks.length, roles: phase.tasks.map((task) => task.role) })) } });
|
|
200
|
+
return { tasks: allTasks, workflow: { ...input.workflow, steps: [...input.workflow.steps, ...steps] }, injected: true, missingPlan: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
79
203
|
function formatTaskProgress(task: TeamTaskState): string {
|
|
80
204
|
return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
|
|
81
205
|
}
|
|
@@ -144,8 +268,22 @@ function applyPolicy(manifest: TeamRunManifest, tasks: TeamTaskState[], limits?:
|
|
|
144
268
|
}
|
|
145
269
|
|
|
146
270
|
export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
271
|
+
let workflow = input.workflow;
|
|
147
272
|
let manifest = updateRunStatus(input.manifest, "running", input.executeWorkers ? "Executing team workflow." : "Creating workflow prompts and placeholder results.");
|
|
148
273
|
let tasks = refreshTaskGraphQueues(input.tasks);
|
|
274
|
+
const initialAdaptive = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
|
|
275
|
+
if (initialAdaptive.missingPlan) {
|
|
276
|
+
tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
|
|
277
|
+
saveRunTasks(manifest, tasks);
|
|
278
|
+
manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
|
|
279
|
+
return { manifest, tasks };
|
|
280
|
+
}
|
|
281
|
+
if (initialAdaptive.injected) {
|
|
282
|
+
tasks = initialAdaptive.tasks;
|
|
283
|
+
workflow = initialAdaptive.workflow;
|
|
284
|
+
} else {
|
|
285
|
+
workflow = initialAdaptive.workflow;
|
|
286
|
+
}
|
|
149
287
|
manifest = writeProgress(manifest, tasks, "team-runner");
|
|
150
288
|
saveRunManifest(manifest);
|
|
151
289
|
const runtimeKind = input.runtime?.kind ?? (input.executeWorkers ? "child-process" : "scaffold");
|
|
@@ -168,9 +306,25 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
168
306
|
return { manifest, tasks };
|
|
169
307
|
}
|
|
170
308
|
|
|
309
|
+
const injectedPlan = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
|
|
310
|
+
if (injectedPlan.missingPlan) {
|
|
311
|
+
tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
|
|
312
|
+
saveRunTasks(manifest, tasks);
|
|
313
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
314
|
+
manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
|
|
315
|
+
return { manifest, tasks };
|
|
316
|
+
}
|
|
317
|
+
if (injectedPlan.injected) {
|
|
318
|
+
tasks = injectedPlan.tasks;
|
|
319
|
+
workflow = injectedPlan.workflow;
|
|
320
|
+
saveRunTasks(manifest, tasks);
|
|
321
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
322
|
+
} else {
|
|
323
|
+
workflow = injectedPlan.workflow;
|
|
324
|
+
}
|
|
171
325
|
const snapshot = taskGraphSnapshot(tasks);
|
|
172
326
|
const readyRoles = snapshot.ready.map((taskId) => tasks.find((task) => task.id === taskId)?.role).filter((role): role is string => Boolean(role));
|
|
173
|
-
const concurrency = resolveBatchConcurrency({ workflowName:
|
|
327
|
+
const concurrency = resolveBatchConcurrency({ workflowName: workflow.name, teamMaxConcurrency: input.team.maxConcurrency, limitMaxConcurrentWorkers: input.limits?.maxConcurrentWorkers, readyCount: snapshot.ready.length, workspaceMode: manifest.workspaceMode, readyRoles });
|
|
174
328
|
const readyBatch = getReadyTasks(tasks, concurrency.selectedCount);
|
|
175
329
|
if (readyBatch.length === 0) {
|
|
176
330
|
tasks = markBlocked(tasks, "No ready queued task; dependency graph may be invalid.");
|
|
@@ -182,12 +336,26 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
|
|
|
182
336
|
|
|
183
337
|
appendEvent(manifest.eventsPath, { type: "task.progress", runId: manifest.runId, message: `Starting ready batch with ${readyBatch.length} task(s).`, data: { taskIds: readyBatch.map((task) => task.id), readyCount: snapshot.ready.length, blockedCount: snapshot.blocked.length, runningCount: snapshot.running.length, doneCount: snapshot.done.length, selectedCount: readyBatch.length, maxConcurrent: concurrency.maxConcurrent, defaultConcurrency: concurrency.defaultConcurrency, concurrencyReason: concurrency.reason } });
|
|
184
338
|
const results = await Promise.all(readyBatch.map((task) => {
|
|
185
|
-
const step = findStep(
|
|
339
|
+
const step = findStep(workflow, task);
|
|
186
340
|
const agent = findAgent(input.agents, task);
|
|
187
341
|
return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, limits: input.limits });
|
|
188
342
|
}));
|
|
189
343
|
manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
|
|
190
344
|
tasks = mergeTaskUpdates(tasks, results);
|
|
345
|
+
const injectedAfterBatch = injectAdaptivePlanIfReady({ manifest, tasks, workflow, team: input.team });
|
|
346
|
+
if (injectedAfterBatch.missingPlan) {
|
|
347
|
+
tasks = markBlocked(tasks, "Adaptive planner did not produce a valid subagent plan.");
|
|
348
|
+
saveRunTasks(manifest, tasks);
|
|
349
|
+
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
350
|
+
manifest = updateRunStatus(manifest, "blocked", "Adaptive planner did not produce a valid subagent plan.");
|
|
351
|
+
return { manifest, tasks };
|
|
352
|
+
}
|
|
353
|
+
if (injectedAfterBatch.injected) {
|
|
354
|
+
tasks = injectedAfterBatch.tasks;
|
|
355
|
+
workflow = injectedAfterBatch.workflow;
|
|
356
|
+
} else {
|
|
357
|
+
workflow = injectedAfterBatch.workflow;
|
|
358
|
+
}
|
|
191
359
|
saveRunTasks(manifest, tasks);
|
|
192
360
|
saveCrewAgents(manifest, recordsForMaterializedTasks(manifest, tasks, runtimeKind));
|
|
193
361
|
const completedBatch = readyBatch.map((task) => tasks.find((item) => item.id === task.id) ?? task);
|
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
|
@@ -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;
|
|
@@ -152,6 +153,10 @@ export interface TeamTaskState {
|
|
|
152
153
|
taskPacket?: TaskPacket;
|
|
153
154
|
verification?: VerificationEvidence;
|
|
154
155
|
graph?: TaskGraphNode;
|
|
156
|
+
adaptive?: {
|
|
157
|
+
phase: string;
|
|
158
|
+
task: string;
|
|
159
|
+
};
|
|
155
160
|
policy?: {
|
|
156
161
|
retryCount?: number;
|
|
157
162
|
lastDecision?: PolicyDecision;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implementation
|
|
3
|
-
description: Full implementation team with
|
|
3
|
+
description: Full implementation team with parallel specialists, critique, execution, review, and verification
|
|
4
4
|
defaultWorkflow: implementation
|
|
5
5
|
workspaceMode: single
|
|
6
6
|
maxConcurrency: 3
|
|
@@ -9,7 +9,10 @@ maxConcurrency: 3
|
|
|
9
9
|
- explorer: agent=explorer map the codebase
|
|
10
10
|
- analyst: agent=analyst clarify requirements and constraints
|
|
11
11
|
- planner: agent=planner create execution plan
|
|
12
|
-
- critic: agent=critic challenge
|
|
12
|
+
- critic: agent=critic challenge and synthesize specialist findings
|
|
13
13
|
- executor: agent=executor implement the plan
|
|
14
14
|
- reviewer: agent=reviewer review the implementation
|
|
15
|
+
- security-reviewer: agent=security-reviewer review security and trust boundaries
|
|
16
|
+
- test-engineer: agent=test-engineer design and run verification
|
|
15
17
|
- verifier: agent=verifier verify done
|
|
18
|
+
- writer: agent=writer summarize documentation or release notes when needed
|
|
@@ -1,47 +1,38 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implementation
|
|
3
|
-
description:
|
|
3
|
+
description: Adaptive implementation workflow where a planner agent decides the subagent fanout
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
##
|
|
7
|
-
role: explorer
|
|
8
|
-
|
|
9
|
-
Map relevant files, APIs, and constraints for: {goal}
|
|
10
|
-
|
|
11
|
-
## analyze
|
|
12
|
-
role: analyst
|
|
13
|
-
dependsOn: explore
|
|
14
|
-
|
|
15
|
-
Analyze requirements, ambiguities, risks, and acceptance criteria for: {goal}
|
|
16
|
-
|
|
17
|
-
## plan
|
|
6
|
+
## assess
|
|
18
7
|
role: planner
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
8
|
+
output: adaptive-plan.json
|
|
9
|
+
|
|
10
|
+
Assess this task and decide how many subagents are actually needed for: {goal}
|
|
11
|
+
|
|
12
|
+
You are the orchestration planner. Inspect the repository enough to choose an efficient crew; do not use a fixed template. Small/simple tasks may need one executor plus one verifier. Risky or broad tasks may need parallel explorers, specialists, implementers, reviewers, security reviewers, or test engineers.
|
|
13
|
+
|
|
14
|
+
Return a concise rationale, then include exactly one JSON block between these markers:
|
|
15
|
+
|
|
16
|
+
ADAPTIVE_PLAN_JSON_START
|
|
17
|
+
{
|
|
18
|
+
"phases": [
|
|
19
|
+
{
|
|
20
|
+
"name": "short-phase-name",
|
|
21
|
+
"tasks": [
|
|
22
|
+
{
|
|
23
|
+
"role": "explorer|analyst|planner|critic|executor|reviewer|security-reviewer|test-engineer|verifier|writer",
|
|
24
|
+
"title": "short task title",
|
|
25
|
+
"task": "specific autonomous task prompt for this subagent"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
ADAPTIVE_PLAN_JSON_END
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- Choose the smallest effective number of subagents.
|
|
35
|
+
- Use parallel tasks in the same phase only when their work is independent.
|
|
36
|
+
- Later phases depend on all tasks in the previous phase.
|
|
37
|
+
- Include verification/review tasks when implementation is requested.
|
|
38
|
+
- Do not include more than 12 total subagents; split or summarize oversized plans instead.
|