@arhen/pi-core-subagent 1.3.59 → 1.3.61
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 +2 -2
- package/package.json +1 -1
- package/src/index.ts +5 -4
- package/src/manager.ts +55 -6
- package/src/schemas.ts +6 -0
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ flowchart LR
|
|
|
51
51
|
- **Proof is an exit code, never a self-report.** Tasks are asked for a runnable `Verify:` command; the leader checks `git diff --stat`. Agents auditing their own work score ~0. ([why](#why-9-is-a-verification-command-not-a-self-report))
|
|
52
52
|
- **No ceremony without edges.** Six independent reviewers stay six independent reviewers — no waves, no gates, no graph vocabulary imposed on flat work.
|
|
53
53
|
- **Agent files respected.** A spawn goal (name + task) that matches a user agent file's `description` (`.agents/agents`, `.claude/agents`, `.pi/agents` — project then home) loads that file — body = system prompt, frontmatter `model`/`tools` apply, file `model` validated against the pi model registry. File wins over inline; no match → on-demand definition.
|
|
54
|
-
- **Two toolsets, plus explicit override.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`); `tools:` sets an explicit per-task allowlist.
|
|
54
|
+
- **Two toolsets, plus explicit override.** Read-only (`read, grep, find, ls, codemode` — default) or write (`read, grep, find, ls, bash, edit, write, codemode` — `write: true`); `tools:` sets an explicit per-task allowlist. Children run with `noExtensions`, so `codemode` is the one extension they get: it lets a child batch `tools.*` calls instead of spending a model turn per call.
|
|
55
55
|
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
56
56
|
- **Zero parent-context injection.** No catalog, no context hook. 7 slim tools total.
|
|
57
57
|
- **Throttled updates** — widget/stream updates coalesce to ~6/s; no per-event deep clones.
|
|
@@ -294,7 +294,7 @@ Background (default) + intercom — the run returns a runId immediately; you sta
|
|
|
294
294
|
| `await_subagent` | block until a run finishes (optional `timeoutMs`) |
|
|
295
295
|
| `reply_subagent` | answer a child's `ask_parent` question |
|
|
296
296
|
| `steer_subagent` | inject a steering message into a running child's session (queues as steer if mid-turn; lands at its next model boundary) |
|
|
297
|
-
| `resume_subagent` | revive a failed/aborted task in its original session (context + branch preserved); optional `model` swap
|
|
297
|
+
| `resume_subagent` | revive a failed/aborted task in its original session (context + branch preserved); optional `model` swap, `thinking` override (the task's stored level is clamped to what the target model accepts), custom `message`. Waits for the reopened session to go idle before prompting, so a task killed mid-turn can still be resumed |
|
|
298
298
|
| `subagent_cancel` | abort a running/queued run |
|
|
299
299
|
|
|
300
300
|
### Per-task fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -381,23 +381,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
381
381
|
name: "resume_subagent",
|
|
382
382
|
label: "Resume Subagent",
|
|
383
383
|
description:
|
|
384
|
-
"Revive a failed/aborted task in its original session (full context + worktree branch preserved). Optional `model` swaps provider (e.g. after a rate limit); optional `message` replaces the default 'recap and continue' prompt. Refuses tasks that never started — respawn those.",
|
|
384
|
+
"Revive a failed/aborted task in its original session (full context + worktree branch preserved). Optional `model` swaps provider (e.g. after a rate limit); optional `thinking` sets the effort — the stored level is clamped to what the target model accepts, so a resume never dies on an unsupported effort; optional `message` replaces the default 'recap and continue' prompt. Refuses tasks that never started — respawn those.",
|
|
385
385
|
parameters: ResumeParam,
|
|
386
386
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
387
|
-
const { runId, taskId, message, model } = params as {
|
|
387
|
+
const { runId, taskId, message, model, thinking } = params as {
|
|
388
388
|
runId: string;
|
|
389
389
|
taskId: string;
|
|
390
390
|
message?: string;
|
|
391
391
|
model?: string;
|
|
392
|
+
thinking?: string;
|
|
392
393
|
};
|
|
393
|
-
const res = manager.resumeTask(runId, taskId, ctx, { message, model });
|
|
394
|
+
const res = manager.resumeTask(runId, taskId, ctx, { message, model, thinking });
|
|
394
395
|
if (!res.ok) return { content: [{ type: "text", text: res.reason }], isError: true, details: {} };
|
|
395
396
|
const run = manager.getRun(runId);
|
|
396
397
|
return {
|
|
397
398
|
content: [
|
|
398
399
|
{
|
|
399
400
|
type: "text",
|
|
400
|
-
text: `Resumed ${runId}/${taskId} (${res.task.agent})${model ? ` on ${model}` : ""} from ${res.task.sessionFile}${res.task.branch ? `, branch ${res.task.branch}` : ""}
|
|
401
|
+
text: `Resumed ${runId}/${taskId} (${res.task.agent})${model ? ` on ${model}` : ""} from ${res.task.sessionFile}${res.task.branch ? `, branch ${res.task.branch}` : ""}.${res.note ? ` Adjusted ${res.note}.` : ""}\nNext: subagent_status("${runId}") to confirm it is running; completion will notify you.`,
|
|
401
402
|
},
|
|
402
403
|
],
|
|
403
404
|
details: { run: run ? cloneRun(run) : undefined },
|
package/src/manager.ts
CHANGED
|
@@ -68,8 +68,8 @@ const DEFAULT_RUNTIME_MS = 3_600_000;
|
|
|
68
68
|
const UNLIMITED_RUNTIME_MS = 21_600_000;
|
|
69
69
|
const PARENT_REPLY_TIMEOUT_MS = 600_000;
|
|
70
70
|
const PARKED_MSG_CAP = 24;
|
|
71
|
-
const READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
72
|
-
const WRITE_TOOLS = ["read", "grep", "find", "ls", "bash", "edit", "write"];
|
|
71
|
+
const READONLY_TOOLS = ["read", "grep", "find", "ls", "codemode"];
|
|
72
|
+
const WRITE_TOOLS = ["read", "grep", "find", "ls", "bash", "edit", "write", "codemode"];
|
|
73
73
|
const WRITE_CAPABLE = ["bash", "edit", "write"];
|
|
74
74
|
const SAFE_TASK_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
75
75
|
const WIDGET_THROTTLE_MS = 150;
|
|
@@ -77,6 +77,25 @@ const WIDGET_THROTTLE_MS = 150;
|
|
|
77
77
|
function newId(prefix: string): string {
|
|
78
78
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
type ChildExtensionFactories = ConstructorParameters<typeof DefaultResourceLoader>[0]["extensionFactories"];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Children run with `noExtensions`, so they get none of the configured extensions — codemode is the
|
|
85
|
+
* one exception, because it is how a child batches tool calls. `createCodemodeExtension()` is the
|
|
86
|
+
* supported factory (pi >= 1.0); hosts that do not export it simply give children no codemode.
|
|
87
|
+
*/
|
|
88
|
+
async function codemodeFactories(): Promise<ChildExtensionFactories> {
|
|
89
|
+
try {
|
|
90
|
+
const host = (await import("@earendil-works/pi-coding-agent")) as unknown as {
|
|
91
|
+
createCodemodeExtension?: () => unknown;
|
|
92
|
+
};
|
|
93
|
+
const factory = host.createCodemodeExtension?.();
|
|
94
|
+
return factory ? ([factory] as ChildExtensionFactories) : [];
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
80
99
|
function emptyUsage(): UsageStats {
|
|
81
100
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
82
101
|
}
|
|
@@ -143,6 +162,18 @@ function updateUsageFromMessage(task: TaskSnapshot, message: AssistantMessage):
|
|
|
143
162
|
export function cloneRun(run: RunSnapshot): RunSnapshot {
|
|
144
163
|
return JSON.parse(JSON.stringify(run)) as RunSnapshot;
|
|
145
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* A resumed task keeps its stored thinking level, clamped to what the target model accepts — a
|
|
167
|
+
* resume that swaps model must not fail on an effort the new model does not define.
|
|
168
|
+
*/
|
|
169
|
+
export function clampResumeThinking(
|
|
170
|
+
model: Model<Api> | undefined,
|
|
171
|
+
thinking: ThinkingLevel | undefined,
|
|
172
|
+
): ThinkingLevel | undefined {
|
|
173
|
+
if (!thinking || !model) return thinking;
|
|
174
|
+
return clampThinkingLevel(model, thinking) as ThinkingLevel;
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
export function resolveChildModel(ctx: ExtensionContext, explicit: string | undefined) {
|
|
147
178
|
if (!explicit?.trim()) return ctx.model;
|
|
148
179
|
const ref = explicit.trim();
|
|
@@ -900,6 +931,7 @@ export class SubagentManager {
|
|
|
900
931
|
cwd: childCwd,
|
|
901
932
|
agentDir: getAgentDir(),
|
|
902
933
|
noExtensions: true,
|
|
934
|
+
extensionFactories: await codemodeFactories(),
|
|
903
935
|
appendSystemPromptOverride: (base) => [
|
|
904
936
|
...base,
|
|
905
937
|
[prompt?.trim(), subagentInstruction].filter(Boolean).join("\n\n"),
|
|
@@ -978,6 +1010,9 @@ export class SubagentManager {
|
|
|
978
1010
|
});
|
|
979
1011
|
|
|
980
1012
|
const maxRuntimeMs = input.maxRuntimeMs ?? (this.autoLimit ? DEFAULT_RUNTIME_MS : UNLIMITED_RUNTIME_MS);
|
|
1013
|
+
// A resumed session can still be draining the turn that failed or timed out. Prompting while
|
|
1014
|
+
// its agent is mid-run is rejected outright ("already processing a prompt"), so wait first.
|
|
1015
|
+
if (resume) await child.waitForIdle();
|
|
981
1016
|
const promptPromise = child.prompt(resume?.message ?? task.task, { source: "extension" });
|
|
982
1017
|
const races: Promise<unknown>[] = [promptPromise, childFailurePromise, childEndPromise];
|
|
983
1018
|
if (maxRuntimeMs > 0) {
|
|
@@ -1380,8 +1415,8 @@ export class SubagentManager {
|
|
|
1380
1415
|
runId: string,
|
|
1381
1416
|
taskId: string,
|
|
1382
1417
|
ctx: ExtensionContext,
|
|
1383
|
-
opts: { message?: string; model?: string } = {},
|
|
1384
|
-
): { ok: true; task: TaskSnapshot } | { ok: false; reason: string } {
|
|
1418
|
+
opts: { message?: string; model?: string; thinking?: string } = {},
|
|
1419
|
+
): { ok: true; task: TaskSnapshot; note?: string } | { ok: false; reason: string } {
|
|
1385
1420
|
const run = this.runs.get(runId);
|
|
1386
1421
|
const task = run?.tasks.find((t) => t.id === taskId);
|
|
1387
1422
|
if (!run || !task) return { ok: false, reason: `Unknown ${runId}/${taskId}.` };
|
|
@@ -1399,6 +1434,19 @@ export class SubagentManager {
|
|
|
1399
1434
|
|
|
1400
1435
|
const tools = task.tools?.filter((t) => !(CHILD_TALK_TOOLS as readonly string[]).includes(t));
|
|
1401
1436
|
const write = tools?.some((t) => WRITE_CAPABLE.includes(t)) ?? false;
|
|
1437
|
+
// A resume may swap the model, so the level stored on the task can be one the new model
|
|
1438
|
+
// rejects (a mode-clamped xhigh onto a model that only takes low|high|max). Clamp it, or take
|
|
1439
|
+
// the caller's explicit level and clamp that.
|
|
1440
|
+
let resumeModel: Model<Api> | undefined;
|
|
1441
|
+
try {
|
|
1442
|
+
resumeModel = resolveChildModel(ctx, opts.model ?? task.model);
|
|
1443
|
+
} catch {}
|
|
1444
|
+
const requestedThinking = (opts.thinking ?? task.thinking) as ThinkingLevel | undefined;
|
|
1445
|
+
const thinking = clampResumeThinking(resumeModel, requestedThinking);
|
|
1446
|
+
const thinkingNote =
|
|
1447
|
+
requestedThinking && thinking !== requestedThinking
|
|
1448
|
+
? `thinking ${requestedThinking} → ${thinking} (${resumeModel?.provider}/${resumeModel?.id} does not accept ${requestedThinking})`
|
|
1449
|
+
: undefined;
|
|
1402
1450
|
const input: TaskInput = {
|
|
1403
1451
|
id: task.id,
|
|
1404
1452
|
agent: task.agent,
|
|
@@ -1407,7 +1455,7 @@ export class SubagentManager {
|
|
|
1407
1455
|
write,
|
|
1408
1456
|
tools: tools?.length ? tools : undefined,
|
|
1409
1457
|
model: opts.model ?? task.model,
|
|
1410
|
-
thinking:
|
|
1458
|
+
thinking: thinking as TaskInput["thinking"],
|
|
1411
1459
|
needs: task.needs,
|
|
1412
1460
|
};
|
|
1413
1461
|
const resume: ResumeInput = {
|
|
@@ -1422,6 +1470,7 @@ export class SubagentManager {
|
|
|
1422
1470
|
this.turnActivity = true;
|
|
1423
1471
|
Object.assign(task, {
|
|
1424
1472
|
status: "queued" as TaskStatus,
|
|
1473
|
+
thinking,
|
|
1425
1474
|
error: undefined,
|
|
1426
1475
|
endedAt: undefined,
|
|
1427
1476
|
finalText: undefined,
|
|
@@ -1454,7 +1503,7 @@ export class SubagentManager {
|
|
|
1454
1503
|
if (run.notifyPerTask) this.notifyTask(run, task, task.status as "completed" | "failed" | "aborted");
|
|
1455
1504
|
this.finishRunIfSettled(run, ctx);
|
|
1456
1505
|
});
|
|
1457
|
-
return { ok: true, task };
|
|
1506
|
+
return { ok: true, task, note: thinkingNote };
|
|
1458
1507
|
}
|
|
1459
1508
|
|
|
1460
1509
|
private finishRunIfSettled(run: RunSnapshot, ctx: ExtensionContext): void {
|
package/src/schemas.ts
CHANGED
|
@@ -89,6 +89,12 @@ export const ResumeParam = Type.Object({
|
|
|
89
89
|
"Model override for the resumed session (provider/model-id) — use when the original provider is rate-limited",
|
|
90
90
|
}),
|
|
91
91
|
),
|
|
92
|
+
thinking: Type.Optional(
|
|
93
|
+
StringEnum(THINKING_LEVELS, {
|
|
94
|
+
description:
|
|
95
|
+
"Thinking level for the resumed session. Default: the task's stored level, clamped to what the target model accepts",
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
92
98
|
});
|
|
93
99
|
export const SteerParam = Type.Object({
|
|
94
100
|
runId: Type.String(),
|