pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import {
|
|
11
12
|
formatUnresolvedMcpDirectToolSelectors,
|
|
12
13
|
resolveMcpDirectToolResolution,
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
import {
|
|
17
18
|
TEMP_ROOT_DIR,
|
|
18
19
|
type JsonSchemaObject,
|
|
19
|
-
type
|
|
20
|
+
type LaunchResolvedChildExtensions,
|
|
20
21
|
} from "../../shared/types.ts";
|
|
21
22
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
22
23
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
@@ -60,6 +61,41 @@ const FAST_MODE_ALLOWED_MODELS = new Set([
|
|
|
60
61
|
"openai-codex/gpt-5.6-sol",
|
|
61
62
|
]);
|
|
62
63
|
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
64
|
+
const PI_BUILTIN_TOOL_NAMES = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
65
|
+
const REPOSITORY_INSPECTION_TOOLS = new Set(["read", "grep", "find", "ls", "bash", "powershell"]);
|
|
66
|
+
const REVIEW_OR_SCOUT_AGENT_PATTERN = /\b(?:reviewer|scout)\b/i;
|
|
67
|
+
// These providers come from child hooks, not the host's builtin tool registry.
|
|
68
|
+
const NATIVE_COORDINATION_TOOL_NAMES = new Set(["subagent", "contact_supervisor", "subagent_supervisor"]);
|
|
69
|
+
|
|
70
|
+
export function isReviewOrScoutLaneAgent(agentName: string | undefined): boolean {
|
|
71
|
+
return typeof agentName === "string" && REVIEW_OR_SCOUT_AGENT_PATTERN.test(agentName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function missingPermittedRepositoryInspectionTools(
|
|
75
|
+
unavailableHostBuiltins: readonly string[],
|
|
76
|
+
excludeTools: readonly string[] = [],
|
|
77
|
+
): string[] {
|
|
78
|
+
const excluded = new Set(excludeTools);
|
|
79
|
+
return unavailableHostBuiltins.filter((tool) => REPOSITORY_INSPECTION_TOOLS.has(tool) && !excluded.has(tool));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formatReviewLaneToolContractFailure(input: {
|
|
83
|
+
agentName?: string;
|
|
84
|
+
missingTools: readonly string[];
|
|
85
|
+
requestedTools?: readonly string[];
|
|
86
|
+
effectiveTools: readonly string[];
|
|
87
|
+
ceilingSources?: readonly string[];
|
|
88
|
+
excludeTools?: readonly string[];
|
|
89
|
+
}): string {
|
|
90
|
+
const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
|
|
91
|
+
return [
|
|
92
|
+
`${subject}: tool contract could not be satisfied; host runtime does not provide permitted required repository tools [${input.missingTools.join(", ")}].`,
|
|
93
|
+
`Requested tool names: ${input.requestedTools ? `[${input.requestedTools.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${input.effectiveTools.join(", ")}].`,
|
|
94
|
+
...(input.ceilingSources?.length ? [`Active capability ceiling sources: [${input.ceilingSources.join(", ")}].`] : []),
|
|
95
|
+
...(input.excludeTools?.length ? [`Explicit excludeTools: [${input.excludeTools.join(", ")}].`] : []),
|
|
96
|
+
"This is a lane infrastructure failure, not a completed review/scout result.",
|
|
97
|
+
].join(" ");
|
|
98
|
+
}
|
|
63
99
|
|
|
64
100
|
export function deriveForkPromptCacheKey(parentSessionId: string | undefined): string | undefined {
|
|
65
101
|
const parent = parentSessionId?.trim();
|
|
@@ -151,6 +187,15 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
151
187
|
agentName?: string;
|
|
152
188
|
permissionRules?: PermissionRules;
|
|
153
189
|
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
190
|
+
/**
|
|
191
|
+
* When provided, child tool plans intersect declared builtin tools with
|
|
192
|
+
* this set. Tools the agent declares but the host does not provide are
|
|
193
|
+
* omitted with a non-fatal warning (tracked in `unavailableHostBuiltins`).
|
|
194
|
+
* Review/scout lanes fail closed when a requested, still-permitted
|
|
195
|
+
* repository inspection tool is among those host omissions. Intentionally
|
|
196
|
+
* empty or ceiling-restricted allowlists are not a minimum-tool contract.
|
|
197
|
+
*/
|
|
198
|
+
hostAvailableBuiltins?: readonly string[];
|
|
154
199
|
}
|
|
155
200
|
|
|
156
201
|
export interface PiLaunchToolPlan {
|
|
@@ -174,6 +219,8 @@ export interface PiLaunchToolPlan {
|
|
|
174
219
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
175
220
|
/** Non-fatal launch warnings; they do not change behavior. */
|
|
176
221
|
warnings: string[];
|
|
222
|
+
/** Builtin tools the agent declared but the host runtime does not provide. */
|
|
223
|
+
unavailableHostBuiltins: string[];
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
function extensionIdentifier(value: string): string {
|
|
@@ -214,7 +261,7 @@ export function projectLaunchResolvedChildExtensions(
|
|
|
214
261
|
| "extensionArgs"
|
|
215
262
|
| "disableAmbientExtensions"
|
|
216
263
|
>,
|
|
217
|
-
):
|
|
264
|
+
): LaunchResolvedChildExtensions {
|
|
218
265
|
const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
|
|
219
266
|
const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
|
|
220
267
|
const effective = boundedExtensionIdentifiers(toolPlan.extensionArgs);
|
|
@@ -289,6 +336,31 @@ export function resolvePermissionSystemExtension(): string | undefined {
|
|
|
289
336
|
return undefined;
|
|
290
337
|
}
|
|
291
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Extract the names of builtin tools the host provides. Use this to pass
|
|
341
|
+
* `hostAvailableBuiltins` to `resolvePiLaunchToolPlan` so child tool plans
|
|
342
|
+
* intersect declared agent tools with what the host actually supports.
|
|
343
|
+
*
|
|
344
|
+
* Returns `undefined` when builtin tool discovery fails or yields nothing,
|
|
345
|
+
* so callers skip the intersection (fail-safe to allowing all declared tools).
|
|
346
|
+
* This handles test mocks without proper tool registration and hosts whose
|
|
347
|
+
* getAllTools() throws before extensions load.
|
|
348
|
+
*/
|
|
349
|
+
export function getHostBuiltinToolNames(pi: Pick<ExtensionAPI, "getAllTools">): string[] | undefined {
|
|
350
|
+
try {
|
|
351
|
+
const builtins = pi
|
|
352
|
+
.getAllTools()
|
|
353
|
+
.filter((tool) => {
|
|
354
|
+
const source = (tool.sourceInfo as { source?: string } | undefined)?.source;
|
|
355
|
+
return source === "builtin" || (source === "auto" && PI_BUILTIN_TOOL_NAMES.has(tool.name));
|
|
356
|
+
})
|
|
357
|
+
.map((tool) => tool.name);
|
|
358
|
+
return builtins.length > 0 ? builtins : undefined;
|
|
359
|
+
} catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
292
364
|
export function resolvePiLaunchToolPlan(
|
|
293
365
|
input: ResolvePiLaunchToolPlanInput,
|
|
294
366
|
): PiLaunchToolPlan {
|
|
@@ -300,17 +372,27 @@ export function resolvePiLaunchToolPlan(
|
|
|
300
372
|
capabilityCeiling?.allowedTools === undefined
|
|
301
373
|
? undefined
|
|
302
374
|
: new Set(capabilityCeiling.allowedTools);
|
|
375
|
+
const hostAvailableSet =
|
|
376
|
+
input.hostAvailableBuiltins === undefined
|
|
377
|
+
? undefined
|
|
378
|
+
: new Set(input.hostAvailableBuiltins);
|
|
303
379
|
const requestedBuiltinTools =
|
|
304
380
|
input.tools?.filter(
|
|
305
381
|
(tool) =>
|
|
306
382
|
!(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
|
|
307
383
|
) ?? [];
|
|
384
|
+
if (input.requireReadTool && hostAvailableSet && !hostAvailableSet.has("read")) {
|
|
385
|
+
const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Host runtime does not provide required tool 'read'${agentLabel} for lazy skill loading.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
308
390
|
if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
|
|
309
391
|
throw new Error(
|
|
310
392
|
`Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`,
|
|
311
393
|
);
|
|
312
394
|
}
|
|
313
|
-
const
|
|
395
|
+
const ceilingFilteredBuiltinTools =
|
|
314
396
|
input.tools === undefined
|
|
315
397
|
? allowedToolSet
|
|
316
398
|
? [...allowedToolSet]
|
|
@@ -322,6 +404,12 @@ export function resolvePiLaunchToolPlan(
|
|
|
322
404
|
? ["read", ...requestedBuiltinTools]
|
|
323
405
|
: requestedBuiltinTools
|
|
324
406
|
).filter((tool) => !allowedToolSet || allowedToolSet.has(tool));
|
|
407
|
+
const declaredBuiltinTools = hostAvailableSet
|
|
408
|
+
? ceilingFilteredBuiltinTools.filter((tool) => hostAvailableSet.has(tool) || NATIVE_COORDINATION_TOOL_NAMES.has(tool))
|
|
409
|
+
: ceilingFilteredBuiltinTools;
|
|
410
|
+
const unavailableHostBuiltins = hostAvailableSet
|
|
411
|
+
? ceilingFilteredBuiltinTools.filter((tool) => !hostAvailableSet.has(tool) && !NATIVE_COORDINATION_TOOL_NAMES.has(tool))
|
|
412
|
+
: [];
|
|
325
413
|
const excludeTools = [...new Set((input.excludeTools ?? []).map((tool) => tool.trim()).filter(Boolean))];
|
|
326
414
|
const excludedToolSet = new Set(excludeTools);
|
|
327
415
|
const effectiveDeclaredBuiltinTools = declaredBuiltinTools.filter((tool) => !excludedToolSet.has(tool));
|
|
@@ -330,6 +418,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
330
418
|
!excludedToolSet.has("subagent") &&
|
|
331
419
|
(!allowedToolSet || allowedToolSet.has("subagent"))
|
|
332
420
|
);
|
|
421
|
+
if (effectiveDeclaredBuiltinTools.includes("subagent_supervisor") && !fanoutAuthorized) {
|
|
422
|
+
throw new Error("Tool 'subagent_supervisor' requires fanout authorization: include 'subagent' in the effective tools allowlist or enable allowNestedSubagents.");
|
|
423
|
+
}
|
|
333
424
|
const toolExtensionPaths: string[] = capabilityCeiling?.denyExtensions
|
|
334
425
|
? []
|
|
335
426
|
: (input.tools ?? []).filter(
|
|
@@ -370,8 +461,8 @@ export function resolvePiLaunchToolPlan(
|
|
|
370
461
|
...internalTools,
|
|
371
462
|
]),
|
|
372
463
|
];
|
|
373
|
-
//
|
|
374
|
-
//
|
|
464
|
+
// Upward contact stays in the --tools allowlist but is not a strict
|
|
465
|
+
// requirement: children register contact_supervisor at runtime through
|
|
375
466
|
// the native supervisor channel (or pi-intercom). The pre-0.50 bridge always
|
|
376
467
|
// appended intercom alongside contact_supervisor, so that exact pairing is
|
|
377
468
|
// legacy plumbing, not a user demand for an external intercom provider;
|
|
@@ -437,6 +528,32 @@ export function resolvePiLaunchToolPlan(
|
|
|
437
528
|
]),
|
|
438
529
|
]
|
|
439
530
|
: undefined;
|
|
531
|
+
const missingPermittedRepositoryTools = input.tools !== undefined
|
|
532
|
+
? missingPermittedRepositoryInspectionTools(unavailableHostBuiltins, excludeTools)
|
|
533
|
+
: [];
|
|
534
|
+
if (missingPermittedRepositoryTools.length > 0 && isReviewOrScoutLaneAgent(input.agentName)) {
|
|
535
|
+
throw new Error(formatReviewLaneToolContractFailure({
|
|
536
|
+
agentName: input.agentName,
|
|
537
|
+
missingTools: missingPermittedRepositoryTools,
|
|
538
|
+
requestedTools: requestedToolNames,
|
|
539
|
+
effectiveTools: effectiveToolAllowlist,
|
|
540
|
+
ceilingSources: capabilityCeiling?.sources,
|
|
541
|
+
excludeTools,
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
// Host pruning also happens without a ceiling (and therefore without an
|
|
545
|
+
// audit). Use the existing non-fatal launch warnings rather than inventing
|
|
546
|
+
// a ceiling or treating the requested allowlist as a minimum requirement.
|
|
547
|
+
if (unavailableHostBuiltins.length > 0) {
|
|
548
|
+
const subject = input.agentName ? `Agent '${input.agentName}'` : "Subagent";
|
|
549
|
+
warnings.push(
|
|
550
|
+
`${subject}: host runtime tool availability omitted [${unavailableHostBuiltins.join(", ")}]. `
|
|
551
|
+
+ `Requested tool names: ${requestedToolNames ? `[${requestedToolNames.join(", ")}]` : "not explicitly specified"}; effective tool allowlist: [${effectiveToolAllowlist.join(", ")}]. `
|
|
552
|
+
+ (capabilityCeiling ? `Active capability ceiling sources: [${capabilityCeiling.sources.join(", ") || "unknown source"}]. ` : "")
|
|
553
|
+
+ (excludeTools.length > 0 ? `Explicit excludeTools: [${excludeTools.join(", ")}]. ` : "")
|
|
554
|
+
+ "This is a non-fatal tool-plan diagnostic, not verification of the child's runtime tool menu.",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
440
557
|
const capabilityAudit = capabilityCeiling
|
|
441
558
|
? ({
|
|
442
559
|
ceiling: capabilityCeiling,
|
|
@@ -474,6 +591,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
474
591
|
capabilityCeilingAgentRestrictionSources(capabilityCeiling),
|
|
475
592
|
}
|
|
476
593
|
: {}),
|
|
594
|
+
...(unavailableHostBuiltins.length > 0 ? { unavailableHostBuiltins } : {}),
|
|
477
595
|
} satisfies SubagentCapabilityAudit)
|
|
478
596
|
: undefined;
|
|
479
597
|
return {
|
|
@@ -495,6 +613,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
495
613
|
extensionArgs,
|
|
496
614
|
disableAmbientExtensions,
|
|
497
615
|
warnings,
|
|
616
|
+
unavailableHostBuiltins,
|
|
498
617
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
499
618
|
};
|
|
500
619
|
}
|
|
@@ -26,7 +26,7 @@ export function planCompletionEvidence(input: {
|
|
|
26
26
|
mutationAttemptObserved: boolean;
|
|
27
27
|
mutationEvidence?: TrackedMutationEvidence;
|
|
28
28
|
arbiterRescued?: boolean;
|
|
29
|
-
|
|
29
|
+
agentContractEnabled: boolean;
|
|
30
30
|
}): CompletionEvidencePlan {
|
|
31
31
|
const guardBlocked = input.guard?.blocked === true;
|
|
32
32
|
const guardTriggered = input.guardTriggered
|
|
@@ -59,7 +59,7 @@ export function planCompletionEvidence(input: {
|
|
|
59
59
|
mutationExpected,
|
|
60
60
|
mutationAttempted,
|
|
61
61
|
fileMutation,
|
|
62
|
-
legacyFailureError: guardTriggered && !input.
|
|
62
|
+
legacyFailureError: guardTriggered && !input.agentContractEnabled
|
|
63
63
|
? MISSING_IMPLEMENTATION_MUTATION_ERROR
|
|
64
64
|
: undefined,
|
|
65
65
|
};
|
|
@@ -13,6 +13,7 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
13
13
|
"web_search",
|
|
14
14
|
"fetch_content",
|
|
15
15
|
"get_search_content",
|
|
16
|
+
"source_check",
|
|
16
17
|
"intercom",
|
|
17
18
|
"contact_supervisor",
|
|
18
19
|
"structured_output",
|
|
@@ -97,10 +98,12 @@ export function validateImplementationToolContract(input: {
|
|
|
97
98
|
const declaredMutationToolsWereRemoved = requestedMutationTools.length > 0 && !hasBuiltinMutationTool(input.tools);
|
|
98
99
|
const configuredExtensionCapability = (input.configuredExtensions?.length ?? 0) > 0 && !declaredMutationToolsWereRemoved;
|
|
99
100
|
if (hasMutationToolCapability(input.tools, input.mcpDirectTools) || configuredExtensionCapability) return undefined;
|
|
100
|
-
const intent = classifyTaskMutationIntent(input.agent, input.task);
|
|
101
|
+
const intent = classifyTaskMutationIntent(input.acceptanceRole === "writer" ? "worker" : input.agent, input.task);
|
|
101
102
|
if (intent.kind === "read-only") return undefined;
|
|
102
|
-
const writerTaskMayMutate =
|
|
103
|
-
|
|
103
|
+
const writerTaskMayMutate = input.acceptanceRole === "writer"
|
|
104
|
+
? true
|
|
105
|
+
: isWriterRole(input.agent, input.acceptanceRole)
|
|
106
|
+
&& (taskMayMutate(input.task) || WRITER_DELIVERY_PATTERN.test(input.task));
|
|
104
107
|
if (intent.kind !== "implementation" && !writerTaskMayMutate && !declaredMutationToolsWereRemoved) return undefined;
|
|
105
108
|
return `Agent '${input.agent}' was given an implementation task, but its tool allowlist has no mutation-capable tools. Add bash, edit, write, or another mutation-capable tool to the agent, or use a read-only task/agent.`;
|
|
106
109
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentConfig } from "../../agents/agents.ts";
|
|
2
|
+
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
3
|
+
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
4
|
+
import { buildSkillInjection } from "../../agents/skills.ts";
|
|
5
|
+
import { injectOutputPathSystemPrompt } from "./single-output.ts";
|
|
6
|
+
|
|
7
|
+
export interface EffectiveSystemPromptInput {
|
|
8
|
+
/** Agent as handed to the child, including runtime-declared overlays such as the Intercom bridge. */
|
|
9
|
+
agent: AgentConfig;
|
|
10
|
+
resolvedSkills: Parameters<typeof buildSkillInjection>[0];
|
|
11
|
+
/** Directory that scopes memory and refinement lookups. */
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Omit when the caller injects the output path through another channel. */
|
|
14
|
+
outputPath?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function appendSection(prompt: string, section: string): string {
|
|
18
|
+
return prompt ? `${prompt}\n\n${section}` : section;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Child system prompt in the order preflight and every execution path hash:
|
|
23
|
+
* base prompt, skills, memory, refinement overlay, output path. Runtime
|
|
24
|
+
* acceptance prose is appended later and stays outside launch identity.
|
|
25
|
+
*/
|
|
26
|
+
export function buildEffectiveSystemPrompt(input: EffectiveSystemPromptInput): string {
|
|
27
|
+
let prompt = input.agent.systemPrompt?.trim() ?? "";
|
|
28
|
+
if (input.resolvedSkills.length > 0) prompt = appendSection(prompt, buildSkillInjection(input.resolvedSkills));
|
|
29
|
+
const memoryInjection = buildAgentMemoryInjection(input.agent, input.cwd);
|
|
30
|
+
if (memoryInjection) prompt = appendSection(prompt, memoryInjection);
|
|
31
|
+
prompt = appendAgentRefinementOverlay(prompt, { cwd: input.cwd, agentName: input.agent.name });
|
|
32
|
+
return injectOutputPathSystemPrompt(prompt, input.outputPath, input.agent);
|
|
33
|
+
}
|
|
@@ -3,7 +3,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { finished } from "node:stream/promises";
|
|
6
|
-
import type { ExternalProcessStatus } from "../../shared/types.ts";
|
|
6
|
+
import type { ExternalProcessStatus, ProcessTreeTerminal } from "../../shared/types.ts";
|
|
7
7
|
import { createOwnedProcessTreeController, type OwnedProcessTreeController } from "../background/owned-process-tree.ts";
|
|
8
8
|
import { omitExtensionBindingsEnv } from "./extension-bindings.ts";
|
|
9
9
|
import {
|
|
@@ -141,7 +141,7 @@ function classifyInvalidation(error: string): "auth" | "permission" | "launch" {
|
|
|
141
141
|
return "launch";
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<
|
|
144
|
+
function terminateExternalProcessTree(pid: number, controller: OwnedProcessTreeController): Promise<ProcessTreeTerminal> {
|
|
145
145
|
if (process.platform !== "win32") return controller.terminate();
|
|
146
146
|
return new Promise((resolve) => {
|
|
147
147
|
const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
@@ -239,7 +239,7 @@ export function runExternalCli(input: {
|
|
|
239
239
|
let settled = false;
|
|
240
240
|
let processTree: OwnedProcessTreeController | undefined;
|
|
241
241
|
let processPid: number | undefined;
|
|
242
|
-
let termination: Promise<
|
|
242
|
+
let termination: Promise<ProcessTreeTerminal> | undefined;
|
|
243
243
|
const flushProgress = () => {
|
|
244
244
|
if (!latestProgress) return;
|
|
245
245
|
input.onParserProgress?.(latestProgress);
|
|
@@ -381,8 +381,7 @@ export function runExternalCli(input: {
|
|
|
381
381
|
input.registerTimeout?.(undefined);
|
|
382
382
|
input.registerStop?.(undefined);
|
|
383
383
|
void (async () => {
|
|
384
|
-
|
|
385
|
-
else if (processTree) await processTree.finishAfterWriterClose();
|
|
384
|
+
const treeProof = termination ? await termination : processTree ? await processTree.finishAfterWriterClose() : undefined;
|
|
386
385
|
const endedAt = Date.now();
|
|
387
386
|
const externalProcess: ExternalProcessStatus = {
|
|
388
387
|
...initialProcess,
|
|
@@ -398,15 +397,18 @@ export function runExternalCli(input: {
|
|
|
398
397
|
input.onProcess?.(externalProcess);
|
|
399
398
|
const stderr = stderrTail.text().trim();
|
|
400
399
|
const parserFailure = parserError?.message ?? (parserTerminal?.state === "failed" ? parserTerminal.error ?? "External CLI parser reported terminal failure." : undefined);
|
|
400
|
+
const treeFailure = process.platform !== "win32" && treeProof?.state === "unknown"
|
|
401
|
+
? `Process-tree cleanup failed: ${treeProof.reason}.`
|
|
402
|
+
: undefined;
|
|
401
403
|
const error = stopped
|
|
402
404
|
? input.stopMessage ?? "Subagent stopped by user."
|
|
403
405
|
: timedOut
|
|
404
406
|
? input.timeoutMessage ?? "Subagent timed out."
|
|
405
|
-
: spawnError?.message ?? parserFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
|
|
407
|
+
: spawnError?.message ?? parserFailure ?? treeFailure ?? (exitCode === 0 ? undefined : stderr || `External CLI exited with code ${exitCode}.`);
|
|
406
408
|
if (error && input.preflight && !parserError) invalidateExternalCliPreflight(input.command, input.preflight, classifyInvalidation(error));
|
|
407
409
|
const result: ExternalCliRunResult = {
|
|
408
410
|
output: (!parserError && parserTerminal?.state === "completed" ? parserTerminal.output ?? "" : stdoutTail.text()).trim(),
|
|
409
|
-
exitCode: timedOut || stopped || spawnError || parserFailure ? 1 : exitCode,
|
|
411
|
+
exitCode: timedOut || stopped || spawnError || parserFailure || treeFailure ? 1 : exitCode,
|
|
410
412
|
...(error ? { error } : {}),
|
|
411
413
|
...(timedOut ? { timedOut: true } : {}),
|
|
412
414
|
...(stopped ? { stopped: true } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AsyncStatus,
|
|
3
|
-
|
|
3
|
+
HostStepNode,
|
|
4
4
|
HostStepState,
|
|
5
5
|
HostStepVerdict,
|
|
6
6
|
WorkflowGraphNode,
|
|
@@ -57,7 +57,7 @@ function assertTimestamp(value: unknown, field: string, source: string, required
|
|
|
57
57
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid host step '${source}': ${field} must be a non-negative safe integer.`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function assertFreshness(value: unknown, source: string): asserts value is
|
|
60
|
+
function assertFreshness(value: unknown, source: string): asserts value is HostStepNode["freshness"] {
|
|
61
61
|
if (!isRecord(value)) throw new Error(`Invalid host step '${source}': freshness must be an object.`);
|
|
62
62
|
const unknownFields = Object.keys(value).filter((field) => !FRESHNESS_FIELDS.has(field));
|
|
63
63
|
if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': freshness has unsupported fields: ${unknownFields.join(", ")}.`);
|
|
@@ -70,7 +70,7 @@ function assertFreshness(value: unknown, source: string): asserts value is HostS
|
|
|
70
70
|
* Validate the persisted host-step contract. This deliberately rejects unknown
|
|
71
71
|
* fields and unbounded values so status and receipt loaders fail closed.
|
|
72
72
|
*/
|
|
73
|
-
export function assertHostStepNode(value: unknown, source = "status"): asserts value is
|
|
73
|
+
export function assertHostStepNode(value: unknown, source = "status"): asserts value is HostStepNode {
|
|
74
74
|
if (!isRecord(value)) throw new Error(`Invalid host step '${source}': expected an object.`);
|
|
75
75
|
const unknownFields = Object.keys(value).filter((field) => !HOST_STEP_FIELDS.has(field));
|
|
76
76
|
if (unknownFields.length > 0) throw new Error(`Invalid host step '${source}': unsupported fields: ${unknownFields.join(", ")}.`);
|
|
@@ -105,7 +105,7 @@ export function assertHostStepNode(value: unknown, source = "status"): asserts v
|
|
|
105
105
|
assertTimestamp(value.deadlineAt, "deadlineAt", source);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function parseHostStepNode(value: unknown, source = "status"):
|
|
108
|
+
export function parseHostStepNode(value: unknown, source = "status"): HostStepNode {
|
|
109
109
|
assertHostStepNode(value, source);
|
|
110
110
|
return {
|
|
111
111
|
...value,
|
|
@@ -113,7 +113,7 @@ export function parseHostStepNode(value: unknown, source = "status"): HostStepNo
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
export function assertUniqueHostStepIds(hostSteps: readonly
|
|
116
|
+
export function assertUniqueHostStepIds(hostSteps: readonly HostStepNode[], source = "status"): void {
|
|
117
117
|
const ids = new Set<string>();
|
|
118
118
|
for (const hostStep of hostSteps) {
|
|
119
119
|
if (ids.has(hostStep.id)) throw new Error(`Invalid host step '${source}': duplicate host step id '${hostStep.id}'.`);
|
|
@@ -122,11 +122,11 @@ export function assertUniqueHostStepIds(hostSteps: readonly HostStepNodeV1[], so
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Return only valid host nodes so an untrusted in-memory projection fails closed. */
|
|
125
|
-
export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined):
|
|
125
|
+
export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): HostStepNode[] {
|
|
126
126
|
const nodes = graph?.nodes ?? [];
|
|
127
127
|
const nodeIdCounts = new Map<string, number>();
|
|
128
128
|
for (const node of nodes) nodeIdCounts.set(node.id, (nodeIdCounts.get(node.id) ?? 0) + 1);
|
|
129
|
-
const hostSteps:
|
|
129
|
+
const hostSteps: HostStepNode[] = [];
|
|
130
130
|
for (const [index, node] of nodes.entries()) {
|
|
131
131
|
if (node.kind !== "host-step") continue;
|
|
132
132
|
try {
|
|
@@ -147,7 +147,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
|
|
|
147
147
|
if (!Array.isArray(graph.nodes)) throw new Error(`Invalid host step '${source}.workflowGraph': nodes must be an array.`);
|
|
148
148
|
const hostStepCount = graph.nodes.filter((node) => node.kind === "host-step").length;
|
|
149
149
|
if (hostStepCount > HOST_STEP_MAX_COUNT) throw new Error(`Invalid host step '${source}': workflowGraph contains more than ${HOST_STEP_MAX_COUNT} host steps.`);
|
|
150
|
-
const hostSteps:
|
|
150
|
+
const hostSteps: HostStepNode[] = [];
|
|
151
151
|
for (const [index, node] of graph.nodes.entries()) {
|
|
152
152
|
if (node.kind !== "host-step") continue;
|
|
153
153
|
const hostStep = parseHostStepNode(node.hostStep, `${source}.workflowGraph.nodes[${index}].hostStep`);
|
|
@@ -161,7 +161,7 @@ export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | unde
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export function hostStepWorkflowNode(hostStep:
|
|
164
|
+
export function hostStepWorkflowNode(hostStep: HostStepNode): WorkflowGraphNode {
|
|
165
165
|
assertHostStepNode(hostStep, hostStep.id);
|
|
166
166
|
return {
|
|
167
167
|
id: hostStep.id,
|
|
@@ -172,7 +172,7 @@ export function hostStepWorkflowNode(hostStep: HostStepNodeV1): WorkflowGraphNod
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
function workflowNodeStatus(hostStep:
|
|
175
|
+
function workflowNodeStatus(hostStep: HostStepNode): WorkflowNodeStatus {
|
|
176
176
|
if (hostStep.state === "pending") return "pending";
|
|
177
177
|
if (hostStep.state === "running") return "running";
|
|
178
178
|
if (hostStep.state === "cancelled") return "stopped";
|
|
@@ -187,7 +187,7 @@ function workflowNodeStatus(hostStep: HostStepNodeV1): WorkflowNodeStatus {
|
|
|
187
187
|
*/
|
|
188
188
|
export function upsertHostStep(input: {
|
|
189
189
|
status: AsyncStatus;
|
|
190
|
-
hostStep:
|
|
190
|
+
hostStep: HostStepNode;
|
|
191
191
|
persist: (status: AsyncStatus) => void;
|
|
192
192
|
}): AsyncStatus {
|
|
193
193
|
const hostStep = parseHostStepNode(input.hostStep, input.hostStep.id);
|
|
@@ -4,6 +4,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
5
5
|
import { Type, type Static } from "typebox";
|
|
6
6
|
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
7
|
+
import { opencodeSessionHeaders } from "../../shared/opencode-session-headers.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* LLM intent arbiter for the completion mutation guard.
|
|
@@ -56,6 +57,8 @@ interface ArbiterRuntime {
|
|
|
56
57
|
/** Registered provider stream, usable only when its api matches the model. */
|
|
57
58
|
registeredStreamFn?: StreamFn;
|
|
58
59
|
registeredApi?: string;
|
|
60
|
+
/** Session id for OpenCode session-routing headers, when the caller has one. */
|
|
61
|
+
sessionId?: string;
|
|
59
62
|
timeoutMs: number;
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -76,9 +79,14 @@ export interface TaskMutationArbiterOptions {
|
|
|
76
79
|
const DEFAULT_ARBITER_TIMEOUT_MS = 10_000;
|
|
77
80
|
|
|
78
81
|
type RegistryModel = ReturnType<NonNullable<ExtensionContext["modelRegistry"]["find"]>>;
|
|
82
|
+
/** Model services the arbiter needs, plus the caller's session id for OpenCode session-routing headers. */
|
|
83
|
+
export type ArbiterModelContext = Pick<ExtensionContext, "model" | "modelRegistry"> & {
|
|
84
|
+
/** Session id the headers attach to (the child's, captured by detached runners; the parent's, passed by foreground callers). */
|
|
85
|
+
sessionId?: string;
|
|
86
|
+
};
|
|
79
87
|
|
|
80
88
|
function resolveArbiterModel(
|
|
81
|
-
ctx:
|
|
89
|
+
ctx: ArbiterModelContext,
|
|
82
90
|
options?: TaskMutationArbiterOptions,
|
|
83
91
|
): NonNullable<RegistryModel> | null {
|
|
84
92
|
const registry = ctx.modelRegistry as {
|
|
@@ -98,7 +106,7 @@ function resolveArbiterModel(
|
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
function resolveArbiterRuntime(
|
|
101
|
-
ctx:
|
|
109
|
+
ctx: ArbiterModelContext,
|
|
102
110
|
options?: TaskMutationArbiterOptions,
|
|
103
111
|
): ArbiterRuntime | null {
|
|
104
112
|
const model = resolveArbiterModel(ctx, options);
|
|
@@ -112,14 +120,15 @@ function resolveArbiterRuntime(
|
|
|
112
120
|
explicitStreamFn: options?.streamFn,
|
|
113
121
|
registeredStreamFn: registered?.streamSimple,
|
|
114
122
|
registeredApi: registered?.api,
|
|
123
|
+
sessionId: ctx.sessionId,
|
|
115
124
|
timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
async function resolveArbiterAuth(
|
|
120
|
-
ctx:
|
|
129
|
+
ctx: ArbiterModelContext,
|
|
121
130
|
model: RegistryModel,
|
|
122
|
-
): Promise<ArbiterAuth> {
|
|
131
|
+
): Promise<ArbiterAuth | undefined> {
|
|
123
132
|
const registry = ctx.modelRegistry as {
|
|
124
133
|
getApiKeyAndHeaders?: (m: RegistryModel) => Promise<{
|
|
125
134
|
ok: boolean;
|
|
@@ -135,14 +144,14 @@ async function resolveArbiterAuth(
|
|
|
135
144
|
if (!registry.getApiKeyAndHeaders) return {};
|
|
136
145
|
try {
|
|
137
146
|
const auth = await registry.getApiKeyAndHeaders(model);
|
|
138
|
-
if (auth.ok === false) return
|
|
147
|
+
if (auth.ok === false) return undefined;
|
|
139
148
|
return {
|
|
140
149
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
141
150
|
...(auth.headers ? { headers: auth.headers } : {}),
|
|
142
151
|
...(auth.env ? { env: auth.env } : {}),
|
|
143
152
|
};
|
|
144
153
|
} catch {
|
|
145
|
-
return
|
|
154
|
+
return undefined;
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -150,12 +159,13 @@ async function resolveArbiterAuth(
|
|
|
150
159
|
function authWrappedStreamFn(
|
|
151
160
|
base: StreamFn,
|
|
152
161
|
auth: ArbiterAuth,
|
|
162
|
+
sessionId: string | undefined,
|
|
153
163
|
): StreamFn {
|
|
154
164
|
return (model, context, streamOptions) => base(model, context, {
|
|
155
165
|
...(streamOptions ?? {}),
|
|
156
166
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
157
167
|
...(auth.env || streamOptions?.env ? { env: { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } } : {}),
|
|
158
|
-
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
168
|
+
headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
159
169
|
});
|
|
160
170
|
}
|
|
161
171
|
|
|
@@ -199,7 +209,7 @@ async function runArbitration(
|
|
|
199
209
|
tools: [tool],
|
|
200
210
|
},
|
|
201
211
|
convertToLlm,
|
|
202
|
-
...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
|
|
212
|
+
...agentStreamOptions(authWrappedStreamFn(streamFn, auth, runtime.sessionId)),
|
|
203
213
|
getApiKey: (providerName) =>
|
|
204
214
|
providerName === runtime.model.provider ? auth.apiKey : undefined,
|
|
205
215
|
beforeToolCall: async ({ toolCall }) =>
|
|
@@ -230,9 +240,9 @@ async function runArbitration(
|
|
|
230
240
|
}
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
/** Create a memoized arbiter bound to the
|
|
243
|
+
/** Create a memoized arbiter bound to the supplied model services, or undefined when disabled/unavailable. */
|
|
234
244
|
export function createTaskMutationArbiter(
|
|
235
|
-
ctx:
|
|
245
|
+
ctx: ArbiterModelContext,
|
|
236
246
|
options?: TaskMutationArbiterOptions,
|
|
237
247
|
): TaskMutationArbiter | undefined {
|
|
238
248
|
if (process.env.PI_SUBAGENTS_LLM_INTENT_ARBITER === "0") return undefined;
|
|
@@ -244,7 +254,7 @@ export function createTaskMutationArbiter(
|
|
|
244
254
|
const cached = cache.get(key);
|
|
245
255
|
if (cached) return cached;
|
|
246
256
|
const auth = await resolveArbiterAuth(ctx, runtime.model);
|
|
247
|
-
const verdict = await runArbitration(runtime, auth, task);
|
|
257
|
+
const verdict = auth ? await runArbitration(runtime, auth, task) : "unavailable";
|
|
248
258
|
if (cache.size > 200) cache.clear();
|
|
249
259
|
cache.set(key, verdict);
|
|
250
260
|
return verdict;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
1
|
+
import { splitKnownThinkingSuffix as splitThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
3
|
import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
4
|
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
@@ -14,9 +14,7 @@ interface ModelAttemptSummary {
|
|
|
14
14
|
usage?: Usage;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
return splitKnownThinkingSuffix(model);
|
|
19
|
-
}
|
|
17
|
+
export { splitThinkingSuffix };
|
|
20
18
|
|
|
21
19
|
/** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
|
|
22
20
|
export function formatSubagentModelVerificationError(
|
|
@@ -38,7 +36,7 @@ export function formatSubagentModelVerificationError(
|
|
|
38
36
|
const expectedFullIdLeaf = expectedEntry.fullId.slice(expectedEntry.fullId.lastIndexOf("/") + 1);
|
|
39
37
|
if (expectedIdLeaf === observedBase || expectedFullIdLeaf === observedBase) return undefined;
|
|
40
38
|
}
|
|
41
|
-
return `model_verification_failed: child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'.`;
|
|
39
|
+
return `model_verification_failed: native Pi child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'. If you have independently verified this response ID identifies the requested model, declare the exact mapping in modelResponseAliases in ~/.pi/agent/extensions/subagent/config.json (see docs/configuration.md#modelresponsealiases). Use the resolved provider/model ID without its thinking suffix as the key. This leaves the outgoing request unchanged. Configuration changes affect new independent native runs; resumed native runs retain their launch-time declaration. External CLI adapters do not use this setting.`;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
/** Sentinel model value requesting that a subagent inherit the parent session's model. */
|
|
@@ -537,6 +535,7 @@ export function buildModelCandidates(
|
|
|
537
535
|
}
|
|
538
536
|
|
|
539
537
|
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
538
|
+
/^REQUEST_LIMIT_EXCEEDED$/,
|
|
540
539
|
/rate\s*limit/i,
|
|
541
540
|
/usage\s*limit/i,
|
|
542
541
|
/too many requests/i,
|
|
@@ -544,6 +543,8 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
544
543
|
/quota/i,
|
|
545
544
|
/billing/i,
|
|
546
545
|
/credit/i,
|
|
546
|
+
// OpenRouter can return only a status-prefixed body, without auth-related prose.
|
|
547
|
+
/^\s*401\s*:/,
|
|
547
548
|
/auth(?:entication)?/i,
|
|
548
549
|
/unauthori[sz]ed/i,
|
|
549
550
|
/forbidden/i,
|
|
@@ -608,8 +609,13 @@ export function isRetryableModelFailureAttempt(input: { error: string | undefine
|
|
|
608
609
|
return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
|
|
609
610
|
}
|
|
610
611
|
|
|
612
|
+
// Request-shape failures can match broad fallback signals such as "upstream",
|
|
613
|
+
// but do not establish that the model is unhealthy for subsequent requests.
|
|
614
|
+
const REQUEST_SHAPE_FAILURE_PATTERN = /\b(?:bad[ _]request|invalid[ _]argument|invalid_request_error)\b/i;
|
|
615
|
+
|
|
611
616
|
export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
|
|
612
|
-
if (!model || !isRetryableModelFailure(error)) return;
|
|
617
|
+
if (!model || !error || !isRetryableModelFailure(error) || isContextOverflow(error)) return;
|
|
618
|
+
if (REQUEST_SHAPE_FAILURE_PATTERN.test(error)) return;
|
|
613
619
|
const { provider, modelId } = parseModelKey(model);
|
|
614
620
|
recordModelFailure({ modelId, reason: error, ...(provider ? { provider } : {}) });
|
|
615
621
|
}
|