@ferris1225/pi-subagents 4.1.24 → 4.2.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 +47 -116
- package/agents/executor.md +53 -0
- package/package.json +55 -55
- package/src/agents.ts +2 -2
- package/src/announcements.ts +0 -26
- package/src/completion.ts +0 -11
- package/src/config.ts +1 -11
- package/src/dispatch.ts +16 -310
- package/src/durable.ts +0 -5
- package/src/format.ts +0 -8
- package/src/monitor.ts +7 -143
- package/src/prompt.ts +7 -30
- package/src/rpc-run.ts +993 -993
- package/src/runtime.ts +3 -10
- package/src/setup.ts +1 -6
- package/src/spawn.ts +0 -10
- package/src/thread-lifecycle.ts +51 -219
- package/src/widget.ts +33 -240
- package/agents/cleaner.md +0 -50
- package/agents/documenter.md +0 -40
- package/agents/reviewer.md +0 -82
- package/agents/synthesizer.md +0 -39
- package/agents/worker.md +0 -43
- package/src/workflow.ts +0 -215
package/src/dispatch.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `subagent` tool: dispatches the enabled agents (explorer,
|
|
3
|
-
*
|
|
4
|
-
* child processes, single or parallel. Owns the public dispatch contract
|
|
5
|
-
* per-run status tracking
|
|
6
|
-
* internal step launching. Stable thread generations, final integration, and
|
|
2
|
+
* The `subagent` tool: dispatches the enabled agents (explorer, executor,
|
|
3
|
+
* plus custom roles) as isolated pi
|
|
4
|
+
* child processes, single or parallel. Owns the public dispatch contract and
|
|
5
|
+
* per-run status tracking. Stable thread generations, final integration, and
|
|
7
6
|
* completion ownership live in thread-lifecycle.ts.
|
|
8
7
|
*/
|
|
9
8
|
|
|
@@ -15,25 +14,13 @@ import { Type } from "typebox";
|
|
|
15
14
|
import { discoverAgents, isWriteCapableAgent, resolveAgentTools, type AgentConfig } from "./agents.ts";
|
|
16
15
|
import { loadConfig } from "./config.ts";
|
|
17
16
|
import { formatCompletionBlock, formatUsage, queuedResult } from "./format.ts";
|
|
18
|
-
import {
|
|
19
|
-
buildFinalReviewBrief,
|
|
20
|
-
buildReReviewBrief,
|
|
21
|
-
buildReviewerFixBrief,
|
|
22
|
-
MAX_REVIEW_FIX_ROUNDS,
|
|
23
|
-
type ChainStep,
|
|
24
|
-
type ManagedWorkflowOutcome,
|
|
25
|
-
type ReviewMode,
|
|
26
|
-
} from "./workflow.ts";
|
|
27
17
|
import {
|
|
28
18
|
formatTaskSummary,
|
|
29
19
|
formatToolActivity,
|
|
30
20
|
monitor,
|
|
31
21
|
statusIcon,
|
|
32
|
-
type RunChainMeta,
|
|
33
22
|
type RunView,
|
|
34
23
|
type RunWaitReason,
|
|
35
|
-
type WorkflowStage,
|
|
36
|
-
type WorkflowStageStatus,
|
|
37
24
|
} from "./monitor.ts";
|
|
38
25
|
import type { SubagentRuntime } from "./runtime.ts";
|
|
39
26
|
import { persistThreadCheckpoint } from "./thread-lifecycle.ts";
|
|
@@ -41,7 +28,6 @@ import {
|
|
|
41
28
|
getProjectRoot,
|
|
42
29
|
getResultOutput,
|
|
43
30
|
isFailedResult,
|
|
44
|
-
reviewVerdict,
|
|
45
31
|
runSingleAgentWithMainFallback,
|
|
46
32
|
type SingleResult,
|
|
47
33
|
type SubagentDetails,
|
|
@@ -54,7 +40,6 @@ import {
|
|
|
54
40
|
runInManagedRepositoryLane,
|
|
55
41
|
withWorktreeSystemPrompt,
|
|
56
42
|
type DispatchEnvironment,
|
|
57
|
-
type ManagedWorkflowRequest,
|
|
58
43
|
} from "./thread-lifecycle.ts";
|
|
59
44
|
import type { IsolationMode } from "./worktree.ts";
|
|
60
45
|
|
|
@@ -63,7 +48,7 @@ export { isWorktreeCapableAgent, runInManagedRepositoryLane } from "./thread-lif
|
|
|
63
48
|
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
64
49
|
|
|
65
50
|
const ISOLATION_DESCRIPTION =
|
|
66
|
-
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including
|
|
51
|
+
"Filesystem isolation: shared uses the caller's working tree; worktree creates a detached temporary Git worktree (write-capable agents, including executor, only)";
|
|
67
52
|
|
|
68
53
|
const IsolationSchema = Type.Optional(
|
|
69
54
|
StringEnum(["shared", "worktree"] as const, { description: ISOLATION_DESCRIPTION }),
|
|
@@ -76,13 +61,6 @@ const WaitSchema = Type.Optional(
|
|
|
76
61
|
}),
|
|
77
62
|
);
|
|
78
63
|
|
|
79
|
-
const REVIEW_DESCRIPTION =
|
|
80
|
-
"Gate intensity for a worker/cleaner task: \"gate\" (default) runs one automatic reviewer after success; \"none\" skips it for mechanical, low-risk edits (typos, comments, doc strings, config value tweaks) that you verify yourself. Keep the default whenever behavior can change.";
|
|
81
|
-
|
|
82
|
-
const ReviewSchema = Type.Optional(
|
|
83
|
-
StringEnum(["gate", "none"] as const, { description: REVIEW_DESCRIPTION }),
|
|
84
|
-
);
|
|
85
|
-
|
|
86
64
|
const TaskItem = Type.Object({
|
|
87
65
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
88
66
|
task: Type.String({
|
|
@@ -91,7 +69,6 @@ const TaskItem = Type.Object({
|
|
|
91
69
|
}),
|
|
92
70
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
93
71
|
isolation: IsolationSchema,
|
|
94
|
-
review: ReviewSchema,
|
|
95
72
|
});
|
|
96
73
|
|
|
97
74
|
const SubagentParams = Type.Object({
|
|
@@ -102,14 +79,13 @@ const SubagentParams = Type.Object({
|
|
|
102
79
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
103
80
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
104
81
|
isolation: IsolationSchema,
|
|
105
|
-
review: ReviewSchema,
|
|
106
82
|
wait: WaitSchema,
|
|
107
83
|
});
|
|
108
84
|
|
|
109
85
|
/** Roles that default to worktree isolation in parallel dispatches even when
|
|
110
86
|
* the live catalog cannot be consulted (render-only call sites). Custom
|
|
111
87
|
* write-capable agents join them via isWriteCapableAgent on the execute path. */
|
|
112
|
-
const WORKTREE_DEFAULT_AGENTS = new Set(["
|
|
88
|
+
const WORKTREE_DEFAULT_AGENTS = new Set(["executor"]);
|
|
113
89
|
|
|
114
90
|
/** Resolve the default isolation for a dispatch. Precedence: an explicit
|
|
115
91
|
* per-call request, then the role's own frontmatter declaration (`worktree`
|
|
@@ -131,27 +107,6 @@ export function defaultIsolationMode(
|
|
|
131
107
|
return mode === "parallel" && writeCapable ? "worktree" : "shared";
|
|
132
108
|
}
|
|
133
109
|
|
|
134
|
-
function workflowStageStatus(result: SingleResult, relation?: string): WorkflowStageStatus {
|
|
135
|
-
if (isFailedResult(result)) return "failed";
|
|
136
|
-
if (relation === "review fix") return "done";
|
|
137
|
-
if (result.agent !== "reviewer") return "done";
|
|
138
|
-
const verdict = reviewVerdict(getResultOutput(result));
|
|
139
|
-
if (verdict === "fail") return "changes";
|
|
140
|
-
return verdict === "pass" ? "done" : "failed";
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** The runtime-granted write continuation of a failed gate: same reviewer
|
|
144
|
-
* role, model, and retained session, but the read-only boundary is lifted for
|
|
145
|
-
* this one stage so it applies its own fix instructions. One line only: the
|
|
146
|
-
* full fix-stage contract is already in the retained session's prompt. */
|
|
147
|
-
function withReviewerFixStageAgent(agent: AgentConfig): AgentConfig {
|
|
148
|
-
return {
|
|
149
|
-
...agent,
|
|
150
|
-
tools: undefined,
|
|
151
|
-
systemPrompt: `${agent.systemPrompt.trimEnd()}\n\nRuntime workflow context: FIX STAGE — your gate just returned REVIEW_FAIL. Your read-only boundary is lifted for this stage only: apply your own fix instructions exactly as specified, verify, and report what changed; never edit during a review and never emit a verdict here.`,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
110
|
/** In-turn wait behind dispatch `wait: true` — the escape hatch for one-shot
|
|
156
111
|
* `pi -p` parents that exit at end of turn: hold the call until every run it
|
|
157
112
|
* started settles, then hand back their result blocks. Interactive sessions
|
|
@@ -240,27 +195,24 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
240
195
|
const environmentRef: { current: DispatchEnvironment | undefined } = { current: undefined };
|
|
241
196
|
|
|
242
197
|
// Finished runs leave the active monitor immediately. Their final findings
|
|
243
|
-
// are sent as a custom message that starts a follow-up turn.
|
|
244
|
-
// removed row so workflow callers can freeze its exact elapsed time onto
|
|
245
|
-
// the stage projection before the row is gone.
|
|
198
|
+
// are sent as a custom message that starts a follow-up turn.
|
|
246
199
|
const finishRun = (
|
|
247
200
|
runId: number,
|
|
248
201
|
status: "done" | "failed",
|
|
249
202
|
opts?: { silent?: boolean },
|
|
250
|
-
):
|
|
203
|
+
): void => {
|
|
251
204
|
monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
|
|
252
205
|
const run = monitor.removeRun(runId);
|
|
253
|
-
if (!run) return
|
|
254
|
-
if (opts?.silent || !runtime.sessionActive) return
|
|
206
|
+
if (!run) return; // already finished — stay idempotent
|
|
207
|
+
if (opts?.silent || !runtime.sessionActive) return;
|
|
255
208
|
const icon = status === "done" ? "✓" : "✗";
|
|
256
209
|
environmentRef.current?.ctx.ui.notify(`${icon} #${run.id} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
|
|
257
|
-
return run;
|
|
258
210
|
};
|
|
259
211
|
|
|
260
212
|
// Live sub-agent activity → concise one-line status ("thinking",
|
|
261
213
|
// "read src/index.ts", ...), never a raw args blob. The live handler
|
|
262
|
-
// only updates monitor state; the queue task
|
|
263
|
-
//
|
|
214
|
+
// only updates monitor state; the queue task owns terminal removal,
|
|
215
|
+
// notification, and lifecycle decisions.
|
|
264
216
|
const makeLiveHandler =
|
|
265
217
|
(runId: number, generation?: number) =>
|
|
266
218
|
(e: SubagentLiveEvent): void => {
|
|
@@ -344,247 +296,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
344
296
|
return ` Pacing: ${parts.join(" · ")}. Keep dispatching independent units.`;
|
|
345
297
|
};
|
|
346
298
|
|
|
347
|
-
/** Launch one workflow-internal child in a fresh model context. It sees the
|
|
348
|
-
* parent's exact repository/worktree state and is registered by its own id,
|
|
349
|
-
* but never enters top-level lifecycle policy or completion delivery.
|
|
350
|
-
* `stage` continues a retained session (the reviewer fix stage) and/or
|
|
351
|
-
* replaces the resolved agent (lifting the reviewer read-only boundary). */
|
|
352
|
-
const launchInWorkflow = async (
|
|
353
|
-
request: ManagedWorkflowRequest,
|
|
354
|
-
agentName: string,
|
|
355
|
-
task: string,
|
|
356
|
-
meta: RunChainMeta,
|
|
357
|
-
stage: {
|
|
358
|
-
agentOverride?: AgentConfig;
|
|
359
|
-
session?: { sessionId: string; sessionDir: string };
|
|
360
|
-
} = {},
|
|
361
|
-
): Promise<{ runId: number; result: SingleResult; elapsedMs?: number }> => {
|
|
362
|
-
const discoveredAgent = request.agents.find((candidate) => candidate.name === agentName);
|
|
363
|
-
if (!discoveredAgent) {
|
|
364
|
-
throw new Error(`Managed workflow requires enabled agent "${agentName}", but discovery did not provide it.`);
|
|
365
|
-
}
|
|
366
|
-
const boundaryAgent = stage.agentOverride ?? discoveredAgent;
|
|
367
|
-
const resolveLiveAgentTools = (candidate: AgentConfig): AgentConfig =>
|
|
368
|
-
resolveAgentTools({ ...candidate, tools: boundaryAgent.tools }, runtime.getActiveTools());
|
|
369
|
-
const agent = resolveLiveAgentTools(boundaryAgent);
|
|
370
|
-
// Workflow policy (agents) stays fixed for the chain, but model/thinking
|
|
371
|
-
// routes are re-read per stage so config edits apply to stages that have
|
|
372
|
-
// not launched yet.
|
|
373
|
-
const stageConfig = await loadConfig(runtime.configPath).catch(() => request.config);
|
|
374
|
-
const resolvedRoute = resolveDispatchModelRoute(agent, stageConfig, request.ctx);
|
|
375
|
-
const route = request.isolation === "worktree"
|
|
376
|
-
? { ...resolvedRoute, agent: withWorktreeSystemPrompt(resolvedRoute.agent) }
|
|
377
|
-
: resolvedRoute;
|
|
378
|
-
const thinkingLevel = route.thinkingLevel;
|
|
379
|
-
const runId = monitor.addRun(agent.name, task, route.agent.model, thinkingLevel, {
|
|
380
|
-
...meta,
|
|
381
|
-
isolation: request.isolation,
|
|
382
|
-
...(request.worktreeId ? { worktreeId: request.worktreeId } : {}),
|
|
383
|
-
// Workflow-internal children spawn immediately: they never enter the
|
|
384
|
-
// process queue, so they must never be reported as slot-waiting.
|
|
385
|
-
waitReason: "starting",
|
|
386
|
-
});
|
|
387
|
-
const onLive = makeLiveHandler(runId);
|
|
388
|
-
const projectRoot = getProjectRoot(runtime.configPath, request.executionCwd);
|
|
389
|
-
try {
|
|
390
|
-
const result = await runSingleAgentWithMainFallback(
|
|
391
|
-
{
|
|
392
|
-
defaultCwd: request.executionCwd,
|
|
393
|
-
cwd: request.executionCwd,
|
|
394
|
-
agent: route.agent,
|
|
395
|
-
resolveAgentForAttempt: resolveLiveAgentTools,
|
|
396
|
-
agentName,
|
|
397
|
-
task,
|
|
398
|
-
thinkingLevel,
|
|
399
|
-
thinkingLevelForModel: route.thinkingLevelForModel,
|
|
400
|
-
signal: request.signal,
|
|
401
|
-
onLive,
|
|
402
|
-
makeDetails: makeDetails("single", true),
|
|
403
|
-
idleTimeoutMs: stageConfig.idleTimeoutSec * 1000,
|
|
404
|
-
sessionRoot: join(projectRoot, "sessions"),
|
|
405
|
-
scratchRoot: join(projectRoot, "tmp"),
|
|
406
|
-
...(stage.session
|
|
407
|
-
? { sessionId: stage.session.sessionId, sessionDir: stage.session.sessionDir, stdinText: task }
|
|
408
|
-
: {}),
|
|
409
|
-
},
|
|
410
|
-
route.mainFallbackRef,
|
|
411
|
-
);
|
|
412
|
-
result.runId = runId;
|
|
413
|
-
result.projectCwd = request.projectCwd;
|
|
414
|
-
result.isolation = request.isolation;
|
|
415
|
-
runtime.retainSession(result);
|
|
416
|
-
monitor.setModel(runId, result.model, result.modelFallbackFrom);
|
|
417
|
-
monitor.setThinking(runId, result.thinking);
|
|
418
|
-
const finished = finishRun(runId, isFailedResult(result) ? "failed" : "done", { silent: true });
|
|
419
|
-
runtime.registerRunResult(runId, result);
|
|
420
|
-
return { runId, result, elapsedMs: finished?.elapsedMs };
|
|
421
|
-
} catch (error) {
|
|
422
|
-
const finished = finishRun(runId, "failed", { silent: true });
|
|
423
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
424
|
-
const crashed: SingleResult = {
|
|
425
|
-
...queuedResult(route.agent, task, thinkingLevel),
|
|
426
|
-
runId,
|
|
427
|
-
projectCwd: request.projectCwd,
|
|
428
|
-
isolation: request.isolation,
|
|
429
|
-
exitCode: 1,
|
|
430
|
-
stderr: errorMessage,
|
|
431
|
-
stopReason: request.signal.aborted ? "aborted" : "error",
|
|
432
|
-
errorMessage,
|
|
433
|
-
dispatchFailed: true,
|
|
434
|
-
};
|
|
435
|
-
runtime.registerRunResult(runId, crashed);
|
|
436
|
-
return { runId, result: crashed };
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
/** Drop any in-flight internal row. Normal internal settlement already
|
|
441
|
-
* removes rows; this is a cancellation/crash guard. */
|
|
442
|
-
const removeWorkflowGroup = (groupId: string): void => {
|
|
443
|
-
for (const run of [...monitor.getRuns()]) {
|
|
444
|
-
if (run.groupId === groupId) monitor.removeRun(run.id);
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
/** Run every downstream role inline under the parent generation's queue
|
|
449
|
-
* controller. That gives park/stop/shutdown one lifecycle owner and keeps
|
|
450
|
-
* isolated worktrees unintegrated until the final reviewer settles. */
|
|
451
|
-
const runManagedWorkflow = async (
|
|
452
|
-
request: ManagedWorkflowRequest,
|
|
453
|
-
): Promise<ManagedWorkflowOutcome> => {
|
|
454
|
-
const initialStepRunId = monitor.reserveRunId();
|
|
455
|
-
const initialStepResult: SingleResult = {
|
|
456
|
-
...request.initialResult,
|
|
457
|
-
runId: initialStepRunId,
|
|
458
|
-
};
|
|
459
|
-
runtime.registerRunResult(initialStepRunId, initialStepResult);
|
|
460
|
-
const steps: ChainStep[] = [{
|
|
461
|
-
runId: initialStepRunId,
|
|
462
|
-
result: initialStepResult,
|
|
463
|
-
relation: request.plan.initialRelation,
|
|
464
|
-
}];
|
|
465
|
-
const enabled = (name: string): boolean =>
|
|
466
|
-
request.agents.some((candidate) => candidate.name === name);
|
|
467
|
-
const canContinue = (): boolean => runtime.sessionActive && !request.signal.aborted;
|
|
468
|
-
|
|
469
|
-
// Keep a live parent-owned projection because settled internal rows are
|
|
470
|
-
// intentionally removed. Only real/currently planned stages enter it.
|
|
471
|
-
const initialStageRelation = initialStepResult.agent === "worker"
|
|
472
|
-
? "implement"
|
|
473
|
-
: initialStepResult.agent === "cleaner"
|
|
474
|
-
? "cleanup"
|
|
475
|
-
: "review";
|
|
476
|
-
const workflowStages: WorkflowStage[] = [{
|
|
477
|
-
agent: initialStepResult.agent,
|
|
478
|
-
relation: initialStageRelation,
|
|
479
|
-
status: workflowStageStatus(initialStepResult),
|
|
480
|
-
// The initial stage is the parent's own run; freeze its telemetry now,
|
|
481
|
-
// before the reopened parent row starts counting workflow-wide time.
|
|
482
|
-
model: initialStepResult.model,
|
|
483
|
-
usage: initialStepResult.usage,
|
|
484
|
-
elapsedMs: monitor.getElapsedMs(request.parentRunId),
|
|
485
|
-
}];
|
|
486
|
-
let reviewStage: WorkflowStage | undefined;
|
|
487
|
-
if (enabled("reviewer")) {
|
|
488
|
-
reviewStage = { agent: "reviewer", relation: "review", status: "pending" };
|
|
489
|
-
workflowStages.push(reviewStage);
|
|
490
|
-
}
|
|
491
|
-
const publishWorkflowStages = (): void => {
|
|
492
|
-
monitor.setWorkflowStages(request.parentRunId, workflowStages);
|
|
493
|
-
};
|
|
494
|
-
publishWorkflowStages();
|
|
495
|
-
|
|
496
|
-
/** Freeze the settled step's telemetry onto its stage: once the child row
|
|
497
|
-
* leaves the monitor, this snapshot is the only per-stage record. */
|
|
498
|
-
const settleStage = (stage: WorkflowStage, step: { result: SingleResult; elapsedMs?: number }): void => {
|
|
499
|
-
stage.model = step.result.model;
|
|
500
|
-
stage.usage = step.result.usage;
|
|
501
|
-
if (step.elapsedMs !== undefined) stage.elapsedMs = step.elapsedMs;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const launchStep = async (
|
|
505
|
-
agentName: string,
|
|
506
|
-
task: string,
|
|
507
|
-
relation: string,
|
|
508
|
-
stage: WorkflowStage,
|
|
509
|
-
stageOptions: {
|
|
510
|
-
agentOverride?: AgentConfig;
|
|
511
|
-
session?: { sessionId: string; sessionDir: string };
|
|
512
|
-
} = {},
|
|
513
|
-
): Promise<SingleResult> => {
|
|
514
|
-
if (!enabled(agentName)) {
|
|
515
|
-
throw new Error(`Managed workflow cannot launch disabled or missing agent "${agentName}".`);
|
|
516
|
-
}
|
|
517
|
-
stage.status = "active";
|
|
518
|
-
publishWorkflowStages();
|
|
519
|
-
try {
|
|
520
|
-
const step = await launchInWorkflow(request, agentName, task, {
|
|
521
|
-
groupId: request.groupId,
|
|
522
|
-
relationLabel: relation,
|
|
523
|
-
parentRunId: request.parentRunId,
|
|
524
|
-
}, stageOptions);
|
|
525
|
-
stage.status = workflowStageStatus(step.result, relation);
|
|
526
|
-
settleStage(stage, step);
|
|
527
|
-
publishWorkflowStages();
|
|
528
|
-
request.rememberLatest(step.result);
|
|
529
|
-
steps.push({ ...step, relation });
|
|
530
|
-
return step.result;
|
|
531
|
-
} catch (error) {
|
|
532
|
-
stage.status = "failed";
|
|
533
|
-
publishWorkflowStages();
|
|
534
|
-
throw error;
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
try {
|
|
539
|
-
// Park/stop/shutdown may win after the top-level child settles but
|
|
540
|
-
// before this continuation starts. Preserve that stable checkpoint and
|
|
541
|
-
// never create an already-aborted downstream child.
|
|
542
|
-
if (!canContinue()) return { steps };
|
|
543
|
-
if (reviewStage) {
|
|
544
|
-
const discoveredReviewer = request.agents.find((candidate) => candidate.name === "reviewer")!;
|
|
545
|
-
let gateReview = await launchStep(
|
|
546
|
-
"reviewer",
|
|
547
|
-
buildFinalReviewBrief(initialStepResult),
|
|
548
|
-
"final review",
|
|
549
|
-
reviewStage,
|
|
550
|
-
);
|
|
551
|
-
// The failing gate owns its fixes: the same retained
|
|
552
|
-
// reviewer session applies its own fix instructions with write access,
|
|
553
|
-
// then a converging re-review verifies the fixes. The cap only stops
|
|
554
|
-
// pathological burn and hands the still-failing gate to the main agent.
|
|
555
|
-
for (let round = 1; round <= MAX_REVIEW_FIX_ROUNDS; round++) {
|
|
556
|
-
const gateSession = gateReview.sessionId && gateReview.sessionDir
|
|
557
|
-
? { sessionId: gateReview.sessionId, sessionDir: gateReview.sessionDir }
|
|
558
|
-
: undefined;
|
|
559
|
-
if (reviewVerdict(getResultOutput(gateReview)) !== "fail" || !gateSession || !canContinue()) break;
|
|
560
|
-
const fixStage: WorkflowStage = { agent: "reviewer", relation: "review fix", status: "pending" };
|
|
561
|
-
workflowStages.push(fixStage);
|
|
562
|
-
publishWorkflowStages();
|
|
563
|
-
const fixResult = await launchStep(
|
|
564
|
-
"reviewer",
|
|
565
|
-
buildReviewerFixBrief(getResultOutput(gateReview)),
|
|
566
|
-
"review fix",
|
|
567
|
-
fixStage,
|
|
568
|
-
{ agentOverride: withReviewerFixStageAgent(discoveredReviewer), session: gateSession },
|
|
569
|
-
);
|
|
570
|
-
if (isFailedResult(fixResult) || !canContinue()) break;
|
|
571
|
-
const reReviewStage: WorkflowStage = { agent: "reviewer", relation: "review", status: "pending" };
|
|
572
|
-
workflowStages.push(reReviewStage);
|
|
573
|
-
publishWorkflowStages();
|
|
574
|
-
gateReview = await launchStep(
|
|
575
|
-
"reviewer",
|
|
576
|
-
buildReReviewBrief(fixResult, round),
|
|
577
|
-
round === 1 ? "re-review" : `re-review ${round}`,
|
|
578
|
-
reReviewStage,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return { steps };
|
|
583
|
-
} finally {
|
|
584
|
-
removeWorkflowGroup(request.groupId);
|
|
585
|
-
}
|
|
586
|
-
};
|
|
587
|
-
|
|
588
299
|
const startBackground = createBackgroundDispatcher({
|
|
589
300
|
runtime,
|
|
590
301
|
getEnvironment: () => {
|
|
@@ -596,7 +307,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
596
307
|
finishRun,
|
|
597
308
|
makeLiveHandler,
|
|
598
309
|
makeDetails,
|
|
599
|
-
runManagedWorkflow,
|
|
600
310
|
});
|
|
601
311
|
runtime.dispatcher = startBackground;
|
|
602
312
|
|
|
@@ -607,10 +317,10 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
607
317
|
"Dispatch enabled agents as isolated leaf Pi child processes: single {agent, task} or parallel {tasks: [...]}. Dispatching never blocks your turn — runs proceed in the background and each completion resumes you automatically; never poll or restate delivered results.",
|
|
608
318
|
"Put every genuinely independent unit in one `tasks` array: there is no per-call cap, and runs beyond the machine's free process slots simply wait and start as slots free.",
|
|
609
319
|
"Parallel write-capable agents default to a detached Git worktree so writers run concurrently; explicit `shared` keeps the caller's checkout and serializes same-repository writers. Worktree setup failure never silently falls back to shared.",
|
|
610
|
-
"
|
|
320
|
+
"A configured child-model failure continues the retained session on the current main model.",
|
|
611
321
|
].join(" "),
|
|
612
322
|
promptSnippet:
|
|
613
|
-
"Dispatch isolated background agents for recon, implementation, cleanup, docs, or
|
|
323
|
+
"Dispatch isolated background agents for recon, implementation, cleanup, docs sync, or result merging; never blocks your turn, and completions wake you automatically.",
|
|
614
324
|
parameters: SubagentParams,
|
|
615
325
|
|
|
616
326
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -695,7 +405,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
695
405
|
catalogAgent ? isWriteCapableAgent(catalogAgent) : undefined,
|
|
696
406
|
catalogAgent?.isolation,
|
|
697
407
|
),
|
|
698
|
-
{ review: item.review as ReviewMode | undefined },
|
|
699
408
|
));
|
|
700
409
|
}
|
|
701
410
|
const startedRuns = results.filter((result) => result.exitCode === -1);
|
|
@@ -757,7 +466,6 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
757
466
|
singleCatalogAgent ? isWriteCapableAgent(singleCatalogAgent) : undefined,
|
|
758
467
|
singleCatalogAgent?.isolation,
|
|
759
468
|
),
|
|
760
|
-
{ review: params.review as ReviewMode | undefined },
|
|
761
469
|
);
|
|
762
470
|
if (result.exitCode !== -1) {
|
|
763
471
|
throw new Error(getResultOutput(result));
|
|
@@ -783,8 +491,7 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
783
491
|
for (const t of args.tasks.slice(0, 4)) {
|
|
784
492
|
const preview = formatTaskSummary(t.task, 48);
|
|
785
493
|
const isolation = defaultIsolationMode("parallel", t.agent, t.isolation) === "worktree" ? " [worktree]" : "";
|
|
786
|
-
|
|
787
|
-
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", `${isolation}${gate}`)} ${theme.fg("dim", preview)}`;
|
|
494
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", isolation)} ${theme.fg("dim", preview)}`;
|
|
788
495
|
}
|
|
789
496
|
if (args.tasks.length > 4) text += `\n ${theme.fg("dim", `… +${args.tasks.length - 4} more`)}`;
|
|
790
497
|
return new Text(text, 0, 0);
|
|
@@ -792,9 +499,8 @@ export function registerSubagentTool(pi: ExtensionAPI, runtime: SubagentRuntime)
|
|
|
792
499
|
const task: string = args.task ?? "";
|
|
793
500
|
const preview = formatTaskSummary(task, 60);
|
|
794
501
|
const isolation = args.isolation === "worktree" ? " [worktree]" : "";
|
|
795
|
-
const gate = args.review === "none" ? " [no gate]" : "";
|
|
796
502
|
return new Text(
|
|
797
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")}${theme.fg("dim", `${isolation}
|
|
503
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "?")}${theme.fg("dim", `${isolation}`)} ${theme.fg("dim", preview)}`,
|
|
798
504
|
0,
|
|
799
505
|
0,
|
|
800
506
|
);
|
package/src/durable.ts
CHANGED
|
@@ -76,9 +76,6 @@ export interface ThreadRecord {
|
|
|
76
76
|
executionCwd: string;
|
|
77
77
|
thinkingLevel?: string;
|
|
78
78
|
isolation: IsolationMode;
|
|
79
|
-
/** Persisted only when the dispatch opted out of the automatic gate, so a
|
|
80
|
-
* restored resume never surprises the caller with a full review. */
|
|
81
|
-
review?: "none";
|
|
82
79
|
state: "parked" | "completed" | "failed";
|
|
83
80
|
elapsedMs: number;
|
|
84
81
|
sessionId?: string;
|
|
@@ -171,7 +168,6 @@ function normalizeRecord(value: unknown): ThreadRecord | undefined {
|
|
|
171
168
|
executionCwd: typeof raw.executionCwd === "string" && raw.executionCwd ? raw.executionCwd : raw.cwd,
|
|
172
169
|
...(typeof raw.thinkingLevel === "string" && raw.thinkingLevel ? { thinkingLevel: raw.thinkingLevel } : {}),
|
|
173
170
|
isolation: raw.isolation,
|
|
174
|
-
...(raw.review === "none" ? { review: "none" as const } : {}),
|
|
175
171
|
state: raw.state,
|
|
176
172
|
elapsedMs: typeof raw.elapsedMs === "number" && Number.isFinite(raw.elapsedMs) ? Math.max(0, raw.elapsedMs) : 0,
|
|
177
173
|
...(typeof raw.sessionId === "string" && raw.sessionId ? { sessionId: raw.sessionId } : {}),
|
|
@@ -285,7 +281,6 @@ export function threadRecordFromThread(
|
|
|
285
281
|
executionCwd: thread.executionCwd,
|
|
286
282
|
...(thread.thinkingLevel ? { thinkingLevel: thread.thinkingLevel } : {}),
|
|
287
283
|
isolation: thread.isolation,
|
|
288
|
-
...(thread.review === "none" ? { review: "none" as const } : {}),
|
|
289
284
|
state,
|
|
290
285
|
elapsedMs: thread.elapsedMs,
|
|
291
286
|
...(thread.sessionId && thread.sessionDir ? { sessionId: thread.sessionId, sessionDir: thread.sessionDir } : {}),
|
package/src/format.ts
CHANGED
|
@@ -154,14 +154,6 @@ export function modelLevelTakeoverNote(result: SingleResult, opts?: { runId?: nu
|
|
|
154
154
|
return `The sub-agent could not complete this task: ${cause}${retry}.${recovery}`;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
/** Instruction appended whenever a failing gate verdict is delivered: the
|
|
158
|
-
* findings return to the main agent, which owns the fix decision — the runtime
|
|
159
|
-
* never auto-fixes. Stating it at this exact decision point keeps the main
|
|
160
|
-
* agent from relaying the findings to the user and stopping. */
|
|
161
|
-
export function reviewFailFollowUpNote(): string {
|
|
162
|
-
return "This gate failed and the findings are yours to resolve now: fix them inline or dispatch a worker briefed with these fix instructions, then re-verify the change. Ask the user only before a genuinely destructive or scope-changing fix; do not deliver while a finding stands.";
|
|
163
|
-
}
|
|
164
|
-
|
|
165
157
|
/** Resolve a run-id request to actual ids: an exact numeric match always wins
|
|
166
158
|
* (so "1" never fans out to 10, 11, …); only when no exact match exists does a
|
|
167
159
|
* prefix match run, as a convenience for partial ids. Keeps single-digit lookups
|