pi-better-harness 0.9.0 → 0.11.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/node_modules/pi-better-goal/README.md +1 -1
- package/node_modules/pi-better-goal/package.json +1 -1
- package/node_modules/pi-better-goal/src/index.ts +165 -57
- package/node_modules/pi-better-goal/src/types.ts +6 -4
- package/node_modules/pi-better-subagents/README.md +7 -0
- package/node_modules/pi-better-subagents/agent-inspection.ts +28 -0
- package/node_modules/pi-better-subagents/agents-catalog-tool.ts +2 -1
- package/node_modules/pi-better-subagents/batch.mjs +2 -2
- package/node_modules/pi-better-subagents/catalog-runtime.ts +52 -1
- package/node_modules/pi-better-subagents/docs/agent-catalog-operations.md +2 -0
- package/node_modules/pi-better-subagents/index.ts +19 -13
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/package.json +3 -3
|
@@ -13,7 +13,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
13
13
|
## Core Features
|
|
14
14
|
|
|
15
15
|
- `/goal` runtime for starting, pausing, resuming, completing, and clearing the current objective.
|
|
16
|
-
- `escape` pauses the active goal and
|
|
16
|
+
- `escape` pauses the active goal, and it stays paused while you talk to the agent. Say "go" (the agent then calls `goal_resume`), or use `/goal resume` or `alt+g`. `/goal pause` resumes only through `/goal resume` or `alt+g`. The status line shows `goal paused · say "go" or /goal resume`, and paused goals are never poked. On a macOS terminal without Option-as-Meta, `alt+g` types `©`; use `/goal resume` there.
|
|
17
17
|
- Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
|
|
18
18
|
- A compact goal widget that does not replace Pi's footer.
|
|
19
19
|
- Background activity tracking for subagents and other registered providers.
|
|
@@ -70,6 +70,34 @@ const MAX_NO_PROGRESS_RETRIES = parseRetryLimit(
|
|
|
70
70
|
* steering after the whole tool batch, and callback batches are follow-ups
|
|
71
71
|
* that wait for the entire run), so the goal harvests them explicitly.
|
|
72
72
|
*/
|
|
73
|
+
/** Model-callable resume, active only while an escape-paused goal waits. */
|
|
74
|
+
export const GOAL_RESUME_TOOL = "goal_resume";
|
|
75
|
+
/** Hotkey that resumes any paused goal, like `/goal resume`. */
|
|
76
|
+
export const GOAL_RESUME_SHORTCUT = "alt+g";
|
|
77
|
+
|
|
78
|
+
/** When the agent may call `goal_resume`; shared by the tool description and the paused prompt. */
|
|
79
|
+
const GOAL_RESUME_RULE =
|
|
80
|
+
"Call goal_resume only when the user's latest message clearly says to proceed (for example \"go\", \"continue\", \"ok do it\", \"approved, proceed\"), " +
|
|
81
|
+
"or answers a decision you explicitly asked for in your previous message with a choice that means proceed. Never call it for questions, \"why...\", \"what about...\", \"let me think\", or discussion.";
|
|
82
|
+
|
|
83
|
+
/** True when the agent may resume this goal with `goal_resume`. */
|
|
84
|
+
export function agentResumable(goal: GoalSnapshot | null): boolean {
|
|
85
|
+
return goal?.status === "paused" && goal.pauseReason === "interrupt";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Footer status for a paused goal, telling the user how to resume it. */
|
|
89
|
+
export function pausedGoalStatus(goal: GoalSnapshot): string {
|
|
90
|
+
return agentResumable(goal) ? 'goal paused · say "go" or /goal resume' : "goal paused · /goal resume";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pausedGoalPrompt(goal: GoalSnapshot): string {
|
|
94
|
+
return [
|
|
95
|
+
`Pi Better Goal is paused because the user pressed escape. Goal: ${goal.objective}`,
|
|
96
|
+
"Treat the user's messages as ordinary conversation: answer them, but do not continue the goal's work until it is resumed.",
|
|
97
|
+
GOAL_RESUME_RULE,
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
const BLOCKING_QUESTION_TOOLS: ReadonlySet<string> = new Set(["ask_user_question"]);
|
|
74
102
|
|
|
75
103
|
const GOAL_ACTIONS: readonly AutocompleteItem[] = [
|
|
@@ -195,6 +223,11 @@ function formatGoal(
|
|
|
195
223
|
`Elapsed time: ${timing.elapsedSeconds}s`,
|
|
196
224
|
`Observable progress: ${stall?.state ?? "unknown"}`,
|
|
197
225
|
continuationStatus,
|
|
226
|
+
...(goal.status === "paused"
|
|
227
|
+
? [agentResumable(goal)
|
|
228
|
+
? 'Resume: say "go" (the agent calls goal_resume), /goal resume, or alt+g'
|
|
229
|
+
: "Resume: /goal resume or alt+g"]
|
|
230
|
+
: []),
|
|
198
231
|
].join("\n");
|
|
199
232
|
}
|
|
200
233
|
|
|
@@ -304,6 +337,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
304
337
|
backgroundDrainTracker = null;
|
|
305
338
|
clearIdleContinuation();
|
|
306
339
|
syncPollingState();
|
|
340
|
+
syncResumeTool(goal);
|
|
341
|
+
applyStatus(ctx);
|
|
307
342
|
// Force a full redraw when the dock height changes (absent ↔ visible clock).
|
|
308
343
|
refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
|
|
309
344
|
};
|
|
@@ -317,14 +352,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
317
352
|
backgroundDrainTracker = null;
|
|
318
353
|
clearIdleContinuation();
|
|
319
354
|
syncPollingState();
|
|
355
|
+
syncResumeTool(null);
|
|
356
|
+
applyStatus(ctx);
|
|
320
357
|
refreshGoalWidget?.(wasVisible);
|
|
321
358
|
};
|
|
322
359
|
|
|
323
360
|
/**
|
|
324
|
-
* An interrupt (escape, or anything else that aborts the running turn)
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
361
|
+
* An interrupt (escape, or anything else that aborts the running turn) pauses
|
|
362
|
+
* the goal. It stays paused while the user talks: messages are ordinary
|
|
363
|
+
* conversation. It resumes through `/goal resume`, the hotkey, or the agent's
|
|
364
|
+
* `goal_resume` once the user clearly says to proceed.
|
|
328
365
|
*/
|
|
329
366
|
const pauseGoalOnInterrupt = (ctx: ExtensionContext): void => {
|
|
330
367
|
const goal = getGoal(ctx);
|
|
@@ -332,7 +369,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
332
369
|
return;
|
|
333
370
|
}
|
|
334
371
|
setGoal(goalWithStatus(goal, "paused", undefined, "interrupt"), ctx, "runtime");
|
|
335
|
-
notifyGoal(ctx,
|
|
372
|
+
notifyGoal(ctx, 'Goal paused. Say "go" to resume it, or use /goal resume.');
|
|
336
373
|
};
|
|
337
374
|
|
|
338
375
|
/** Why a paused goal cannot become active again, or null when it can. */
|
|
@@ -346,6 +383,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
346
383
|
return null;
|
|
347
384
|
};
|
|
348
385
|
|
|
386
|
+
/**
|
|
387
|
+
* The one resume path shared by `/goal resume`, the hotkey, and `goal_resume`:
|
|
388
|
+
* reactivate the paused goal and queue its continuation.
|
|
389
|
+
*/
|
|
390
|
+
const resumeGoal = (
|
|
391
|
+
ctx: ExtensionContext,
|
|
392
|
+
source: GoalEntrySource,
|
|
393
|
+
): { ok: true; goal: GoalSnapshot } | { ok: false; message: string } => {
|
|
394
|
+
const current = getGoal(ctx);
|
|
395
|
+
if (!current || current.status !== "paused") {
|
|
396
|
+
return { ok: false, message: "Only paused goals can be resumed." };
|
|
397
|
+
}
|
|
398
|
+
const blocker = resumeBlocker(current);
|
|
399
|
+
if (blocker) {
|
|
400
|
+
return { ok: false, message: blocker };
|
|
401
|
+
}
|
|
402
|
+
const goal = goalWithStatus(current, "active");
|
|
403
|
+
setGoal(goal, ctx, source);
|
|
404
|
+
queueGoalContinuation(goal, ctx);
|
|
405
|
+
return { ok: true, goal };
|
|
406
|
+
};
|
|
407
|
+
|
|
349
408
|
const boundCommandReady = (goal: GoalSnapshot, ctx: ExtensionContext): boolean => {
|
|
350
409
|
if (!goal.command || commandAvailable(pi, goal.command)) return true;
|
|
351
410
|
setGoal(goalWithStatus(goal, "paused"), ctx, "runtime");
|
|
@@ -503,36 +562,62 @@ export default function (pi: ExtensionAPI): void {
|
|
|
503
562
|
clearIdleContinuation();
|
|
504
563
|
}
|
|
505
564
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
565
|
+
applyStatus(ctx);
|
|
566
|
+
|
|
567
|
+
return snapshot;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/** Footer status from the goal state and the latest activity snapshot. */
|
|
571
|
+
const statusText = (ctx: ExtensionContext, snapshot: ActivitySnapshot | null): string | undefined => {
|
|
572
|
+
const goal = getGoal(ctx);
|
|
573
|
+
let waitingOnAnswer = 0;
|
|
574
|
+
if (snapshot) {
|
|
514
575
|
for (const activeAtStart of pendingQuestions.values()) {
|
|
515
576
|
waitingOnAnswer += finishedSinceQuestion(activeAtStart, snapshot).length;
|
|
516
577
|
}
|
|
517
|
-
const status = waitingOnAnswer > 0
|
|
518
|
-
? `${waitingOnAnswer} background done; waiting on your answer`
|
|
519
|
-
: snapshot.backgroundRunning
|
|
520
|
-
? `bg ${snapshot.activeBackgroundCount}${snapshot.unhealthyBackgroundCount ? `, ${snapshot.unhealthyBackgroundCount} unhealthy` : ""}`
|
|
521
|
-
: continuation?.blocked
|
|
522
|
-
? "waiting: no progress"
|
|
523
|
-
: goalStall?.state === "stalled"
|
|
524
|
-
? "goal stalled"
|
|
525
|
-
: undefined;
|
|
526
|
-
try {
|
|
527
|
-
ctx.ui.setStatus(EXTENSION_NAME, status);
|
|
528
|
-
} catch {
|
|
529
|
-
// UI status is best-effort only.
|
|
530
|
-
}
|
|
531
578
|
}
|
|
532
|
-
|
|
533
|
-
|
|
579
|
+
if (waitingOnAnswer > 0) return `${waitingOnAnswer} background done; waiting on your answer`;
|
|
580
|
+
const background = snapshot?.backgroundRunning
|
|
581
|
+
? `bg ${snapshot.activeBackgroundCount}${snapshot.unhealthyBackgroundCount ? `, ${snapshot.unhealthyBackgroundCount} unhealthy` : ""}`
|
|
582
|
+
: undefined;
|
|
583
|
+
if (goal?.status === "paused") return background ? `${pausedGoalStatus(goal)} · ${background}` : pausedGoalStatus(goal);
|
|
584
|
+
if (background) return background;
|
|
585
|
+
const continuation = goal ? currentContinuationState(ctx, goal.goalId) : null;
|
|
586
|
+
if (continuation?.blocked) return "waiting: no progress";
|
|
587
|
+
const goalStall = observeGoalStall(goal, continuation, {
|
|
588
|
+
foregroundRunning,
|
|
589
|
+
backgroundRunning: snapshot?.backgroundRunning ?? false,
|
|
590
|
+
});
|
|
591
|
+
return goalStall?.state === "stalled" ? "goal stalled" : undefined;
|
|
534
592
|
};
|
|
535
593
|
|
|
594
|
+
function applyStatus(ctx: ExtensionContext): void {
|
|
595
|
+
if (!ctx.hasUI) return;
|
|
596
|
+
try {
|
|
597
|
+
ctx.ui.setStatus(EXTENSION_NAME, statusText(ctx, latestSnapshot));
|
|
598
|
+
} catch {
|
|
599
|
+
// UI status is best-effort only.
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* `goal_resume` is in the model's tool list only while an escape-paused goal
|
|
605
|
+
* waits. Pi activates newly registered tools by default, so this also removes
|
|
606
|
+
* it at session start. Only this one tool is toggled; other extensions' tool
|
|
607
|
+
* choices are left as they are.
|
|
608
|
+
*/
|
|
609
|
+
function syncResumeTool(goal: GoalSnapshot | null): void {
|
|
610
|
+
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
611
|
+
try {
|
|
612
|
+
const active = pi.getActiveTools();
|
|
613
|
+
const wanted = agentResumable(goal);
|
|
614
|
+
if (active.includes(GOAL_RESUME_TOOL) === wanted) return;
|
|
615
|
+
pi.setActiveTools(wanted ? [...active, GOAL_RESUME_TOOL] : active.filter((name) => name !== GOAL_RESUME_TOOL));
|
|
616
|
+
} catch {
|
|
617
|
+
// Tool activation is unavailable before Pi binds the session; the tool also refuses on its own.
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
536
621
|
const collectIfPossible = (): void => {
|
|
537
622
|
const ctx = currentCtx;
|
|
538
623
|
if (!ctx) {
|
|
@@ -675,18 +760,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
675
760
|
}
|
|
676
761
|
|
|
677
762
|
if (trimmed === "resume") {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
const blocker = resumeBlocker(current);
|
|
683
|
-
if (blocker) {
|
|
684
|
-
notifyGoal(ctx, blocker, "error");
|
|
763
|
+
const result = resumeGoal(ctx, "command");
|
|
764
|
+
if (!result.ok) {
|
|
765
|
+
notifyGoal(ctx, result.message, current?.status === "paused" ? "error" : "warning");
|
|
685
766
|
return;
|
|
686
767
|
}
|
|
687
|
-
const goal = goalWithStatus(current, "active");
|
|
688
|
-
setGoal(goal, ctx, "command");
|
|
689
|
-
queueGoalContinuation(goal, ctx);
|
|
690
768
|
notifyGoal(ctx, "Goal resumed.");
|
|
691
769
|
return;
|
|
692
770
|
}
|
|
@@ -789,6 +867,44 @@ export default function (pi: ExtensionAPI): void {
|
|
|
789
867
|
},
|
|
790
868
|
});
|
|
791
869
|
|
|
870
|
+
pi.registerTool({
|
|
871
|
+
name: GOAL_RESUME_TOOL,
|
|
872
|
+
label: "Resume Goal",
|
|
873
|
+
description:
|
|
874
|
+
"Resume the goal the user paused with escape, exactly like /goal resume. " + GOAL_RESUME_RULE,
|
|
875
|
+
promptSnippet: "Resume the escape-paused goal, only on the user's clear go-ahead.",
|
|
876
|
+
parameters: Type.Object({
|
|
877
|
+
reason: Type.Optional(Type.String({ description: "Short quote or summary of the user's go-ahead." })),
|
|
878
|
+
}),
|
|
879
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
880
|
+
const reason = (params as { reason?: string }).reason;
|
|
881
|
+
const current = getGoal(ctx);
|
|
882
|
+
if (!agentResumable(current)) {
|
|
883
|
+
const text = current?.status === "paused"
|
|
884
|
+
? "This goal was paused with /goal pause or by the runtime. Only the user can resume it, with /goal resume."
|
|
885
|
+
: "No goal is paused, so there is nothing to resume.";
|
|
886
|
+
return { content: [{ type: "text", text }], details: { ok: false, goal: current } };
|
|
887
|
+
}
|
|
888
|
+
const result = resumeGoal(ctx, "tool");
|
|
889
|
+
if (!result.ok) {
|
|
890
|
+
return { content: [{ type: "text", text: `Goal stays paused. ${result.message}` }], details: { ok: false, goal: getGoal(ctx) } };
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
content: [{ type: "text", text: "Goal resumed. Its continuation runs after this turn." }],
|
|
894
|
+
details: { ok: true, goal: result.goal, ...(reason ? { reason } : {}) },
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
pi.registerShortcut?.(GOAL_RESUME_SHORTCUT, {
|
|
900
|
+
description: "Resume the paused goal",
|
|
901
|
+
handler: (ctx) => {
|
|
902
|
+
currentCtx = ctx;
|
|
903
|
+
const result = resumeGoal(ctx, "command");
|
|
904
|
+
notifyGoal(ctx, result.ok ? "Goal resumed." : result.message, result.ok ? "info" : "warning");
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
|
|
792
908
|
pi.registerCommand("better-activity", {
|
|
793
909
|
description: "Show foreground/background activity known to pi-better-goal",
|
|
794
910
|
handler: async (_args, ctx) => {
|
|
@@ -849,6 +965,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
849
965
|
notifyGoal(ctx, "Goal paused: invoke its skill directly before resuming.", "error");
|
|
850
966
|
}
|
|
851
967
|
pi.events.emit(EVENT_READY, { version: EXTENSION_VERSION });
|
|
968
|
+
syncResumeTool(getGoal(ctx));
|
|
852
969
|
installGoalWidget(ctx);
|
|
853
970
|
latestSnapshot = await publishSnapshot(ctx);
|
|
854
971
|
syncPollingState();
|
|
@@ -874,23 +991,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
874
991
|
}
|
|
875
992
|
}
|
|
876
993
|
}
|
|
994
|
+
// A paused goal stays paused while the user talks: the message is ordinary
|
|
995
|
+
// conversation. Only /goal resume, the hotkey, or goal_resume resume it.
|
|
877
996
|
const goal = getGoal(ctx);
|
|
878
|
-
if (goal?.status === "paused" && goal.pauseReason === "interrupt") {
|
|
879
|
-
// Conversational input after an interrupt means the user has taken the
|
|
880
|
-
// wheel, not stopped the goal: reactivate it so continuation resumes once
|
|
881
|
-
// this exchange settles. Pi's built-in commands (/settings, /model, ...)
|
|
882
|
-
// and extension commands never reach this handler.
|
|
883
|
-
const blocker = resumeBlocker(goal);
|
|
884
|
-
if (blocker) {
|
|
885
|
-
notifyGoal(ctx, `Goal stays paused. ${blocker}`, "warning");
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
const resumed = goalWithStatus(goal, "active");
|
|
889
|
-
setGoal(resumed, ctx, "runtime");
|
|
890
|
-
resetContinuationState(resumed);
|
|
891
|
-
notifyGoal(ctx, "Goal resumed; it continues after this exchange.");
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
997
|
if (goal?.status === "active") {
|
|
895
998
|
resetContinuationState(goal);
|
|
896
999
|
}
|
|
@@ -901,8 +1004,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
901
1004
|
const goal = currentGoalSnapshot(ctx);
|
|
902
1005
|
const owner = getWorkflow(ctx);
|
|
903
1006
|
const snapshot = await publishSnapshot(ctx);
|
|
1007
|
+
const pausedInstruction = agentResumable(goal) ? pausedGoalPrompt(goal!) : "";
|
|
904
1008
|
if (!isPokeable(goal) && !owner) {
|
|
905
|
-
return;
|
|
1009
|
+
return pausedInstruction ? { systemPrompt: `${event.systemPrompt}\n\n${pausedInstruction}` } : undefined;
|
|
906
1010
|
}
|
|
907
1011
|
|
|
908
1012
|
const questionInstruction = snapshot.backgroundRunning
|
|
@@ -923,7 +1027,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
923
1027
|
return { systemPrompt: `${event.systemPrompt}\n\nWorkflow ${owner.name} is unavailable. Stop work and ask the user to restore or reinvoke the skill.` };
|
|
924
1028
|
}
|
|
925
1029
|
return {
|
|
926
|
-
systemPrompt: `${event.systemPrompt}\n\
|
|
1030
|
+
systemPrompt: `${event.systemPrompt}\n\n` +
|
|
1031
|
+
(pausedInstruction
|
|
1032
|
+
? `${pausedInstruction}\nWhile the goal is paused, this overrides the workflow instructions below: do not advance the workflow until the goal is resumed.\n\n`
|
|
1033
|
+
: "") +
|
|
1034
|
+
`Active workflow: ${owner.name} (${owner.path}). Its task plan owns planning and the parent is a coordinator, not a product-code implementer. Follow the workflow instructions below, including on resumed turns:\n\n${instructions}` +
|
|
927
1035
|
(isPokeable(goal) ? `\n\nActive objective: ${goal.objective}. Complete it only after the workflow completion audit.` : "") +
|
|
928
1036
|
(questionInstruction ? `\n\n${questionInstruction.trim()}` : ""),
|
|
929
1037
|
};
|
|
@@ -66,10 +66,12 @@ export interface ActivitySnapshot {
|
|
|
66
66
|
export type GoalStatus = "active" | "paused" | "budgetLimited" | "complete";
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Why a paused goal is paused. `interrupt` marks a
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* Why a paused goal is paused. `interrupt` marks a pause from escape (or any
|
|
70
|
+
* other abort of the running turn). Every pause is sticky: user messages never
|
|
71
|
+
* resume a goal on their own. An `interrupt` pause may be resumed by the agent
|
|
72
|
+
* with `goal_resume` when the user clearly says to proceed. A paused goal
|
|
73
|
+
* without a reason (`/goal pause`, an unavailable command or workflow) resumes
|
|
74
|
+
* only through `/goal resume` or the resume hotkey.
|
|
73
75
|
*/
|
|
74
76
|
export type GoalPauseReason = "interrupt";
|
|
75
77
|
|
|
@@ -43,6 +43,13 @@ uses `agents_catalog` to discover current role descriptions and delegates every
|
|
|
43
43
|
nontrivial role-owned task, while the foreground coordinates, integrates, and
|
|
44
44
|
verifies. See [usage notes](docs/usage.md#delegation-mode).
|
|
45
45
|
|
|
46
|
+
`agents_catalog` shows each role's and named agent's default model and effort,
|
|
47
|
+
such as `default openai/gpt-6-sol@high`. To launch on that default, omit
|
|
48
|
+
`model` and `thinking` on a role or agent spawn; name one only for a stated
|
|
49
|
+
reason. When a launch's model or effort differs from the default, its launch
|
|
50
|
+
line says so, for example
|
|
51
|
+
`model openai/gpt-6-astra@high (role default openai/gpt-6-sol@high)`.
|
|
52
|
+
|
|
46
53
|
## When To Use
|
|
47
54
|
|
|
48
55
|
Use this package for independent coding, review, research, or verification work that can finish later. Do not use it for steps that need immediate foreground interaction or user clarification.
|
|
@@ -80,6 +80,23 @@ export interface OperationField {
|
|
|
80
80
|
inherited: boolean;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export interface DefinitionDefaults {
|
|
84
|
+
model: string | null;
|
|
85
|
+
effort: string | null;
|
|
86
|
+
/** Compact `model@effort` form, as shown in the catalog list and launch notes. */
|
|
87
|
+
label: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `model@effort`, `model`, or `effort <level>` when only an effort is set. Undefined when neither is set. */
|
|
91
|
+
export function formatModelEffort(model: string | null | undefined, effort: string | null | undefined): string | undefined {
|
|
92
|
+
const m = typeof model === "string" && model.trim() !== "" ? model : undefined;
|
|
93
|
+
const e = typeof effort === "string" && effort.trim() !== "" ? effort : undefined;
|
|
94
|
+
if (m && e) return `${m}@${e}`;
|
|
95
|
+
if (m) return m;
|
|
96
|
+
if (e) return `effort ${e}`;
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
83
100
|
export interface OperationCapabilities {
|
|
84
101
|
grantedByCatalog: false;
|
|
85
102
|
sameAsLegacySpawn: true;
|
|
@@ -113,6 +130,8 @@ export interface OperationView {
|
|
|
113
130
|
instructionMode?: string;
|
|
114
131
|
instructions?: string;
|
|
115
132
|
fields?: { model: OperationField; effort: OperationField; tier: OperationField };
|
|
133
|
+
/** The definition's own default model and effort (role default, or the agent's override or inherited value). Null when neither is set. */
|
|
134
|
+
defaults: DefinitionDefaults | null;
|
|
116
135
|
requestedModel: string | null;
|
|
117
136
|
requestedEffort: string | null;
|
|
118
137
|
actualModel: string | null;
|
|
@@ -311,6 +330,7 @@ export function presentCatalogEntry(snapshot: CatalogSnapshot, id: string, enric
|
|
|
311
330
|
instructionMode: inspection.instructionMode,
|
|
312
331
|
instructions: inspection.instructions,
|
|
313
332
|
fields,
|
|
333
|
+
defaults: definitionDefaults(fields),
|
|
314
334
|
requestedModel: enrichment?.requestedModel ?? fields?.model.value ?? null,
|
|
315
335
|
requestedEffort: enrichment?.requestedEffort ?? (fields?.effort.value ?? null),
|
|
316
336
|
actualModel: enrichment?.actualModel ?? null,
|
|
@@ -350,6 +370,7 @@ export function renderOperationView(view: OperationView): string {
|
|
|
350
370
|
`definitionValid=${yesNo(view.definitionValid)}`,
|
|
351
371
|
`catalogLaunchable=${yesNo(view.catalogLaunchable)}`,
|
|
352
372
|
`launchable=${view.launchable === null ? "unknown" : yesNo(view.launchable)}`,
|
|
373
|
+
...(view.defaults ? [`default ${view.defaults.label}`] : []),
|
|
353
374
|
].join(" ");
|
|
354
375
|
const lines = [
|
|
355
376
|
header,
|
|
@@ -397,6 +418,13 @@ export function renderOperationView(view: OperationView): string {
|
|
|
397
418
|
return lines.join("\n");
|
|
398
419
|
}
|
|
399
420
|
|
|
421
|
+
function definitionDefaults(fields: OperationView["fields"]): DefinitionDefaults | null {
|
|
422
|
+
const model = fields?.model.value ?? null;
|
|
423
|
+
const effort = fields?.effort.value ?? null;
|
|
424
|
+
const label = formatModelEffort(model, effort);
|
|
425
|
+
return label ? { model, effort, label } : null;
|
|
426
|
+
}
|
|
427
|
+
|
|
400
428
|
function field(value: { value: string | null; source: OperationField["source"]; explicit: boolean }): OperationField {
|
|
401
429
|
return {
|
|
402
430
|
value: value.value,
|
|
@@ -37,7 +37,7 @@ export function agentsCatalogTool(Type: TypeModule, deps: DiscoveryDeps = {}) {
|
|
|
37
37
|
return {
|
|
38
38
|
name: "agents_catalog" as const,
|
|
39
39
|
label: "Agents catalog",
|
|
40
|
-
description: "List or inspect role and named-agent definitions, including inheritance, diagnostics, restrictions, and whether launchability is actually known. This tool does not create, import, or edit definitions.",
|
|
40
|
+
description: "List or inspect role and named-agent definitions, including inheritance, default model and effort, diagnostics, restrictions, and whether launchability is actually known. This tool does not create, import, or edit definitions.",
|
|
41
41
|
promptSnippet: "List and inspect catalog roles and named agents, including inheritance and whether launchability is known.",
|
|
42
42
|
promptGuidelines: [
|
|
43
43
|
"Use agents_catalog to discover roles and named agents before subagent_spawn. It is read-only.",
|
|
@@ -46,6 +46,7 @@ export function agentsCatalogTool(Type: TypeModule, deps: DiscoveryDeps = {}) {
|
|
|
46
46
|
"Ask the user to run /agents create or /agents import-codex for writes. Those commands confirm role, scope, and import replacement.",
|
|
47
47
|
"Launch with the existing subagent_spawn or batch tool. Pass one agent or one role, not both. Two roles for one run need the user to choose one or split the work.",
|
|
48
48
|
"Put an authoritative model or effort on the structured invocation. Do not rely on copying a model name into the child prompt. Explicit invocation and workflow choices override definition defaults.",
|
|
49
|
+
"Each entry shows its default model@effort when set. Omit model and thinking on the spawn to use it; name one only for a stated reason.",
|
|
49
50
|
],
|
|
50
51
|
parameters: Type.Object({
|
|
51
52
|
action: Type.String({ description: "list or inspect. No other action is accepted." }),
|
|
@@ -215,8 +215,8 @@ export function formatBatchLaunchResponse({ batchId, batchName, launched, skippe
|
|
|
215
215
|
lines.push(
|
|
216
216
|
`Batch ${label} launched ${launched.length} subagent(s):`,
|
|
217
217
|
);
|
|
218
|
-
for (const { name, id } of launched) {
|
|
219
|
-
lines.push(`• ${name} → ${id}`);
|
|
218
|
+
for (const { name, id, modelNote } of launched) {
|
|
219
|
+
lines.push(`• ${name} → ${id}${modelNote ? ` · ${modelNote}` : ""}`);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `spawnSubagentRun`. This module does not grant tools, sandbox modes, or
|
|
5
5
|
* extensions, and it does not read task prose for model choices.
|
|
6
6
|
*/
|
|
7
|
-
import { describeDefaultLaunchCapabilities, LEGACY_CAPABILITY_CONTROLS, type LaunchEnricher, type LaunchEnrichment } from "./agent-inspection.ts";
|
|
7
|
+
import { describeDefaultLaunchCapabilities, formatModelEffort, LEGACY_CAPABILITY_CONTROLS, type LaunchEnricher, type LaunchEnrichment } from "./agent-inspection.ts";
|
|
8
8
|
import { resolveSelection, type EffectiveDefinition } from "./catalog-resolver.ts";
|
|
9
9
|
import { defaultUserRoot, loadCatalog, type CatalogSnapshot } from "./catalog-store.ts";
|
|
10
10
|
import { loadConfig, type SubagentConfig } from "./config.ts";
|
|
@@ -252,6 +252,57 @@ export async function clarifyCatalogRequest(
|
|
|
252
252
|
return { status: "resolved", jobs: ordered.map(stripBookkeeping) };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Launch-line note for a catalog run whose effective model or effort differs
|
|
257
|
+
* from the role or agent default. The wording names the cause, so a fallback
|
|
258
|
+
* or a capped effort is not mistaken for a caller override:
|
|
259
|
+
* - override: `model openai/gpt-6-astra@high (role default openai/gpt-6-sol@high)`
|
|
260
|
+
* - fallback: `model xai/grok-4.7@high (role default openai/gpt-6-sol@high unavailable; foreground fallback)`
|
|
261
|
+
* - capped effort: `model openai/gpt-6-sol@medium (role default openai/gpt-6-sol@high; effort capped at medium by the model)`
|
|
262
|
+
* Undefined for a non-catalog run, a definition with no default, or a launch
|
|
263
|
+
* that matches the default. Only the fields the definition sets are compared.
|
|
264
|
+
*/
|
|
265
|
+
export function catalogDefaultNote(
|
|
266
|
+
record: Pick<CatalogRunRecord, "kind" | "effective" | "modelSelection" | "effortSelection"> | undefined,
|
|
267
|
+
model: string | undefined,
|
|
268
|
+
thinking: string | undefined,
|
|
269
|
+
): string | undefined {
|
|
270
|
+
if (!record) return undefined;
|
|
271
|
+
const defaultModel = record.effective.model.value;
|
|
272
|
+
const defaultEffort = record.effective.effort.value;
|
|
273
|
+
const defaultLabel = formatModelEffort(defaultModel, defaultEffort);
|
|
274
|
+
if (!defaultLabel) return undefined;
|
|
275
|
+
const modelSource = record.modelSelection?.source;
|
|
276
|
+
const modelFallback = defaultModel !== null && modelSource !== undefined && FALLBACK_SOURCES.has(modelSource);
|
|
277
|
+
const modelDiffers = defaultModel !== null && (modelFallback || !sameModel(defaultModel, model, modelSource));
|
|
278
|
+
const effortCapped = defaultEffort !== null && record.effortSelection?.adjusted === true && (thinking ?? null) !== defaultEffort;
|
|
279
|
+
const effortDiffers = defaultEffort !== null && (thinking ?? null) !== defaultEffort;
|
|
280
|
+
if (!modelDiffers && !effortDiffers) return undefined;
|
|
281
|
+
const actual = formatModelEffort(model ?? "Pi default", thinking) ?? "Pi default";
|
|
282
|
+
const causes = [
|
|
283
|
+
modelFallback ? `${record.kind} default ${defaultLabel} unavailable; ${FALLBACK_LABELS[modelSource!] ?? modelSource} fallback` : `${record.kind} default ${defaultLabel}`,
|
|
284
|
+
...(effortCapped ? [`effort capped at ${thinking ?? "the model default"} by the model`] : []),
|
|
285
|
+
];
|
|
286
|
+
return `model ${actual} (${causes.join("; ")})`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Model sources that mean the default could not be used, not that the caller chose another model. */
|
|
290
|
+
const FALLBACK_SOURCES: ReadonlySet<string> = new Set(["tier-candidate", "foreground", "configured-default"]);
|
|
291
|
+
const FALLBACK_LABELS: Record<string, string> = {
|
|
292
|
+
"tier-candidate": "same-tier",
|
|
293
|
+
foreground: "foreground",
|
|
294
|
+
"configured-default": "configured-default",
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
function sameModel(defaultModel: string, actual: string | undefined, source: string | undefined): boolean {
|
|
298
|
+
// The resolver launched the definition's own preference: same model, however it was spelled.
|
|
299
|
+
if (source === "role-default" || source === "agent-override") return true;
|
|
300
|
+
if (actual === undefined) return false;
|
|
301
|
+
if (defaultModel === actual) return true;
|
|
302
|
+
// A providerless default names the id; the resolved provider/id (the id may itself contain "/") still matches.
|
|
303
|
+
return !defaultModel.includes("/") ? actual.endsWith(`/${defaultModel}`) : false;
|
|
304
|
+
}
|
|
305
|
+
|
|
255
306
|
export async function prepareCatalogJob(snapshot: CatalogSnapshot, job: CatalogJobFields, host: CatalogHost): Promise<PreparedCatalogJob> {
|
|
256
307
|
const agent = agentIdOf(job);
|
|
257
308
|
const roles = roleIdsOf(job);
|
|
@@ -52,6 +52,8 @@ Codex applies the agent file's model and `model_reasoning_effort` ahead of the c
|
|
|
52
52
|
|
|
53
53
|
`agents_catalog` accepts `list` and `inspect` only. It uses the same inspection view as `/agents show`. It has no write path. Launch remains `subagent_spawn` or the batch tool, with at most one `agent` or `role` selector. Those selectors are lifecycle's wiring, not this tool.
|
|
54
54
|
|
|
55
|
+
Each list line and the inspect header end with the definition's default model and effort when one is set, such as `default openai/gpt-6-sol@high`: a role's own default, or a named agent's override or inherited role default. The structured view carries the same value as `defaults: { model, effort, label }`, or `null` when neither is set. A spawn that omits `model` and `thinking` uses it. When a catalog launch's model or effort differs from it, the launch line (and each batch job line) adds a note such as `model openai/gpt-6-astra@high (role default openai/gpt-6-sol@high)`. Only the fields the definition sets are compared. The note names the cause: a fallback from an unavailable default reads `(role default openai/gpt-6-sol@high unavailable; foreground fallback)` (or `same-tier` / `configured-default`), and an effort the model cannot run adds `effort capped at <level> by the model`.
|
|
56
|
+
|
|
55
57
|
## Lifecycle attachment
|
|
56
58
|
|
|
57
59
|
```ts
|
|
@@ -151,6 +151,7 @@ import {
|
|
|
151
151
|
loadLaunchSnapshot,
|
|
152
152
|
noteCatalogHost,
|
|
153
153
|
prepareCatalogJob,
|
|
154
|
+
catalogDefaultNote,
|
|
154
155
|
tiersForLaunch,
|
|
155
156
|
type CatalogHost,
|
|
156
157
|
type CatalogJobFields,
|
|
@@ -175,6 +176,7 @@ const projectConfigDirName = typeof (PiCodingAgent as { CONFIG_DIR_NAME?: unknow
|
|
|
175
176
|
: ".pi";
|
|
176
177
|
|
|
177
178
|
const CATALOG_GUIDELINES = [
|
|
179
|
+
"With an agent or role, omit model and thinking to launch on its default model and effort, which agents_catalog shows. Name a model or effort only for a stated reason, such as a task or workflow instruction, and never copy one from another role's runs.",
|
|
178
180
|
"When a task, workflow, or skill instruction names a model or effort, translate that authoritative choice into the structured model and thinking arguments before spawning. The runtime does not parse prose, quoted model names, or comparisons, and copying a model into the child prompt does not change the launch.",
|
|
179
181
|
"Optional agent, role, and alias select a catalog definition. Pass one agent id or one role id. Pass an array of role ids when one run was given more than one role: that call asks to choose one or split, and without a UI choice it returns clarification-needed and starts no child. Naming both an agent and a role does the same. One job's choice does not change another job. A named agent displays its defined name. A direct role displays the label allocated from the local run registry. Calls without agent or role keep the existing name and model chain.",
|
|
180
182
|
"Catalog model and effort are resolved before the child starts. An unavailable explicit model or unsupported explicit effort does not launch and does not fall back. The catalog grants no tools, sandbox modes, extensions, or permissions.",
|
|
@@ -1551,6 +1553,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1551
1553
|
runtime: string;
|
|
1552
1554
|
warn: string;
|
|
1553
1555
|
sandboxDir?: string;
|
|
1556
|
+
/** Set only when a catalog run's model or effort differs from its role or agent default. */
|
|
1557
|
+
modelNote?: string;
|
|
1554
1558
|
}> {
|
|
1555
1559
|
assertThinkingLevel(p.thinking);
|
|
1556
1560
|
const permissionPlan = resolveSubagentPermissions(pi, p.sandbox);
|
|
@@ -1766,7 +1770,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1766
1770
|
`${resolution.unmapped.length > 1 ? "these tools" : "this tool"} will NOT exist in the child. ` +
|
|
1767
1771
|
`Add a toolExtensions entry in config.json.\n`
|
|
1768
1772
|
: "");
|
|
1769
|
-
|
|
1773
|
+
const modelNote = catalogDefaultNote(p.catalog, model, thinking);
|
|
1774
|
+
return { id, meta, spawned, runtime: runtime + (meta.timing ? `${formatTimingLimits(meta.timing, startedAt)}\n` : ""), warn, sandboxDir, modelNote };
|
|
1770
1775
|
}
|
|
1771
1776
|
|
|
1772
1777
|
// ---- subagent_spawn -------------------------------------------------
|
|
@@ -1783,7 +1788,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1783
1788
|
"After subagent_spawn, do NOT call subagent_output or subagent_result in a loop to wait for the result, and do NOT sleep. The run completes on its own and reports back on the next turn.",
|
|
1784
1789
|
"Call subagent_result after a completion or attention callback, or when the user explicitly asks for the result. Use subagent_output only when the user explicitly asks how a run is progressing; never use either tool to poll.",
|
|
1785
1790
|
...SUBAGENT_ORCHESTRATION_GUIDELINES,
|
|
1786
|
-
"The tools param is both the tool allowlist AND what determines which extensions load in the child (e.g. tools='read,bash,web_fetch' loads only the web-tools package). Ask for the tools the task needs and nothing more; clean:true gives a built-ins-only child.
|
|
1791
|
+
"The tools param is both the tool allowlist AND what determines which extensions load in the child (e.g. tools='read,bash,web_fetch' loads only the web-tools package). Ask for the tools the task needs and nothing more; clean:true gives a built-ins-only child. Without an agent or role, pick a model with the model param (e.g. 'xai/grok-4.5@high'); providerless model patterns are resolved by Pi, while provider/model is deterministic and loads mapped provider extensions.",
|
|
1787
1792
|
...CATALOG_GUIDELINES,
|
|
1788
1793
|
"By default the subagent is sandboxed. Human settings in /sandbox control file, credential-file, command, and network permissions; sandbox:false cannot override an enabled human profile. Without published settings, legacy write confinement applies. Set callback:false to finish quietly — then read the result on demand via subagent_result.",
|
|
1789
1794
|
"Every run is timed by the harness: a soft deadline (default 30 min) steers the child to wrap up and wakes you once, the run is stopped after grace_minutes (reason deadline), a hard ceiling (default 90 min) stops it without grace (reason ceiling), and no progress for stuck_minutes (default 10) wakes you once (reason stuck). Set deadline_minutes/max_minutes/stuck_minutes to fit the task instead of writing a time limit into the prompt; do not stop a slow child that is still making progress.",
|
|
@@ -1795,8 +1800,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1795
1800
|
agent: Type.Optional(Type.String({ description: "Named agent id (agent.<slug>). Mutually exclusive with role. The navigator shows the defined agent name." })),
|
|
1796
1801
|
role: catalogRoleSchema("direct role launch"),
|
|
1797
1802
|
alias: Type.Optional(Type.String({ description: "Per-run display alias for a direct role launch, such as checkout. Not a reusable agent. Colliding aliases gain a numeric suffix." })),
|
|
1798
|
-
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort (for example openai/gpt-5.5@high). Providerless patterns are resolved by Pi.
|
|
1799
|
-
thinking: Type.Optional(Type.String({ description: "Reasoning effort for the child: off, minimal, low, medium, high, xhigh, or max
|
|
1803
|
+
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort (for example openai/gpt-5.5@high). Providerless patterns are resolved by Pi. Omit to use the agent or role default; without one, inherit the foreground model. Put authoritative model choices here; do not rely on prompt text." })),
|
|
1804
|
+
thinking: Type.Optional(Type.String({ description: "Reasoning effort for the child: off, minimal, low, medium, high, xhigh, or max. Omit to use the agent or role default; without one, the Pi/model default." })),
|
|
1800
1805
|
tools: Type.Optional(Type.String({ description: "Tool allowlist: comma-separated names the child may use (e.g. 'read,bash,web_fetch'). This ALSO selects which extensions load — only packages backing a requested tool are loaded. Defaults to the configured safe set." })),
|
|
1801
1806
|
exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist, applied on top of the allowlist." })),
|
|
1802
1807
|
clean: Type.Optional(Type.Boolean({ description: "Run a hermetic child with NO extensions at all (only built-ins: read, bash, edit, write). Default false — the extensions backing the requested tools load, so web_fetch and model auth (e.g. xai) work." })),
|
|
@@ -1841,21 +1846,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
1841
1846
|
throw new Error(`Choosing split needs ${catalog.jobs.length} subagent slots, but only one was free. Nothing was launched.`);
|
|
1842
1847
|
}
|
|
1843
1848
|
reserved += extra;
|
|
1844
|
-
const launched: { name?: string; id: string }[] = [];
|
|
1849
|
+
const launched: { name?: string; id: string; modelNote?: string }[] = [];
|
|
1845
1850
|
for (const job of catalog.jobs) {
|
|
1846
|
-
const { id } = await spawnSubagentRun(ctx, job);
|
|
1851
|
+
const { id, modelNote } = await spawnSubagentRun(ctx, job);
|
|
1847
1852
|
gate.commit(1);
|
|
1848
1853
|
reserved -= 1;
|
|
1849
|
-
launched.push({ name: job.name, id });
|
|
1854
|
+
launched.push({ name: job.name, id, modelNote });
|
|
1850
1855
|
}
|
|
1851
|
-
return text(launched.map((item) => `Subagent launched: ${item.name ? `${item.name} ` : ""}id=${item.id}.`).join("\n"));
|
|
1856
|
+
return text(launched.map((item) => `Subagent launched: ${item.name ? `${item.name} ` : ""}id=${item.id}.${item.modelNote ? ` ${item.modelNote}.` : ""}`).join("\n"));
|
|
1852
1857
|
}
|
|
1853
1858
|
Object.assign(p, catalog.jobs[0]);
|
|
1854
|
-
const { id, spawned, runtime, warn, sandboxDir } = await spawnSubagentRun(ctx, p);
|
|
1859
|
+
const { id, spawned, runtime, warn, sandboxDir, modelNote } = await spawnSubagentRun(ctx, p);
|
|
1855
1860
|
gate.commit(1);
|
|
1856
1861
|
reserved = 0;
|
|
1857
1862
|
return text(
|
|
1858
1863
|
`Subagent launched: ${p.name ? `${p.name} ` : ""}id=${id} (pid ${spawned.pid}).\n` +
|
|
1864
|
+
(modelNote ? `${modelNote[0]!.toUpperCase()}${modelNote.slice(1)}.\n` : "") +
|
|
1859
1865
|
(p.callback === false
|
|
1860
1866
|
? `Running in the background; the foreground is free. It will finish quietly — read the result with subagent_result id=${id}.\n`
|
|
1861
1867
|
: `Running in the background; the foreground is free. Its result will be posted back here when it finishes.\n`) +
|
|
@@ -1898,7 +1904,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1898
1904
|
agent: Type.Optional(Type.String({ description: "Named agent id applied to jobs that do not select their own agent or role." })),
|
|
1899
1905
|
role: catalogRoleSchema("shared role selector"),
|
|
1900
1906
|
alias: Type.Optional(Type.String({ description: "Direct-role alias applied when a job does not set alias." })),
|
|
1901
|
-
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort
|
|
1907
|
+
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort. Omit to use the agent or role default; without one, inherit the foreground model. Put authoritative model choices here; prompt text is not parsed." })),
|
|
1902
1908
|
thinking: Type.Optional(Type.String({ description: "Reasoning effort applied to every job: off, minimal, low, medium, high, xhigh, or max." })),
|
|
1903
1909
|
tools: Type.Optional(Type.String({ description: "Tool allowlist applied to every job." })),
|
|
1904
1910
|
exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist applied to every job." })),
|
|
@@ -1997,7 +2003,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1997
2003
|
|
|
1998
2004
|
const names = assignBatchJobNames(p.jobs);
|
|
1999
2005
|
const batchId = nextBatchId();
|
|
2000
|
-
const launched: { name: string; id: string }[] = [];
|
|
2006
|
+
const launched: { name: string; id: string; modelNote?: string }[] = [];
|
|
2001
2007
|
const failed: { name: string; reason: string }[] = [];
|
|
2002
2008
|
const skipped: { name: string }[] = [];
|
|
2003
2009
|
// How many reject-mode reserved slots are still held (not yet committed/released).
|
|
@@ -2035,10 +2041,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2035
2041
|
name = prepared.assign.name;
|
|
2036
2042
|
}
|
|
2037
2043
|
}
|
|
2038
|
-
const { id } = await spawnSubagentRun(ctx, { ...merged, name }, { batchId, batchName: p.batchName });
|
|
2044
|
+
const { id, modelNote } = await spawnSubagentRun(ctx, { ...merged, name }, { batchId, batchName: p.batchName });
|
|
2039
2045
|
gate.commit(1);
|
|
2040
2046
|
if (!launchAvailable) reservedRemaining -= 1;
|
|
2041
|
-
launched.push({ name, id });
|
|
2047
|
+
launched.push({ name, id, ...(modelNote ? { modelNote } : {}) });
|
|
2042
2048
|
} catch (err) {
|
|
2043
2049
|
gate.release(1);
|
|
2044
2050
|
if (!launchAvailable) reservedRemaining -= 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"pi-better-background-tasks": "0.6.0",
|
|
54
|
-
"pi-better-goal": "0.
|
|
54
|
+
"pi-better-goal": "0.5.0",
|
|
55
55
|
"pi-better-plan": "0.5.0",
|
|
56
56
|
"pi-better-sandbox": "0.7.1",
|
|
57
57
|
"pi-better-ssh": "0.1.1",
|
|
58
|
-
"pi-better-subagents": "0.
|
|
58
|
+
"pi-better-subagents": "0.8.0",
|
|
59
59
|
"smol-toml": "1.9.0",
|
|
60
60
|
"yaml": "^2.9.1"
|
|
61
61
|
},
|