pi-subagents 0.82.0 → 0.83.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/CHANGELOG.md +46 -0
- package/README.md +1 -1
- package/bundle/agents.js +4 -4
- package/bundle/background-work.js +1 -1
- package/bundle/child-tool-plan.js +3 -3
- package/bundle/{chunk-7PCKZHR5.js → chunk-4BW55BTL.js} +6 -6
- package/bundle/{chunk-7IVSUMQQ.js → chunk-527E3D2J.js} +6 -6
- package/bundle/{chunk-NRQ75MGH.js → chunk-5ESXK62X.js} +40 -1076
- package/bundle/{chunk-ZQGSDHL6.js → chunk-6B4ITO2W.js} +8 -4
- package/bundle/{chunk-CIZQXJ5Q.js → chunk-7F5SK2U3.js} +39 -6
- package/bundle/{chunk-OYF6AFDY.js → chunk-BKFWQ642.js} +2 -2
- package/bundle/{chunk-ZLINIYTP.js → chunk-CDI3GMNV.js} +48 -32
- package/bundle/{chunk-GC4DSPMH.js → chunk-CGXWSNPA.js} +12 -18
- package/bundle/{chunk-SIJ4S3EN.js → chunk-CNLDYEH4.js} +12 -12
- package/bundle/{chunk-4SDU2BZX.js → chunk-CSSZRTKY.js} +2 -2
- package/bundle/{chunk-W2SZ22EM.js → chunk-CZTMUBUI.js} +10 -8
- package/bundle/{chunk-WVD4UEL4.js → chunk-HJJAVHFB.js} +28 -24
- package/bundle/{chunk-GJECHSAR.js → chunk-HLE22FSH.js} +2 -2
- package/bundle/{chunk-M2CTFYT3.js → chunk-HXVKGS33.js} +5 -5
- package/bundle/{chunk-5LFLVSTM.js → chunk-JEZOSEWG.js} +2 -0
- package/bundle/{chunk-YKEWIB4M.js → chunk-JHZ6DZ5M.js} +1 -1
- package/bundle/{chunk-B5EKXDHE.js → chunk-LDMXJIA6.js} +458 -657
- package/bundle/{chunk-43SCHIMD.js → chunk-MBDOYZ2V.js} +1 -1
- package/bundle/{chunk-XSWOTIA3.js → chunk-MHX22USZ.js} +1 -1
- package/bundle/{chunk-VH5OAWVG.js → chunk-OOFW75PV.js} +5 -5
- package/bundle/{chunk-RMQFMMAT.js → chunk-P2NCCTZP.js} +231 -10
- package/bundle/{chunk-NGGAFDL6.js → chunk-RZJN63GU.js} +12 -5
- package/bundle/{chunk-7YMXOYEU.js → chunk-TJQJ3MEA.js} +12 -11
- package/bundle/{chunk-5TYMSZX3.js → chunk-VTMNBPCQ.js} +2 -2
- package/bundle/{chunk-52UBALJL.js → chunk-WG2OTTAK.js} +2 -2
- package/bundle/{chunk-N2BN7BF5.js → chunk-XA3DY6UG.js} +7 -5
- package/bundle/{chunk-MHZ4G23A.js → chunk-XXKG3P2W.js} +9 -6
- package/bundle/{chunk-PLCCBLPY.js → chunk-Y3I6H6DV.js} +2 -2
- package/bundle/control-channel.js +3 -3
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +3 -3
- package/bundle/preflight.js +9 -9
- package/bundle/project-panes.js +4 -4
- package/bundle/shared-types.js +1 -1
- package/docs/agents.md +6 -7
- package/docs/configuration.md +1 -1
- package/docs/extension-api.md +19 -4
- package/docs/observability.md +1 -0
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +1 -4
- package/skills/pi-subagents/SKILL.md +130 -107
- package/skills/pi-subagents/references/orchestration.md +124 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
- package/src/agents/agent-management.js +8 -14
- package/src/agents/agent-serializer.js +1 -2
- package/src/agents/agents.js +19 -3
- package/src/api/background-work.d.ts +9 -6
- package/src/api/background-work.js +14 -7
- package/src/extension/index.js +4 -1
- package/src/extension/public-execution.d.ts +2 -2
- package/src/extension/public-execution.js +3 -3
- package/src/extension/reply-workflow-script.js +5 -3
- package/src/extension/rpc.js +3 -1
- package/src/extension/subagent-guide.js +1 -2
- package/src/extension/subagent-options.js +17 -4
- package/src/runs/background/async-execution.js +4 -1
- package/src/runs/background/async-job-tracker.js +2 -1
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +1 -1
- package/src/runs/background/auto-drain.js +2 -3
- package/src/runs/background/fleet-view.js +1 -1
- package/src/runs/background/run-child-session.js +22 -6
- package/src/runs/background/subagent-wait.d.ts +2 -1
- package/src/runs/background/subagent-wait.js +5 -2
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +40 -1
- package/src/runs/foreground/execution.js +22 -6
- package/src/runs/foreground/subagent-executor.js +2 -5
- package/src/runs/shared/child-launch.js +5 -2
- package/src/runs/shared/child-session.d.ts +6 -0
- package/src/runs/shared/child-session.js +20 -4
- package/src/runs/shared/herdr-machine.d.ts +2 -0
- package/src/runs/shared/herdr-machine.js +2 -0
- package/src/runs/shared/herdr-placed-run.js +11 -4
- package/src/runs/shared/subagent-prompt-runtime.js +11 -17
- package/src/shared/disabled-features.d.ts +3 -1
- package/src/shared/disabled-features.js +11 -3
- package/src/shared/types.d.ts +3 -1
- package/src/shared/types.js +2 -0
- package/src/tui/fleet.js +1 -0
- package/src/workflows/scripted-workflow.js +9 -1070
- package/workflow-script-worker.cjs +1067 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
- package/skills/pi-subagents/references/execution-controls.md +0 -564
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
- package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
- package/skills/pi-subagents/references/review-and-validation.md +0 -73
|
@@ -4,7 +4,7 @@ const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
|
4
4
|
const MAX_PROVIDERS = 100;
|
|
5
5
|
const MAX_ITEM_ID_LENGTH = 256;
|
|
6
6
|
/**
|
|
7
|
-
* A
|
|
7
|
+
* A session can be identified by its file path, which routinely exceeds a short
|
|
8
8
|
* identity budget in nested worktrees, so it is bounded like the other paths.
|
|
9
9
|
*/
|
|
10
10
|
const MAX_SESSION_ID_LENGTH = 4_096;
|
|
@@ -120,22 +120,29 @@ export function listBackgroundWorkWakeChannels() {
|
|
|
120
120
|
}
|
|
121
121
|
return [...channels];
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Reconcile and snapshot active provider work owned by one exact Pi session.
|
|
125
|
+
* `sessionIds` lists that session's identity forms; the first is passed to
|
|
126
|
+
* providers as `sessionId`, and items keyed by any of them are kept.
|
|
127
|
+
*/
|
|
128
|
+
export function snapshotBackgroundWork(sessionIds, nowMs = Date.now()) {
|
|
129
|
+
const accepted = [...new Set(sessionIds.map((value) => validateString(value, "Background-work snapshot sessionId", MAX_SESSION_ID_LENGTH)))];
|
|
130
|
+
const sessionId = accepted[0];
|
|
131
|
+
if (sessionId === undefined)
|
|
132
|
+
throw new Error("Background-work snapshot needs at least one session id.");
|
|
126
133
|
const providers = listBackgroundWorkProviders();
|
|
127
134
|
const items = [];
|
|
128
135
|
const identities = new Set();
|
|
129
136
|
for (const provider of providers) {
|
|
130
137
|
try {
|
|
131
|
-
provider.reconcile?.({ sessionId, nowMs });
|
|
138
|
+
provider.reconcile?.({ sessionId, sessionIds: [...accepted], nowMs });
|
|
132
139
|
}
|
|
133
140
|
catch (error) {
|
|
134
141
|
throw new Error(`Background-work provider '${provider.name}' reconcile failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
135
142
|
}
|
|
136
143
|
let active;
|
|
137
144
|
try {
|
|
138
|
-
active = provider.listActiveWork({ sessionId, nowMs });
|
|
145
|
+
active = provider.listActiveWork({ sessionId, sessionIds: [...accepted], nowMs });
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
141
148
|
throw new Error(`Background-work provider '${provider.name}' listActiveWork failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -153,7 +160,7 @@ export function snapshotBackgroundWork(sessionId, nowMs = Date.now()) {
|
|
|
153
160
|
throw new Error(`Background-work provider '${provider.name}' returned duplicate item '${item.id}' for session '${item.sessionId}'.`);
|
|
154
161
|
}
|
|
155
162
|
identities.add(identity);
|
|
156
|
-
if (item.sessionId
|
|
163
|
+
if (accepted.includes(item.sessionId))
|
|
157
164
|
items.push({ provider: provider.name, ...item });
|
|
158
165
|
});
|
|
159
166
|
}
|
package/src/extension/index.js
CHANGED
|
@@ -566,8 +566,11 @@ export default function registerSubagentExtension(pi) {
|
|
|
566
566
|
// Pi sends custom messages to the model as plain user messages, so every model-visible
|
|
567
567
|
// pi-subagents message is wrapped in a marker naming its source. The marker depends only on
|
|
568
568
|
// the message, so history renders the same bytes every turn and cached prefixes stay valid.
|
|
569
|
+
// It runs on context_with_system, not context: Pi treats any change a context handler makes
|
|
570
|
+
// to the message list as a rewritten conversation and folds every mid-conversation system
|
|
571
|
+
// message into the leading one, which misses the prompt cache on every later turn.
|
|
569
572
|
const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
|
|
570
|
-
pi.on("
|
|
573
|
+
pi.on("context_with_system", (event) => {
|
|
571
574
|
const messages = [];
|
|
572
575
|
for (const message of event.messages) {
|
|
573
576
|
if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
|
|
@@ -57,8 +57,8 @@ export type PublicSubagentExecutionNormalization<T> = {
|
|
|
57
57
|
/** A workflow value containing "/" or "\" is a script path; named workflow resource names contain neither. */
|
|
58
58
|
export declare function isWorkflowScriptPath(workflow: string): boolean;
|
|
59
59
|
/**
|
|
60
|
-
* Model tool calls supply script text
|
|
61
|
-
* workflowScript
|
|
60
|
+
* Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
|
|
61
|
+
* which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
|
|
62
62
|
*/
|
|
63
63
|
export declare function removedModelWorkflowFieldError(params: object): string | undefined;
|
|
64
64
|
export declare function validateWorkflowCapacityOverrides(params: PublicSubagentExecutionParams): string | undefined;
|
|
@@ -7,8 +7,8 @@ export function isWorkflowScriptPath(workflow) {
|
|
|
7
7
|
return workflow.includes("/") || workflow.includes("\\");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Model tool calls supply script text
|
|
11
|
-
* workflowScript
|
|
10
|
+
* Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
|
|
11
|
+
* which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
|
|
12
12
|
*/
|
|
13
13
|
export function removedModelWorkflowFieldError(params) {
|
|
14
14
|
if (Object.hasOwn(params, "workflowScript"))
|
|
@@ -56,7 +56,7 @@ export function normalizePublicSubagentExecution(params, options = {}) {
|
|
|
56
56
|
return { ok: false, error: "workflow must be true (the ```js workflow block in this reply), a script path containing '/', or a named workflow resource.", mode: "workflow" };
|
|
57
57
|
}
|
|
58
58
|
const hasNamedWorkflow = typeof workflow === "string" && !isWorkflowScriptPath(workflow);
|
|
59
|
-
// workflowScript is the internal script carrier; model tool calls
|
|
59
|
+
// workflowScript is the internal script carrier; model tool calls set it only through options.script.
|
|
60
60
|
if (workflow !== undefined && params.workflowScript !== undefined) {
|
|
61
61
|
return { ok: false, error: "workflow cannot be combined with an internal workflowScript.", mode: "workflow" };
|
|
62
62
|
}
|
|
@@ -2,7 +2,9 @@ const WORKFLOW_FENCE = /^```(?:js|javascript) workflow[ \t]*$/;
|
|
|
2
2
|
const PLAIN_JS_FENCE = /^```(?:js|javascript)[ \t]*$/;
|
|
3
3
|
const OPEN_FENCE = /^(`{3,}|~{3,})/;
|
|
4
4
|
const CLOSE_FENCE = /^(`{3,}|~{3,})[ \t]*$/;
|
|
5
|
-
|
|
5
|
+
// Through the Claude Agent SDK (pi-claude-bridge#35), text written after a tool call in the same turn reaches the session
|
|
6
|
+
// only as a server summary, which drops fenced blocks; retrying the block fails again. Tool arguments arrive intact.
|
|
7
|
+
const SUMMARY_NOTE = "On some hosts, text written after a tool call in the same turn reaches the session only as a summary without code blocks, so for the rest of this turn pass the script's source as a tool argument: options: { script: \"...\" } in place of workflow: true, with the call's other fields unchanged.";
|
|
6
8
|
/**
|
|
7
9
|
* Pi persists the whole assistant message before running its tool calls, so the
|
|
8
10
|
* message that issued this subagent call is on the branch and carries the script.
|
|
@@ -28,7 +30,7 @@ function scriptFromReply(content) {
|
|
|
28
30
|
if (replyCalls > 1)
|
|
29
31
|
return { error: `This reply has ${replyCalls} subagent calls with workflow: true; a reply can carry only one. Pass other scripts as workflow file paths.` };
|
|
30
32
|
if (!content.some((block) => block.type === "text"))
|
|
31
|
-
return { error:
|
|
33
|
+
return { error: `The assistant message that issued this call has no text in the session, so the reply's visible text, including any \`\`\`js workflow block, never reached it. ${SUMMARY_NOTE}` };
|
|
32
34
|
const text = content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []).join("\n");
|
|
33
35
|
const { tagged, plain, unclosed } = workflowBlocks(text);
|
|
34
36
|
if (unclosed)
|
|
@@ -37,7 +39,7 @@ function scriptFromReply(content) {
|
|
|
37
39
|
const blocks = tagged.length > 0 ? tagged : plain.length === 1 ? plain : [];
|
|
38
40
|
if (blocks.length !== 1) {
|
|
39
41
|
const untagged = tagged.length === 0 && plain.length > 1 ? ` This reply has ${plain.length} untagged js blocks; open the script's block with the line "\`\`\`js workflow".` : "";
|
|
40
|
-
return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} ${
|
|
42
|
+
return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} If the block is in your reply: ${SUMMARY_NOTE}` };
|
|
41
43
|
}
|
|
42
44
|
const script = blocks[0];
|
|
43
45
|
if (!script.trim())
|
package/src/extension/rpc.js
CHANGED
|
@@ -5,7 +5,7 @@ import { deliverAsyncRunStop } from "../runs/foreground/async-stop-action.js";
|
|
|
5
5
|
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
6
6
|
import { resolveCurrentSessionId } from "../shared/session-identity.js";
|
|
7
7
|
import { ownerSessionIds, ownsRun } from "../runs/shared/run-ownership.js";
|
|
8
|
-
import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
|
|
8
|
+
import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_JOBS_CHANGED_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
|
|
9
9
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.js";
|
|
10
10
|
import { readStatus } from "../shared/utils.js";
|
|
11
11
|
import { SubagentFlatParams } from "./schemas.js";
|
|
@@ -336,8 +336,10 @@ function pingData(ctx, options) {
|
|
|
336
336
|
request: SUBAGENT_RPC_REQUEST_EVENT,
|
|
337
337
|
replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
|
|
338
338
|
asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
339
|
+
asyncStarted: SUBAGENT_ASYNC_STARTED_EVENT,
|
|
339
340
|
childStatus: SUBAGENT_CHILD_STATUS_EVENT,
|
|
340
341
|
processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
342
|
+
jobsChanged: SUBAGENT_JOBS_CHANGED_EVENT,
|
|
341
343
|
},
|
|
342
344
|
session: sessionData(ctx),
|
|
343
345
|
};
|
|
@@ -19,11 +19,10 @@ export const SUBAGENT_GUIDE_TOPICS = [
|
|
|
19
19
|
const SUBAGENT_GUIDE_SECTION_MAX_CHARS = 8000;
|
|
20
20
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
21
21
|
// /council must work under `pi --no-skills`, which drops the package skills from context,
|
|
22
|
-
// so this topic serves the council skill together with the
|
|
22
|
+
// so this topic serves the council skill together with the reference it tells the model to read.
|
|
23
23
|
const COUNCIL_FILES = [
|
|
24
24
|
"skills/council-mode/SKILL.md",
|
|
25
25
|
"skills/council-mode/references/pass-contracts.md",
|
|
26
|
-
"skills/pi-subagents/references/execution-controls.md",
|
|
27
26
|
];
|
|
28
27
|
function isGuideTopic(value) {
|
|
29
28
|
return SUBAGENT_GUIDE_TOPICS.includes(value);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { Compile } from "typebox/compile";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
3
4
|
import { closestMatch } from "../shared/edit-distance.js";
|
|
4
5
|
import { removedModelWorkflowFieldError } from "./public-execution.js";
|
|
5
6
|
import { SUBAGENT_OPTION_KEYS, SubagentOptionParams, createSubagentParamsSchema } from "./schemas.js";
|
|
@@ -22,6 +23,15 @@ export function flattenSubagentToolOptions(params, disabled) {
|
|
|
22
23
|
const removedField = removedModelWorkflowFieldError(options);
|
|
23
24
|
if (removedField)
|
|
24
25
|
throw new Error(removedField);
|
|
26
|
+
// options.script carries script text as a tool argument for hosts that drop the reply's ```js block (SUMMARY_NOTE).
|
|
27
|
+
const { script, ...rest } = options;
|
|
28
|
+
if (script !== undefined) {
|
|
29
|
+
if (typeof script !== "string" || !script.trim())
|
|
30
|
+
throw new Error("subagent: options.script must be the workflow script's source text.");
|
|
31
|
+
if (topLevel.workflow !== undefined)
|
|
32
|
+
throw new Error("subagent: options.script replaces workflow; pass only one of them.");
|
|
33
|
+
return { ...flattenSubagentToolOptions({ ...topLevel, options: rest }, disabled), workflowScript: script };
|
|
34
|
+
}
|
|
25
35
|
const entries = Object.entries(options);
|
|
26
36
|
const enabledKeys = SUBAGENT_OPTION_KEYS.filter((key) => !disabled?.params.has(key));
|
|
27
37
|
const unknown = entries.map(([key]) => key).filter((key) => !optionKeys.has(key));
|
|
@@ -40,12 +50,15 @@ export function flattenSubagentToolOptions(params, disabled) {
|
|
|
40
50
|
const disabledKey = entries.find(([key]) => disabled?.params.has(key))?.[0];
|
|
41
51
|
if (disabledKey)
|
|
42
52
|
throw new Error(`subagent option 'options.${disabledKey}' is disabled by config ${disabled.params.get(disabledKey)}.`);
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
// Pi converts only the declared top-level parameters ("true" -> true); `options` is an untyped bag in the
|
|
54
|
+
// tool schema, so its values get the same conversion here. Some providers send nested scalars as strings.
|
|
55
|
+
const converted = Value.Convert(SubagentOptionParams, structuredClone(options));
|
|
56
|
+
if (!optionsValidator.Check(converted)) {
|
|
57
|
+
const errors = [...optionsValidator.Errors(converted)].slice(0, 4).map((error) => `options${error.instancePath.replaceAll("/", ".")} ${error.message}`);
|
|
45
58
|
throw new Error(`subagent: ${errors.join("; ")}.`);
|
|
46
59
|
}
|
|
47
|
-
const conflicts = entries.filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
|
|
60
|
+
const conflicts = Object.entries(converted).filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
|
|
48
61
|
if (conflicts.length > 0)
|
|
49
62
|
throw new Error(`subagent: ${conflicts.join(", ")} given both at the top level and in options with different values; give each once.`);
|
|
50
|
-
return { ...topLevel, ...
|
|
63
|
+
return { ...topLevel, ...converted };
|
|
51
64
|
}
|
|
@@ -13,7 +13,7 @@ import { childCacheRetentionEnv } from "../../shared/child-cache-retention.js";
|
|
|
13
13
|
import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.js";
|
|
14
14
|
import { currentCompletionOwnerId } from "../../shared/completion-owner.js";
|
|
15
15
|
import { planChildLaunch, projectChainOutputSchemas, resolveStepBehavior, suppressProgressForReadOnlyTask } from "../shared/child-launch-plan.js";
|
|
16
|
-
import { formatHerdrMachineRunnerUnsupported, resolveMachinePlacement } from "../shared/herdr-machine.js";
|
|
16
|
+
import { formatHerdrMachineRunnerUnsupported, PLACED_RESTORE_UNSUPPORTED, resolveMachinePlacement } from "../shared/herdr-machine.js";
|
|
17
17
|
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.js";
|
|
18
18
|
import { assertClaudeCodeModelScope, isClaudeCodeAdapterId, resolveClaudeCodeOverride } from "../shared/claude-code-adapter.js";
|
|
19
19
|
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.js";
|
|
@@ -1612,6 +1612,9 @@ export function executeAsyncSingle(id, params) {
|
|
|
1612
1612
|
const machineUnsupported = formatHerdrMachineRunnerUnsupported({ machine: requestedMachine, agentName: agentConfig.name, runnerType: agentConfig.runner?.type, adapter: agentConfig.runner?.type === "external-cli" ? agentConfig.runner.adapter : undefined, worktree: params.worktree });
|
|
1613
1613
|
if (machineUnsupported)
|
|
1614
1614
|
return formatAsyncStartError("single", machineUnsupported);
|
|
1615
|
+
// Refuse before the runner starts; the placed launch would refuse only after the run exists.
|
|
1616
|
+
if (requestedMachine && params.revivalLease)
|
|
1617
|
+
return formatAsyncStartError("single", PLACED_RESTORE_UNSUPPORTED);
|
|
1615
1618
|
const resolvedLauncher = resolveAgentRunnerLauncher(agentConfig, ctx.runnerLaunchers, requestedMachine);
|
|
1616
1619
|
if (resolvedLauncher.error)
|
|
1617
1620
|
return formatAsyncStartError("single", resolvedLauncher.error);
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { renderWidget, widgetRenderKey } from "../../tui/render.js";
|
|
4
4
|
import { formatControlNoticeMessage } from "../shared/subagent-control.js";
|
|
5
|
-
import { DIRS, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_ANIMATION_INTERVAL_MS, } from "../../shared/types.js";
|
|
5
|
+
import { DIRS, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_JOBS_CHANGED_EVENT, SUBAGENT_STEERING_NOTICE_EVENT, WIDGET_ANIMATION_INTERVAL_MS, } from "../../shared/types.js";
|
|
6
6
|
import { readStatus, resolveWatchPath } from "../../shared/utils.js";
|
|
7
7
|
import { normalizeParallelGroups } from "./parallel-groups.js";
|
|
8
8
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.js";
|
|
@@ -63,6 +63,7 @@ export function createAsyncJobTracker(pi, state, asyncDirRoot, options = {}) {
|
|
|
63
63
|
};
|
|
64
64
|
const rerenderWidget = (ctx, jobs = Array.from(state.asyncJobs.values())) => {
|
|
65
65
|
options.onJobsChanged?.();
|
|
66
|
+
pi.events.emit(SUBAGENT_JOBS_CHANGED_EVENT, { version: 1 });
|
|
66
67
|
if (state.widgetsSuspended)
|
|
67
68
|
return;
|
|
68
69
|
renderWidget(ctx, options.widgetEnabled === false ? [] : jobs, options.widgetCollapsed, options.widgetLayout);
|
|
@@ -144,6 +144,8 @@ interface AsyncRunListOptions {
|
|
|
144
144
|
repairScan?: boolean;
|
|
145
145
|
/** This process's completion owner, adopted by an owner-exited workflow repair of a run the session owns; needs `sessionId`. */
|
|
146
146
|
adoptCompletionOwnerId?: string;
|
|
147
|
+
/** Receives the warning for a skipped corrupt terminal status, so an open TUI can show it as a notice instead of console output. */
|
|
148
|
+
onCorruptStatus?: (warning: string) => void;
|
|
147
149
|
}
|
|
148
150
|
type TargetedAsyncRunResolution = {
|
|
149
151
|
kind: "exact";
|
|
@@ -404,7 +404,7 @@ export function listAsyncRuns(asyncDirRoot, options = {}, observeStatus) {
|
|
|
404
404
|
}
|
|
405
405
|
if (wantsTerminal) {
|
|
406
406
|
for (const sessionId of owners ?? [undefined]) {
|
|
407
|
-
for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) }))
|
|
407
|
+
for (const entry of readRecentTerminalRunIndex(asyncDirRoot, { sessionId, onCorruptStatus: options.onCorruptStatus, ...(options.entryLimit !== undefined ? { limit: options.entryLimit } : {}) }))
|
|
408
408
|
indexed.add(entry);
|
|
409
409
|
}
|
|
410
410
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { snapshotBackgroundWork } from "../../api/background-work.js";
|
|
2
1
|
import { listAsyncRuns } from "./async-status.js";
|
|
3
|
-
import { waitForSubagents, waitRunScopes } from "./subagent-wait.js";
|
|
2
|
+
import { backgroundWorkForSession, waitForSubagents, waitRunScopes } from "./subagent-wait.js";
|
|
4
3
|
export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
5
4
|
function resultText(value) {
|
|
6
5
|
return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
|
|
@@ -14,7 +13,7 @@ function hasOutstandingWork(deps, sessionId, nowMs, observation) {
|
|
|
14
13
|
includeNested: false,
|
|
15
14
|
}, observation?.status));
|
|
16
15
|
const detachedForeground = [...(deps.state.foregroundRuns?.values() ?? [])].some((run) => run.sessionId === sessionId && run.children.some((child) => child.status === "detached"));
|
|
17
|
-
return asyncRuns.length > 0 ||
|
|
16
|
+
return asyncRuns.length > 0 || backgroundWorkForSession(deps, nowMs).items.length > 0 || detachedForeground;
|
|
18
17
|
}
|
|
19
18
|
/** Drain all work owned by the current headless session, including work added while draining. */
|
|
20
19
|
export async function drainOutstandingWork(deps, observation) {
|
|
@@ -164,7 +164,7 @@ function sessionMessageParts(record) {
|
|
|
164
164
|
}
|
|
165
165
|
if (entry.type === "toolCall" || entry.type === "tool_call") {
|
|
166
166
|
const name = typeof entry.name === "string" ? entry.name : typeof entry.toolName === "string" ? entry.toolName : "tool";
|
|
167
|
-
parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.
|
|
167
|
+
parts.push({ role, kind: "toolCall", name, text: `[tool: ${name}${entry.arguments === undefined ? "" : ` ${stringifyJsonPreview(entry.arguments)}`}]` });
|
|
168
168
|
continue;
|
|
169
169
|
}
|
|
170
170
|
if (entry.type === "toolResult" || entry.type === "tool_result") {
|
|
@@ -69,6 +69,8 @@ export function runChildSession(input) {
|
|
|
69
69
|
let queuedDrainHold = false;
|
|
70
70
|
let compactionStartedReceived = false;
|
|
71
71
|
let afterCompactionSettlement = false;
|
|
72
|
+
let terminalAgentEndReceived = false;
|
|
73
|
+
let compactionAfterFinalOpen = false;
|
|
72
74
|
let finalDrainTimer;
|
|
73
75
|
let finalHardFinishTimer;
|
|
74
76
|
let watchdogTailTimer;
|
|
@@ -216,7 +218,8 @@ export function runChildSession(input) {
|
|
|
216
218
|
finalDrainTimer = setTimeout(() => {
|
|
217
219
|
if (settled || promptSettled)
|
|
218
220
|
return;
|
|
219
|
-
|
|
221
|
+
// Pi runs auto-compaction after the final agent_end and only then emits agent_settled.
|
|
222
|
+
if (input.launch.capture.finalDrainHeld() || observeQueuedDrainHold() || compactionAfterFinalOpen) {
|
|
220
223
|
finalDrainTimer = undefined;
|
|
221
224
|
armFinalDrainTimer();
|
|
222
225
|
return;
|
|
@@ -231,16 +234,22 @@ export function runChildSession(input) {
|
|
|
231
234
|
return;
|
|
232
235
|
settle(undefined, true);
|
|
233
236
|
}, HARD_FINISH_MS);
|
|
234
|
-
finalHardFinishTimer.unref?.();
|
|
235
237
|
}, FINAL_STOP_GRACE_MS);
|
|
236
|
-
finalDrainTimer.unref?.();
|
|
237
238
|
}
|
|
238
|
-
const applyChildLifecycle = (action) => {
|
|
239
|
+
const applyChildLifecycle = (action, event) => {
|
|
239
240
|
if (action === "cancel-drain") {
|
|
240
|
-
|
|
241
|
+
// After a clean final stop, a non-retrying agent_end may be the child's last event
|
|
242
|
+
// (Pi may not emit agent_settled), so it re-arms the drain instead of cancelling it.
|
|
243
|
+
terminalAgentEndReceived = event?.type === "agent_end" && event.willRetry !== true && cleanTerminalAssistantStopReceived;
|
|
241
244
|
agentSettledReceived = false;
|
|
242
245
|
clearFinalDrainTimers();
|
|
243
246
|
clearWatchdogTailTimer();
|
|
247
|
+
if (terminalAgentEndReceived) {
|
|
248
|
+
startFinalDrain();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
cleanTerminalAssistantStopReceived = false;
|
|
252
|
+
compactionAfterFinalOpen = false;
|
|
244
253
|
return;
|
|
245
254
|
}
|
|
246
255
|
if (action === "start-drain")
|
|
@@ -314,11 +323,18 @@ export function runChildSession(input) {
|
|
|
314
323
|
compactionStartedReceived = true;
|
|
315
324
|
if (agentSettledReceived)
|
|
316
325
|
afterCompactionSettlement = true;
|
|
326
|
+
if (terminalAgentEndReceived)
|
|
327
|
+
compactionAfterFinalOpen = true;
|
|
317
328
|
}
|
|
318
329
|
if (event.type === "compaction_end" && event.willRetry === true) {
|
|
319
330
|
compactionStartedReceived = false;
|
|
320
331
|
afterCompactionSettlement = false;
|
|
321
332
|
}
|
|
333
|
+
if (event.type === "compaction_end" && event.willRetry !== true && compactionAfterFinalOpen) {
|
|
334
|
+
compactionAfterFinalOpen = false;
|
|
335
|
+
clearFinalDrainTimers();
|
|
336
|
+
startFinalDrain();
|
|
337
|
+
}
|
|
322
338
|
if (event.type === "turn_start" || event.type === "agent_start" || event.type === "auto_retry_start") {
|
|
323
339
|
queuedDrainHold = false;
|
|
324
340
|
}
|
|
@@ -331,7 +347,7 @@ export function runChildSession(input) {
|
|
|
331
347
|
agentSettledReceived = true;
|
|
332
348
|
afterCompactionSettlement = compactionStartedReceived;
|
|
333
349
|
}
|
|
334
|
-
applyChildLifecycle(lifecycleAction);
|
|
350
|
+
applyChildLifecycle(lifecycleAction, event);
|
|
335
351
|
if (isChildWatchdogStatusEvent(event)) {
|
|
336
352
|
if (!input.childWatchdog)
|
|
337
353
|
return;
|
|
@@ -100,7 +100,7 @@ export interface SubagentWaitDeps {
|
|
|
100
100
|
};
|
|
101
101
|
/** Injectable provider protocol surfaces for deterministic tests. */
|
|
102
102
|
backgroundWork?: {
|
|
103
|
-
snapshot(
|
|
103
|
+
snapshot(sessionIds: readonly string[], nowMs: number): BackgroundWorkSnapshot;
|
|
104
104
|
wakeChannels(): readonly string[];
|
|
105
105
|
};
|
|
106
106
|
/**
|
|
@@ -111,6 +111,7 @@ export interface SubagentWaitDeps {
|
|
|
111
111
|
*/
|
|
112
112
|
events?: WaitEventBus;
|
|
113
113
|
}
|
|
114
|
+
export declare function backgroundWorkForSession(deps: Pick<SubagentWaitDeps, "state" | "backgroundWork">, nowMs: number): BackgroundWorkSnapshot;
|
|
114
115
|
export declare function waitRunScopes(deps: Pick<SubagentWaitDeps, "asyncDirRoot" | "resultsDir" | "nestedRootRunId">): Array<{
|
|
115
116
|
asyncDirRoot: string;
|
|
116
117
|
resultsDir: string;
|
|
@@ -181,11 +181,14 @@ function hasSupervisorTool(run) {
|
|
|
181
181
|
function backgroundWorkIdentity(item) {
|
|
182
182
|
return `${item.provider}\0${item.sessionId}\0${item.id}`;
|
|
183
183
|
}
|
|
184
|
-
function backgroundWorkForSession(deps, nowMs) {
|
|
184
|
+
export function backgroundWorkForSession(deps, nowMs) {
|
|
185
185
|
const sessionId = deps.state.currentSessionId;
|
|
186
186
|
if (!sessionId)
|
|
187
187
|
throw new Error("bg_wait requires an active session identity to scope background work safely.");
|
|
188
|
-
|
|
188
|
+
// Providers may key items by the session file path or by getSessionId(); accept both.
|
|
189
|
+
const piSessionId = deps.state.supervisorOwnerSessionId;
|
|
190
|
+
const sessionIds = piSessionId ? [sessionId, piSessionId] : [sessionId];
|
|
191
|
+
return deps.backgroundWork?.snapshot(sessionIds, nowMs) ?? snapshotBackgroundWork(sessionIds, nowMs);
|
|
189
192
|
}
|
|
190
193
|
export function waitRunScopes(deps) {
|
|
191
194
|
return [
|
|
@@ -12,6 +12,35 @@ const TOOL_CALL_INDEX_DIR = "@tool-calls";
|
|
|
12
12
|
function isTerminalState(state) {
|
|
13
13
|
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped" || state === "rejected";
|
|
14
14
|
}
|
|
15
|
+
function isCorruptStatusError(error, statusPath) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
return message.startsWith(`Failed to parse async status file '${statusPath}'`)
|
|
18
|
+
|| message.startsWith(`Failed to validate async status file '${statusPath}'`);
|
|
19
|
+
}
|
|
20
|
+
// A corrupt indexed status is retried on every read. Emit a warning only when
|
|
21
|
+
// its observed file identity or parse outcome changes, like active-run diagnostics.
|
|
22
|
+
const reportedCorruptTerminalStatus = new Map();
|
|
23
|
+
function warnCorruptTerminalStatus(runId, statusPath, error, onCorruptStatus) {
|
|
24
|
+
let fileSignature = "unavailable";
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.statSync(statusPath);
|
|
27
|
+
fileSignature = `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.ino}`;
|
|
28
|
+
}
|
|
29
|
+
catch (statError) {
|
|
30
|
+
if (statError.code !== "ENOENT")
|
|
31
|
+
throw statError;
|
|
32
|
+
}
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
const signature = `${fileSignature}\n${message}`;
|
|
35
|
+
if (reportedCorruptTerminalStatus.get(statusPath) === signature)
|
|
36
|
+
return;
|
|
37
|
+
reportedCorruptTerminalStatus.set(statusPath, signature);
|
|
38
|
+
const warning = `Skipping indexed terminal run '${runId}' with corrupt status at '${statusPath}': ${message}`;
|
|
39
|
+
if (onCorruptStatus)
|
|
40
|
+
onCorruptStatus(warning);
|
|
41
|
+
else
|
|
42
|
+
console.error(`[pi-subagents] ${warning}`);
|
|
43
|
+
}
|
|
15
44
|
function indexRoot(asyncDirRoot) {
|
|
16
45
|
return path.join(asyncDirRoot, TERMINAL_RUN_INDEX_DIR);
|
|
17
46
|
}
|
|
@@ -154,7 +183,17 @@ export function readRecentTerminalRunIndex(asyncDirRoot, options = {}) {
|
|
|
154
183
|
continue;
|
|
155
184
|
}
|
|
156
185
|
const asyncDir = path.join(asyncDirRoot, entry.runId);
|
|
157
|
-
const
|
|
186
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
187
|
+
let status;
|
|
188
|
+
try {
|
|
189
|
+
status = readStatus(asyncDir);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (!isCorruptStatusError(error, statusPath))
|
|
193
|
+
throw error;
|
|
194
|
+
warnCorruptTerminalStatus(entry.runId, statusPath, error, options.onCorruptStatus);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
158
197
|
if (!status || !isTerminalState(status.state) || status.sessionId !== entry.sessionId || (status.runId && status.runId !== entry.runId)) {
|
|
159
198
|
removeInvalidMarker(marker);
|
|
160
199
|
continue;
|
|
@@ -561,6 +561,8 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
561
561
|
let agentSettledReceived = false;
|
|
562
562
|
let queuedDrainHold = false;
|
|
563
563
|
let compactionStartedReceived = false;
|
|
564
|
+
let terminalAgentEndReceived = false;
|
|
565
|
+
let compactionAfterFinalOpen = false;
|
|
564
566
|
let finalDrainTimer;
|
|
565
567
|
let finalHardFinishTimer;
|
|
566
568
|
let watchdogTailTimer;
|
|
@@ -608,7 +610,8 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
608
610
|
finalDrainTimer = setTimeout(() => {
|
|
609
611
|
if (lifecycleFinished || sessionSettled)
|
|
610
612
|
return;
|
|
611
|
-
|
|
613
|
+
// Pi runs auto-compaction after the final agent_end and only then emits agent_settled.
|
|
614
|
+
if (capture.finalDrainHeld() || observeQueuedDrainHold() || compactionAfterFinalOpen) {
|
|
612
615
|
finalDrainTimer = undefined;
|
|
613
616
|
armFinalDrainTimer();
|
|
614
617
|
return;
|
|
@@ -623,9 +626,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
623
626
|
return;
|
|
624
627
|
settle(undefined, true);
|
|
625
628
|
}, HARD_FINISH_MS);
|
|
626
|
-
finalHardFinishTimer.unref?.();
|
|
627
629
|
}, FINAL_STOP_GRACE_MS);
|
|
628
|
-
finalDrainTimer.unref?.();
|
|
629
630
|
};
|
|
630
631
|
function armWatchdogTail() {
|
|
631
632
|
if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || sessionSettled)
|
|
@@ -644,12 +645,20 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
644
645
|
}, childWatchdog?.watchdogTailTimeoutMs ?? 120_000);
|
|
645
646
|
watchdogTailTimer.unref?.();
|
|
646
647
|
}
|
|
647
|
-
const applyChildLifecycle = (action) => {
|
|
648
|
+
const applyChildLifecycle = (action, event) => {
|
|
648
649
|
if (action === "cancel-drain") {
|
|
649
|
-
|
|
650
|
+
// After a clean final stop, a non-retrying agent_end may be the child's last event
|
|
651
|
+
// (Pi may not emit agent_settled), so it re-arms the drain instead of cancelling it.
|
|
652
|
+
terminalAgentEndReceived = event?.type === "agent_end" && event.willRetry !== true && cleanTerminalAssistantStopReceived;
|
|
650
653
|
agentSettledReceived = false;
|
|
651
654
|
clearFinalDrainTimers();
|
|
652
655
|
clearWatchdogTailTimer();
|
|
656
|
+
if (terminalAgentEndReceived) {
|
|
657
|
+
startFinalDrain();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
cleanTerminalAssistantStopReceived = false;
|
|
661
|
+
compactionAfterFinalOpen = false;
|
|
653
662
|
return;
|
|
654
663
|
}
|
|
655
664
|
if (action === "start-drain")
|
|
@@ -925,11 +934,18 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
925
934
|
compactionStartedReceived = true;
|
|
926
935
|
if (agentSettledReceived)
|
|
927
936
|
afterCompactionSettlement = true;
|
|
937
|
+
if (terminalAgentEndReceived)
|
|
938
|
+
compactionAfterFinalOpen = true;
|
|
928
939
|
}
|
|
929
940
|
if (evt.type === "compaction_end" && evt.willRetry === true) {
|
|
930
941
|
compactionStartedReceived = false;
|
|
931
942
|
afterCompactionSettlement = false;
|
|
932
943
|
}
|
|
944
|
+
if (evt.type === "compaction_end" && evt.willRetry !== true && compactionAfterFinalOpen) {
|
|
945
|
+
compactionAfterFinalOpen = false;
|
|
946
|
+
clearFinalDrainTimers();
|
|
947
|
+
startFinalDrain();
|
|
948
|
+
}
|
|
933
949
|
if (evt.type === "turn_start" || evt.type === "agent_start" || evt.type === "auto_retry_start") {
|
|
934
950
|
queuedDrainHold = false;
|
|
935
951
|
}
|
|
@@ -956,7 +972,7 @@ async function runSingleAttempt(runtimeCwd, agent, task, model, options, shared)
|
|
|
956
972
|
agentSettledReceived = true;
|
|
957
973
|
afterCompactionSettlement = compactionStartedReceived;
|
|
958
974
|
}
|
|
959
|
-
applyChildLifecycle(lifecycleAction);
|
|
975
|
+
applyChildLifecycle(lifecycleAction, evt);
|
|
960
976
|
if (isChildWatchdogStatusEvent(evt)) {
|
|
961
977
|
if (!childWatchdog)
|
|
962
978
|
return;
|
|
@@ -17,7 +17,7 @@ import { buildDoctorReport } from "../../extension/doctor.js";
|
|
|
17
17
|
import { readSubagentGuide } from "../../extension/subagent-guide.js";
|
|
18
18
|
import { isWorkflowScriptPath, normalizePublicSubagentExecution, validateWorkflowCapacityOverrides } from "../../extension/public-execution.js";
|
|
19
19
|
import { readReplyWorkflowScript } from "../../extension/reply-workflow-script.js";
|
|
20
|
-
import { disabledFeatureNotice, disabledFeatureUseError, resolveDisabledFeatureSurface } from "../../shared/disabled-features.js";
|
|
20
|
+
import { disabledFeatureNotice, disabledFeatureSectionNotice, disabledFeatureUseError, resolveDisabledFeatureSurface } from "../../shared/disabled-features.js";
|
|
21
21
|
import { runSync } from "./execution.js";
|
|
22
22
|
import { commandAction } from "./command-action.js";
|
|
23
23
|
import { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.js";
|
|
@@ -6689,10 +6689,7 @@ export function createSubagentExecutor(deps) {
|
|
|
6689
6689
|
if (action === "guide") {
|
|
6690
6690
|
try {
|
|
6691
6691
|
const guide = readSubagentGuide(paramsWithResolvedCwd.topic);
|
|
6692
|
-
const
|
|
6693
|
-
const fullNotice = topic === "tool-reference" ? disabledFeatureNotice(disabledFeatures) : undefined;
|
|
6694
|
-
// Sections are capped at 8,000 chars, so they get a one-line pointer instead of the full list.
|
|
6695
|
-
const notice = fullNotice && section !== undefined ? "Some options and actions documented here are disabled by config in this session; guide topic tool-reference lists them." : fullNotice;
|
|
6692
|
+
const notice = paramsWithResolvedCwd.topic?.includes("/") ? disabledFeatureSectionNotice(disabledFeatures) : disabledFeatureNotice(disabledFeatures);
|
|
6696
6693
|
return {
|
|
6697
6694
|
content: [{ type: "text", text: notice ? `${notice}\n\n${guide}` : guide }],
|
|
6698
6695
|
details: { mode: "management", results: [] },
|
|
@@ -45,7 +45,7 @@ function escapeXmlAttr(value) {
|
|
|
45
45
|
function inheritedCapabilityCeiling(inherited) {
|
|
46
46
|
return inherited?.capabilityCeiling;
|
|
47
47
|
}
|
|
48
|
-
/** Environment values external child extensions read
|
|
48
|
+
/** Environment values external child extensions read in the runner, which hosts only children. */
|
|
49
49
|
function childProcessEnv(input, toolPlan) {
|
|
50
50
|
const env = {};
|
|
51
51
|
env[PI_SUBAGENT_EXTENSION_BINDINGS_ENV] = encodeExtensionBindings(input.extensionBindings);
|
|
@@ -222,7 +222,10 @@ export function buildInProcessChildLaunch(input) {
|
|
|
222
222
|
requiredExtensions: toolPlan.requiredExtensions,
|
|
223
223
|
ambientExtensions,
|
|
224
224
|
hooks: capturedHooks.hooks,
|
|
225
|
-
|
|
225
|
+
// A foreground child gets only its own bindings, so it never sees those of the session that launched it.
|
|
226
|
+
...(input.host === "runner"
|
|
227
|
+
? { processEnv: childProcessEnv(input, toolPlan) }
|
|
228
|
+
: { processEnv: { [PI_SUBAGENT_EXTENSION_BINDINGS_ENV]: encodeExtensionBindings(input.extensionBindings) }, restoreProcessEnv: true }),
|
|
226
229
|
runtime: config,
|
|
227
230
|
noSkills: !input.inheritSkills,
|
|
228
231
|
noContextFiles: !input.inheritProjectContext,
|
|
@@ -80,6 +80,12 @@ export interface ChildSessionLaunch {
|
|
|
80
80
|
* window one at a time. An undefined value removes the variable.
|
|
81
81
|
*/
|
|
82
82
|
processEnv?: Record<string, string | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Restore the hosting process's previous values of `processEnv` once that
|
|
85
|
+
* window ends, so a child hosted in the parent's own process leaves the
|
|
86
|
+
* parent's environment as it found it.
|
|
87
|
+
*/
|
|
88
|
+
restoreProcessEnv?: boolean;
|
|
83
89
|
/** The typed runtime config the hooks were built from; informational for factories. */
|
|
84
90
|
runtime: ChildRuntimeConfig;
|
|
85
91
|
onExtensionError?: (error: ChildSessionExtensionError) => void;
|