@ferris1225/pi-subagents 4.2.13 → 4.3.1
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 +34 -0
- package/README.md +113 -149
- package/agents/artisan.md +19 -0
- package/agents/scout.md +18 -0
- package/agents/steward.md +19 -0
- package/package.json +4 -3
- package/src/agents.ts +24 -28
- package/src/announcements.ts +11 -18
- package/src/background.ts +56 -9
- package/src/completion.ts +0 -6
- package/src/config.ts +55 -90
- package/src/dispatch.ts +52 -67
- package/src/durable.ts +6 -53
- package/src/index.ts +7 -10
- package/src/monitor.ts +2 -2
- package/src/prompt.ts +104 -43
- package/src/recovery.ts +35 -10
- package/src/rpc-run.ts +59 -1
- package/src/runtime.ts +74 -17
- package/src/setup.ts +108 -102
- package/src/spawn.ts +6 -4
- package/src/thread-lifecycle.ts +127 -95
- package/src/tools.ts +4 -20
- package/agents/executor.md +0 -54
- package/agents/explorer.md +0 -37
package/src/thread-lifecycle.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { rm } from "node:fs/promises";
|
|
13
13
|
import { realpath } from "node:fs/promises";
|
|
14
|
-
import { join, resolve
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
15
|
import {
|
|
16
16
|
discoverAgents,
|
|
17
17
|
isWriteCapableAgent,
|
|
@@ -20,14 +20,13 @@ import {
|
|
|
20
20
|
} from "./agents.ts";
|
|
21
21
|
import { type CompletionMessageItem } from "./completion.ts";
|
|
22
22
|
import {
|
|
23
|
-
DEFAULT_THINKING_LEVEL,
|
|
24
23
|
loadConfig,
|
|
24
|
+
roleThinkingLevel,
|
|
25
25
|
type SubagentsConfig,
|
|
26
26
|
type ThinkingLevel,
|
|
27
27
|
} from "./config.ts";
|
|
28
28
|
import {
|
|
29
29
|
isCurrentBoot,
|
|
30
|
-
migrateLegacyThreadsManifest,
|
|
31
30
|
readThreadRecords,
|
|
32
31
|
referencedDurablePaths,
|
|
33
32
|
removeThreadRecord,
|
|
@@ -35,7 +34,7 @@ import {
|
|
|
35
34
|
pruneThreadRecords,
|
|
36
35
|
restoredResultFromSummary,
|
|
37
36
|
threadRecordFromThread,
|
|
38
|
-
ThreadRecord,
|
|
37
|
+
type ThreadRecord,
|
|
39
38
|
upsertThreadRecord,
|
|
40
39
|
} from "./durable.ts";
|
|
41
40
|
import {
|
|
@@ -54,13 +53,15 @@ import {
|
|
|
54
53
|
resolveThinkingLevel,
|
|
55
54
|
} from "./models.ts";
|
|
56
55
|
import { monitor } from "./monitor.ts";
|
|
56
|
+
import { findDuplicateActiveDispatch } from "./prompt.ts";
|
|
57
57
|
import { persistRecoveryRecords, recoveryRecordFromFinalization } from "./recovery.ts";
|
|
58
|
+
import { emptyUsage } from "./rpc-run.ts";
|
|
58
59
|
import type { SubagentRuntime, SubagentThread, ThreadState } from "./runtime.ts";
|
|
59
60
|
import { forkRetainedSession } from "./session-fork.ts";
|
|
60
61
|
import {
|
|
61
62
|
buildResumePrompt,
|
|
62
63
|
getProjectRoot,
|
|
63
|
-
|
|
64
|
+
getSubagentsRoot,
|
|
64
65
|
RpcRunControl,
|
|
65
66
|
isFailedResult,
|
|
66
67
|
isModelLevelFailure,
|
|
@@ -265,17 +266,16 @@ export interface ResumeReservation {
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
/** The dispatcher's full internal entry point; the public tool surface only
|
|
268
|
-
* uses the first four parameters
|
|
269
|
+
* uses the first four parameters. */
|
|
269
270
|
export interface StartBackgroundOptions {
|
|
270
|
-
/** Reasoning strength this dispatch asked for; the user's manual
|
|
271
|
-
* `/subagents-setup` choice still outranks it (see resolveDispatchModelRoute). */
|
|
272
|
-
thinking?: ThinkingLevel;
|
|
273
271
|
/** Resume path only: the thread whose retained context continues. */
|
|
274
272
|
existingThread?: SubagentThread;
|
|
275
273
|
appendedObjectiveOnResume?: boolean;
|
|
276
274
|
environment?: DispatchEnvironment;
|
|
277
275
|
seed?: SessionSeed;
|
|
278
276
|
resumeReservation?: ResumeReservation;
|
|
277
|
+
/** Chosen by the tool call before the queue can start a fast child. */
|
|
278
|
+
deliveryRoute?: "background" | "await";
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
export type StartBackgroundInternal = (
|
|
@@ -307,7 +307,6 @@ export function resolveDispatchModelRoute(
|
|
|
307
307
|
agent: AgentConfig,
|
|
308
308
|
config: SubagentsConfig,
|
|
309
309
|
ctx: ExtensionContext,
|
|
310
|
-
requestedThinking?: ThinkingLevel,
|
|
311
310
|
): DispatchModelRoute {
|
|
312
311
|
const availableModels = availableModelsInScope(ctx);
|
|
313
312
|
const mainRef = currentModelRef(ctx);
|
|
@@ -316,11 +315,10 @@ export function resolveDispatchModelRoute(
|
|
|
316
315
|
mainRef,
|
|
317
316
|
availableRefs: availableModels.map(modelRef),
|
|
318
317
|
});
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// dispatching model's per-call guess, which in turn outranks frontmatter.
|
|
318
|
+
// A `/subagents-setup` override wins; otherwise the role default. No
|
|
319
|
+
// per-call or frontmatter thinking.
|
|
322
320
|
const preferred =
|
|
323
|
-
config.agentThinkingLevels[agent.name] ??
|
|
321
|
+
config.agentThinkingLevels[agent.name] ?? roleThinkingLevel(agent.name);
|
|
324
322
|
const thinkingLevelForModel = (ref?: string): ThinkingLevel => {
|
|
325
323
|
const model = ref === mainRef && ctx.model
|
|
326
324
|
? ctx.model
|
|
@@ -372,12 +370,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
372
370
|
startOptions: StartBackgroundOptions = {},
|
|
373
371
|
): Promise<SingleResult> => {
|
|
374
372
|
const {
|
|
375
|
-
thinking,
|
|
376
373
|
existingThread,
|
|
377
374
|
appendedObjectiveOnResume = false,
|
|
378
375
|
environment,
|
|
379
376
|
seed,
|
|
380
377
|
resumeReservation,
|
|
378
|
+
deliveryRoute = "background",
|
|
381
379
|
} = startOptions;
|
|
382
380
|
if (!runtime.sessionActive) {
|
|
383
381
|
return failedStartResult(agentName, task, "Parent session shut down before this subagent generation could start.");
|
|
@@ -396,43 +394,40 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
396
394
|
const agent = resolveLiveAgentTools(discoveredAgent);
|
|
397
395
|
if (isolation === "worktree" && !isWorktreeCapableAgent(agent)) {
|
|
398
396
|
return {
|
|
399
|
-
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as
|
|
397
|
+
...failedStartResult(agentName, task, `Agent "${agentName}" is read-only; worktree isolation is available only to write-capable agents such as artisan.`),
|
|
400
398
|
isolation,
|
|
401
399
|
};
|
|
402
400
|
}
|
|
403
401
|
|
|
404
402
|
const originalCwd = resolve(cwd ?? runCtx.cwd);
|
|
403
|
+
if (!existingThread) {
|
|
404
|
+
const duplicate = findDuplicateActiveDispatch(runtime.threads.values(), task, originalCwd);
|
|
405
|
+
if (duplicate) {
|
|
406
|
+
return failedStartResult(
|
|
407
|
+
agentName,
|
|
408
|
+
task,
|
|
409
|
+
`Duplicate active dispatch matches run #${duplicate.id} (${duplicate.agentName}). Use that logical thread instead; resume #${duplicate.id} when it is eligible.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
405
413
|
const projectRoot = getProjectRoot(runtime.configPath, originalCwd);
|
|
406
414
|
const sessionsRoot = join(projectRoot, "sessions");
|
|
407
415
|
const worktreesRoot = join(projectRoot, "worktrees");
|
|
408
416
|
const scratchRoot = join(projectRoot, "tmp");
|
|
409
417
|
const previousWorktree = existingThread?.worktree;
|
|
410
418
|
let worktree = seed?.worktree ?? previousWorktree;
|
|
411
|
-
if (isolation === "worktree") {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
if (!worktree) {
|
|
420
|
-
try {
|
|
421
|
-
worktree = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
|
|
422
|
-
} catch (error) {
|
|
423
|
-
return {
|
|
424
|
-
...failedStartResult(agentName, task, error instanceof Error ? error.message : String(error)),
|
|
425
|
-
isolation,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
}
|
|
419
|
+
if (isolation === "worktree" && worktree && worktree.state !== "active") {
|
|
420
|
+
return {
|
|
421
|
+
...failedStartResult(agentName, task, `Run #${existingThread?.id ?? "?"} has no active continuation worktree.`),
|
|
422
|
+
isolation,
|
|
423
|
+
integrationStatus: worktree.state === "finalizing" ? "pending" : worktree.state,
|
|
424
|
+
};
|
|
429
425
|
}
|
|
430
|
-
|
|
431
|
-
|
|
426
|
+
let executionCwd = worktree?.cwd ?? originalCwd;
|
|
427
|
+
let worktreeGroup = worktree ? worktreeGroupId(worktree) : undefined;
|
|
432
428
|
// A resume re-runs at the strength its dispatch asked for, so the retained
|
|
433
429
|
// request survives generations (and, via the durable record, restarts).
|
|
434
|
-
const
|
|
435
|
-
const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx, requestedThinking);
|
|
430
|
+
const resolvedRoute = resolveDispatchModelRoute(agent, runConfig, runCtx);
|
|
436
431
|
// Isolation is a persistent system-level invariant, not a one-shot task
|
|
437
432
|
// prefix: resumes and main-model
|
|
438
433
|
// handoffs all keep the same worktree boundary.
|
|
@@ -447,6 +442,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
447
442
|
isolation,
|
|
448
443
|
...(worktreeGroup ? { worktreeId: worktreeGroup } : {}),
|
|
449
444
|
});
|
|
445
|
+
runtime.claimRunDelivery(runId, deliveryRoute);
|
|
450
446
|
const generation = (existingThread?.generation ?? 0) + 1;
|
|
451
447
|
const pending: SingleResult = {
|
|
452
448
|
...queuedResult(route.agent, task, thinkingLevel),
|
|
@@ -493,7 +489,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
493
489
|
thread.cwd = originalCwd;
|
|
494
490
|
thread.executionCwd = executionCwd;
|
|
495
491
|
thread.thinkingLevel = thinkingLevel;
|
|
496
|
-
thread.requestedThinkingLevel = requestedThinking;
|
|
497
492
|
thread.isolation = isolation;
|
|
498
493
|
thread.worktree = worktree;
|
|
499
494
|
thread.state = "queued";
|
|
@@ -517,7 +512,6 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
517
512
|
cwd: originalCwd,
|
|
518
513
|
executionCwd,
|
|
519
514
|
thinkingLevel,
|
|
520
|
-
requestedThinkingLevel: requestedThinking,
|
|
521
515
|
isolation,
|
|
522
516
|
worktree,
|
|
523
517
|
state: "queued",
|
|
@@ -532,11 +526,12 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
532
526
|
};
|
|
533
527
|
runtime.threads.set(runId, thread);
|
|
534
528
|
}
|
|
535
|
-
installThreadLifecycle(thread, {
|
|
529
|
+
const installCurrentLifecycle = (): void => installThreadLifecycle(thread, {
|
|
536
530
|
runtime,
|
|
537
531
|
runCtx,
|
|
538
532
|
startBackground: (...args) => startBackground(...args),
|
|
539
533
|
});
|
|
534
|
+
if (isolation === "shared" || worktree) installCurrentLifecycle();
|
|
540
535
|
|
|
541
536
|
const onLive = makeLiveHandler(runId, generation);
|
|
542
537
|
// Shared write-capable runs serialize on the repository lane so their
|
|
@@ -545,6 +540,20 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
545
540
|
const reserveManagedLane = isolation === "shared" && isWriteCapableAgent(agent);
|
|
546
541
|
const runGeneration = async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
547
542
|
if (runtime.threads.get(runId)?.generation !== generation) return;
|
|
543
|
+
if (isolation === "worktree" && !worktree) {
|
|
544
|
+
const prepared = await createWorktreeIsolation(originalCwd, { tempBaseDir: worktreesRoot });
|
|
545
|
+
if (runtime.threads.get(runId)?.generation !== generation || backgroundSignal.aborted) {
|
|
546
|
+
await prepared.discard().catch(() => undefined);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
worktree = prepared;
|
|
550
|
+
executionCwd = prepared.cwd;
|
|
551
|
+
worktreeGroup = worktreeGroupId(prepared);
|
|
552
|
+
thread.worktree = prepared;
|
|
553
|
+
thread.executionCwd = executionCwd;
|
|
554
|
+
monitor.setIsolation(runId, "worktree", "pending", worktreeGroup);
|
|
555
|
+
installCurrentLifecycle();
|
|
556
|
+
}
|
|
548
557
|
// The generation body owns a process slot from here (a lane wait, if
|
|
549
558
|
// any, was granted above). Recording the transition synchronously keeps
|
|
550
559
|
// every "queued" surface truthful: only runs still pending in the pool
|
|
@@ -557,7 +566,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
557
566
|
let activeIdleTimeoutMs = runConfig.idleTimeoutSec * 1000;
|
|
558
567
|
try {
|
|
559
568
|
const startConfig = await loadConfig(runtime.configPath);
|
|
560
|
-
const resolvedStart = resolveDispatchModelRoute(agent, startConfig, runCtx
|
|
569
|
+
const resolvedStart = resolveDispatchModelRoute(agent, startConfig, runCtx);
|
|
561
570
|
activeRoute = isolation === "worktree"
|
|
562
571
|
? { ...resolvedStart, agent: withWorktreeSystemPrompt(resolvedStart.agent) }
|
|
563
572
|
: resolvedStart;
|
|
@@ -658,10 +667,10 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
658
667
|
thread.lifecycleOperation === "settle" &&
|
|
659
668
|
!thread.retired;
|
|
660
669
|
try {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
670
|
+
// The child process has exited. Isolated Git finalization remains
|
|
671
|
+
// lifecycle-owned and awaited, but no longer consumes a process slot.
|
|
672
|
+
if (isolation === "worktree") runtime.backgroundQueue.suspend(controller);
|
|
673
|
+
await thread.finalizeIsolation(generation, result);
|
|
665
674
|
if (!ownsSettlement()) return;
|
|
666
675
|
|
|
667
676
|
const failed = isFailedResult(result);
|
|
@@ -692,41 +701,33 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
692
701
|
block: modelLevel
|
|
693
702
|
? `${formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) })}\n\n${modelLevelTakeoverNote(result, { runId })}`
|
|
694
703
|
: formatCompletionBlock(result, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, result.projectCwd ?? originalCwd) }),
|
|
695
|
-
triggerTurn: true,
|
|
696
704
|
usage: result.usage,
|
|
697
705
|
};
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
runtime.sendCompletionGroup([completion]);
|
|
706
|
-
runtime.completionBatcher.flush();
|
|
707
|
-
} else {
|
|
708
|
-
runtime.completionBatcher.push(completion);
|
|
709
|
-
}
|
|
706
|
+
if (modelLevel) {
|
|
707
|
+
const detail = result.errorMessage?.trim() || "model unavailable or broken";
|
|
708
|
+
runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${detail} — task handed to the main window`, "error");
|
|
709
|
+
} else if (dispatchFailed) {
|
|
710
|
+
runCtx.ui.notify(`✗ ${result.agent} dispatch failed: ${result.errorMessage ?? "dispatch crashed"}`, "error");
|
|
711
|
+
}
|
|
712
|
+
runtime.publishRunCompletion(runId, completion, failed);
|
|
710
713
|
} finally {
|
|
711
714
|
if (ownsSettlement()) thread.lifecycleOperation = undefined;
|
|
712
715
|
}
|
|
713
716
|
};
|
|
714
717
|
const queuedGeneration = reserveManagedLane
|
|
715
718
|
? async (backgroundSignal: AbortSignal, controller: AbortController): Promise<void> => {
|
|
716
|
-
//
|
|
717
|
-
// lane
|
|
718
|
-
//
|
|
719
|
-
// writers park the entire pool and starve independent read-only
|
|
720
|
-
// dispatches that could have started immediately, so the slot is
|
|
721
|
-
// released up front; the lane itself still serializes same-repo
|
|
722
|
-
// writers, and abort/quiesce guarantees are unchanged.
|
|
719
|
+
// Waiting for repository serialization must not consume a process
|
|
720
|
+
// slot. Once the lane is granted, reacquire through the same FIFO
|
|
721
|
+
// scheduler before any child process can spawn.
|
|
723
722
|
runtime.backgroundQueue.suspend(controller);
|
|
724
|
-
// A lane wait is write serialization, not slot pacing: the model
|
|
725
|
-
// must never read it as an exhausted pool.
|
|
726
723
|
monitor.setWaitReason(runId, "repository-lane");
|
|
727
724
|
await runInManagedRepositoryLane(
|
|
728
725
|
originalCwd,
|
|
729
|
-
() =>
|
|
726
|
+
async () => {
|
|
727
|
+
monitor.setWaitReason(runId, "process-slot");
|
|
728
|
+
if (!(await runtime.backgroundQueue.acquire(controller))) return;
|
|
729
|
+
await runGeneration(backgroundSignal, controller);
|
|
730
|
+
},
|
|
730
731
|
backgroundSignal,
|
|
731
732
|
);
|
|
732
733
|
}
|
|
@@ -785,6 +786,7 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
785
786
|
};
|
|
786
787
|
thread.lastResult = crashed;
|
|
787
788
|
runtime.retainSession(crashed);
|
|
789
|
+
if (isolation === "worktree" && thread.worktree) runtime.backgroundQueue.suspend(thread.queueController);
|
|
788
790
|
await thread.finalizeIsolation(generation, crashed);
|
|
789
791
|
if (!ownsSettlement()) return;
|
|
790
792
|
thread.state = "failed";
|
|
@@ -798,15 +800,11 @@ export function createBackgroundDispatcher(options: BackgroundDispatcherOptions)
|
|
|
798
800
|
if (!runtime.sessionActive || !ownsSettlement()) return;
|
|
799
801
|
try {
|
|
800
802
|
runCtx.ui.notify(`✗ ${crashed.agent} dispatch failed: ${crashed.errorMessage}`, "error");
|
|
801
|
-
runtime.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
usage: crashed.usage,
|
|
807
|
-
},
|
|
808
|
-
]);
|
|
809
|
-
runtime.completionBatcher.flush();
|
|
803
|
+
runtime.publishRunCompletion(runId, {
|
|
804
|
+
agent: crashed.agent,
|
|
805
|
+
block: formatCompletionBlock(crashed, runConfig.maxResultLines, { resultRoot: projectResultsRoot(runtime.configPath, crashed.projectCwd ?? originalCwd) }),
|
|
806
|
+
usage: crashed.usage,
|
|
807
|
+
}, true);
|
|
810
808
|
} catch {
|
|
811
809
|
/* a second delivery failure must not throw through the queue */
|
|
812
810
|
}
|
|
@@ -986,6 +984,9 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
986
984
|
return failedStartResult(thread.agentName, thread.task, "resume objective must be non-blank when provided.");
|
|
987
985
|
}
|
|
988
986
|
if (thread.retired) return failedStartResult(thread.agentName, thread.task, `Run #${runId} was retired by subagent_stop.`);
|
|
987
|
+
if (thread.resumeUnavailableReason) {
|
|
988
|
+
return failedStartResult(thread.agentName, thread.task, thread.resumeUnavailableReason);
|
|
989
|
+
}
|
|
989
990
|
if (thread.lifecycleOperation) {
|
|
990
991
|
return failedStartResult(thread.agentName, thread.task, `Run #${runId} is already resuming.`);
|
|
991
992
|
}
|
|
@@ -1182,9 +1183,6 @@ function createRestoredThread(
|
|
|
1182
1183
|
cwd: record.cwd,
|
|
1183
1184
|
executionCwd: record.executionCwd,
|
|
1184
1185
|
...(record.thinkingLevel ? { thinkingLevel: record.thinkingLevel as ThinkingLevel } : {}),
|
|
1185
|
-
...(record.requestedThinkingLevel
|
|
1186
|
-
? { requestedThinkingLevel: record.requestedThinkingLevel as ThinkingLevel }
|
|
1187
|
-
: {}),
|
|
1188
1186
|
isolation: record.isolation,
|
|
1189
1187
|
worktree,
|
|
1190
1188
|
state,
|
|
@@ -1249,16 +1247,53 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
1249
1247
|
const worktree = record.worktree
|
|
1250
1248
|
? await restoreWorktreeIsolation(record.worktree).catch(() => undefined)
|
|
1251
1249
|
: undefined;
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const state: ThreadState = worktree
|
|
1255
|
-
? "parked"
|
|
1256
|
-
: record.isolation === "worktree" && record.worktree
|
|
1257
|
-
? "failed"
|
|
1258
|
-
: "parked";
|
|
1259
|
-
const thread = createRestoredThread(runtime, record, worktree, state);
|
|
1250
|
+
const restorationFailed = record.isolation === "worktree" && record.worktree !== undefined && !worktree;
|
|
1251
|
+
const thread = createRestoredThread(runtime, record, worktree, restorationFailed ? "failed" : "parked");
|
|
1260
1252
|
runtime.threads.set(record.runId, thread);
|
|
1261
1253
|
runtime.sessionDirs.add(record.sessionDir!);
|
|
1254
|
+
if (restorationFailed) {
|
|
1255
|
+
const reason = `Run #${record.runId}'s recorded worktree could not be restored; isolated edits may be unavailable. The retained session and durable record were kept, but this thread cannot be resumed.`;
|
|
1256
|
+
thread.resumeUnavailableReason = reason;
|
|
1257
|
+
thread.restorationRecord = record;
|
|
1258
|
+
const previous = restoredResultFromSummary(record);
|
|
1259
|
+
const failed: SingleResult = {
|
|
1260
|
+
agent: record.agentName,
|
|
1261
|
+
task: record.task,
|
|
1262
|
+
exitCode: 1,
|
|
1263
|
+
messages: previous?.messages ?? [],
|
|
1264
|
+
stderr: reason,
|
|
1265
|
+
usage: previous?.usage ?? emptyUsage(),
|
|
1266
|
+
model: previous?.model,
|
|
1267
|
+
thinking: previous?.thinking,
|
|
1268
|
+
stopReason: "error",
|
|
1269
|
+
errorMessage: reason,
|
|
1270
|
+
dispatchFailed: true,
|
|
1271
|
+
sessionId: record.sessionId,
|
|
1272
|
+
sessionDir: record.sessionDir,
|
|
1273
|
+
projectCwd: record.cwd,
|
|
1274
|
+
runId: record.runId,
|
|
1275
|
+
isolation: "worktree",
|
|
1276
|
+
integrationStatus: "retained",
|
|
1277
|
+
};
|
|
1278
|
+
thread.lastResult = failed;
|
|
1279
|
+
runtime.registerRunResult(record.runId, failed);
|
|
1280
|
+
monitor.restoreRun({
|
|
1281
|
+
id: record.runId,
|
|
1282
|
+
agent: record.agentName,
|
|
1283
|
+
task: record.task,
|
|
1284
|
+
status: "failed",
|
|
1285
|
+
elapsedMs: record.elapsedMs,
|
|
1286
|
+
isolation: "worktree",
|
|
1287
|
+
integrationStatus: "retained",
|
|
1288
|
+
});
|
|
1289
|
+
runtime.claimRunDelivery(record.runId, "background");
|
|
1290
|
+
runtime.publishRunCompletion(record.runId, {
|
|
1291
|
+
agent: record.agentName,
|
|
1292
|
+
block: `### Subagent restoration failed: #${record.runId} ${record.agentName}\n\n${reason}`,
|
|
1293
|
+
usage: failed.usage,
|
|
1294
|
+
}, true);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1262
1297
|
monitor.restoreRun({
|
|
1263
1298
|
id: record.runId,
|
|
1264
1299
|
agent: record.agentName,
|
|
@@ -1280,9 +1315,9 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
1280
1315
|
return restoredIds;
|
|
1281
1316
|
}
|
|
1282
1317
|
|
|
1283
|
-
/**
|
|
1318
|
+
/** Session-start durable bootstrap: restore threads, age out expired records, and
|
|
1284
1319
|
* sweep leaked temp/state directories. Every stage is best-effort so a broken
|
|
1285
|
-
* manifest never blocks
|
|
1320
|
+
* manifest never blocks the session.
|
|
1286
1321
|
*
|
|
1287
1322
|
* Restore is published on the runtime as `durableRestore` before this returns,
|
|
1288
1323
|
* so callers that must see restored threads await that pass alone and never the
|
|
@@ -1291,9 +1326,6 @@ export async function restoreDurableThreads(runtime: SubagentRuntime): Promise<n
|
|
|
1291
1326
|
export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
1292
1327
|
const restore = (async () => {
|
|
1293
1328
|
try {
|
|
1294
|
-
// Records from a pre-per-project manifest must land in their project
|
|
1295
|
-
// roots before restore reads anything.
|
|
1296
|
-
await migrateLegacyThreadsManifest(runtime.configPath);
|
|
1297
1329
|
runtime.restoredRunIds = await restoreDurableThreads(runtime);
|
|
1298
1330
|
} catch {
|
|
1299
1331
|
/* restore is best-effort */
|
|
@@ -1307,7 +1339,7 @@ export function bootstrapDurableState(runtime: SubagentRuntime): Promise<void> {
|
|
|
1307
1339
|
} catch {
|
|
1308
1340
|
/* retention is best-effort */
|
|
1309
1341
|
}
|
|
1310
|
-
const projectRoots =
|
|
1342
|
+
const projectRoots = getSubagentsRoot(runtime.configPath);
|
|
1311
1343
|
try {
|
|
1312
1344
|
sweepProjectTempDirs(projectRoots);
|
|
1313
1345
|
} catch {
|
package/src/tools.ts
CHANGED
|
@@ -46,14 +46,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
46
46
|
pi.registerTool({
|
|
47
47
|
name: "subagent_control",
|
|
48
48
|
label: "Subagent Control",
|
|
49
|
-
description:
|
|
50
|
-
"Resume an existing sub-agent thread by stable run id: a parked, completed, or failed retained thread restarts with the same run id and cumulative active time.",
|
|
51
|
-
"Omit objective to continue the current goal, or provide one to append it to retained context and make it the displayed goal. Threads parked or interrupted by a shutdown/reload are restorable; use subagent_stop for destructive cancellation.",
|
|
52
|
-
].join(" "),
|
|
53
|
-
promptSnippet: "Resume a parked or settled subagent thread with its retained context.",
|
|
54
|
-
promptGuidelines: [
|
|
55
|
-
"Resume keeps the run id and retained context; use subagent_stop only for destructive cancellation, which retires that thread's session.",
|
|
56
|
-
],
|
|
49
|
+
description: "Resume a parked or settled child thread by run id, reusing its retained session when available. An optional objective is appended to its current goal.",
|
|
57
50
|
parameters: SubagentControlParams,
|
|
58
51
|
|
|
59
52
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -91,10 +84,8 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
91
84
|
const mode = objective
|
|
92
85
|
? `appended objective: ${currentObjective}`
|
|
93
86
|
: `continuing current objective: ${currentObjective}`;
|
|
94
|
-
const context = hadRetainedSession
|
|
95
|
-
|
|
96
|
-
: "no prior child session existed, so only the logical run and objective are continued";
|
|
97
|
-
return { content: [{ type: "text", text: `Resumed run #${thread.id}, ${mode}; ${context}, and cumulative active time is preserved. It runs in the background — keep working; the result resumes you automatically.` }], details: {} };
|
|
87
|
+
const context = hadRetainedSession ? "retained context reused" : "no prior child context";
|
|
88
|
+
return { content: [{ type: "text", text: `Resumed run #${thread.id}: ${mode}; ${context}.` }], details: {} };
|
|
98
89
|
}
|
|
99
90
|
}
|
|
100
91
|
} catch (error) {
|
|
@@ -125,14 +116,7 @@ export function registerLookupTools(pi: ExtensionAPI, runtime: SubagentRuntime):
|
|
|
125
116
|
pi.registerTool({
|
|
126
117
|
name: "subagent_stop",
|
|
127
118
|
label: "Subagent Stop",
|
|
128
|
-
description:
|
|
129
|
-
"Destructively stop a sub-agent thread: terminate active work, deliver its aborted partial result, and retire any retained session so it cannot be resumed.",
|
|
130
|
-
"Pass id (run id or prefix) to stop one active, parked, or completed thread; all: true stops every active run.",
|
|
131
|
-
].join(" "),
|
|
132
|
-
promptSnippet: "Stop a running background subagent (id from dispatch output; or all: true).",
|
|
133
|
-
promptGuidelines: [
|
|
134
|
-
"Stop a run when its task is obsolete, stuck, or superseded — do not leave it burning tokens. It then reports as failed with 'aborted' plus its partial output.",
|
|
135
|
-
],
|
|
119
|
+
description: "Stop and retire one child thread by run id/prefix, or every active thread with all: true.",
|
|
136
120
|
parameters: SubagentStopParams,
|
|
137
121
|
|
|
138
122
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
package/agents/executor.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: executor
|
|
3
|
-
description: A self-contained unit that changes the repository or condenses inputs — implement, fix, refactor, test, clean up, sync docs, merge fan-out results — carried through verification to a result-only handoff.
|
|
4
|
-
thinking: high
|
|
5
|
-
# No `tools` field => inherits all tools (full capability).
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are an executor agent with full capabilities in an isolated context window. You own one delegated, self-contained task end to end so the main conversation stays clean. You have NOT got the caller's conversation history — the task brief is your source of truth.
|
|
9
|
-
|
|
10
|
-
Repository instructions (AGENTS.md) and any skills available in this session apply to you as to any agent: follow their process for the domains they own (language style, tests, debugging, cleanup discipline, verification). Where a skill covers the same ground as this brief, the skill's discipline wins — except for the release boundary below, which always wins.
|
|
11
|
-
|
|
12
|
-
## Procedure
|
|
13
|
-
|
|
14
|
-
1. **Context.** Read the brief fully, plus referenced files and images, before acting. If critical context is missing, state what is missing rather than guessing.
|
|
15
|
-
2. **Plan.** Inspect existing code and conventions first; form the smallest coherent root-cause change that satisfies the brief. Prefer the design that deletes complexity over one that rearranges it. No unrelated refactors or standalone docs work unless the brief asks.
|
|
16
|
-
3. **Confirm.** A finding is not a change. Re-read the current code and confirm each defect you are about to fix is real — not a misread, a stale report, or an intended tradeoff — even when the brief said "fix it". A false positive means zero edits and a note.
|
|
17
|
-
4. **Implement.** Preserve the user's work; limit edits to the request plus required validation. Follow the project's error handling, naming, and style. Synchronize README/docs/comments your change directly affects; never defer that drift.
|
|
18
|
-
5. **Verify.** Run the project's format/build/tests when they exist. NEVER report an unrun check as passed — report it as unavailable or a pre-existing failure, with the exact error.
|
|
19
|
-
|
|
20
|
-
## Conditional playbooks
|
|
21
|
-
|
|
22
|
-
Skip both unless the brief matches one.
|
|
23
|
-
|
|
24
|
-
**Cleanup** (dead code, duplication, simplification): a candidate is not a deletion. Re-read the load-bearing files and repeat the decisive searches yourself — never inherit proof from another agent's report — and search the whole repository for consumers first. Keep a candidate when a real consumer exists, dynamic reachability is unresolved, or the cut removes a user capability, public API, persisted format, or compatibility path the brief did not explicitly approve. Finding no safe cut and making zero edits is valid.
|
|
25
|
-
|
|
26
|
-
**Merging inputs** (result artifacts, reports, logs): read every named input fully before writing, deduplicate restatements into one attributed entry, and report surviving conflicts side by side instead of averaging them away. Stay within the named inputs; report what they cannot answer as a gap.
|
|
27
|
-
|
|
28
|
-
## Boundaries
|
|
29
|
-
|
|
30
|
-
- Never commit, push, publish, tag, release, or bump a package version — the caller owns every release action, even when repository instructions normally automate release after green checks.
|
|
31
|
-
- Children are leaf processes: you cannot dispatch sub-agents.
|
|
32
|
-
- Never change runtime behavior to make documentation true; report the defect instead.
|
|
33
|
-
|
|
34
|
-
## Output format
|
|
35
|
-
|
|
36
|
-
Return only the concrete outcome. Do not repeat the task brief, the plan, the root-cause investigation, or the tool chronology.
|
|
37
|
-
|
|
38
|
-
## Completed
|
|
39
|
-
|
|
40
|
-
What was done, in a few lines.
|
|
41
|
-
|
|
42
|
-
## Files Changed
|
|
43
|
-
|
|
44
|
-
- `path/to/file.ts` — what changed.
|
|
45
|
-
|
|
46
|
-
## Verification
|
|
47
|
-
|
|
48
|
-
Which checks you ACTUALLY ran and their result (e.g. `tsc --noEmit` clean). State explicitly anything you could not run and why.
|
|
49
|
-
|
|
50
|
-
## Notes (only when material)
|
|
51
|
-
|
|
52
|
-
Unresolved blockers, rejected requirements, or decisions the caller must know. Omit when nothing actionable.
|
|
53
|
-
|
|
54
|
-
Keep the final response comfortably below the 40-line delivery cap unless the result genuinely requires more.
|
package/agents/explorer.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: explorer
|
|
3
|
-
description: Fast read-only reconnaissance for broad or multi-file search in unfamiliar areas; returns exact paths and compressed findings as retrieval leads.
|
|
4
|
-
tools: read, grep, find, ls, bash
|
|
5
|
-
# At launch, this shell slot follows the parent and parent-active plugin tools
|
|
6
|
-
# are appended; the listed non-shell Pi built-ins remain the permission boundary.
|
|
7
|
-
thinking: low
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
You are an explorer agent: a fast, read-only reconnaissance specialist. You investigate a codebase and return compressed, structured findings so another agent does not repeat the whole search. You have NOT got the caller's conversation history — the task brief is your only input.
|
|
11
|
-
|
|
12
|
-
## Hard constraints
|
|
13
|
-
|
|
14
|
-
- You are READ-ONLY. Never create, edit, or delete files; never run mutating commands. Reach for your `read`/`grep`/`find`/`ls` tools before the shell — they behave the same on every platform, while the shell you were given may be POSIX or PowerShell. Keep shell use to read-only inspection (`git log/show/diff/status` and that shell's own read-only commands); no installs, builds, or state changes. Permissions are not perfectly enforceable — keep every command strictly read-only by intent.
|
|
15
|
-
- Every finding is a retrieval lead, never sufficient proof for deletion, security claims, public/API compatibility, persistence, or other load-bearing decisions. The caller must re-read the cited line ranges before acting on your results.
|
|
16
|
-
|
|
17
|
-
## Workflow
|
|
18
|
-
|
|
19
|
-
1. Orient with `grep`/`find` to locate the relevant code fast. Prefer bare identifiers as patterns; scope by path and exclude noisy dirs (node_modules, dist, generated).
|
|
20
|
-
2. Read KEY SECTIONS, not whole files. After 1-2 greps, read the top match instead of running more greps.
|
|
21
|
-
3. Identify the types, interfaces, and key function signatures involved; note how files depend on each other.
|
|
22
|
-
4. Record exact paths and line ranges so the caller can jump straight in.
|
|
23
|
-
5. If the brief asks you to inspect images (screenshots, mockups, designs), `read` them — the model receives them as attachments when it supports vision.
|
|
24
|
-
|
|
25
|
-
Thoroughness scales with the task (default medium): quick = targeted lookups in key files; medium = follow imports and callers, read critical sections; thorough = trace dependencies across modules, check tests and types.
|
|
26
|
-
|
|
27
|
-
## Final response
|
|
28
|
-
|
|
29
|
-
Return only retrieval results, one bare bullet per finding — a single line: path, the fact, nothing else:
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
- `path/to/file.ts:10-50` — the fact
|
|
33
|
-
Start here: `path/to/file.ts` — entry symbol and why (only when the caller could not guess it)
|
|
34
|
-
Gaps: unresolved uncertainty (only when real)
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
No preamble or closing summary. Do not repeat the task brief, inventory every file opened, paste nonessential code, or narrate the search; every line must carry a path with a fact or name a gap — delete anything else. State uncertainty and missing coverage — a plausible guess is more expensive than an honest gap. Stay under 15 lines by default; go longer only when the brief genuinely demands a wide survey — the 40-line delivery cap truncates your tail (usually the Gaps) and the caller pays for every line.
|