@wichayutdew/pi-workflows 3.6.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 +110 -13
- package/package.json +1 -1
- package/src/config/validation/step.ts +11 -0
- package/src/harness/delegation-plan.ts +2 -0
- package/src/integrations/subagents/child-policy-types.ts +2 -0
- package/src/integrations/subagents/child-policy-validation.ts +21 -2
- package/src/integrations/subagents/child-runtime-policy.ts +6 -2
- package/src/integrations/subagents/child-runtime-repair.ts +29 -1
- package/src/integrations/subagents/child-runtime.ts +38 -10
- package/src/prompt/step-task.ts +5 -0
- package/src/runtime/step-result.ts +32 -0
package/dist/index.js
CHANGED
|
@@ -731,6 +731,13 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
731
731
|
errors.push(`${path}.transitions: artifact-contract retry requires a "retry" transition`);
|
|
732
732
|
}
|
|
733
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
|
+
}
|
|
734
741
|
if (workspace) {
|
|
735
742
|
workspace.bindOn.forEach((outcome) => {
|
|
736
743
|
if (!Object.hasOwn(transitions, outcome)) {
|
|
@@ -2615,6 +2622,22 @@ var RESULT_KEYS = new Set([
|
|
|
2615
2622
|
var isObject = (value) => {
|
|
2616
2623
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2617
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
|
+
}
|
|
2618
2641
|
function parseResultWorkspace(value, outcome, policy) {
|
|
2619
2642
|
const requiresWorkspace = policy.workspace?.bindOn.includes(outcome) === true;
|
|
2620
2643
|
if (!requiresWorkspace) {
|
|
@@ -2669,6 +2692,7 @@ function parseWorkflowStepResult(value, policy) {
|
|
|
2669
2692
|
if (summary.length > policy.summaryMaxChars) {
|
|
2670
2693
|
throw new Error(`workflow step summary exceeds ${policy.summaryMaxChars} characters`);
|
|
2671
2694
|
}
|
|
2695
|
+
validateNonSuccessSummary(value.outcome, summary);
|
|
2672
2696
|
if (value.artifact !== undefined && typeof value.artifact !== "string") {
|
|
2673
2697
|
throw new Error("workflow step artifact must be a string");
|
|
2674
2698
|
}
|
|
@@ -6059,6 +6083,7 @@ var resolveStep = (workflow, run) => {
|
|
|
6059
6083
|
return step;
|
|
6060
6084
|
};
|
|
6061
6085
|
var RESUME_INPUT_PLACEHOLDER = /\{\{\s*resume\.input\s*\}\}/;
|
|
6086
|
+
var WORKFLOW_INPUT_PLACEHOLDER = /\{\{\s*workflow\.input\s*\}\}/;
|
|
6062
6087
|
var renderStepPrompt = ({
|
|
6063
6088
|
workflow,
|
|
6064
6089
|
run,
|
|
@@ -6100,6 +6125,7 @@ function buildStepTask(options) {
|
|
|
6100
6125
|
const isDelegated = execution === "delegated";
|
|
6101
6126
|
const promptTemplate = workflow.prompts[run.currentStepId] ?? "";
|
|
6102
6127
|
const prompt = renderStepPrompt({ workflow, run, step, execution });
|
|
6128
|
+
const showWorkflowInput = WORKFLOW_INPUT_PLACEHOLDER.test(promptTemplate);
|
|
6103
6129
|
const handoff = currentStepHandoff(run);
|
|
6104
6130
|
const contract = createStepContract({ workflow, run, step });
|
|
6105
6131
|
const completionTool = isDelegated ? "structured_output" : "workflow_complete_step";
|
|
@@ -6119,6 +6145,7 @@ function buildStepTask(options) {
|
|
|
6119
6145
|
...step.agent ? ["## Role prompt", "", rolePrompt(step.agent.name), ""] : [],
|
|
6120
6146
|
prompt,
|
|
6121
6147
|
"",
|
|
6148
|
+
...showWorkflowInput ? ["## Original workflow request", "", run.input || "(none supplied)", ""] : [],
|
|
6122
6149
|
...isDelegated ? buildDelegatedHandoffSection(handoff) : [],
|
|
6123
6150
|
...buildRestartWorkspaceSection(run.restartWorkspaceCwd),
|
|
6124
6151
|
...buildResumeInputSection(run, RESUME_INPUT_PLACEHOLDER.test(promptTemplate)),
|
|
@@ -6402,6 +6429,7 @@ var POLICY_KEYS = new Set([
|
|
|
6402
6429
|
"maxToolCalls",
|
|
6403
6430
|
"handoffReserve",
|
|
6404
6431
|
"totalToolCalls",
|
|
6432
|
+
"handoffOutcome",
|
|
6405
6433
|
"gateSubmitOutcome",
|
|
6406
6434
|
"workspace"
|
|
6407
6435
|
]);
|
|
@@ -6497,10 +6525,28 @@ var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) =
|
|
|
6497
6525
|
if (!isRecord11(value))
|
|
6498
6526
|
throw new Error("child policy must be an object");
|
|
6499
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
|
+
}
|
|
6500
6545
|
return {
|
|
6501
6546
|
...parseIdentityAndPaths(value, environment),
|
|
6502
|
-
...
|
|
6503
|
-
...
|
|
6547
|
+
...sections,
|
|
6548
|
+
...toolBudget,
|
|
6549
|
+
...handoffOutcome ? { handoffOutcome } : {}
|
|
6504
6550
|
};
|
|
6505
6551
|
};
|
|
6506
6552
|
|
|
@@ -6695,7 +6741,8 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6695
6741
|
const toolBudget = step.maxToolCalls === undefined ? {} : {
|
|
6696
6742
|
maxToolCalls: step.maxToolCalls,
|
|
6697
6743
|
handoffReserve: HANDOFF_RESERVE,
|
|
6698
|
-
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE
|
|
6744
|
+
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE,
|
|
6745
|
+
handoffOutcome: "handoff"
|
|
6699
6746
|
};
|
|
6700
6747
|
const policyDigest = digest({
|
|
6701
6748
|
version: 1,
|
|
@@ -6730,6 +6777,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6730
6777
|
pauseOutcomes: Object.entries(step.transitions).filter(([outcome, target]) => target === "$pause" && outcomeSet.has(outcome)).map(([outcome]) => outcome),
|
|
6731
6778
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
6732
6779
|
...toolBudget,
|
|
6780
|
+
...step.maxToolCalls === undefined ? {} : { handoffOutcome: "handoff" },
|
|
6733
6781
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
6734
6782
|
...step.workspace ? { workspace: structuredClone(step.workspace) } : {}
|
|
6735
6783
|
};
|
|
@@ -8007,9 +8055,30 @@ var toolBudgetWarningPrompt = ({
|
|
|
8007
8055
|
var TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
8008
8056
|
"The productive tool-call budget is exhausted.",
|
|
8009
8057
|
"Work tools are locked; do not execute further work.",
|
|
8010
|
-
"Call `structured_output` exactly once, alone, with
|
|
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."
|
|
8011
8060
|
].join(`
|
|
8012
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
|
+
};
|
|
8013
8082
|
var needsCompletionRepair = ({
|
|
8014
8083
|
policy,
|
|
8015
8084
|
dependencies
|
|
@@ -8094,6 +8163,10 @@ var childSystemPrompt = (policy) => {
|
|
|
8094
8163
|
"",
|
|
8095
8164
|
"The parent workflow harness owns orchestration and state transitions.",
|
|
8096
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.",
|
|
8097
8170
|
"When finished, call `structured_output` exactly once and as the only tool call in that message.",
|
|
8098
8171
|
"Pass the workflow result as its `value`: outcome, summary, optional artifact, and workspace only when required below.",
|
|
8099
8172
|
`Valid outcomes: ${policy.outcomes.join(", ")}`,
|
|
@@ -8105,9 +8178,9 @@ var childSystemPrompt = (policy) => {
|
|
|
8105
8178
|
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
8106
8179
|
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
8107
8180
|
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
8108
|
-
"At 2 productive calls remaining,
|
|
8181
|
+
"At 2 productive calls remaining, prepare a concise handoff with completed work, current state, remaining work, and any blocker.",
|
|
8109
8182
|
"Work tools are locked when the productive budget is exhausted.",
|
|
8110
|
-
"
|
|
8183
|
+
"If the child settles without a result after exhaustion, the extension writes the configured `handoff` structured result."
|
|
8111
8184
|
],
|
|
8112
8185
|
...policy.gateSubmitOutcome ? [
|
|
8113
8186
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`
|
|
@@ -8245,14 +8318,27 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8245
8318
|
if (productiveCalls >= policy.maxToolCalls) {
|
|
8246
8319
|
state = {
|
|
8247
8320
|
...state,
|
|
8248
|
-
effectiveTools: new Set
|
|
8321
|
+
effectiveTools: new Set,
|
|
8249
8322
|
productiveToolCallIds,
|
|
8250
8323
|
runtimeMode: "handoff"
|
|
8251
8324
|
};
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
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
|
+
}
|
|
8256
8342
|
return;
|
|
8257
8343
|
}
|
|
8258
8344
|
state = { ...state, productiveToolCallIds };
|
|
@@ -8267,9 +8353,20 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8267
8353
|
});
|
|
8268
8354
|
pi.on("agent_settled", () => {
|
|
8269
8355
|
const policy = state.activePolicy;
|
|
8270
|
-
if (!policy || state.repairRequested
|
|
8356
|
+
if (!policy || state.repairRequested)
|
|
8271
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 {}
|
|
8272
8367
|
}
|
|
8368
|
+
if (!needsCompletionRepair({ policy, dependencies }))
|
|
8369
|
+
return;
|
|
8273
8370
|
state = {
|
|
8274
8371
|
...state,
|
|
8275
8372
|
repairRequested: true,
|
|
@@ -8331,7 +8428,7 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8331
8428
|
if (state.runtimeMode === "handoff") {
|
|
8332
8429
|
return {
|
|
8333
8430
|
block: true,
|
|
8334
|
-
reason: "Productive tool-call budget is exhausted;
|
|
8431
|
+
reason: "Productive tool-call budget is exhausted; the extension-owned handoff is already persisted"
|
|
8335
8432
|
};
|
|
8336
8433
|
}
|
|
8337
8434
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
package/package.json
CHANGED
|
@@ -438,6 +438,17 @@ export function parseWorkflowStep(
|
|
|
438
438
|
);
|
|
439
439
|
}
|
|
440
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
|
+
}
|
|
441
452
|
if (workspace) {
|
|
442
453
|
workspace.bindOn.forEach((outcome) => {
|
|
443
454
|
if (!Object.hasOwn(transitions, outcome)) {
|
|
@@ -165,6 +165,7 @@ export function createDelegationPlan(
|
|
|
165
165
|
maxToolCalls: step.maxToolCalls,
|
|
166
166
|
handoffReserve: HANDOFF_RESERVE,
|
|
167
167
|
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE,
|
|
168
|
+
handoffOutcome: 'handoff',
|
|
168
169
|
};
|
|
169
170
|
const policyDigest = digest({
|
|
170
171
|
version: 1,
|
|
@@ -203,6 +204,7 @@ export function createDelegationPlan(
|
|
|
203
204
|
.map(([outcome]) => outcome),
|
|
204
205
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
205
206
|
...toolBudget,
|
|
207
|
+
...(step.maxToolCalls === undefined ? {} : { handoffOutcome: 'handoff' }),
|
|
206
208
|
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
207
209
|
...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
|
|
208
210
|
};
|
|
@@ -28,6 +28,8 @@ export type ChildStepPolicy = {
|
|
|
28
28
|
readonly handoffReserve?: number;
|
|
29
29
|
/** Productive calls plus the mandatory handoff reserve. */
|
|
30
30
|
readonly totalToolCalls?: number;
|
|
31
|
+
/** Extension-owned outcome persisted when an exhausted child settles without a result. */
|
|
32
|
+
readonly handoffOutcome?: string;
|
|
31
33
|
readonly gateSubmitOutcome?: string;
|
|
32
34
|
readonly workspace?: StepWorkspaceBinding;
|
|
33
35
|
};
|
|
@@ -31,6 +31,7 @@ const POLICY_KEYS: ReadonlySet<string> = new Set([
|
|
|
31
31
|
'maxToolCalls',
|
|
32
32
|
'handoffReserve',
|
|
33
33
|
'totalToolCalls',
|
|
34
|
+
'handoffOutcome',
|
|
34
35
|
'gateSubmitOutcome',
|
|
35
36
|
'workspace',
|
|
36
37
|
]);
|
|
@@ -206,9 +207,27 @@ export const parseChildPolicy = (
|
|
|
206
207
|
if (!isRecord(value)) throw new Error('child policy must be an object');
|
|
207
208
|
|
|
208
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
|
+
}
|
|
209
227
|
return {
|
|
210
228
|
...parseIdentityAndPaths(value, environment),
|
|
211
|
-
...
|
|
212
|
-
...
|
|
229
|
+
...sections,
|
|
230
|
+
...toolBudget,
|
|
231
|
+
...(handoffOutcome ? { handoffOutcome } : {}),
|
|
213
232
|
};
|
|
214
233
|
};
|
|
@@ -29,6 +29,10 @@ 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(', ')}`,
|
|
@@ -42,9 +46,9 @@ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
|
42
46
|
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
43
47
|
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
44
48
|
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
45
|
-
'At 2 productive calls remaining,
|
|
49
|
+
'At 2 productive calls remaining, prepare a concise handoff with completed work, current state, remaining work, and any blocker.',
|
|
46
50
|
'Work tools are locked when the productive budget is exhausted.',
|
|
47
|
-
'
|
|
51
|
+
'If the child settles without a result after exhaustion, the extension writes the configured `handoff` structured result.',
|
|
48
52
|
]),
|
|
49
53
|
...(policy.gateSubmitOutcome
|
|
50
54
|
? [
|
|
@@ -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
|
|
|
@@ -27,9 +28,36 @@ export const toolBudgetWarningPrompt = ({
|
|
|
27
28
|
export const TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
28
29
|
'The productive tool-call budget is exhausted.',
|
|
29
30
|
'Work tools are locked; do not execute further work.',
|
|
30
|
-
'Call `structured_output` exactly once, alone, with
|
|
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.',
|
|
31
33
|
].join('\n');
|
|
32
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
|
+
|
|
33
61
|
/** Returns whether a same-child completion repair may be requested safely. */
|
|
34
62
|
export const needsCompletionRepair = ({
|
|
35
63
|
policy,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
COMPLETION_REPAIR_PROMPT,
|
|
18
18
|
needsCompletionRepair,
|
|
19
19
|
TOOL_BUDGET_HANDOFF_PROMPT,
|
|
20
|
+
toolBudgetHandoffResult,
|
|
20
21
|
toolBudgetWarningPrompt,
|
|
21
22
|
} from './child-runtime-repair.ts';
|
|
22
23
|
import {
|
|
@@ -210,14 +211,27 @@ export const registerSubagentChildRuntime = (
|
|
|
210
211
|
if (productiveCalls >= policy.maxToolCalls) {
|
|
211
212
|
state = {
|
|
212
213
|
...state,
|
|
213
|
-
effectiveTools: new Set(
|
|
214
|
+
effectiveTools: new Set(),
|
|
214
215
|
productiveToolCallIds,
|
|
215
216
|
runtimeMode: 'handoff',
|
|
216
217
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}
|
|
221
235
|
return;
|
|
222
236
|
}
|
|
223
237
|
|
|
@@ -237,13 +251,27 @@ export const registerSubagentChildRuntime = (
|
|
|
237
251
|
|
|
238
252
|
pi.on('agent_settled', () => {
|
|
239
253
|
const policy = state.activePolicy;
|
|
254
|
+
if (!policy || state.repairRequested) return;
|
|
240
255
|
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
!needsCompletionRepair({ policy, dependencies })
|
|
256
|
+
state.runtimeMode === 'handoff' &&
|
|
257
|
+
needsCompletionRepair({ policy, dependencies })
|
|
244
258
|
) {
|
|
245
|
-
|
|
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
|
+
}
|
|
246
273
|
}
|
|
274
|
+
if (!needsCompletionRepair({ policy, dependencies })) return;
|
|
247
275
|
state = {
|
|
248
276
|
...state,
|
|
249
277
|
repairRequested: true,
|
|
@@ -309,7 +337,7 @@ export const registerSubagentChildRuntime = (
|
|
|
309
337
|
return {
|
|
310
338
|
block: true,
|
|
311
339
|
reason:
|
|
312
|
-
'Productive tool-call budget is exhausted;
|
|
340
|
+
'Productive tool-call budget is exhausted; the extension-owned handoff is already persisted',
|
|
313
341
|
};
|
|
314
342
|
}
|
|
315
343
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
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
|
}
|