pi-subagents 0.65.0 → 0.65.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +16 -0
- package/docs/extension-api.md +3 -0
- package/docs/tool-reference.md +8 -2
- package/docs/workflows.md +8 -0
- package/package.json +3 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +5 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/api/preflight.ts +5 -1
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +31 -2
- package/src/extension/schemas.ts +1 -1
- package/src/extension/tool-description.ts +5 -1
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +22 -36
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +2 -1
- package/src/runs/background/runner-aliases.ts +32 -5
- package/src/runs/background/subagent-runner.ts +19 -0
- package/src/runs/foreground/execution.ts +15 -1
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +61 -34
- package/src/runs/shared/acceptance.ts +14 -1
- package/src/runs/shared/child-session.ts +13 -18
- package/src/runs/shared/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +31 -2
- package/src/runs/shared/nested-events.ts +3 -3
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree.ts +75 -10
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +8 -0
- package/src/shared/utils.ts +3 -0
- package/src/shared/watch-strategy.ts +2 -0
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/render.ts +21 -10
- package/src/workflows/scripted-workflow.ts +32 -6
- package/src/workflows/workflow-checklist.ts +4 -3
|
@@ -49,6 +49,7 @@ import { updateActiveRunIndex } from "../background/active-run-index.ts";
|
|
|
49
49
|
import { steeringReceipt } from "../background/steering.ts";
|
|
50
50
|
import { acquireActiveAsyncCapacity, ActiveAsyncCapacityError, getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession, transferActiveAsyncCapacity, type ActiveAsyncCapacityHandle } from "../background/active-async-capacity.ts";
|
|
51
51
|
import { isScheduledRunAction } from "../background/scheduled-runs.ts";
|
|
52
|
+
import { encodeIndexSegment } from "../background/index-segment.ts";
|
|
52
53
|
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
53
54
|
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
54
55
|
import { normalizeGateAcceptance, resolveAcceptanceReportMode, validateExecutionAcceptance } from "../shared/acceptance.ts";
|
|
@@ -60,6 +61,7 @@ import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBrid
|
|
|
60
61
|
import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
|
|
61
62
|
import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts";
|
|
62
63
|
import { claimRunFanoutBatch, claimRunFanoutBatchWithCommit, createRunFanoutBudget, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, readRunFanoutBudgetDescriptor, RunFanoutLimitError, writeRunFanoutBudgetDescriptor } from "../shared/run-fanout-budget.ts";
|
|
64
|
+
import { retainLiveForegroundNestedRoute } from "../../integrations/pi-web-session-liveness.ts";
|
|
63
65
|
import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
|
|
64
66
|
import { usageBudgetExceededMessage, usageBudgetState, validateUsageBudgetConfig } from "../shared/usage-budget.ts";
|
|
65
67
|
import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
@@ -438,6 +440,7 @@ interface ExecutorDeps {
|
|
|
438
440
|
allowMutatingManagementActions?: boolean;
|
|
439
441
|
activateSupervisorTransport?: () => void;
|
|
440
442
|
refreshResultDelivery?: () => void;
|
|
443
|
+
trackRetainedNestedRoute?: (rootRunId: string) => void;
|
|
441
444
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
442
445
|
/** Set when this executor runs inside a child session; carries the runtime settings the host passes instead of environment variables. */
|
|
443
446
|
childRuntime?: ChildRuntimeConfig;
|
|
@@ -526,9 +529,16 @@ function loadWorkflowScriptPath(params: SubagentParamsLike, runtimeCwd: string):
|
|
|
526
529
|
return { params: { ...rest, workflowScript } };
|
|
527
530
|
}
|
|
528
531
|
|
|
529
|
-
function removeForegroundControlIfIdle(state: SubagentState, runId: string): boolean {
|
|
532
|
+
export function removeForegroundControlIfIdle(state: SubagentState, runId: string, trackRetainedNestedRoute?: (rootRunId: string) => void): boolean {
|
|
530
533
|
const control = state.foregroundControls.get(runId);
|
|
531
534
|
if (control && (!foregroundSchedulingSettled(control) || (control.activeChildren?.size ?? 0) > 0)) return false;
|
|
535
|
+
if (control?.nestedRoute && trackRetainedNestedRoute) {
|
|
536
|
+
try {
|
|
537
|
+
if (retainLiveForegroundNestedRoute(state, control.nestedRoute)) trackRetainedNestedRoute(runId);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error(`Failed to retain live nested descendants for foreground run '${runId}':`, error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
532
542
|
state.foregroundControls.delete(runId);
|
|
533
543
|
if (state.lastForegroundControlId === runId) state.lastForegroundControlId = null;
|
|
534
544
|
return true;
|
|
@@ -723,7 +733,7 @@ function foregroundChildActivityFromProgress(progress: SingleResult["progress"]
|
|
|
723
733
|
};
|
|
724
734
|
}
|
|
725
735
|
|
|
726
|
-
function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; params: SubagentParamsLike; effectiveOutput?: string | boolean; effectiveOutputMode: OutputMode; extensionBindings?: ExtensionBindings }): void {
|
|
736
|
+
function rememberForegroundRun(state: SubagentState, input: { modelResponseAliases?: Record<string, string[]>; runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; results: SingleResult[]; params: SubagentParamsLike; effectiveOutput?: string | boolean; effectiveOutputMode: OutputMode; extensionBindings?: ExtensionBindings }): void {
|
|
727
737
|
state.foregroundRuns ??= new Map();
|
|
728
738
|
const previous = state.foregroundRuns.get(input.runId);
|
|
729
739
|
const updatedAt = Date.now();
|
|
@@ -735,6 +745,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
|
|
|
735
745
|
updatedAt,
|
|
736
746
|
children: input.results.map((result, index) => {
|
|
737
747
|
const resumeContract = omitUndefinedProperties({
|
|
748
|
+
modelResponseAliases: input.modelResponseAliases,
|
|
738
749
|
outputSchema: input.params.outputSchema,
|
|
739
750
|
agentContract: input.params.agentContract,
|
|
740
751
|
acceptance: input.params.acceptance,
|
|
@@ -1313,6 +1324,7 @@ function appendStepToAsyncChain(input: {
|
|
|
1313
1324
|
currentModelProvider: parentModel?.provider,
|
|
1314
1325
|
currentModel: parentModel,
|
|
1315
1326
|
modelScope: discoveredForAppend.modelScope,
|
|
1327
|
+
modelResponseAliases: input.deps.config.modelResponseAliases,
|
|
1316
1328
|
interactive: input.ctx.hasUI,
|
|
1317
1329
|
permissions: input.deps.config.permissions,
|
|
1318
1330
|
childRuntime: input.deps.childRuntime,
|
|
@@ -1694,6 +1706,7 @@ async function resumeExternalJobFollowUp(input: {
|
|
|
1694
1706
|
currentModelProvider: parentModel?.provider,
|
|
1695
1707
|
currentModel: parentModel,
|
|
1696
1708
|
modelScope: input.modelScope,
|
|
1709
|
+
modelResponseAliases: input.deps.config.modelResponseAliases,
|
|
1697
1710
|
interactive: input.ctx.hasUI,
|
|
1698
1711
|
permissions: input.deps.config.permissions,
|
|
1699
1712
|
childRuntime: input.deps.childRuntime,
|
|
@@ -1947,6 +1960,7 @@ async function resumeAsyncRun(input: {
|
|
|
1947
1960
|
currentModelProvider: parentModel?.provider,
|
|
1948
1961
|
currentModel: parentModel,
|
|
1949
1962
|
modelScope,
|
|
1963
|
+
modelResponseAliases: input.deps.config.modelResponseAliases,
|
|
1950
1964
|
interactive: input.ctx.hasUI,
|
|
1951
1965
|
permissions: input.deps.config.permissions,
|
|
1952
1966
|
childRuntime: input.deps.childRuntime,
|
|
@@ -2055,6 +2069,8 @@ async function resumeAsyncRun(input: {
|
|
|
2055
2069
|
currentModelProvider: parentModel?.provider,
|
|
2056
2070
|
currentModel: parentModel,
|
|
2057
2071
|
modelScope,
|
|
2072
|
+
// Absence in the retained contract is meaningful; never acquire current aliases.
|
|
2073
|
+
modelResponseAliases: recoveryDescriptor ? recoveryDescriptor.modelResponseAliases : foregroundContract?.modelResponseAliases,
|
|
2058
2074
|
interactive: input.ctx.hasUI,
|
|
2059
2075
|
permissions: input.deps.config.permissions,
|
|
2060
2076
|
childRuntime: input.deps.childRuntime,
|
|
@@ -3210,6 +3226,7 @@ async function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3210
3226
|
currentModelProvider: parentModel?.provider,
|
|
3211
3227
|
currentModel: parentModel,
|
|
3212
3228
|
modelScope: data.modelScope,
|
|
3229
|
+
modelResponseAliases: deps.config.modelResponseAliases,
|
|
3213
3230
|
interactive: ctx.hasUI,
|
|
3214
3231
|
permissions: deps.config.permissions,
|
|
3215
3232
|
childRuntime: deps.childRuntime,
|
|
@@ -3808,6 +3825,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3808
3825
|
}));
|
|
3809
3826
|
}
|
|
3810
3827
|
|
|
3828
|
+
const modelResponseAliases = deps.config.modelResponseAliases === undefined ? undefined : structuredClone(deps.config.modelResponseAliases);
|
|
3811
3829
|
const forwardSingleUpdate = onUpdate
|
|
3812
3830
|
? (update: AgentToolResult<Details>) => {
|
|
3813
3831
|
if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]);
|
|
@@ -3867,6 +3885,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3867
3885
|
thinkingCeiling: agentConfig.maxThinking,
|
|
3868
3886
|
extensionBindings: params.extensionBindings,
|
|
3869
3887
|
availableModels,
|
|
3888
|
+
modelResponseAliases,
|
|
3870
3889
|
preferredModelProvider: currentProvider,
|
|
3871
3890
|
modelScope: modelScopes,
|
|
3872
3891
|
skills: effectiveSkills,
|
|
@@ -3911,7 +3930,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3911
3930
|
try {
|
|
3912
3931
|
if (foregroundControl) finishForegroundChild(foregroundControl, 0);
|
|
3913
3932
|
} finally {
|
|
3914
|
-
removeForegroundControlIfIdle(deps.state, runId);
|
|
3933
|
+
removeForegroundControlIfIdle(deps.state, runId, deps.trackRetainedNestedRoute);
|
|
3915
3934
|
}
|
|
3916
3935
|
}
|
|
3917
3936
|
}
|
|
@@ -3984,7 +4003,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
3984
4003
|
usageBudget: usageBudgetState(data.usageBudget, totalCost),
|
|
3985
4004
|
...(worktreeHandoff?.reference ? { parallelHandoff: worktreeHandoff.reference } : {}),
|
|
3986
4005
|
}));
|
|
3987
|
-
rememberForegroundRun(deps.state, { runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, params, effectiveOutput, effectiveOutputMode, extensionBindings: params.extensionBindings });
|
|
4006
|
+
rememberForegroundRun(deps.state, { modelResponseAliases, runId, mode: "single", cwd: singleCwd, sessionId: data.parentSessionId, results: details.results, params, effectiveOutput, effectiveOutputMode, extensionBindings: params.extensionBindings });
|
|
3988
4007
|
|
|
3989
4008
|
const suppressRoutineResultIntercom = shouldSuppressRoutineResultIntercom({ suppressRoutineResultIntercom: params.suppressRoutineResultIntercom, results: [r] });
|
|
3990
4009
|
if (!r.detached && !r.interrupted && !suppressRoutineResultIntercom) {
|
|
@@ -4140,9 +4159,13 @@ function workflowChildResult(
|
|
|
4140
4159
|
? result.details.results[0].finalOutput
|
|
4141
4160
|
: receiptOutput;
|
|
4142
4161
|
const childError = result.details.results.map((child) => child.error).find((error): error is string => Boolean(error));
|
|
4143
|
-
const
|
|
4144
|
-
? `${childError}\n\n${receiptOutput}`
|
|
4162
|
+
const failureErrorBase = childError && receiptOutput
|
|
4163
|
+
? receiptOutput.includes(childError) ? receiptOutput : `${childError}\n\n${receiptOutput}`
|
|
4145
4164
|
: childError || receiptOutput || output || "Child run failed.";
|
|
4165
|
+
const savedOutputEvidence = [...new Set(result.details.results.map((child) => child.savedOutputPath).filter((value): value is string => Boolean(value)))]
|
|
4166
|
+
.filter((savedOutputPath) => !failureErrorBase.includes(savedOutputPath))
|
|
4167
|
+
.map((savedOutputPath) => `Saved output: ${savedOutputPath}`);
|
|
4168
|
+
const failureError = [failureErrorBase, ...savedOutputEvidence].join("\n");
|
|
4146
4169
|
const detached = result.details.results.some((child) => child.detached);
|
|
4147
4170
|
const interrupted = result.details.results.some((child) => child.interrupted);
|
|
4148
4171
|
const stopped = result.details.results.some((child) => child.stopped);
|
|
@@ -4514,12 +4537,13 @@ export function prepareWorkflowLaunchParams(
|
|
|
4514
4537
|
};
|
|
4515
4538
|
}
|
|
4516
4539
|
const control = mergeWorkflowControlOverrides(workflowDefaults.control, childParams.control as ControlConfig | undefined);
|
|
4540
|
+
const asyncOmitted = childParams.async === undefined && workflowDefaults.async === undefined;
|
|
4517
4541
|
const launchParams = {
|
|
4518
4542
|
...workflowDefaults,
|
|
4519
|
-
|
|
4543
|
+
...(options.externalAsyncRequired === true && asyncOmitted ? { async: true } : {}),
|
|
4520
4544
|
...childParams,
|
|
4521
4545
|
...(control !== undefined ? { control } : {}),
|
|
4522
|
-
...(
|
|
4546
|
+
...(asyncOmitted ? { workflowAwaitAsync: true } : {}),
|
|
4523
4547
|
...(options.missionDetached ? { mission: false } : {}),
|
|
4524
4548
|
workflowParentRunId: parentWorkflowRunId,
|
|
4525
4549
|
workflowKey,
|
|
@@ -4815,8 +4839,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4815
4839
|
workflowResource = { permit: workflowResourcePermit, ...consumed };
|
|
4816
4840
|
}
|
|
4817
4841
|
if (requestParams.workflowScript !== undefined && normalizedAction === undefined) {
|
|
4842
|
+
const foregroundWorkflowRunId = encodeIndexSegment(_id);
|
|
4818
4843
|
if (delegatedWorkflowPermit) {
|
|
4819
|
-
const permitError = validateWorkflowChildPermitRoot(delegatedWorkflowPermit,
|
|
4844
|
+
const permitError = validateWorkflowChildPermitRoot(delegatedWorkflowPermit, foregroundWorkflowRunId);
|
|
4820
4845
|
if (permitError) return buildRequestedModeError(requestParams, permitError);
|
|
4821
4846
|
if (requestParams.async !== false) return buildRequestedModeError(requestParams, "Workflow child permit supports foreground workflow roots only; set async:false.");
|
|
4822
4847
|
}
|
|
@@ -5359,7 +5384,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5359
5384
|
if (child.runId) workflowChildRunIds.set(key, child.runId);
|
|
5360
5385
|
const step = status.steps?.find((candidate) => candidate.workflowKey === key);
|
|
5361
5386
|
if (step) {
|
|
5362
|
-
step.async = Boolean(result.details.asyncId || result.details.asyncDir);
|
|
5387
|
+
step.async = step.async === true || Boolean(result.details.asyncId || result.details.asyncDir);
|
|
5363
5388
|
if (child.runId) step.runId = child.runId;
|
|
5364
5389
|
if (child.lane) step.lane = child.lane;
|
|
5365
5390
|
}
|
|
@@ -5468,7 +5493,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5468
5493
|
const { workflowScript: _workflowScript, action: _action, agent: _agent, task: _task, resume: _resume, tasks: _tasks, chain: _chain, concurrency: _concurrency, async: _async, foregroundOnly: _foregroundOnly, clarify: _clarify, timeoutMs: _timeoutMs, maxRuntimeMs: _maxRuntimeMs, usageBudget: _usageBudget, chatProgress: _chatProgress, missionId: _missionId, mission: _mission, preflight: _preflight, globalConcurrencyLimit: _globalConcurrencyLimit, maxSubagentSpawnsPerRun: _maxSubagentSpawnsPerRun, ...workflowChildDefaults } = requestParams;
|
|
5469
5494
|
const workflowOutput = typeof workflowChildDefaults.output === "string" || typeof workflowChildDefaults.output === "boolean" ? workflowChildDefaults.output : undefined;
|
|
5470
5495
|
const configuredOutputBaseDir = resolveConfiguredSingleRunOutputBaseDir(deps);
|
|
5471
|
-
const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir,
|
|
5496
|
+
const workflowAggregateOutputPath = resolveWorkflowAggregateOutputPath(workflowOutput, ctx.cwd, workflowCwd, resolveSingleRunOutputBaseDir(deps, workflowArtifactsDir, foregroundWorkflowRunId));
|
|
5472
5497
|
const claimedOutputPaths = new Map<string, string>();
|
|
5473
5498
|
const childOutputOverrides = new Map<string, string>();
|
|
5474
5499
|
const childOutputClaimPaths = new Map<string, string>();
|
|
@@ -5478,7 +5503,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5478
5503
|
const runHostCommand = workflowHostCommandRunner({
|
|
5479
5504
|
workflowCwd,
|
|
5480
5505
|
artifactsDir: workflowArtifactsDir,
|
|
5481
|
-
workflowRunId:
|
|
5506
|
+
workflowRunId: foregroundWorkflowRunId,
|
|
5482
5507
|
claimedOutputPaths,
|
|
5483
5508
|
producedOutputPaths: producedChildOutputPaths,
|
|
5484
5509
|
...(workflowResource ? { authorize: (key, params) => authorizeWorkflowResourceHost(workflowResource!.permit, key, params.command) } : {}),
|
|
@@ -5490,17 +5515,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5490
5515
|
: publicExecution ? undefined : runHostCommand;
|
|
5491
5516
|
const workflowHostSteps = new Map<string, HostStepNodeV1>();
|
|
5492
5517
|
let liveWorkflow: NonNullable<Details["workflow"]> = { trace: [], emits: [], console: [], ...(workflowResource ? { resource: workflowResource.provenance } : {}) };
|
|
5493
|
-
let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId:
|
|
5518
|
+
let liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false });
|
|
5494
5519
|
const workflowDeadlineAt = timeout === undefined ? undefined : Date.now() + timeout;
|
|
5495
5520
|
const workflowCapabilityCeiling = intersectSubagentCapabilityCeilings(requestParams.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(resolveCurrentSessionId(ctx.sessionManager)));
|
|
5496
5521
|
const sendWorkflowProgress = () => {
|
|
5497
|
-
const update = workflowChatProgressUpdate(
|
|
5522
|
+
const update = workflowChatProgressUpdate(foregroundWorkflowRunId, chatProgress, liveWorkflow, liveWorkflowChildren, workflowPreflight);
|
|
5498
5523
|
if (update) onUpdate?.(update);
|
|
5499
5524
|
};
|
|
5500
5525
|
try {
|
|
5501
5526
|
const workflow = await runWorkflowScript({
|
|
5502
5527
|
script: requestParams.workflowScript,
|
|
5503
|
-
...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit,
|
|
5528
|
+
...(delegatedWorkflowPermit ? { oneUsePermit: { claim: (key: string) => claimWorkflowChildPermit(delegatedWorkflowPermit, foregroundWorkflowRunId, key) } } : {}),
|
|
5504
5529
|
globalConcurrencyLimit: requestParams.globalConcurrencyLimit ?? deps.config.globalConcurrencyLimit,
|
|
5505
5530
|
timeoutMs: timeout,
|
|
5506
5531
|
signal,
|
|
@@ -5513,11 +5538,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5513
5538
|
const projectedTrace = annotateWorkflowPreflightTrace(trace, workflowPreflight);
|
|
5514
5539
|
const preflightWarnings = workflowPreflightWarnings(workflowPreflight, trace);
|
|
5515
5540
|
liveWorkflow = { ...liveWorkflow, trace: projectedTrace, ...(preflightWarnings.length ? { preflightWarnings } : {}) };
|
|
5516
|
-
liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId:
|
|
5541
|
+
liveWorkflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "running", inventoryComplete: false, trace });
|
|
5517
5542
|
sendWorkflowProgress();
|
|
5518
5543
|
},
|
|
5519
5544
|
admit: (calls) => {
|
|
5520
|
-
const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId:
|
|
5545
|
+
const outputClaims = workflowChildOutputClaims({ ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, workflowRunId: foregroundWorkflowRunId, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, state: deps.state, claimedOutputPaths, entries: calls });
|
|
5521
5546
|
if (outputClaims.error) throw new Error(outputClaims.error);
|
|
5522
5547
|
claimRunFanoutBatch(workflowFanoutBudget, calls.map(({ key }) => `workflow[${key}]`));
|
|
5523
5548
|
if (outputClaims.claims) applyWorkflowChildOutputClaims(claimedOutputPaths, outputClaims.claims);
|
|
@@ -5537,7 +5562,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5537
5562
|
if (budgetState?.exhausted) return workflowChildResult(key, buildRequestedModeError(childParams as SubagentParamsLike, usageBudgetExceededMessage(budgetState)), childParams, deps.state, { state: "partial", reason: "budget_exhausted" });
|
|
5538
5563
|
const childPhase = typeof childParams.phase === "string" && childParams.phase.trim() ? childParams.phase.trim() : undefined;
|
|
5539
5564
|
const childLabel = typeof childParams.label === "string" && childParams.label.trim() ? childParams.label.trim() : undefined;
|
|
5540
|
-
recordMissionWorkflowChild(missionBinding,
|
|
5565
|
+
recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
|
|
5541
5566
|
status: "running",
|
|
5542
5567
|
...(typeof childParams.agent === "string" && childParams.agent.trim() ? { agent: childParams.agent.trim() } : {}),
|
|
5543
5568
|
...(childLabel ? { label: childLabel } : {}),
|
|
@@ -5545,9 +5570,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5545
5570
|
heartbeat: { status: "running", ...(childPhase ? { phase: childPhase } : {}) },
|
|
5546
5571
|
});
|
|
5547
5572
|
let preparedChildParams: SubagentParamsLike | undefined;
|
|
5548
|
-
const result = await runMissionWorkflowChild(missionBinding,
|
|
5573
|
+
const result = await runMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, childPhase, () => {
|
|
5549
5574
|
const childRequest = bindMissionWorkflowChildAsyncLaunch(
|
|
5550
|
-
{ ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams, parentWorkflowRunId:
|
|
5575
|
+
{ ...prepareWorkflowChildLaunchParams({ workflowDefaults: workflowChildDefaults, childParams: delegatedWorkflowPermit ? { ...childParams, async: false } : childParams, parentWorkflowRunId: foregroundWorkflowRunId, workflowKey: key, ctxCwd: ctx.cwd, workflowCwd, artifactsDir: workflowArtifactsDir, aggregateOutputPath: workflowAggregateOutputPath, configuredOutputBaseDir, discoverAgents: discoverWorkflowAgents, agents: workflowAgents, workflowAgentScope: workflowChildDefaults.agentScope, outputOverride: childOutputOverrides.get(key), outputClaimPath: childOutputClaimPaths.get(key), options: { missionDetached: detachWorkflowChildMissions, suppressRoutineResultIntercom: chatProgress.mode === "live-card", runFanoutBudget: workflowFanoutBudget, parentDeadlineAt: workflowDeadlineAt, capabilityCeiling: workflowCapabilityCeiling } }), runFanoutAdmitted: admission.admitted },
|
|
5551
5576
|
missionBinding,
|
|
5552
5577
|
deps.asyncByDefault,
|
|
5553
5578
|
);
|
|
@@ -5559,9 +5584,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5559
5584
|
? resolveAgentName(childRequest.agent, discoverWorkflowAgents(childCwd, resolveExecutionAgentScope(childRequest.agentScope)).agents).agent
|
|
5560
5585
|
: undefined;
|
|
5561
5586
|
if (childAgent?.runner?.type === "external-cli" || childAgent?.runner?.type === "external-job") throw new Error("Workflow child permit supports native Pi children only.");
|
|
5562
|
-
workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId:
|
|
5587
|
+
workflowPermitContexts.set(childRequest, { child: { permit: delegatedWorkflowPermit, workflowRunId: foregroundWorkflowRunId, childKey: key } });
|
|
5563
5588
|
}
|
|
5564
|
-
workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding,
|
|
5589
|
+
workflowLaunchObservers.set(childRequest, (launch) => recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
|
|
5565
5590
|
status: "running",
|
|
5566
5591
|
agent: launch.agent,
|
|
5567
5592
|
...(launch.sessionFile ? { sessionPath: launch.sessionFile } : {}),
|
|
@@ -5570,7 +5595,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5570
5595
|
const progress = update.details.progress?.[0];
|
|
5571
5596
|
if (!progress) return;
|
|
5572
5597
|
const progressStatus = progress.status === "completed" ? "completed" : progress.status === "failed" ? "failed" : "running";
|
|
5573
|
-
recordMissionWorkflowChild(missionBinding,
|
|
5598
|
+
recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
|
|
5574
5599
|
status: progressStatus,
|
|
5575
5600
|
heartbeat: { status: progressStatus, ...(childPhase ? { phase: childPhase } : {}) },
|
|
5576
5601
|
});
|
|
@@ -5584,7 +5609,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5584
5609
|
const child = workflowChildResult(key, result, preparedChildParams ?? childParams, deps.state);
|
|
5585
5610
|
if (child.runId) workflowChildRunIds.set(key, child.runId);
|
|
5586
5611
|
const childStatus = missionWorkflowChildStatus(result);
|
|
5587
|
-
recordMissionWorkflowChild(missionBinding,
|
|
5612
|
+
recordMissionWorkflowChild(missionBinding, foregroundWorkflowRunId, key, {
|
|
5588
5613
|
status: childStatus,
|
|
5589
5614
|
...(child.runId ? { runId: child.runId } : {}),
|
|
5590
5615
|
...(result.details.results[0]?.agent ? { agent: result.details.results[0].agent } : {}),
|
|
@@ -5597,12 +5622,12 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5597
5622
|
},
|
|
5598
5623
|
status: async (keyOrRunId, workflowSignal) => workflowChildResult(keyOrRunId, await execute(randomUUID(), { action: "status", id: keyOrRunId }, workflowSignal, undefined, ctx, preserveActiveSession, workflowParentModel)),
|
|
5599
5624
|
resolveResume: (reference) => resolveKeyedWorkflowResume(reference, deps.state),
|
|
5600
|
-
steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId:
|
|
5625
|
+
steer: (key, message, options, workflowSignal) => steerWorkflowChildByKey({ state: deps.state, workflowRunId: foregroundWorkflowRunId, key, message, options, signal: workflowSignal, resolveRunId: () => workflowChildRunIds.get(key) }),
|
|
5601
5626
|
});
|
|
5602
5627
|
const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, workflow.trace, { settled: true });
|
|
5603
5628
|
const finalPreflightTrace = annotateWorkflowPreflightTrace(workflow.trace, workflowPreflight);
|
|
5604
|
-
const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId:
|
|
5605
|
-
const receipt = terminalWorkflowReceipt(
|
|
5629
|
+
const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "completed", inventoryComplete: true, trace: workflow.trace, children: workflow.children });
|
|
5630
|
+
const receipt = terminalWorkflowReceipt(foregroundWorkflowRunId, "complete", workflow.children, workflowChildren, undefined, [...workflowHostSteps.values()], workflowResource?.provenance);
|
|
5606
5631
|
const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.durationMs !== undefined ? ` in ${entry.durationMs}ms` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
|
|
5607
5632
|
const sections = [
|
|
5608
5633
|
...(workflowPreflight ? [formatWorkflowPreflight(workflowPreflight)] : []),
|
|
@@ -5620,11 +5645,11 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5620
5645
|
const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
|
|
5621
5646
|
return attachWorkflowMission(withRunFanoutBudget({
|
|
5622
5647
|
content: [{ type: "text", text: displayText }],
|
|
5623
|
-
details: compactOptional<Details>({ mode: "workflow", runId:
|
|
5648
|
+
details: compactOptional<Details>({ mode: "workflow", runId: foregroundWorkflowRunId, results: workflowDetailsResults(workflow.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { value: workflow.value, trace: finalPreflightTrace, emits: workflow.emits, console: workflow.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
|
|
5624
5649
|
}, workflowFanoutBudget));
|
|
5625
5650
|
} catch (error) {
|
|
5626
5651
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
5627
|
-
const text = workflowFailureMessage(error,
|
|
5652
|
+
const text = workflowFailureMessage(error, foregroundWorkflowRunId, partial.children);
|
|
5628
5653
|
const finalPreflightWarnings = workflowPreflightWarnings(workflowPreflight, partial.trace, { settled: true });
|
|
5629
5654
|
const finalPreflightTrace = annotateWorkflowPreflightTrace(partial.trace, workflowPreflight);
|
|
5630
5655
|
const traceLines = finalPreflightTrace.map((entry) => `- ${entry.operation} ${entry.key}: ${entry.state}${entry.runId ? ` (${entry.runId})` : ""}${entry.warning ? ` — ${entry.warning}` : ""}${entry.error ? ` — ${entry.error}` : ""}`);
|
|
@@ -5641,13 +5666,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5641
5666
|
const workflowText = sections.join("\n\n");
|
|
5642
5667
|
const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, workflowText, producedChildOutputPaths);
|
|
5643
5668
|
const displayText = appendWorkflowOutputWarning(workflowText, outputWarning);
|
|
5644
|
-
const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId:
|
|
5669
|
+
const workflowChildren = workflowChildSummary({ parentToolCallId: _id, workflowRunId: foregroundWorkflowRunId, workflowState: "failed", inventoryComplete: true, trace: partial.trace, children: partial.children });
|
|
5645
5670
|
const terminalOutcome = workflowFailureTerminalOutcome(error, partial.children, usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)));
|
|
5646
|
-
const receipt = terminalWorkflowReceipt(
|
|
5671
|
+
const receipt = terminalWorkflowReceipt(foregroundWorkflowRunId, "failed", partial.children, workflowChildren, terminalOutcome, [...workflowHostSteps.values()], workflowResource?.provenance);
|
|
5647
5672
|
return attachWorkflowMission(withRunFanoutBudget({
|
|
5648
5673
|
content: [{ type: "text", text: displayText }],
|
|
5649
5674
|
isError: true,
|
|
5650
|
-
details: compactOptional<Details>({ mode: "workflow", runId:
|
|
5675
|
+
details: compactOptional<Details>({ mode: "workflow", runId: foregroundWorkflowRunId, results: workflowDetailsResults(partial.children), ...(workflowPreflight ? { preflight: workflowPreflight } : {}), workflowChildren, totalChildUsage: sumResultsUsage(workflowResults), totalCost: sumResultsCost(workflowResults), usageBudget: usageBudgetState(workflowUsageBudget.budget, sumResultsCost(workflowResults)), workflow: { trace: finalPreflightTrace, emits: partial.emits, console: partial.console, ...(workflowResource ? { resource: workflowResource.provenance } : {}), ...(finalPreflightWarnings.length ? { preflightWarnings: finalPreflightWarnings } : {}), receipt }, chatProgress }),
|
|
5651
5676
|
}, workflowFanoutBudget));
|
|
5652
5677
|
}
|
|
5653
5678
|
}
|
|
@@ -6551,7 +6576,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6551
6576
|
|
|
6552
6577
|
let sessionRoot: string;
|
|
6553
6578
|
if (effectiveParams.sessionDir) {
|
|
6554
|
-
|
|
6579
|
+
// An explicit sessionDir is a root keyed by this launch's run id so
|
|
6580
|
+
// concurrent children resolve distinct per-child session files.
|
|
6581
|
+
sessionRoot = path.join(path.resolve(deps.expandTilde(effectiveParams.sessionDir)), runId);
|
|
6555
6582
|
} else {
|
|
6556
6583
|
const baseSessionRoot = deps.config.defaultSessionDir
|
|
6557
6584
|
? path.resolve(deps.expandTilde(deps.config.defaultSessionDir))
|
|
@@ -6902,7 +6929,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6902
6929
|
if (activeAsyncCapacity && !activeAsyncCapacity.owner.runnerStartedAt) activeAsyncCapacity.rollback();
|
|
6903
6930
|
if (foregroundControl) {
|
|
6904
6931
|
settleForegroundSchedulingOwner(foregroundControl);
|
|
6905
|
-
removeForegroundControlIfIdle(deps.state, runId);
|
|
6932
|
+
removeForegroundControlIfIdle(deps.state, runId, deps.trackRetainedNestedRoute);
|
|
6906
6933
|
}
|
|
6907
6934
|
}
|
|
6908
6935
|
|
|
@@ -202,10 +202,23 @@ export function normalizeGateAcceptance(gate: unknown, acceptance: AcceptanceInp
|
|
|
202
202
|
return normalized.error ? { ok: false, error: normalized.error } : { ok: true, acceptance: normalized.value as AcceptanceInput };
|
|
203
203
|
}
|
|
204
204
|
if (typeof gate !== "string" || !gate.trim()) return { ok: false, error: "gate must be a non-empty command string." };
|
|
205
|
-
if (acceptance !== undefined) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." };
|
|
205
|
+
if (acceptance !== undefined && acceptance !== false) return { ok: false, error: "gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(gate, acceptance) };
|
|
206
206
|
return { ok: true, acceptance: { level: "verified", verify: [{ id: "gate", command: gate.trim() }] } };
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function describeGateAcceptanceConflict(gate: unknown, acceptance: unknown): string {
|
|
210
|
+
const render = (value: unknown): string => {
|
|
211
|
+
let encoded: string;
|
|
212
|
+
try {
|
|
213
|
+
encoded = JSON.stringify(value) ?? String(value);
|
|
214
|
+
} catch {
|
|
215
|
+
encoded = String(value);
|
|
216
|
+
}
|
|
217
|
+
return encoded.length > 120 ? `${encoded.slice(0, 120)}...` : encoded;
|
|
218
|
+
};
|
|
219
|
+
return ` Both fields were present: gate=${render(gate)} acceptance=${render(acceptance)}.`;
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
function explicitAcceptanceCanDisable(explicit: AcceptanceConfig): boolean {
|
|
210
223
|
return explicit.level === "none" && typeof explicit.reason === "string" && explicit.reason.trim().length > 0;
|
|
211
224
|
}
|
|
@@ -115,17 +115,6 @@ export interface DefaultChildSessionFactoryOptions {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
type ModelRuntimeInstance = Awaited<ReturnType<PiCodingAgentModule["ModelRuntime"]["create"]>>;
|
|
118
|
-
type QueuedProviderRegistration = { name: string; config: Parameters<ModelRuntimeInstance["registerProvider"]>[1]; extensionPath: string };
|
|
119
|
-
type QueuedNativeProviderRegistration = { provider: Parameters<ModelRuntimeInstance["registerNativeProvider"]>[0]; extensionPath: string };
|
|
120
|
-
|
|
121
|
-
interface LoaderWithExtensions {
|
|
122
|
-
getExtensions(): {
|
|
123
|
-
runtime: {
|
|
124
|
-
pendingProviderRegistrations: QueuedProviderRegistration[];
|
|
125
|
-
pendingNativeProviderRegistrations: QueuedNativeProviderRegistration[];
|
|
126
|
-
};
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
118
|
|
|
130
119
|
/** One launch at a time from env application through `session_start`, so parallel launches never observe each other's `processEnv` while their extensions load and start. */
|
|
131
120
|
let loading: Promise<unknown> = Promise.resolve();
|
|
@@ -151,25 +140,29 @@ function applyProcessEnv(values: Record<string, string | undefined> | undefined)
|
|
|
151
140
|
}
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
function flushQueuedProviderRegistrations(loader:
|
|
143
|
+
async function flushQueuedProviderRegistrations(loader: InstanceType<PiCodingAgentModule["DefaultResourceLoader"]>, modelRuntime: ModelRuntimeInstance, onError: ((error: ChildSessionExtensionError) => void) | undefined): Promise<void> {
|
|
155
144
|
if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function") return;
|
|
156
|
-
const { runtime } =
|
|
157
|
-
|
|
145
|
+
const { runtime } = loader.getExtensions();
|
|
146
|
+
let registered = false;
|
|
147
|
+
for (const { name, config, extensionPath } of runtime.pendingProviderRegistrations ?? []) {
|
|
158
148
|
try {
|
|
159
149
|
modelRuntime.registerProvider(name, config);
|
|
150
|
+
registered = true;
|
|
160
151
|
} catch (error) {
|
|
161
152
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
162
153
|
}
|
|
163
154
|
}
|
|
164
|
-
runtime.pendingProviderRegistrations = [];
|
|
165
|
-
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations) {
|
|
155
|
+
if (Array.isArray(runtime.pendingProviderRegistrations)) runtime.pendingProviderRegistrations = [];
|
|
156
|
+
for (const { provider, extensionPath } of runtime.pendingNativeProviderRegistrations ?? []) {
|
|
166
157
|
try {
|
|
167
158
|
modelRuntime.registerNativeProvider(provider);
|
|
159
|
+
registered = true;
|
|
168
160
|
} catch (error) {
|
|
169
161
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
170
162
|
}
|
|
171
163
|
}
|
|
172
|
-
runtime.pendingNativeProviderRegistrations = [];
|
|
164
|
+
if (Array.isArray(runtime.pendingNativeProviderRegistrations)) runtime.pendingNativeProviderRegistrations = [];
|
|
165
|
+
if (registered) await modelRuntime.refresh({ allowNetwork: false });
|
|
173
166
|
}
|
|
174
167
|
|
|
175
168
|
/**
|
|
@@ -196,6 +189,8 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
196
189
|
const modelRuntime = await sharedRuntime(pi);
|
|
197
190
|
const agentDir = getAgentDir();
|
|
198
191
|
const settingsManager = pi.SettingsManager.create(launch.cwd, agentDir);
|
|
192
|
+
// Headless sessions skip Pi's CLI theme setup; extensions still need ctx.ui.theme.
|
|
193
|
+
if (typeof pi.initTheme === "function") pi.initTheme(settingsManager.getTheme());
|
|
199
194
|
const loader = new pi.DefaultResourceLoader({
|
|
200
195
|
cwd: launch.cwd,
|
|
201
196
|
agentDir,
|
|
@@ -214,7 +209,7 @@ export function createDefaultChildSessionFactory(options: DefaultChildSessionFac
|
|
|
214
209
|
applyProcessEnv(launch.processEnv);
|
|
215
210
|
if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length)) launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
|
|
216
211
|
await loader.reload();
|
|
217
|
-
flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
|
|
212
|
+
await flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError);
|
|
218
213
|
const sessionManager = launch.storage.kind === "file"
|
|
219
214
|
? pi.SessionManager.open(launch.storage.sessionFile, undefined, launch.cwd)
|
|
220
215
|
: launch.storage.kind === "dir"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { Agent,
|
|
3
|
-
import {
|
|
4
|
-
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import type { Agent, AgentTool, StreamFn } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
4
|
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
6
5
|
import { Type, type Static } from "typebox";
|
|
7
6
|
import { agentStreamOptions } from "../../shared/agent-stream-options.ts";
|
|
@@ -52,7 +51,11 @@ export function mapArbiterDecision(
|
|
|
52
51
|
|
|
53
52
|
interface ArbiterRuntime {
|
|
54
53
|
model: NonNullable<RegistryModel>;
|
|
55
|
-
|
|
54
|
+
/** Explicit override; it always wins over a registered provider stream. */
|
|
55
|
+
explicitStreamFn?: StreamFn;
|
|
56
|
+
/** Registered provider stream, usable only when its api matches the model. */
|
|
57
|
+
registeredStreamFn?: StreamFn;
|
|
58
|
+
registeredApi?: string;
|
|
56
59
|
timeoutMs: number;
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -103,15 +106,12 @@ function resolveArbiterRuntime(
|
|
|
103
106
|
const registry = ctx.modelRegistry as {
|
|
104
107
|
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|
|
105
108
|
};
|
|
106
|
-
const modelApi = (model as { api?: string }).api;
|
|
107
109
|
const registered = registry.getRegisteredProviderConfig?.(model.provider);
|
|
108
|
-
const baseStreamFn = options?.streamFn
|
|
109
|
-
?? (registered?.streamSimple && registered.api === modelApi
|
|
110
|
-
? registered.streamSimple
|
|
111
|
-
: streamSimple);
|
|
112
110
|
return {
|
|
113
111
|
model,
|
|
114
|
-
|
|
112
|
+
explicitStreamFn: options?.streamFn,
|
|
113
|
+
registeredStreamFn: registered?.streamSimple,
|
|
114
|
+
registeredApi: registered?.api,
|
|
115
115
|
timeoutMs: options?.timeoutMs ?? DEFAULT_ARBITER_TIMEOUT_MS,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -164,6 +164,15 @@ async function runArbitration(
|
|
|
164
164
|
auth: ArbiterAuth,
|
|
165
165
|
task: string,
|
|
166
166
|
): Promise<TaskMutationVerdict> {
|
|
167
|
+
// Keep optional Pi peers out of the detached runner's static import graph.
|
|
168
|
+
const [{ Agent }, { convertToLlm }, { streamSimple }] = await Promise.all([
|
|
169
|
+
import("@earendil-works/pi-agent-core"),
|
|
170
|
+
import("@earendil-works/pi-coding-agent"),
|
|
171
|
+
import("@earendil-works/pi-ai/compat"),
|
|
172
|
+
]);
|
|
173
|
+
const streamFn: StreamFn = runtime.explicitStreamFn
|
|
174
|
+
?? (runtime.registeredApi !== undefined && runtime.registeredApi === runtime.model.api ? runtime.registeredStreamFn : undefined)
|
|
175
|
+
?? streamSimple;
|
|
167
176
|
let decision: DecisionParams | undefined;
|
|
168
177
|
const tool: AgentTool<typeof DecisionParams, { recorded: boolean }> = {
|
|
169
178
|
name: "task_mutation_decision",
|
|
@@ -190,7 +199,7 @@ async function runArbitration(
|
|
|
190
199
|
tools: [tool],
|
|
191
200
|
},
|
|
192
201
|
convertToLlm,
|
|
193
|
-
...agentStreamOptions(authWrappedStreamFn(
|
|
202
|
+
...agentStreamOptions(authWrappedStreamFn(streamFn, auth)),
|
|
194
203
|
getApiKey: (providerName) =>
|
|
195
204
|
providerName === runtime.model.provider ? auth.apiKey : undefined,
|
|
196
205
|
beforeToolCall: async ({ toolCall }) =>
|
|
@@ -317,6 +317,7 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
|
|
|
317
317
|
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
318
318
|
now?: number;
|
|
319
319
|
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
320
|
+
ignoreExclusion?: (candidate: string, exclusion: Readonly<ModelExclusion>) => boolean;
|
|
320
321
|
}): string[] {
|
|
321
322
|
ensureLoaded();
|
|
322
323
|
invalidateAuthExclusions();
|
|
@@ -326,7 +327,7 @@ export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
|
326
327
|
for (const raw of candidates) {
|
|
327
328
|
if (!raw || seen.has(raw)) continue;
|
|
328
329
|
const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
|
|
329
|
-
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
|
|
330
|
+
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp) && opts?.ignoreExclusion?.(raw, entry) !== true);
|
|
330
331
|
if (exclusion) {
|
|
331
332
|
opts?.onExcluded?.(raw, exclusion);
|
|
332
333
|
continue;
|
|
@@ -18,9 +18,17 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
|
|
|
18
18
|
return splitKnownThinkingSuffix(model);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Aliases apply only to the resolved launch candidate (without its thinking suffix) and the exact raw response ID. */
|
|
22
|
+
export function formatSubagentModelVerificationError(
|
|
23
|
+
expectedModel: string,
|
|
24
|
+
observedModel: string,
|
|
25
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
26
|
+
modelResponseAliases?: Record<string, string[]>,
|
|
27
|
+
): string | undefined {
|
|
22
28
|
if (!availableModels || availableModels.length === 0) return undefined;
|
|
23
29
|
const expectedBase = splitThinkingSuffix(expectedModel).baseModel;
|
|
30
|
+
if (modelResponseAliases && Object.hasOwn(modelResponseAliases, expectedBase)
|
|
31
|
+
&& modelResponseAliases[expectedBase]?.includes(observedModel)) return undefined;
|
|
24
32
|
const observedBase = splitThinkingSuffix(observedModel).baseModel;
|
|
25
33
|
if (expectedBase === observedBase) return undefined;
|
|
26
34
|
const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
|
|
@@ -309,6 +317,24 @@ function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullab
|
|
|
309
317
|
return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
|
|
310
318
|
}
|
|
311
319
|
|
|
320
|
+
const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
|
|
321
|
+
/model.*not found/i,
|
|
322
|
+
/unknown model/i,
|
|
323
|
+
/model.*unavailable/i,
|
|
324
|
+
/model.*disabled/i,
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
328
|
+
if (!availableModels || availableModels.length === 0) return false;
|
|
329
|
+
const { baseModel } = splitThinkingSuffix(candidate);
|
|
330
|
+
return availableModels.some((entry) => entry.fullId === baseModel);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
334
|
+
const reason = exclusion.reason ?? "";
|
|
335
|
+
return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
|
|
336
|
+
}
|
|
337
|
+
|
|
312
338
|
function throwForExplicitModelExclusion(model: string): void {
|
|
313
339
|
const exclusion = findModelExclusion(model);
|
|
314
340
|
if (!exclusion) return;
|
|
@@ -487,7 +513,10 @@ export function buildModelCandidates(
|
|
|
487
513
|
seen.add(normalized);
|
|
488
514
|
candidates.push(normalized);
|
|
489
515
|
}
|
|
490
|
-
const resolved = filterFallbackCandidates(candidates, {
|
|
516
|
+
const resolved = filterFallbackCandidates(candidates, {
|
|
517
|
+
onExcluded: warnCachedExclusion,
|
|
518
|
+
ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
|
|
519
|
+
});
|
|
491
520
|
if (resolved.length === 0) {
|
|
492
521
|
if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
|
|
493
522
|
if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
|
|
@@ -286,7 +286,7 @@ function sanitizeTurnBudget(value: unknown): TurnBudgetState | undefined {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function sanitizeState(value: unknown, fallback: NestedRunState): NestedRunState {
|
|
289
|
-
return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped"
|
|
289
|
+
return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "partial" || value === "paused" || value === "stopped" || value === "rejected"
|
|
290
290
|
? value
|
|
291
291
|
: fallback;
|
|
292
292
|
}
|
|
@@ -296,7 +296,7 @@ function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefi
|
|
|
296
296
|
const raw = input as Record<string, unknown>;
|
|
297
297
|
const agent = stringValue(raw.agent, 128);
|
|
298
298
|
if (!agent) return undefined;
|
|
299
|
-
const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "paused" || raw.status === "stopped"
|
|
299
|
+
const status = raw.status === "pending" || raw.status === "running" || raw.status === "complete" || raw.status === "completed" || raw.status === "failed" || raw.status === "partial" || raw.status === "paused" || raw.status === "stopped" || raw.status === "rejected"
|
|
300
300
|
? raw.status
|
|
301
301
|
: "pending";
|
|
302
302
|
const model = stringValue(raw.model);
|
|
@@ -438,7 +438,7 @@ export function parseNestedEventRecords(content: string, route: NestedRoute): Ne
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
function terminal(state: NestedRunState): boolean {
|
|
441
|
-
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "stopped";
|
|
441
|
+
return state === "complete" || state === "failed" || state === "partial" || state === "paused" || state === "rejected" || state === "stopped";
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
function mergeBoundedChildren(existing: NestedRunSummary[] | undefined, incoming: NestedRunSummary[] | undefined): NestedRunSummary[] | undefined {
|
|
@@ -41,6 +41,7 @@ export interface RunnerSubagentStep {
|
|
|
41
41
|
/** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
|
|
42
42
|
skipPrimaryModelVerification?: boolean;
|
|
43
43
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
44
|
+
modelResponseAliases?: Record<string, string[]>;
|
|
44
45
|
tools?: string[];
|
|
45
46
|
excludeTools?: string[];
|
|
46
47
|
allowNestedSubagents?: boolean;
|