@wichayutdew/pi-workflows 3.5.0 → 3.7.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/dist/index.js +212 -4
- package/package.json +1 -1
- package/schemas/workflow.schema.json +5 -0
- package/src/config/types.ts +1 -0
- package/src/config/validation/step.ts +20 -0
- package/src/harness/delegation-plan.ts +14 -0
- package/src/integrations/subagents/child-policy-types.ts +8 -0
- package/src/integrations/subagents/child-policy-validation.ts +66 -1
- package/src/integrations/subagents/child-runtime-policy.ts +16 -0
- package/src/integrations/subagents/child-runtime-repair.ts +51 -0
- package/src/integrations/subagents/child-runtime.ts +91 -3
- package/src/prompt/step-task.ts +5 -0
- package/src/runtime/step-result.ts +32 -0
package/dist/index.js
CHANGED
|
@@ -695,6 +695,7 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
695
695
|
"title",
|
|
696
696
|
"prompt",
|
|
697
697
|
"agent",
|
|
698
|
+
"maxToolCalls",
|
|
698
699
|
"permissions",
|
|
699
700
|
"requires",
|
|
700
701
|
"transitions",
|
|
@@ -707,6 +708,10 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
707
708
|
pattern: AGENT_PROFILE_NAME_PATTERN
|
|
708
709
|
});
|
|
709
710
|
const agent = agentName ? { name: agentName } : undefined;
|
|
711
|
+
const maxToolCalls = value.maxToolCalls === undefined ? undefined : readInteger(value.maxToolCalls, 1, `${path}.maxToolCalls`, errors, {
|
|
712
|
+
min: 1,
|
|
713
|
+
max: 1e5
|
|
714
|
+
});
|
|
710
715
|
const permissions = parsePermissions(value.permissions, `${path}.permissions`, errors);
|
|
711
716
|
const requires = parseRequirements(value.requires, permissions, `${path}.requires`, errors);
|
|
712
717
|
const transitions = parseTransitions(value.transitions, `${path}.transitions`, errors);
|
|
@@ -726,6 +731,13 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
726
731
|
errors.push(`${path}.transitions: artifact-contract retry requires a "retry" transition`);
|
|
727
732
|
}
|
|
728
733
|
}
|
|
734
|
+
if (agent && maxToolCalls !== undefined) {
|
|
735
|
+
if (!Object.hasOwn(transitions, "handoff")) {
|
|
736
|
+
errors.push(`${path}.transitions: maxToolCalls requires a "handoff" transition`);
|
|
737
|
+
} else if (transitions.handoff !== stepId) {
|
|
738
|
+
errors.push(`${path}.transitions: handoff transition must target "${stepId}"`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
729
741
|
if (workspace) {
|
|
730
742
|
workspace.bindOn.forEach((outcome) => {
|
|
731
743
|
if (!Object.hasOwn(transitions, outcome)) {
|
|
@@ -739,6 +751,7 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
739
751
|
title,
|
|
740
752
|
prompt,
|
|
741
753
|
...agent ? { agent } : {},
|
|
754
|
+
...maxToolCalls === undefined ? {} : { maxToolCalls },
|
|
742
755
|
permissions,
|
|
743
756
|
requires,
|
|
744
757
|
transitions,
|
|
@@ -2609,6 +2622,22 @@ var RESULT_KEYS = new Set([
|
|
|
2609
2622
|
var isObject = (value) => {
|
|
2610
2623
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2611
2624
|
};
|
|
2625
|
+
function validateNonSuccessSummary(outcome, summary) {
|
|
2626
|
+
if (outcome === "blocked") {
|
|
2627
|
+
const isActionable = /^# Blocked:\s+.+/m.test(summary) && /^\s*\*\*Action:\*\*\s+.+/m.test(summary) && /^\s*\*\*Next:\*\*\s+.+/m.test(summary);
|
|
2628
|
+
if (!isActionable) {
|
|
2629
|
+
throw new Error("blocked summary must identify the missing user prerequisite and next action");
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
if (outcome === "retry") {
|
|
2633
|
+
const describesTransientFailure = /\btransient\b/i.test(summary);
|
|
2634
|
+
const hasSafeRetryCondition = /\b(safe (retry|to retry)|retry (when|after|once))\b/i.test(summary);
|
|
2635
|
+
const requestsUserInput = /\b(provide|confirm|choose|approve|decide)\b/i.test(summary);
|
|
2636
|
+
if (!/^# Retry:\s+.+/m.test(summary) || !describesTransientFailure || !hasSafeRetryCondition || requestsUserInput) {
|
|
2637
|
+
throw new Error("retry summary must identify a transient failure and safe retry condition");
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2612
2641
|
function parseResultWorkspace(value, outcome, policy) {
|
|
2613
2642
|
const requiresWorkspace = policy.workspace?.bindOn.includes(outcome) === true;
|
|
2614
2643
|
if (!requiresWorkspace) {
|
|
@@ -2663,6 +2692,7 @@ function parseWorkflowStepResult(value, policy) {
|
|
|
2663
2692
|
if (summary.length > policy.summaryMaxChars) {
|
|
2664
2693
|
throw new Error(`workflow step summary exceeds ${policy.summaryMaxChars} characters`);
|
|
2665
2694
|
}
|
|
2695
|
+
validateNonSuccessSummary(value.outcome, summary);
|
|
2666
2696
|
if (value.artifact !== undefined && typeof value.artifact !== "string") {
|
|
2667
2697
|
throw new Error("workflow step artifact must be a string");
|
|
2668
2698
|
}
|
|
@@ -6053,6 +6083,7 @@ var resolveStep = (workflow, run) => {
|
|
|
6053
6083
|
return step;
|
|
6054
6084
|
};
|
|
6055
6085
|
var RESUME_INPUT_PLACEHOLDER = /\{\{\s*resume\.input\s*\}\}/;
|
|
6086
|
+
var WORKFLOW_INPUT_PLACEHOLDER = /\{\{\s*workflow\.input\s*\}\}/;
|
|
6056
6087
|
var renderStepPrompt = ({
|
|
6057
6088
|
workflow,
|
|
6058
6089
|
run,
|
|
@@ -6094,6 +6125,7 @@ function buildStepTask(options) {
|
|
|
6094
6125
|
const isDelegated = execution === "delegated";
|
|
6095
6126
|
const promptTemplate = workflow.prompts[run.currentStepId] ?? "";
|
|
6096
6127
|
const prompt = renderStepPrompt({ workflow, run, step, execution });
|
|
6128
|
+
const showWorkflowInput = WORKFLOW_INPUT_PLACEHOLDER.test(promptTemplate);
|
|
6097
6129
|
const handoff = currentStepHandoff(run);
|
|
6098
6130
|
const contract = createStepContract({ workflow, run, step });
|
|
6099
6131
|
const completionTool = isDelegated ? "structured_output" : "workflow_complete_step";
|
|
@@ -6113,6 +6145,7 @@ function buildStepTask(options) {
|
|
|
6113
6145
|
...step.agent ? ["## Role prompt", "", rolePrompt(step.agent.name), ""] : [],
|
|
6114
6146
|
prompt,
|
|
6115
6147
|
"",
|
|
6148
|
+
...showWorkflowInput ? ["## Original workflow request", "", run.input || "(none supplied)", ""] : [],
|
|
6116
6149
|
...isDelegated ? buildDelegatedHandoffSection(handoff) : [],
|
|
6117
6150
|
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
6118
6151
|
...buildResumeInputSection(run, RESUME_INPUT_PLACEHOLDER.test(promptTemplate)),
|
|
@@ -6393,6 +6426,10 @@ var POLICY_KEYS = new Set([
|
|
|
6393
6426
|
"outcomes",
|
|
6394
6427
|
"pauseOutcomes",
|
|
6395
6428
|
"summaryMaxChars",
|
|
6429
|
+
"maxToolCalls",
|
|
6430
|
+
"handoffReserve",
|
|
6431
|
+
"totalToolCalls",
|
|
6432
|
+
"handoffOutcome",
|
|
6396
6433
|
"gateSubmitOutcome",
|
|
6397
6434
|
"workspace"
|
|
6398
6435
|
]);
|
|
@@ -6411,6 +6448,29 @@ var rejectUnknownProperties = (value) => {
|
|
|
6411
6448
|
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
6412
6449
|
}
|
|
6413
6450
|
};
|
|
6451
|
+
var parseToolBudget = (value) => {
|
|
6452
|
+
const fields2 = [
|
|
6453
|
+
value.maxToolCalls,
|
|
6454
|
+
value.handoffReserve,
|
|
6455
|
+
value.totalToolCalls
|
|
6456
|
+
];
|
|
6457
|
+
if (fields2.every((field) => field === undefined))
|
|
6458
|
+
return {};
|
|
6459
|
+
if (fields2.some((field) => field === undefined)) {
|
|
6460
|
+
throw new Error("child policy tool budget fields must be provided together");
|
|
6461
|
+
}
|
|
6462
|
+
const [maxToolCalls, handoffReserve, totalToolCalls] = fields2;
|
|
6463
|
+
if (typeof maxToolCalls !== "number" || !Number.isInteger(maxToolCalls) || maxToolCalls < 1 || maxToolCalls > 1e5) {
|
|
6464
|
+
throw new Error("child policy maxToolCalls is invalid");
|
|
6465
|
+
}
|
|
6466
|
+
if (handoffReserve !== 2) {
|
|
6467
|
+
throw new Error("child policy handoffReserve is invalid");
|
|
6468
|
+
}
|
|
6469
|
+
if (typeof totalToolCalls !== "number" || !Number.isInteger(totalToolCalls) || totalToolCalls !== maxToolCalls + handoffReserve) {
|
|
6470
|
+
throw new Error("child policy totalToolCalls is invalid");
|
|
6471
|
+
}
|
|
6472
|
+
return { maxToolCalls, handoffReserve, totalToolCalls };
|
|
6473
|
+
};
|
|
6414
6474
|
var parseIdentityAndPaths = (value, environment) => {
|
|
6415
6475
|
const requestId = requiredString(value, "requestId");
|
|
6416
6476
|
const agent = requiredString(value, "agent");
|
|
@@ -6465,9 +6525,28 @@ var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) =
|
|
|
6465
6525
|
if (!isRecord11(value))
|
|
6466
6526
|
throw new Error("child policy must be an object");
|
|
6467
6527
|
rejectUnknownProperties(value);
|
|
6528
|
+
const sections = parseChildPolicySections(value);
|
|
6529
|
+
const toolBudget = parseToolBudget(value);
|
|
6530
|
+
const handoffOutcome = value.handoffOutcome;
|
|
6531
|
+
if (handoffOutcome !== undefined) {
|
|
6532
|
+
if (typeof handoffOutcome !== "string" || handoffOutcome !== "handoff") {
|
|
6533
|
+
throw new Error("child policy handoffOutcome is invalid");
|
|
6534
|
+
}
|
|
6535
|
+
if (toolBudget.maxToolCalls === undefined) {
|
|
6536
|
+
throw new Error("child policy handoffOutcome requires a tool budget");
|
|
6537
|
+
}
|
|
6538
|
+
if (!sections.outcomes.includes(handoffOutcome)) {
|
|
6539
|
+
throw new Error("child policy handoffOutcome must be an allowed outcome");
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
if (toolBudget.maxToolCalls !== undefined && handoffOutcome === undefined) {
|
|
6543
|
+
throw new Error("child policy tool budget requires handoffOutcome");
|
|
6544
|
+
}
|
|
6468
6545
|
return {
|
|
6469
6546
|
...parseIdentityAndPaths(value, environment),
|
|
6470
|
-
...
|
|
6547
|
+
...sections,
|
|
6548
|
+
...toolBudget,
|
|
6549
|
+
...handoffOutcome ? { handoffOutcome } : {}
|
|
6471
6550
|
};
|
|
6472
6551
|
};
|
|
6473
6552
|
|
|
@@ -6564,6 +6643,7 @@ var parseDelegatedStepResult = (value, policy) => {
|
|
|
6564
6643
|
}
|
|
6565
6644
|
};
|
|
6566
6645
|
// src/harness/delegation-plan.ts
|
|
6646
|
+
var HANDOFF_RESERVE = 2;
|
|
6567
6647
|
function createDelegationPlan(input, dependencies) {
|
|
6568
6648
|
const { workflow, run, step, latestContext } = input;
|
|
6569
6649
|
const agent = step.agent?.name;
|
|
@@ -6658,6 +6738,12 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6658
6738
|
const workspace = dependencies.createDelegationWorkspace();
|
|
6659
6739
|
const outcomes = allowedOutcomes(workflow, run);
|
|
6660
6740
|
const outcomeSet = new Set(outcomes);
|
|
6741
|
+
const toolBudget = step.maxToolCalls === undefined ? {} : {
|
|
6742
|
+
maxToolCalls: step.maxToolCalls,
|
|
6743
|
+
handoffReserve: HANDOFF_RESERVE,
|
|
6744
|
+
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE,
|
|
6745
|
+
handoffOutcome: "handoff"
|
|
6746
|
+
};
|
|
6661
6747
|
const policyDigest = digest({
|
|
6662
6748
|
version: 1,
|
|
6663
6749
|
requestId,
|
|
@@ -6670,6 +6756,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6670
6756
|
resultPath: workspace.resultPath,
|
|
6671
6757
|
permissions: step.permissions,
|
|
6672
6758
|
outcomes,
|
|
6759
|
+
...toolBudget,
|
|
6673
6760
|
...step.workspace ? { workspace: step.workspace } : {}
|
|
6674
6761
|
});
|
|
6675
6762
|
const policy = {
|
|
@@ -6689,6 +6776,8 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6689
6776
|
outcomes,
|
|
6690
6777
|
pauseOutcomes: Object.entries(step.transitions).filter(([outcome, target]) => target === "$pause" && outcomeSet.has(outcome)).map(([outcome]) => outcome),
|
|
6691
6778
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
6779
|
+
...toolBudget,
|
|
6780
|
+
...step.maxToolCalls === undefined ? {} : { handoffOutcome: "handoff" },
|
|
6692
6781
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
6693
6782
|
...step.workspace ? { workspace: structuredClone(step.workspace) } : {}
|
|
6694
6783
|
};
|
|
@@ -7951,6 +8040,45 @@ var COMPLETION_REPAIR_PROMPT = [
|
|
|
7951
8040
|
"Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields."
|
|
7952
8041
|
].join(`
|
|
7953
8042
|
`);
|
|
8043
|
+
var toolBudgetWarningPrompt = ({
|
|
8044
|
+
productiveCalls,
|
|
8045
|
+
productiveRemaining,
|
|
8046
|
+
handoffReserve
|
|
8047
|
+
}) => [
|
|
8048
|
+
"Tool-call budget warning.",
|
|
8049
|
+
`Productive calls used: ${productiveCalls}.`,
|
|
8050
|
+
`Productive calls remaining: ${productiveRemaining}.`,
|
|
8051
|
+
`Handoff reserve: ${handoffReserve} calls.`,
|
|
8052
|
+
"Begin preparing a compact handoff now."
|
|
8053
|
+
].join(`
|
|
8054
|
+
`);
|
|
8055
|
+
var TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
8056
|
+
"The productive tool-call budget is exhausted.",
|
|
8057
|
+
"Work tools are locked; do not execute further work.",
|
|
8058
|
+
"Call `structured_output` exactly once, alone, with a detailed configured result if you can complete now.",
|
|
8059
|
+
"If you settle without a result, the extension persists an `handoff` result for a fresh child."
|
|
8060
|
+
].join(`
|
|
8061
|
+
`);
|
|
8062
|
+
var toolBudgetHandoffResult = (policy, productiveCalls) => {
|
|
8063
|
+
if (policy.handoffOutcome !== "handoff") {
|
|
8064
|
+
throw new Error("tool-budget handoff requires a configured handoff outcome");
|
|
8065
|
+
}
|
|
8066
|
+
return {
|
|
8067
|
+
version: 1,
|
|
8068
|
+
policyDigest: policy.policyDigest,
|
|
8069
|
+
outcome: policy.handoffOutcome,
|
|
8070
|
+
summary: [
|
|
8071
|
+
"# Handoff: Productive tool-call budget exhausted.",
|
|
8072
|
+
"",
|
|
8073
|
+
`- Completed work: Productive calls completed: ${productiveCalls}.`,
|
|
8074
|
+
"- Current state: Work tools are locked; no further productive tool calls ran.",
|
|
8075
|
+
"- Remaining work: A fresh child must inspect the previous handoff and continue this same step.",
|
|
8076
|
+
"- Blocker: The configured productive tool-call budget is exhausted.",
|
|
8077
|
+
"**Next:** Start a fresh delegated attempt for this step using the persisted handoff."
|
|
8078
|
+
].join(`
|
|
8079
|
+
`)
|
|
8080
|
+
};
|
|
8081
|
+
};
|
|
7954
8082
|
var needsCompletionRepair = ({
|
|
7955
8083
|
policy,
|
|
7956
8084
|
dependencies
|
|
@@ -8035,11 +8163,25 @@ var childSystemPrompt = (policy) => {
|
|
|
8035
8163
|
"",
|
|
8036
8164
|
"The parent workflow harness owns orchestration and state transitions.",
|
|
8037
8165
|
"Perform only this delegated step. Its child-side tool policy is enforced.",
|
|
8166
|
+
"Do not launch subagents while executing this declarative workflow step.",
|
|
8167
|
+
"Use `blocked` only when progress requires user-provided information, a decision, authority, credentials, or approval.",
|
|
8168
|
+
"Use `retry` only for a transient failure that can be retried without new user input.",
|
|
8169
|
+
"Do not open a skill unless this step YAML lists that skill.",
|
|
8038
8170
|
"When finished, call `structured_output` exactly once and as the only tool call in that message.",
|
|
8039
8171
|
"Pass the workflow result as its `value`: outcome, summary, optional artifact, and workspace only when required below.",
|
|
8040
8172
|
`Valid outcomes: ${policy.outcomes.join(", ")}`,
|
|
8041
8173
|
`Pause outcomes: ${policy.pauseOutcomes.join(", ") || "(none)"}`,
|
|
8042
8174
|
`Summary limit: ${policy.summaryMaxChars} characters`,
|
|
8175
|
+
...policy.maxToolCalls === undefined ? [] : [
|
|
8176
|
+
"",
|
|
8177
|
+
"## Tool-call budget",
|
|
8178
|
+
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
8179
|
+
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
8180
|
+
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
8181
|
+
"At 2 productive calls remaining, prepare a concise handoff with completed work, current state, remaining work, and any blocker.",
|
|
8182
|
+
"Work tools are locked when the productive budget is exhausted.",
|
|
8183
|
+
"If the child settles without a result after exhaustion, the extension writes the configured `handoff` structured result."
|
|
8184
|
+
],
|
|
8043
8185
|
...policy.gateSubmitOutcome ? [
|
|
8044
8186
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`
|
|
8045
8187
|
] : [],
|
|
@@ -8060,7 +8202,9 @@ var INITIAL_STATE = {
|
|
|
8060
8202
|
policyError: undefined,
|
|
8061
8203
|
invalidCompletionCalls: new Set,
|
|
8062
8204
|
effectiveTools: new Set,
|
|
8063
|
-
|
|
8205
|
+
productiveToolCallIds: new Set,
|
|
8206
|
+
repairRequested: false,
|
|
8207
|
+
runtimeMode: "working"
|
|
8064
8208
|
};
|
|
8065
8209
|
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
8066
8210
|
var invalidPolicyInput = (pi, policyError, images) => {
|
|
@@ -8127,7 +8271,9 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8127
8271
|
activePolicy: extracted.policy,
|
|
8128
8272
|
policyError: undefined,
|
|
8129
8273
|
effectiveTools,
|
|
8130
|
-
|
|
8274
|
+
productiveToolCallIds: new Set,
|
|
8275
|
+
repairRequested: false,
|
|
8276
|
+
runtimeMode: "working"
|
|
8131
8277
|
};
|
|
8132
8278
|
} catch (error) {
|
|
8133
8279
|
const policyError = errorMessage2(error);
|
|
@@ -8160,11 +8306,67 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8160
8306
|
pi.on("turn_start", () => {
|
|
8161
8307
|
state = { ...state, invalidCompletionCalls: new Set };
|
|
8162
8308
|
});
|
|
8309
|
+
pi.on("tool_execution_end", (event) => {
|
|
8310
|
+
const policy = state.activePolicy;
|
|
8311
|
+
const handoffReserve = policy?.handoffReserve;
|
|
8312
|
+
if (!policy || policy.maxToolCalls === undefined || handoffReserve === undefined || event.toolName === CHILD_COMPLETION_TOOL || state.runtimeMode === "handoff" || state.productiveToolCallIds.has(event.toolCallId)) {
|
|
8313
|
+
return;
|
|
8314
|
+
}
|
|
8315
|
+
const productiveToolCallIds = new Set(state.productiveToolCallIds);
|
|
8316
|
+
productiveToolCallIds.add(event.toolCallId);
|
|
8317
|
+
const productiveCalls = productiveToolCallIds.size;
|
|
8318
|
+
if (productiveCalls >= policy.maxToolCalls) {
|
|
8319
|
+
state = {
|
|
8320
|
+
...state,
|
|
8321
|
+
effectiveTools: new Set,
|
|
8322
|
+
productiveToolCallIds,
|
|
8323
|
+
runtimeMode: "handoff"
|
|
8324
|
+
};
|
|
8325
|
+
try {
|
|
8326
|
+
writeChildResult({
|
|
8327
|
+
policy,
|
|
8328
|
+
result: toolBudgetHandoffResult(policy, productiveCalls),
|
|
8329
|
+
dependencies
|
|
8330
|
+
});
|
|
8331
|
+
pi.setActiveTools([]);
|
|
8332
|
+
} catch {
|
|
8333
|
+
state = {
|
|
8334
|
+
...state,
|
|
8335
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL])
|
|
8336
|
+
};
|
|
8337
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
8338
|
+
pi.sendUserMessage(TOOL_BUDGET_HANDOFF_PROMPT, {
|
|
8339
|
+
deliverAs: "followUp"
|
|
8340
|
+
});
|
|
8341
|
+
}
|
|
8342
|
+
return;
|
|
8343
|
+
}
|
|
8344
|
+
state = { ...state, productiveToolCallIds };
|
|
8345
|
+
const productiveRemaining = policy.maxToolCalls - productiveCalls;
|
|
8346
|
+
if (productiveRemaining <= handoffReserve) {
|
|
8347
|
+
pi.sendUserMessage(toolBudgetWarningPrompt({
|
|
8348
|
+
productiveCalls,
|
|
8349
|
+
productiveRemaining,
|
|
8350
|
+
handoffReserve
|
|
8351
|
+
}), { deliverAs: "followUp" });
|
|
8352
|
+
}
|
|
8353
|
+
});
|
|
8163
8354
|
pi.on("agent_settled", () => {
|
|
8164
8355
|
const policy = state.activePolicy;
|
|
8165
|
-
if (!policy || state.repairRequested
|
|
8356
|
+
if (!policy || state.repairRequested)
|
|
8166
8357
|
return;
|
|
8358
|
+
if (state.runtimeMode === "handoff" && needsCompletionRepair({ policy, dependencies })) {
|
|
8359
|
+
try {
|
|
8360
|
+
writeChildResult({
|
|
8361
|
+
policy,
|
|
8362
|
+
result: toolBudgetHandoffResult(policy, state.productiveToolCallIds.size),
|
|
8363
|
+
dependencies
|
|
8364
|
+
});
|
|
8365
|
+
return;
|
|
8366
|
+
} catch {}
|
|
8167
8367
|
}
|
|
8368
|
+
if (!needsCompletionRepair({ policy, dependencies }))
|
|
8369
|
+
return;
|
|
8168
8370
|
state = {
|
|
8169
8371
|
...state,
|
|
8170
8372
|
repairRequested: true,
|
|
@@ -8223,6 +8425,12 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8223
8425
|
};
|
|
8224
8426
|
}
|
|
8225
8427
|
}
|
|
8428
|
+
if (state.runtimeMode === "handoff") {
|
|
8429
|
+
return {
|
|
8430
|
+
block: true,
|
|
8431
|
+
reason: "Productive tool-call budget is exhausted; the extension-owned handoff is already persisted"
|
|
8432
|
+
};
|
|
8433
|
+
}
|
|
8226
8434
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
|
8227
8435
|
return {
|
|
8228
8436
|
block: true,
|
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -87,6 +87,7 @@ export type WorkflowStep = {
|
|
|
87
87
|
readonly prompt: PromptSpec;
|
|
88
88
|
/** Optional workflow-owned role prompt for this main-agent step. */
|
|
89
89
|
readonly agent?: StepAgent;
|
|
90
|
+
readonly maxToolCalls?: number;
|
|
90
91
|
readonly permissions: StepPermissions;
|
|
91
92
|
readonly requires: StepRequirements;
|
|
92
93
|
readonly transitions: Readonly<Record<string, StepTarget>>;
|
|
@@ -358,6 +358,7 @@ export function parseWorkflowStep(
|
|
|
358
358
|
'title',
|
|
359
359
|
'prompt',
|
|
360
360
|
'agent',
|
|
361
|
+
'maxToolCalls',
|
|
361
362
|
'permissions',
|
|
362
363
|
'requires',
|
|
363
364
|
'transitions',
|
|
@@ -382,6 +383,13 @@ export function parseWorkflowStep(
|
|
|
382
383
|
const agent: StepAgent | undefined = agentName
|
|
383
384
|
? { name: agentName }
|
|
384
385
|
: undefined;
|
|
386
|
+
const maxToolCalls =
|
|
387
|
+
value.maxToolCalls === undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: readInteger(value.maxToolCalls, 1, `${path}.maxToolCalls`, errors, {
|
|
390
|
+
min: 1,
|
|
391
|
+
max: 100_000,
|
|
392
|
+
});
|
|
385
393
|
const permissions = parsePermissions(
|
|
386
394
|
value.permissions,
|
|
387
395
|
`${path}.permissions`,
|
|
@@ -430,6 +438,17 @@ export function parseWorkflowStep(
|
|
|
430
438
|
);
|
|
431
439
|
}
|
|
432
440
|
}
|
|
441
|
+
if (agent && maxToolCalls !== undefined) {
|
|
442
|
+
if (!Object.hasOwn(transitions, 'handoff')) {
|
|
443
|
+
errors.push(
|
|
444
|
+
`${path}.transitions: maxToolCalls requires a "handoff" transition`,
|
|
445
|
+
);
|
|
446
|
+
} else if (transitions.handoff !== stepId) {
|
|
447
|
+
errors.push(
|
|
448
|
+
`${path}.transitions: handoff transition must target "${stepId}"`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
433
452
|
if (workspace) {
|
|
434
453
|
workspace.bindOn.forEach((outcome) => {
|
|
435
454
|
if (!Object.hasOwn(transitions, outcome)) {
|
|
@@ -445,6 +464,7 @@ export function parseWorkflowStep(
|
|
|
445
464
|
title,
|
|
446
465
|
prompt,
|
|
447
466
|
...(agent ? { agent } : {}),
|
|
467
|
+
...(maxToolCalls === undefined ? {} : { maxToolCalls }),
|
|
448
468
|
permissions,
|
|
449
469
|
requires,
|
|
450
470
|
transitions,
|
|
@@ -13,6 +13,8 @@ import { buildDelegatedStepTask } from '../prompt.ts';
|
|
|
13
13
|
import type { WorkflowHarnessDependencies } from './dependencies.ts';
|
|
14
14
|
import type { ActiveDelegation } from './types.ts';
|
|
15
15
|
|
|
16
|
+
const HANDOFF_RESERVE = 2;
|
|
17
|
+
|
|
16
18
|
type DelegationPlanInput = {
|
|
17
19
|
workflow: LoadedWorkflow;
|
|
18
20
|
run: WorkflowRun;
|
|
@@ -156,6 +158,15 @@ export function createDelegationPlan(
|
|
|
156
158
|
const workspace = dependencies.createDelegationWorkspace();
|
|
157
159
|
const outcomes = allowedOutcomes(workflow, run);
|
|
158
160
|
const outcomeSet = new Set(outcomes);
|
|
161
|
+
const toolBudget =
|
|
162
|
+
step.maxToolCalls === undefined
|
|
163
|
+
? {}
|
|
164
|
+
: {
|
|
165
|
+
maxToolCalls: step.maxToolCalls,
|
|
166
|
+
handoffReserve: HANDOFF_RESERVE,
|
|
167
|
+
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE,
|
|
168
|
+
handoffOutcome: 'handoff',
|
|
169
|
+
};
|
|
159
170
|
const policyDigest = digest({
|
|
160
171
|
version: 1,
|
|
161
172
|
requestId,
|
|
@@ -168,6 +179,7 @@ export function createDelegationPlan(
|
|
|
168
179
|
resultPath: workspace.resultPath,
|
|
169
180
|
permissions: step.permissions,
|
|
170
181
|
outcomes,
|
|
182
|
+
...toolBudget,
|
|
171
183
|
...(step.workspace ? { workspace: step.workspace } : {}),
|
|
172
184
|
});
|
|
173
185
|
const policy: ChildStepPolicy = {
|
|
@@ -191,6 +203,8 @@ export function createDelegationPlan(
|
|
|
191
203
|
)
|
|
192
204
|
.map(([outcome]) => outcome),
|
|
193
205
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
206
|
+
...toolBudget,
|
|
207
|
+
...(step.maxToolCalls === undefined ? {} : { handoffOutcome: 'handoff' }),
|
|
194
208
|
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
195
209
|
...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
|
|
196
210
|
};
|
|
@@ -22,6 +22,14 @@ export type ChildStepPolicy = {
|
|
|
22
22
|
/** Outcomes that pause instead of advancing to another workflow step. */
|
|
23
23
|
readonly pauseOutcomes: ReadonlyArray<string>;
|
|
24
24
|
readonly summaryMaxChars: number;
|
|
25
|
+
/** Productive calls available before the mandatory handoff reserve. */
|
|
26
|
+
readonly maxToolCalls?: number;
|
|
27
|
+
/** Calls reserved for the child to hand off after productive work. */
|
|
28
|
+
readonly handoffReserve?: number;
|
|
29
|
+
/** Productive calls plus the mandatory handoff reserve. */
|
|
30
|
+
readonly totalToolCalls?: number;
|
|
31
|
+
/** Extension-owned outcome persisted when an exhausted child settles without a result. */
|
|
32
|
+
readonly handoffOutcome?: string;
|
|
25
33
|
readonly gateSubmitOutcome?: string;
|
|
26
34
|
readonly workspace?: StepWorkspaceBinding;
|
|
27
35
|
};
|
|
@@ -28,6 +28,10 @@ const POLICY_KEYS: ReadonlySet<string> = new Set([
|
|
|
28
28
|
'outcomes',
|
|
29
29
|
'pauseOutcomes',
|
|
30
30
|
'summaryMaxChars',
|
|
31
|
+
'maxToolCalls',
|
|
32
|
+
'handoffReserve',
|
|
33
|
+
'totalToolCalls',
|
|
34
|
+
'handoffOutcome',
|
|
31
35
|
'gateSubmitOutcome',
|
|
32
36
|
'workspace',
|
|
33
37
|
]);
|
|
@@ -75,6 +79,48 @@ const rejectUnknownProperties = (
|
|
|
75
79
|
}
|
|
76
80
|
};
|
|
77
81
|
|
|
82
|
+
type ToolBudget = Pick<
|
|
83
|
+
ChildStepPolicy,
|
|
84
|
+
'maxToolCalls' | 'handoffReserve' | 'totalToolCalls'
|
|
85
|
+
>;
|
|
86
|
+
|
|
87
|
+
const parseToolBudget = (
|
|
88
|
+
value: Readonly<Record<string, unknown>>,
|
|
89
|
+
): ToolBudget => {
|
|
90
|
+
const fields = [
|
|
91
|
+
value.maxToolCalls,
|
|
92
|
+
value.handoffReserve,
|
|
93
|
+
value.totalToolCalls,
|
|
94
|
+
];
|
|
95
|
+
if (fields.every((field) => field === undefined)) return {};
|
|
96
|
+
if (fields.some((field) => field === undefined)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'child policy tool budget fields must be provided together',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const [maxToolCalls, handoffReserve, totalToolCalls] = fields;
|
|
103
|
+
if (
|
|
104
|
+
typeof maxToolCalls !== 'number' ||
|
|
105
|
+
!Number.isInteger(maxToolCalls) ||
|
|
106
|
+
maxToolCalls < 1 ||
|
|
107
|
+
maxToolCalls > 100_000
|
|
108
|
+
) {
|
|
109
|
+
throw new Error('child policy maxToolCalls is invalid');
|
|
110
|
+
}
|
|
111
|
+
if (handoffReserve !== 2) {
|
|
112
|
+
throw new Error('child policy handoffReserve is invalid');
|
|
113
|
+
}
|
|
114
|
+
if (
|
|
115
|
+
typeof totalToolCalls !== 'number' ||
|
|
116
|
+
!Number.isInteger(totalToolCalls) ||
|
|
117
|
+
totalToolCalls !== maxToolCalls + handoffReserve
|
|
118
|
+
) {
|
|
119
|
+
throw new Error('child policy totalToolCalls is invalid');
|
|
120
|
+
}
|
|
121
|
+
return { maxToolCalls, handoffReserve, totalToolCalls };
|
|
122
|
+
};
|
|
123
|
+
|
|
78
124
|
type IdentityAndPaths = Pick<
|
|
79
125
|
ChildStepPolicy,
|
|
80
126
|
| 'version'
|
|
@@ -161,8 +207,27 @@ export const parseChildPolicy = (
|
|
|
161
207
|
if (!isRecord(value)) throw new Error('child policy must be an object');
|
|
162
208
|
|
|
163
209
|
rejectUnknownProperties(value);
|
|
210
|
+
const sections = parseChildPolicySections(value);
|
|
211
|
+
const toolBudget = parseToolBudget(value);
|
|
212
|
+
const handoffOutcome = value.handoffOutcome;
|
|
213
|
+
if (handoffOutcome !== undefined) {
|
|
214
|
+
if (typeof handoffOutcome !== 'string' || handoffOutcome !== 'handoff') {
|
|
215
|
+
throw new Error('child policy handoffOutcome is invalid');
|
|
216
|
+
}
|
|
217
|
+
if (toolBudget.maxToolCalls === undefined) {
|
|
218
|
+
throw new Error('child policy handoffOutcome requires a tool budget');
|
|
219
|
+
}
|
|
220
|
+
if (!sections.outcomes.includes(handoffOutcome)) {
|
|
221
|
+
throw new Error('child policy handoffOutcome must be an allowed outcome');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (toolBudget.maxToolCalls !== undefined && handoffOutcome === undefined) {
|
|
225
|
+
throw new Error('child policy tool budget requires handoffOutcome');
|
|
226
|
+
}
|
|
164
227
|
return {
|
|
165
228
|
...parseIdentityAndPaths(value, environment),
|
|
166
|
-
...
|
|
229
|
+
...sections,
|
|
230
|
+
...toolBudget,
|
|
231
|
+
...(handoffOutcome ? { handoffOutcome } : {}),
|
|
167
232
|
};
|
|
168
233
|
};
|
|
@@ -29,11 +29,27 @@ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
|
29
29
|
'',
|
|
30
30
|
'The parent workflow harness owns orchestration and state transitions.',
|
|
31
31
|
'Perform only this delegated step. Its child-side tool policy is enforced.',
|
|
32
|
+
'Do not launch subagents while executing this declarative workflow step.',
|
|
33
|
+
'Use `blocked` only when progress requires user-provided information, a decision, authority, credentials, or approval.',
|
|
34
|
+
'Use `retry` only for a transient failure that can be retried without new user input.',
|
|
35
|
+
'Do not open a skill unless this step YAML lists that skill.',
|
|
32
36
|
'When finished, call `structured_output` exactly once and as the only tool call in that message.',
|
|
33
37
|
'Pass the workflow result as its `value`: outcome, summary, optional artifact, and workspace only when required below.',
|
|
34
38
|
`Valid outcomes: ${policy.outcomes.join(', ')}`,
|
|
35
39
|
`Pause outcomes: ${policy.pauseOutcomes.join(', ') || '(none)'}`,
|
|
36
40
|
`Summary limit: ${policy.summaryMaxChars} characters`,
|
|
41
|
+
...(policy.maxToolCalls === undefined
|
|
42
|
+
? []
|
|
43
|
+
: [
|
|
44
|
+
'',
|
|
45
|
+
'## Tool-call budget',
|
|
46
|
+
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
47
|
+
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
48
|
+
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
49
|
+
'At 2 productive calls remaining, prepare a concise handoff with completed work, current state, remaining work, and any blocker.',
|
|
50
|
+
'Work tools are locked when the productive budget is exhausted.',
|
|
51
|
+
'If the child settles without a result after exhaustion, the extension writes the configured `handoff` structured result.',
|
|
52
|
+
]),
|
|
37
53
|
...(policy.gateSubmitOutcome
|
|
38
54
|
? [
|
|
39
55
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkflowStepResult } from '../../runtime/step-result.ts';
|
|
1
2
|
import type { ChildStepPolicy } from './child-policy-types.ts';
|
|
2
3
|
import type { SubagentChildRuntimeDependencies } from './child-runtime-types.ts';
|
|
3
4
|
|
|
@@ -7,6 +8,56 @@ export const COMPLETION_REPAIR_PROMPT = [
|
|
|
7
8
|
'Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields.',
|
|
8
9
|
].join('\n');
|
|
9
10
|
|
|
11
|
+
export const toolBudgetWarningPrompt = ({
|
|
12
|
+
productiveCalls,
|
|
13
|
+
productiveRemaining,
|
|
14
|
+
handoffReserve,
|
|
15
|
+
}: {
|
|
16
|
+
readonly productiveCalls: number;
|
|
17
|
+
readonly productiveRemaining: number;
|
|
18
|
+
readonly handoffReserve: number;
|
|
19
|
+
}): string =>
|
|
20
|
+
[
|
|
21
|
+
'Tool-call budget warning.',
|
|
22
|
+
`Productive calls used: ${productiveCalls}.`,
|
|
23
|
+
`Productive calls remaining: ${productiveRemaining}.`,
|
|
24
|
+
`Handoff reserve: ${handoffReserve} calls.`,
|
|
25
|
+
'Begin preparing a compact handoff now.',
|
|
26
|
+
].join('\n');
|
|
27
|
+
|
|
28
|
+
export const TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
29
|
+
'The productive tool-call budget is exhausted.',
|
|
30
|
+
'Work tools are locked; do not execute further work.',
|
|
31
|
+
'Call `structured_output` exactly once, alone, with a detailed configured result if you can complete now.',
|
|
32
|
+
'If you settle without a result, the extension persists an `handoff` result for a fresh child.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
/** Builds the only extension-owned result allowed after productive budget exhaustion. */
|
|
36
|
+
export const toolBudgetHandoffResult = (
|
|
37
|
+
policy: ChildStepPolicy,
|
|
38
|
+
productiveCalls: number,
|
|
39
|
+
): WorkflowStepResult => {
|
|
40
|
+
if (policy.handoffOutcome !== 'handoff') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'tool-budget handoff requires a configured handoff outcome',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
version: 1,
|
|
47
|
+
policyDigest: policy.policyDigest,
|
|
48
|
+
outcome: policy.handoffOutcome,
|
|
49
|
+
summary: [
|
|
50
|
+
'# Handoff: Productive tool-call budget exhausted.',
|
|
51
|
+
'',
|
|
52
|
+
`- Completed work: Productive calls completed: ${productiveCalls}.`,
|
|
53
|
+
'- Current state: Work tools are locked; no further productive tool calls ran.',
|
|
54
|
+
'- Remaining work: A fresh child must inspect the previous handoff and continue this same step.',
|
|
55
|
+
'- Blocker: The configured productive tool-call budget is exhausted.',
|
|
56
|
+
'**Next:** Start a fresh delegated attempt for this step using the persisted handoff.',
|
|
57
|
+
].join('\n'),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
10
61
|
/** Returns whether a same-child completion repair may be requested safely. */
|
|
11
62
|
export const needsCompletionRepair = ({
|
|
12
63
|
policy,
|
|
@@ -16,6 +16,9 @@ import { DEFAULT_CHILD_RUNTIME_DEPENDENCIES } from './child-runtime-dependencies
|
|
|
16
16
|
import {
|
|
17
17
|
COMPLETION_REPAIR_PROMPT,
|
|
18
18
|
needsCompletionRepair,
|
|
19
|
+
TOOL_BUDGET_HANDOFF_PROMPT,
|
|
20
|
+
toolBudgetHandoffResult,
|
|
21
|
+
toolBudgetWarningPrompt,
|
|
19
22
|
} from './child-runtime-repair.ts';
|
|
20
23
|
import {
|
|
21
24
|
verifyChildCapability,
|
|
@@ -43,7 +46,9 @@ type ChildRuntimeState = {
|
|
|
43
46
|
readonly policyError: string | undefined;
|
|
44
47
|
readonly invalidCompletionCalls: ReadonlySet<string>;
|
|
45
48
|
readonly effectiveTools: ReadonlySet<string>;
|
|
49
|
+
readonly productiveToolCallIds: ReadonlySet<string>;
|
|
46
50
|
readonly repairRequested: boolean;
|
|
51
|
+
readonly runtimeMode: 'working' | 'handoff';
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
const INITIAL_STATE: ChildRuntimeState = {
|
|
@@ -51,7 +56,9 @@ const INITIAL_STATE: ChildRuntimeState = {
|
|
|
51
56
|
policyError: undefined,
|
|
52
57
|
invalidCompletionCalls: new Set(),
|
|
53
58
|
effectiveTools: new Set(),
|
|
59
|
+
productiveToolCallIds: new Set(),
|
|
54
60
|
repairRequested: false,
|
|
61
|
+
runtimeMode: 'working',
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
const errorMessage = (error: unknown): string =>
|
|
@@ -148,7 +155,9 @@ export const registerSubagentChildRuntime = (
|
|
|
148
155
|
activePolicy: extracted.policy,
|
|
149
156
|
policyError: undefined,
|
|
150
157
|
effectiveTools,
|
|
158
|
+
productiveToolCallIds: new Set(),
|
|
151
159
|
repairRequested: false,
|
|
160
|
+
runtimeMode: 'working',
|
|
152
161
|
};
|
|
153
162
|
} catch (error) {
|
|
154
163
|
const policyError = errorMessage(error);
|
|
@@ -182,15 +191,87 @@ export const registerSubagentChildRuntime = (
|
|
|
182
191
|
state = { ...state, invalidCompletionCalls: new Set() };
|
|
183
192
|
});
|
|
184
193
|
|
|
185
|
-
pi.on('
|
|
194
|
+
pi.on('tool_execution_end', (event) => {
|
|
186
195
|
const policy = state.activePolicy;
|
|
196
|
+
const handoffReserve = policy?.handoffReserve;
|
|
187
197
|
if (
|
|
188
198
|
!policy ||
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
policy.maxToolCalls === undefined ||
|
|
200
|
+
handoffReserve === undefined ||
|
|
201
|
+
event.toolName === CHILD_COMPLETION_TOOL ||
|
|
202
|
+
state.runtimeMode === 'handoff' ||
|
|
203
|
+
state.productiveToolCallIds.has(event.toolCallId)
|
|
191
204
|
) {
|
|
192
205
|
return;
|
|
193
206
|
}
|
|
207
|
+
|
|
208
|
+
const productiveToolCallIds = new Set(state.productiveToolCallIds);
|
|
209
|
+
productiveToolCallIds.add(event.toolCallId);
|
|
210
|
+
const productiveCalls = productiveToolCallIds.size;
|
|
211
|
+
if (productiveCalls >= policy.maxToolCalls) {
|
|
212
|
+
state = {
|
|
213
|
+
...state,
|
|
214
|
+
effectiveTools: new Set(),
|
|
215
|
+
productiveToolCallIds,
|
|
216
|
+
runtimeMode: 'handoff',
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
writeChildResult({
|
|
220
|
+
policy,
|
|
221
|
+
result: toolBudgetHandoffResult(policy, productiveCalls),
|
|
222
|
+
dependencies,
|
|
223
|
+
});
|
|
224
|
+
pi.setActiveTools([]);
|
|
225
|
+
} catch {
|
|
226
|
+
state = {
|
|
227
|
+
...state,
|
|
228
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL]),
|
|
229
|
+
};
|
|
230
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
231
|
+
pi.sendUserMessage(TOOL_BUDGET_HANDOFF_PROMPT, {
|
|
232
|
+
deliverAs: 'followUp',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
state = { ...state, productiveToolCallIds };
|
|
239
|
+
const productiveRemaining = policy.maxToolCalls - productiveCalls;
|
|
240
|
+
if (productiveRemaining <= handoffReserve) {
|
|
241
|
+
pi.sendUserMessage(
|
|
242
|
+
toolBudgetWarningPrompt({
|
|
243
|
+
productiveCalls,
|
|
244
|
+
productiveRemaining,
|
|
245
|
+
handoffReserve,
|
|
246
|
+
}),
|
|
247
|
+
{ deliverAs: 'followUp' },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
pi.on('agent_settled', () => {
|
|
253
|
+
const policy = state.activePolicy;
|
|
254
|
+
if (!policy || state.repairRequested) return;
|
|
255
|
+
if (
|
|
256
|
+
state.runtimeMode === 'handoff' &&
|
|
257
|
+
needsCompletionRepair({ policy, dependencies })
|
|
258
|
+
) {
|
|
259
|
+
try {
|
|
260
|
+
writeChildResult({
|
|
261
|
+
policy,
|
|
262
|
+
result: toolBudgetHandoffResult(
|
|
263
|
+
policy,
|
|
264
|
+
state.productiveToolCallIds.size,
|
|
265
|
+
),
|
|
266
|
+
dependencies,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
} catch {
|
|
270
|
+
// Preserve the normal completion-repair path so the parent gets the
|
|
271
|
+
// correlated result failure if the protected write cannot be made.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!needsCompletionRepair({ policy, dependencies })) return;
|
|
194
275
|
state = {
|
|
195
276
|
...state,
|
|
196
277
|
repairRequested: true,
|
|
@@ -252,6 +333,13 @@ export const registerSubagentChildRuntime = (
|
|
|
252
333
|
};
|
|
253
334
|
}
|
|
254
335
|
}
|
|
336
|
+
if (state.runtimeMode === 'handoff') {
|
|
337
|
+
return {
|
|
338
|
+
block: true,
|
|
339
|
+
reason:
|
|
340
|
+
'Productive tool-call budget is exhausted; the extension-owned handoff is already persisted',
|
|
341
|
+
};
|
|
342
|
+
}
|
|
255
343
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
|
256
344
|
return {
|
|
257
345
|
block: true,
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -51,6 +51,7 @@ type RenderStepPromptOptions = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const RESUME_INPUT_PLACEHOLDER = /\{\{\s*resume\.input\s*\}\}/;
|
|
54
|
+
const WORKFLOW_INPUT_PLACEHOLDER = /\{\{\s*workflow\.input\s*\}\}/;
|
|
54
55
|
|
|
55
56
|
const renderStepPrompt = ({
|
|
56
57
|
workflow,
|
|
@@ -110,6 +111,7 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
110
111
|
const isDelegated = execution === 'delegated';
|
|
111
112
|
const promptTemplate = workflow.prompts[run.currentStepId] ?? '';
|
|
112
113
|
const prompt = renderStepPrompt({ workflow, run, step, execution });
|
|
114
|
+
const showWorkflowInput = WORKFLOW_INPUT_PLACEHOLDER.test(promptTemplate);
|
|
113
115
|
const handoff = currentStepHandoff(run);
|
|
114
116
|
const contract = createStepContract({ workflow, run, step });
|
|
115
117
|
const completionTool = isDelegated
|
|
@@ -135,6 +137,9 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
135
137
|
: []),
|
|
136
138
|
prompt,
|
|
137
139
|
'',
|
|
140
|
+
...(showWorkflowInput
|
|
141
|
+
? ['## Original workflow request', '', run.input || '(none supplied)', '']
|
|
142
|
+
: []),
|
|
138
143
|
...(isDelegated ? buildDelegatedHandoffSection(handoff) : []),
|
|
139
144
|
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
140
145
|
...buildResumeInputSection(
|
|
@@ -45,6 +45,37 @@ const isObject = (value: unknown): value is Record<string, unknown> => {
|
|
|
45
45
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
function validateNonSuccessSummary(outcome: string, summary: string): void {
|
|
49
|
+
if (outcome === 'blocked') {
|
|
50
|
+
const isActionable =
|
|
51
|
+
/^# Blocked:\s+.+/m.test(summary) &&
|
|
52
|
+
/^\s*\*\*Action:\*\*\s+.+/m.test(summary) &&
|
|
53
|
+
/^\s*\*\*Next:\*\*\s+.+/m.test(summary);
|
|
54
|
+
if (!isActionable) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'blocked summary must identify the missing user prerequisite and next action',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (outcome === 'retry') {
|
|
61
|
+
const describesTransientFailure = /\btransient\b/i.test(summary);
|
|
62
|
+
const hasSafeRetryCondition =
|
|
63
|
+
/\b(safe (retry|to retry)|retry (when|after|once))\b/i.test(summary);
|
|
64
|
+
const requestsUserInput =
|
|
65
|
+
/\b(provide|confirm|choose|approve|decide)\b/i.test(summary);
|
|
66
|
+
if (
|
|
67
|
+
!/^# Retry:\s+.+/m.test(summary) ||
|
|
68
|
+
!describesTransientFailure ||
|
|
69
|
+
!hasSafeRetryCondition ||
|
|
70
|
+
requestsUserInput
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'retry summary must identify a transient failure and safe retry condition',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
function parseResultWorkspace(
|
|
49
80
|
value: unknown,
|
|
50
81
|
outcome: string,
|
|
@@ -131,6 +162,7 @@ export function parseWorkflowStepResult(
|
|
|
131
162
|
`workflow step summary exceeds ${policy.summaryMaxChars} characters`,
|
|
132
163
|
);
|
|
133
164
|
}
|
|
165
|
+
validateNonSuccessSummary(value.outcome, summary);
|
|
134
166
|
if (value.artifact !== undefined && typeof value.artifact !== 'string') {
|
|
135
167
|
throw new Error('workflow step artifact must be a string');
|
|
136
168
|
}
|