pi-subagents 0.49.0 → 0.50.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 +35 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/docs/agents.md +2 -0
- package/docs/configuration.md +47 -3
- package/docs/extension-api.md +36 -0
- package/docs/missions.md +1 -1
- package/docs/observability.md +16 -0
- package/docs/tool-reference.md +19 -2
- package/docs/workflows.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +31 -15
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +10 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +12 -6
- package/src/extension/config.ts +33 -0
- package/src/extension/index.ts +48 -5
- package/src/extension/public-execution.ts +3 -1
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +4 -2
- package/src/inspectors/herdr/actions.ts +3 -7
- package/src/inspectors/herdr/project-panes.ts +2 -6
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +19 -42
- package/src/missions/goal-driver.ts +3 -1
- package/src/runs/background/active-run-index.ts +71 -1
- package/src/runs/background/async-execution.ts +49 -7
- package/src/runs/background/async-job-tracker.ts +1 -0
- package/src/runs/background/async-resume.ts +3 -5
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/result-files.ts +437 -0
- package/src/runs/background/result-watcher.ts +188 -41
- package/src/runs/background/retained-children.ts +69 -20
- package/src/runs/background/run-id-resolver.ts +30 -24
- package/src/runs/background/run-status.ts +7 -2
- package/src/runs/background/scheduled-runs.ts +54 -28
- package/src/runs/background/stale-run-reconciler.ts +27 -13
- package/src/runs/background/subagent-runner.ts +294 -32
- package/src/runs/background/subagent-wait.ts +2 -0
- package/src/runs/background/wait-completions.ts +5 -2
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/chain-execution.ts +16 -0
- package/src/runs/foreground/execution.ts +219 -15
- package/src/runs/foreground/subagent-executor.ts +114 -28
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/model-fallback.ts +16 -2
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +375 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/subagent-control.ts +15 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/shared/types.ts +29 -2
- package/src/slash/slash-commands.ts +34 -25
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +125 -15
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +6 -4
- package/src/workflows/scripted-workflow.ts +227 -70
|
@@ -101,6 +101,7 @@ import { stopAsyncRun } from "./async-stop-action.ts";
|
|
|
101
101
|
import { dismissRecoveredWorkflow } from "./async-dismiss-action.ts";
|
|
102
102
|
import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts";
|
|
103
103
|
import { resolveAsyncRootResultPath, waitForImportedAsyncRoot } from "../background/chain-root-attachment.ts";
|
|
104
|
+
import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
|
|
104
105
|
import { attachRootChildrenToSteps, createNestedRoute, findNestedControlResult, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, snapshotNestedEventFiles, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
105
106
|
import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
|
|
106
107
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
@@ -108,7 +109,7 @@ import { inspectSubagentStatus } from "../background/run-status.ts";
|
|
|
108
109
|
import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
|
|
109
110
|
import { handleMissionAction, MISSION_ACTIONS } from "../../missions/actions.ts";
|
|
110
111
|
import { attachMissionToLaunchResult, prepareMissionLaunch, writeMissionAsyncBinding, type MissionLaunchBinding } from "../../missions/lifecycle.ts";
|
|
111
|
-
import { updateMission } from "../../missions/store.ts";
|
|
112
|
+
import { MissionNotFoundError, updateMission } from "../../missions/store.ts";
|
|
112
113
|
import type { MissionWorkflowChildUpdate } from "../../missions/types.ts";
|
|
113
114
|
import { createMissionWorkflowState } from "../../missions/workflow-state.ts";
|
|
114
115
|
import { resolveAuthorityDecision } from "../../policy/authority.ts";
|
|
@@ -307,6 +308,8 @@ export interface SubagentParamsLike {
|
|
|
307
308
|
foregroundOnly?: boolean;
|
|
308
309
|
timeoutMs?: number;
|
|
309
310
|
maxRuntimeMs?: number;
|
|
311
|
+
/** Optional hard per-tool-call timeout (ms). Known-fast tools also have a default. */
|
|
312
|
+
toolTimeoutMs?: number;
|
|
310
313
|
turnBudget?: TurnBudgetConfig;
|
|
311
314
|
/** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */
|
|
312
315
|
enforceHardTurnLimit?: boolean;
|
|
@@ -409,6 +412,8 @@ interface ExecutionContextData {
|
|
|
409
412
|
nestedRoute?: NestedRouteInfo;
|
|
410
413
|
timeoutMs?: number;
|
|
411
414
|
deadlineAt?: number;
|
|
415
|
+
/** Raw global config.toolTimeoutMs, for per-step resolution in async runners. */
|
|
416
|
+
configToolTimeoutMs?: number;
|
|
412
417
|
turnBudget?: ResolvedTurnBudget;
|
|
413
418
|
toolBudget?: ResolvedToolBudget;
|
|
414
419
|
usageBudget?: UsageBudgetConfig;
|
|
@@ -2506,16 +2511,19 @@ function resolveStaticLaunchSummary(input: {
|
|
|
2506
2511
|
thinkingOverrideForTask: ForkThinkingOverrideForTask;
|
|
2507
2512
|
}): StaticLaunchSummary {
|
|
2508
2513
|
const agentConfig = input.agents.find((agent) => agent.name === input.agent);
|
|
2509
|
-
const
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2514
|
+
const externalRunner = agentConfig?.runner?.type === "external-cli";
|
|
2515
|
+
const model = externalRunner
|
|
2516
|
+
? undefined
|
|
2517
|
+
: resolveEffectiveSubagentModel(
|
|
2518
|
+
input.explicitModel,
|
|
2519
|
+
agentConfig?.model,
|
|
2520
|
+
input.parentModel,
|
|
2521
|
+
input.availableModels,
|
|
2522
|
+
input.currentProvider,
|
|
2523
|
+
input.modelScope === undefined ? {} : { scope: input.modelScope },
|
|
2524
|
+
);
|
|
2525
|
+
const thinkingOverride = externalRunner ? undefined : input.thinkingOverrideForTask(input.agent, input.index, model);
|
|
2526
|
+
const thinking = externalRunner ? undefined : resolveEffectiveThinking(model, thinkingOverride ?? agentConfig?.thinking);
|
|
2519
2527
|
return {
|
|
2520
2528
|
agent: input.agent,
|
|
2521
2529
|
...(model ? { model } : {}),
|
|
@@ -2798,6 +2806,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2798
2806
|
toolBudget: data.toolBudget,
|
|
2799
2807
|
usageBudget: data.usageBudget,
|
|
2800
2808
|
configToolBudget: data.configToolBudget,
|
|
2809
|
+
callToolTimeoutMs: data.params?.toolTimeoutMs,
|
|
2810
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
2801
2811
|
capabilityCeiling: data.capabilityCeiling,
|
|
2802
2812
|
runFanoutBudget: data.runFanoutBudget,
|
|
2803
2813
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
@@ -2845,6 +2855,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2845
2855
|
toolBudget: data.toolBudget,
|
|
2846
2856
|
usageBudget: data.usageBudget,
|
|
2847
2857
|
configToolBudget: data.configToolBudget,
|
|
2858
|
+
callToolTimeoutMs: data.params?.toolTimeoutMs,
|
|
2859
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
2848
2860
|
capabilityCeiling: data.capabilityCeiling,
|
|
2849
2861
|
runFanoutBudget: data.runFanoutBudget,
|
|
2850
2862
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
@@ -2871,8 +2883,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2871
2883
|
const externalRunnerWithoutExplicitModel = a.runner?.type === "external-cli"
|
|
2872
2884
|
&& params.model === undefined
|
|
2873
2885
|
&& (a.model === undefined || (a.modelSource?.type === "subagents.defaultModel" && a.model === a.modelSource.model));
|
|
2874
|
-
const modelOverride =
|
|
2875
|
-
? undefined
|
|
2886
|
+
const modelOverride = a.runner?.type === "external-cli"
|
|
2887
|
+
? params.model ?? (externalRunnerWithoutExplicitModel ? undefined : a.model)
|
|
2876
2888
|
: resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
|
|
2877
2889
|
return executeAsyncSingle(id, compactOptional<Parameters<typeof executeAsyncSingle>[1]>({
|
|
2878
2890
|
agent: params.agent!,
|
|
@@ -2916,6 +2928,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
2916
2928
|
toolBudget: data.toolBudget,
|
|
2917
2929
|
usageBudget: data.usageBudget,
|
|
2918
2930
|
configToolBudget: data.configToolBudget,
|
|
2931
|
+
toolTimeoutMs: data.params?.toolTimeoutMs,
|
|
2932
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
2919
2933
|
capabilityCeiling: data.capabilityCeiling,
|
|
2920
2934
|
runFanoutBudget: data.runFanoutBudget,
|
|
2921
2935
|
parentWorkflowRunId: params.workflowParentRunId,
|
|
@@ -3005,6 +3019,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3005
3019
|
toolBudget: data.toolBudget,
|
|
3006
3020
|
usageBudget: data.usageBudget,
|
|
3007
3021
|
configToolBudget: data.configToolBudget,
|
|
3022
|
+
callToolTimeoutMs: data.params?.toolTimeoutMs,
|
|
3023
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
3008
3024
|
permissions: deps.config.permissions,
|
|
3009
3025
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
3010
3026
|
capabilityCeiling: data.capabilityCeiling,
|
|
@@ -3072,6 +3088,8 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
3072
3088
|
toolBudget: data.toolBudget,
|
|
3073
3089
|
usageBudget: data.usageBudget,
|
|
3074
3090
|
configToolBudget: data.configToolBudget,
|
|
3091
|
+
callToolTimeoutMs: data.params?.toolTimeoutMs,
|
|
3092
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
3075
3093
|
capabilityCeiling: data.capabilityCeiling,
|
|
3076
3094
|
runFanoutBudget: data.runFanoutBudget,
|
|
3077
3095
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
@@ -3159,6 +3177,10 @@ interface ForegroundParallelRunInput {
|
|
|
3159
3177
|
worktreeSetup?: WorktreeSetup;
|
|
3160
3178
|
timeoutMs?: number;
|
|
3161
3179
|
deadlineAt?: number;
|
|
3180
|
+
/** Per-call per-tool timeout, resolved separately for each task's agent. */
|
|
3181
|
+
toolTimeoutMs?: number;
|
|
3182
|
+
/** Raw global config.toolTimeoutMs, used by each task's resolver. */
|
|
3183
|
+
configToolTimeoutMs?: number;
|
|
3162
3184
|
turnBudget?: ResolvedTurnBudget;
|
|
3163
3185
|
usageBudget?: UsageBudgetConfig;
|
|
3164
3186
|
toolBudgets: (ResolvedToolBudget | undefined)[];
|
|
@@ -3535,6 +3557,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
3535
3557
|
share: input.shareEnabled || undefined,
|
|
3536
3558
|
maxOutput: input.maxOutput,
|
|
3537
3559
|
timeoutMs: input.timeoutMs,
|
|
3560
|
+
toolTimeoutMs: input.toolTimeoutMs,
|
|
3561
|
+
configToolTimeoutMs: input.configToolTimeoutMs,
|
|
3538
3562
|
turnBudget: input.turnBudget,
|
|
3539
3563
|
...(model ? { model } : {}),
|
|
3540
3564
|
...(effectiveSkills !== undefined ? { skill: effectiveSkills } : {}),
|
|
@@ -3614,6 +3638,8 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
3614
3638
|
acceptanceContext: { mode: "parallel" },
|
|
3615
3639
|
onEffectivePrompt: input.foregroundControl ? (prompt) => updateLiveEffectivePrompt(input.foregroundControl!, index, prompt) : undefined,
|
|
3616
3640
|
timeoutMs: input.timeoutMs,
|
|
3641
|
+
toolTimeoutMs: input.toolTimeoutMs,
|
|
3642
|
+
configToolTimeoutMs: input.configToolTimeoutMs,
|
|
3617
3643
|
deadlineAt: input.deadlineAt,
|
|
3618
3644
|
turnBudget: input.turnBudget,
|
|
3619
3645
|
toolBudget: input.toolBudgets[index],
|
|
@@ -3741,7 +3767,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3741
3767
|
...(task.model !== undefined ? { model: task.model } : {}),
|
|
3742
3768
|
}));
|
|
3743
3769
|
const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
|
|
3744
|
-
|
|
3770
|
+
agentConfigs[i]?.runner?.type === "external-cli"
|
|
3771
|
+
? undefined
|
|
3772
|
+
: resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope }),
|
|
3745
3773
|
);
|
|
3746
3774
|
|
|
3747
3775
|
if (params.clarify === true && ctx.hasUI) {
|
|
@@ -3776,7 +3804,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3776
3804
|
for (let i = 0; i < result.behaviorOverrides.length; i++) {
|
|
3777
3805
|
const override = result.behaviorOverrides[i];
|
|
3778
3806
|
if (override?.model !== undefined) {
|
|
3779
|
-
modelOverrides[i] =
|
|
3807
|
+
modelOverrides[i] = agentConfigs[i]?.runner?.type === "external-cli"
|
|
3808
|
+
? undefined
|
|
3809
|
+
: resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, data.modelScope === undefined ? {} : { scope: data.modelScope });
|
|
3780
3810
|
behaviorOverrides[i]!.model = override.model;
|
|
3781
3811
|
}
|
|
3782
3812
|
if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output;
|
|
@@ -3863,6 +3893,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3863
3893
|
usageBudget: data.usageBudget,
|
|
3864
3894
|
toolBudget: data.toolBudget,
|
|
3865
3895
|
configToolBudget: data.configToolBudget,
|
|
3896
|
+
callToolTimeoutMs: data.params?.toolTimeoutMs,
|
|
3897
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
3866
3898
|
globalConcurrencyLimit: deps.config.globalConcurrencyLimit,
|
|
3867
3899
|
runFanoutBudget: data.runFanoutBudget,
|
|
3868
3900
|
}));
|
|
@@ -3983,6 +4015,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
3983
4015
|
worktreeSetup,
|
|
3984
4016
|
timeoutMs: data.timeoutMs,
|
|
3985
4017
|
deadlineAt,
|
|
4018
|
+
toolTimeoutMs: data.params?.toolTimeoutMs,
|
|
4019
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
3986
4020
|
turnBudget: data.turnBudget,
|
|
3987
4021
|
usageBudget: data.usageBudget,
|
|
3988
4022
|
toolBudgets,
|
|
@@ -4377,6 +4411,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
4377
4411
|
},
|
|
4378
4412
|
timeoutMs: data.timeoutMs,
|
|
4379
4413
|
deadlineAt,
|
|
4414
|
+
toolTimeoutMs: params.toolTimeoutMs,
|
|
4415
|
+
configToolTimeoutMs: data.configToolTimeoutMs,
|
|
4380
4416
|
turnBudget: data.turnBudget,
|
|
4381
4417
|
enforceHardTurnLimit: params.enforceHardTurnLimit,
|
|
4382
4418
|
toolBudget: effectiveToolBudget.toolBudget,
|
|
@@ -4455,8 +4491,15 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
4455
4491
|
}
|
|
4456
4492
|
|
|
4457
4493
|
if (r.detached) {
|
|
4494
|
+
const statusRecovery = `subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.`;
|
|
4495
|
+
const blockingRecovery = `subagent_wait({ id: "${runId}" }). Use ${statusRecovery}`;
|
|
4496
|
+
const message = r.detachedReason === "intercom coordination"
|
|
4497
|
+
? `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first, then wait with ${blockingRecovery}`
|
|
4498
|
+
: r.detachedReason === "user request"
|
|
4499
|
+
? `Detached at user request: ${params.agent}. The child continues independently. Register a completion wake-up with subagent_wait({ id: "${runId}", nonBlocking: true }), or use ${statusRecovery}`
|
|
4500
|
+
: `Detached before task completion: ${params.agent}. Wait with ${blockingRecovery}`;
|
|
4458
4501
|
return {
|
|
4459
|
-
content: [{ type: "text", text:
|
|
4502
|
+
content: [{ type: "text", text: message }],
|
|
4460
4503
|
details,
|
|
4461
4504
|
};
|
|
4462
4505
|
}
|
|
@@ -4500,17 +4543,28 @@ function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResul
|
|
|
4500
4543
|
|
|
4501
4544
|
const workflowLaunchObservers = new WeakMap<object, (launch: { agent: string; sessionFile?: string; async: boolean; runId?: string }) => void>();
|
|
4502
4545
|
|
|
4546
|
+
/**
|
|
4547
|
+
* Terminal-mission retention can remove a mission while its children still
|
|
4548
|
+
* report. Remember it to prevent later heartbeats from warning repeatedly.
|
|
4549
|
+
*/
|
|
4550
|
+
const missionsMissingFromStore = new Set<string>();
|
|
4551
|
+
|
|
4503
4552
|
function recordMissionWorkflowChild(
|
|
4504
4553
|
binding: MissionLaunchBinding | undefined,
|
|
4505
4554
|
workflowRunId: string,
|
|
4506
4555
|
key: string,
|
|
4507
4556
|
update: Omit<MissionWorkflowChildUpdate, "workflowRunId" | "key">,
|
|
4508
4557
|
): void {
|
|
4509
|
-
if (!binding) return;
|
|
4558
|
+
if (!binding || missionsMissingFromStore.has(binding.missionId)) return;
|
|
4510
4559
|
const { task: _task, ...durableUpdate } = update;
|
|
4511
4560
|
try {
|
|
4512
4561
|
updateMission(binding.location, binding.missionId, { upsertWorkflowChildren: [{ workflowRunId, key, ...durableUpdate }] });
|
|
4513
4562
|
} catch (error) {
|
|
4563
|
+
if (error instanceof MissionNotFoundError) {
|
|
4564
|
+
missionsMissingFromStore.add(binding.missionId);
|
|
4565
|
+
console.warn(`[pi-subagents] Mission '${binding.missionId}' is no longer in the mission store; stopped recording its workflow children. Terminal-mission retention can prune a mission while its run is still active.`);
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
4514
4568
|
console.warn(`[pi-subagents] Failed to record mission workflow child '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
4515
4569
|
}
|
|
4516
4570
|
}
|
|
@@ -4580,6 +4634,7 @@ function workflowChildResult(key: string, result: AgentToolResult<Details>): Wor
|
|
|
4580
4634
|
...(result.details.runId || result.details.asyncId ? { runId: result.details.runId ?? result.details.asyncId } : {}),
|
|
4581
4635
|
output,
|
|
4582
4636
|
...(!ok ? { error: receiptOutput || output || "Child run failed." } : {}),
|
|
4637
|
+
...(detached ? { detached: true } : {}),
|
|
4583
4638
|
...(structured.length === 1 ? { structuredOutput: structured[0] } : structured.length > 1 ? { structuredOutput: structured } : {}),
|
|
4584
4639
|
artifactPaths: [...artifactPaths],
|
|
4585
4640
|
results: result.details.results,
|
|
@@ -4888,7 +4943,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4888
4943
|
if (workflowRunId) {
|
|
4889
4944
|
const toolCallId = _id;
|
|
4890
4945
|
const asyncDir = path.join(DIRS.async, workflowRunId);
|
|
4891
|
-
const resultPath =
|
|
4946
|
+
const resultPath = resultFilePath(DIRS.results, workflowRunId);
|
|
4892
4947
|
const statusPath = path.join(asyncDir, "status.json");
|
|
4893
4948
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
4894
4949
|
const startedAt = Date.now();
|
|
@@ -4927,7 +4982,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4927
4982
|
status.lastUpdate = Date.now();
|
|
4928
4983
|
writeAtomicJson(statusPath, status);
|
|
4929
4984
|
if (indexedState !== status.state) {
|
|
4930
|
-
updateActiveRunIndex(asyncDir, status.state);
|
|
4985
|
+
updateActiveRunIndex(asyncDir, status.state, status.toolCallId);
|
|
4931
4986
|
indexedState = status.state;
|
|
4932
4987
|
}
|
|
4933
4988
|
const job = deps.state.asyncJobs.get(workflowRunId);
|
|
@@ -4952,6 +5007,17 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4952
5007
|
job.workflow = status.workflow;
|
|
4953
5008
|
}
|
|
4954
5009
|
};
|
|
5010
|
+
const writeWorkflowResult = (payload: Record<string, unknown>): boolean => {
|
|
5011
|
+
try {
|
|
5012
|
+
writeAsyncResultFile(resultPath, payload);
|
|
5013
|
+
return true;
|
|
5014
|
+
} catch (error) {
|
|
5015
|
+
const message = `Failed to write async workflow result ${resultPath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
5016
|
+
console.error(message, error);
|
|
5017
|
+
appendWorkflowEvent({ type: "subagent.workflow.result_write_failed", error: message });
|
|
5018
|
+
return false;
|
|
5019
|
+
}
|
|
5020
|
+
};
|
|
4955
5021
|
const projectWorkflowActivity = () => {
|
|
4956
5022
|
const steps = status.steps ?? [];
|
|
4957
5023
|
const runningSteps = steps.filter((step) => step.status === "running");
|
|
@@ -4973,7 +5039,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
4973
5039
|
status.toolCount = toolCounts.length > 0 ? toolCounts.reduce((total, count) => total + count, 0) : undefined;
|
|
4974
5040
|
status.currentStep = runningSteps.length === 1 ? steps.indexOf(runningSteps[0]!) : undefined;
|
|
4975
5041
|
};
|
|
4976
|
-
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
|
|
5042
|
+
const workflowJob: AsyncJobState = { asyncId: workflowRunId, asyncDir, toolCallId, cwd: workflowCwd, status: "running", sessionId: currentSessionId ?? undefined, mode: "workflow", agents: [], steps: [], startedAt, updatedAt: startedAt, ...(timeout !== undefined ? { timeoutMs: timeout, deadlineAt: startedAt + timeout } : {}), workflow: status.workflow };
|
|
4977
5043
|
deps.state.asyncJobs.set(workflowRunId, workflowJob);
|
|
4978
5044
|
deps.state.fleetJobs ??= new Map();
|
|
4979
5045
|
deps.state.fleetJobs.set(workflowRunId, workflowJob);
|
|
@@ -5012,12 +5078,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5012
5078
|
? "completed"
|
|
5013
5079
|
: entry.state === "stopped"
|
|
5014
5080
|
? "stopped"
|
|
5015
|
-
: "
|
|
5081
|
+
: entry.state === "detached"
|
|
5082
|
+
? "paused"
|
|
5083
|
+
: "failed";
|
|
5016
5084
|
if (existing) {
|
|
5017
5085
|
existing.status = mapped;
|
|
5018
5086
|
if (entry.agent) existing.agent = entry.agent;
|
|
5019
5087
|
if (entry.runId) existing.runId = entry.runId;
|
|
5020
5088
|
if (entry.state === "failed" && !entry.runId && existing.async === undefined) existing.async = false;
|
|
5089
|
+
if (entry.state === "detached") existing.activityState = "needs_attention";
|
|
5090
|
+
else if (existing.status !== "running") delete existing.activityState;
|
|
5021
5091
|
if (entry.state === "stopped") existing.stopped = true;
|
|
5022
5092
|
else delete existing.stopped;
|
|
5023
5093
|
if (entry.error === undefined) delete existing.error;
|
|
@@ -5034,6 +5104,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5034
5104
|
startedAt: Date.now(),
|
|
5035
5105
|
...(entry.runId ? { runId: entry.runId } : {}),
|
|
5036
5106
|
...(entry.state === "failed" && !entry.runId ? { async: false } : {}),
|
|
5107
|
+
...(entry.state === "detached" ? { activityState: "needs_attention" as const } : {}),
|
|
5037
5108
|
...(entry.state === "stopped" ? { stopped: true } : {}),
|
|
5038
5109
|
};
|
|
5039
5110
|
status.steps?.push(step);
|
|
@@ -5156,18 +5227,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
5156
5227
|
const resultSummary = appendWorkflowOutputWarning(summary, outputWarning);
|
|
5157
5228
|
const workflowUsage = sumResultsUsage(workflowResults);
|
|
5158
5229
|
status = { ...status, state: "complete", endedAt: Date.now(), workflow: { value: workflow.value, trace: workflow.trace, emits: workflow.emits, console: workflow.console }, totalTokens: { input: workflowUsage.input, output: workflowUsage.output, total: workflowUsage.input + workflowUsage.output }, totalCost: sumResultsCost(workflowResults) };
|
|
5159
|
-
|
|
5230
|
+
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: true, state: "complete", summary: resultSummary, output: resultSummary, results: workflow.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
5160
5231
|
persist();
|
|
5161
|
-
appendWorkflowEvent({ type: "subagent.workflow.completed", state:
|
|
5232
|
+
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, ...(status.error ? { error: status.error } : {}) });
|
|
5162
5233
|
} catch (error) {
|
|
5163
5234
|
const partial = error instanceof WorkflowScriptError ? error.partial : { trace: [], emits: [], console: [], children: [] };
|
|
5164
5235
|
const stopped = controller.signal.aborted;
|
|
5165
|
-
|
|
5166
|
-
const
|
|
5167
|
-
const
|
|
5168
|
-
|
|
5236
|
+
const detachedChildKeys = new Set(partial.children.filter((child) => child.detached).map((child) => child.key));
|
|
5237
|
+
const hasRealFailedChild = partial.children.some((child) => !child.ok && !child.detached);
|
|
5238
|
+
const pauseForDetached = !stopped && error instanceof WorkflowScriptError && error.errorKind === "detached-child" && detachedChildKeys.size > 0 && !hasRealFailedChild;
|
|
5239
|
+
const state = stopped ? "stopped" : pauseForDetached ? "paused" : "failed";
|
|
5240
|
+
for (const step of status.steps ?? []) {
|
|
5241
|
+
if (step.workflowKey && detachedChildKeys.has(step.workflowKey)) {
|
|
5242
|
+
step.status = "paused";
|
|
5243
|
+
step.activityState = "needs_attention";
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
status = compactOptional<AsyncStatus>({ ...status, state, stopped: stopped || undefined, activityState: pauseForDetached ? "needs_attention" : undefined, error: error instanceof Error ? error.message : String(error), endedAt: Date.now(), workflow: { trace: partial.trace, emits: partial.emits, console: partial.console } });
|
|
5247
|
+
const outputWarning = writeWorkflowAggregateOutput(workflowAggregateOutputPath, status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."));
|
|
5248
|
+
const resultSummary = appendWorkflowOutputWarning(status.error ?? (pauseForDetached ? "Workflow paused." : "Workflow failed."), outputWarning);
|
|
5249
|
+
if (!writeWorkflowResult({ id: workflowRunId, runId: workflowRunId, toolCallId, agent: "workflow", mode: "workflow", success: false, state: status.state, summary: resultSummary, error: status.error, stopped: status.stopped, activityState: status.activityState, results: partial.children.map((child) => ({ workflowKey: child.key, ...(child.agent ? { agent: child.agent } : {}), ...(child.runId ? { runId: child.runId } : {}), output: child.output, outputState: child.output.trim() || child.structuredOutput !== undefined ? "present" : "absent", structuredOutput: child.structuredOutput, success: child.ok, ...(child.detached ? { detached: true } : {}), ...(child.artifactPaths[0] ? { artifactPaths: { outputPath: child.artifactPaths[0] } } : {}) })), workflow: status.workflow, asyncDir, cwd: workflowCwd, sessionId: currentSessionId, timestamp: Date.now(), durationMs: Date.now() - startedAt })) return;
|
|
5169
5250
|
persist();
|
|
5170
|
-
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error });
|
|
5251
|
+
appendWorkflowEvent({ type: "subagent.workflow.completed", state: status.state, error: status.error, ...(status.activityState ? { activityState: status.activityState } : {}) });
|
|
5171
5252
|
} finally {
|
|
5172
5253
|
deps.state.workflowControllers?.delete(workflowRunId);
|
|
5173
5254
|
deps.state.activeAsyncCapacity = workflowCapacity?.reconcile(new Set(deps.state.workflowControllers?.keys() ?? []))
|
|
@@ -6013,6 +6094,10 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6013
6094
|
const parentModel = requestParentModel;
|
|
6014
6095
|
prepareForkThinking = (agentName, index, modelOverride) => {
|
|
6015
6096
|
const agentConfig = agents.find((agent) => agent.name === agentName);
|
|
6097
|
+
if (agentConfig?.runner?.type === "external-cli") {
|
|
6098
|
+
forkThinkingRequirements.set(index, true);
|
|
6099
|
+
return;
|
|
6100
|
+
}
|
|
6016
6101
|
const primaryModel = resolveEffectiveSubagentModel(
|
|
6017
6102
|
modelOverride,
|
|
6018
6103
|
agentConfig?.model,
|
|
@@ -6249,6 +6334,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
6249
6334
|
usageBudget: usageBudget.budget,
|
|
6250
6335
|
allowZeroToolBudget,
|
|
6251
6336
|
configToolBudget: configToolBudget.toolBudget,
|
|
6337
|
+
configToolTimeoutMs: deps.config.toolTimeoutMs,
|
|
6252
6338
|
contextPolicy,
|
|
6253
6339
|
modelScope,
|
|
6254
6340
|
parentModel: requestParentModel,
|
|
@@ -22,9 +22,22 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
22
22
|
const CURSOR_FILE_MUTATION_THINKING =
|
|
23
23
|
/(?:^|\n)\s*Cursor (?:edit|write)\s*:/i;
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
25
|
+
const REVIVED_TASK_PREFIX = /^You are reviving a previous subagent conversation\.\n\nOriginal run: .+\nOriginal agent: .+(?:\nOriginal session file: .+)?\n\nUse the stored session context as background\. Answer the orchestrator's follow-up below\. Do not assume the original child process is still alive\.\n\nFollow-up:\n/;
|
|
26
|
+
const IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN = /^(?:Run )?implementation challenge pass (?:one|two|\d+)(?:\s+and implement any better current[- ]scope change\.?|\s+for the accepted candidate\.\s+Reconsider it and implement any better current[- ]scope change\.?)?$/i;
|
|
27
|
+
const NO_BETTER_CHANGE_NEEDED_PATTERN = /^\s*no (?:better|further|additional) (?:current[- ]scope )?(?:code |source |file )?(?:change|changes|edit|edits|patch|patches) (?:is|are) needed\b/i;
|
|
28
|
+
const KEPT_CURRENT_IMPLEMENTATION_PATTERN = /\b(?:kept (?:the )?current (?:implementation|candidate|shape)|(?:the )?current (?:implementation|candidate|shape) was kept)\b/i;
|
|
29
|
+
const NO_CHANGE_MADE_PATTERN = /\bno (?:new )?(?:(?:code|source|file|test)(?:\s*(?:,\s*or|,|\/|or)\s*(?:code|source|file|test))* changes?|changes?) (?:were|was) made\b/i;
|
|
30
|
+
const NO_BETTER_CHANGE_QUALIFIER_PATTERN = /\b(?:do\s+not|don't|dont|not|never|cannot|can't|cant|unable|uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting|maybe|might|may|\w+n['’]t)\b/i;
|
|
31
|
+
const REPORT_SENTENCE_PATTERN = /[^.!?]+(?:[.!?]+(?=(?:["”'’]?\s)|["”'’]?$)|$)/g;
|
|
32
|
+
const QUOTED_CLAIM_PATTERN = /["“]([^"”]+)["”]([.!?]?\s*[^.!?]*)/g;
|
|
33
|
+
const QUOTED_CLAIM_REFERENCE_PATTERN = /\b(?:that|this|it|claim|report|message|disagree)\b/i;
|
|
34
|
+
const CLAIM_CONTRADICTION_PATTERN = /\bbut\b[^.!?]*\b(?:uncertain|unsure|unclear|disagree|wrong|false|reject|rejected|rejecting)\b/i;
|
|
35
|
+
const IMPLEMENTATION_RETRACTION_PATTERN = /\b(?:found|identified)\s+(?:(?:a|an|the)\s+)?(?:required|necessary|additional|better)?\s*(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:(?:implementation|code|source|file|test)\s+)?work\s+(?:remains?|is\s+(?:needed|required))\b|\b(?:(?:a|an|the)\s+)?(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\s+(?:are|is)\s+(?:needed|required)\b|\b(?:required|necessary|additional|better)\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:I|we)?\s*need\s+(?:(?:code|source|file|test)\s+)?(?:changes?|edits?|patches?)\b|\b(?:need|needs|require|requires)\s+(?:to\s+)?(?:implement|make|apply)\b/i;
|
|
36
|
+
const IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN = new RegExp(IMPLEMENTATION_RETRACTION_PATTERN.source, "gi");
|
|
37
|
+
const DIRECT_CLAIM_RETRACTION_PATTERN = /^\s*(?:I\s+)?(?:disagree|reject|retract)\b|\b(?:disagree|retract|retracted|retracting|reject|rejected|rejecting)\b[^.!?]*\b(?:that|this|it|claim|report|message)\b|\b(?:that|this|it|claim|report|message)\b[^.!?]*\b(?:is|was)\s+(?:rejected|retracted)\b/i;
|
|
38
|
+
const NEGATED_RETRACTION_PREFIX_PATTERN = /\b(?:no|not|\w+n['’]t)\b(?:\s+\w+){0,2}\s*$/i;
|
|
39
|
+
const REPORTED_CLAIM_PREFIX_PATTERN = /["“]|\b(?:prior|previous)\s+(?:message|report)\b|\b(?:said|stated|reported)\b/i;
|
|
40
|
+
const POST_CLAIM_SEPARATOR_PATTERN = /^[\s,;:.—–'"“‘’-]+/;
|
|
28
41
|
|
|
29
42
|
interface CompletionMutationGuardInput {
|
|
30
43
|
agent: string;
|
|
@@ -40,6 +53,11 @@ interface CompletionMutationGuardResult {
|
|
|
40
53
|
triggered: boolean;
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
interface ReportSentence {
|
|
57
|
+
text: string;
|
|
58
|
+
offset: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
export function hasMutationToolCapability(tools: string[] | undefined, mcpDirectTools: string[] | undefined): boolean {
|
|
44
62
|
if ((mcpDirectTools?.length ?? 0) > 0) return true;
|
|
45
63
|
if (tools === undefined) return true;
|
|
@@ -86,14 +104,86 @@ export function hasMutationToolCall(messages: Message[]): boolean {
|
|
|
86
104
|
return false;
|
|
87
105
|
}
|
|
88
106
|
|
|
107
|
+
function hasImplementationRetraction(text: string): boolean {
|
|
108
|
+
return Array.from(text.matchAll(IMPLEMENTATION_RETRACTION_GLOBAL_PATTERN)).some((retraction) =>
|
|
109
|
+
!NEGATED_RETRACTION_PREFIX_PATTERN.test(text.slice(0, retraction.index!)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function hasDirectClaimRetraction(text: string): boolean {
|
|
113
|
+
return DIRECT_CLAIM_RETRACTION_PATTERN.test(text.replace(POST_CLAIM_SEPARATOR_PATTERN, ""));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isInsideQuotedText(report: string, index: number): boolean {
|
|
117
|
+
let quote: "double" | "single" | undefined;
|
|
118
|
+
for (const char of report.slice(0, index)) {
|
|
119
|
+
if (quote === "double") {
|
|
120
|
+
if (char === "\"" || char === "”") quote = undefined;
|
|
121
|
+
} else if (quote === "single") {
|
|
122
|
+
if (char === "’") quote = undefined;
|
|
123
|
+
} else if (char === "\"" || char === "“") {
|
|
124
|
+
quote = "double";
|
|
125
|
+
} else if (char === "‘") {
|
|
126
|
+
quote = "single";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return quote !== undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unqualifiedClaimIndex(report: string, sentences: ReportSentence[], pattern: RegExp): number {
|
|
133
|
+
return sentences.findIndex((sentence) => {
|
|
134
|
+
const claim = sentence.text.match(pattern);
|
|
135
|
+
if (claim === null) return false;
|
|
136
|
+
const before = sentence.text.slice(0, claim.index!);
|
|
137
|
+
const after = sentence.text.slice(claim.index! + claim[0].length);
|
|
138
|
+
const previous = before.trimEnd().at(-1);
|
|
139
|
+
return previous !== "'"
|
|
140
|
+
&& previous !== "‘"
|
|
141
|
+
&& !isInsideQuotedText(report, sentence.offset + claim.index!)
|
|
142
|
+
&& !REPORTED_CLAIM_PREFIX_PATTERN.test(before)
|
|
143
|
+
&& !NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(before)
|
|
144
|
+
&& !CLAIM_CONTRADICTION_PATTERN.test(after)
|
|
145
|
+
&& !hasImplementationRetraction(after)
|
|
146
|
+
&& !hasDirectClaimRetraction(after);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hasLaterClaimRetraction(sentences: ReportSentence[], claimIndex: number): boolean {
|
|
151
|
+
return sentences.slice(claimIndex + 1).some((sentence) => hasImplementationRetraction(sentence.text)
|
|
152
|
+
|| hasDirectClaimRetraction(sentence.text));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function explicitlyRejectsQuotedClaim(report: string): boolean {
|
|
156
|
+
for (const [, claim, response] of report.matchAll(QUOTED_CLAIM_PATTERN)) {
|
|
157
|
+
if ((KEPT_CURRENT_IMPLEMENTATION_PATTERN.test(claim!) || NO_CHANGE_MADE_PATTERN.test(claim!))
|
|
158
|
+
&& NO_BETTER_CHANGE_QUALIFIER_PATTERN.test(response!)
|
|
159
|
+
&& QUOTED_CLAIM_REFERENCE_PATTERN.test(response!)) return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isImplementationChallengeTask(task: string): boolean {
|
|
165
|
+
const followUp = task.replace(REVIVED_TASK_PREFIX, "");
|
|
166
|
+
return followUp !== task && IMPLEMENTATION_CHALLENGE_FOLLOW_UP_PATTERN.test(followUp.trim());
|
|
167
|
+
}
|
|
168
|
+
|
|
89
169
|
function reportsNoBetterChallengeChange(messages: Message[]): boolean {
|
|
90
170
|
const report = messages
|
|
91
171
|
.filter((message) => message.role === "assistant")
|
|
92
172
|
.flatMap((message) => message.content)
|
|
93
173
|
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
94
174
|
.join("\n");
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
if (explicitlyRejectsQuotedClaim(report)) return false;
|
|
176
|
+
const sentences = Array.from(report.matchAll(REPORT_SENTENCE_PATTERN), (match) => ({
|
|
177
|
+
text: match[0],
|
|
178
|
+
offset: match.index!,
|
|
179
|
+
}));
|
|
180
|
+
const noBetterIndex = unqualifiedClaimIndex(report, sentences, NO_BETTER_CHANGE_NEEDED_PATTERN);
|
|
181
|
+
if (noBetterIndex >= 0) return !hasLaterClaimRetraction(sentences, noBetterIndex);
|
|
182
|
+
const keptIndex = unqualifiedClaimIndex(report, sentences, KEPT_CURRENT_IMPLEMENTATION_PATTERN);
|
|
183
|
+
const noChangeIndex = unqualifiedClaimIndex(report, sentences, NO_CHANGE_MADE_PATTERN);
|
|
184
|
+
return keptIndex >= 0
|
|
185
|
+
&& noChangeIndex >= 0
|
|
186
|
+
&& !hasLaterClaimRetraction(sentences, Math.min(keptIndex, noChangeIndex));
|
|
97
187
|
}
|
|
98
188
|
|
|
99
189
|
export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
|
|
@@ -101,7 +191,7 @@ export function evaluateCompletionMutationGuard(input: CompletionMutationGuardIn
|
|
|
101
191
|
? expectsImplementationMutation(input.agent, input.task)
|
|
102
192
|
: false;
|
|
103
193
|
const attemptedMutation = hasMutationToolCall(input.messages);
|
|
104
|
-
const noEditChallengeComplete =
|
|
194
|
+
const noEditChallengeComplete = isImplementationChallengeTask(input.task)
|
|
105
195
|
&& reportsNoBetterChallengeChange(input.messages);
|
|
106
196
|
return {
|
|
107
197
|
expectedMutation,
|
|
@@ -35,6 +35,8 @@ export function runExternalCli(input: {
|
|
|
35
35
|
timeoutMessage?: string;
|
|
36
36
|
stopMessage?: string;
|
|
37
37
|
onProcess?: (process: ExternalProcessStatus) => void;
|
|
38
|
+
onStdout?: (chunk: Buffer) => void;
|
|
39
|
+
onStderr?: (chunk: Buffer) => void;
|
|
38
40
|
}): Promise<ExternalCliRunResult> {
|
|
39
41
|
return new Promise((resolve, reject) => {
|
|
40
42
|
const startedAt = Date.now();
|
|
@@ -65,11 +67,13 @@ export function runExternalCli(input: {
|
|
|
65
67
|
input.onProcess?.(initialProcess);
|
|
66
68
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
67
69
|
stdoutStream.write(chunk);
|
|
70
|
+
input.onStdout?.(chunk);
|
|
68
71
|
stdoutTail = Buffer.concat([stdoutTail, chunk]);
|
|
69
72
|
if (stdoutTail.length > MAX_OUTPUT_TAIL_BYTES) stdoutTail = stdoutTail.subarray(stdoutTail.length - MAX_OUTPUT_TAIL_BYTES);
|
|
70
73
|
});
|
|
71
74
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
72
75
|
stderrStream.write(chunk);
|
|
76
|
+
input.onStderr?.(chunk);
|
|
73
77
|
stderrTail = Buffer.concat([stderrTail, chunk]);
|
|
74
78
|
if (stderrTail.length > MAX_ERROR_TAIL_BYTES) stderrTail = stderrTail.subarray(stderrTail.length - MAX_ERROR_TAIL_BYTES);
|
|
75
79
|
});
|
|
@@ -163,6 +163,20 @@ export function resolveModelCandidate(
|
|
|
163
163
|
return model;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
function resolveRequiredSubagentModelCandidate(
|
|
167
|
+
model: string,
|
|
168
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
169
|
+
preferredProvider?: string,
|
|
170
|
+
): string {
|
|
171
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
172
|
+
const resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);
|
|
173
|
+
if (resolvedWhole) return resolvedWhole;
|
|
174
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
175
|
+
const resolvedBase = thinkingSuffix ? resolveBaseModelCandidate(baseModel, availableModels, preferredProvider) : undefined;
|
|
176
|
+
if (resolvedBase) return `${resolvedBase}${thinkingSuffix}`;
|
|
177
|
+
throw new Error(`Unknown subagent model '${model}' in the active Pi model registry.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
export interface ResolveSubagentModelOverrideOptions {
|
|
167
181
|
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
168
182
|
scope?: ModelScopeConfig;
|
|
@@ -207,7 +221,7 @@ export function resolveSubagentModelOverride(
|
|
|
207
221
|
if (explicit === undefined) {
|
|
208
222
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
209
223
|
} else {
|
|
210
|
-
resolved =
|
|
224
|
+
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
211
225
|
}
|
|
212
226
|
if (resolved && options?.scope?.enforce) {
|
|
213
227
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
@@ -264,7 +278,7 @@ export function buildModelCandidates(
|
|
|
264
278
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
265
279
|
const raw = rawCandidates[index];
|
|
266
280
|
if (!raw) continue;
|
|
267
|
-
const normalized =
|
|
281
|
+
const normalized = resolveRequiredSubagentModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
268
282
|
if (!normalized || seen.has(normalized)) continue;
|
|
269
283
|
if ((index > 0 || options?.scope?.strict === true) && options?.scope?.enforce) {
|
|
270
284
|
const violation = checkModelScope(normalized, options.scope, "inherited");
|