@wichayutdew/pi-workflows 3.5.0 → 3.6.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 +114 -3
- 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 +9 -0
- package/src/harness/delegation-plan.ts +12 -0
- package/src/integrations/subagents/child-policy-types.ts +6 -0
- package/src/integrations/subagents/child-policy-validation.ts +46 -0
- package/src/integrations/subagents/child-runtime-policy.ts +12 -0
- package/src/integrations/subagents/child-runtime-repair.ts +23 -0
- package/src/integrations/subagents/child-runtime.ts +60 -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);
|
|
@@ -739,6 +744,7 @@ function parseWorkflowStep(value, stepId, path, errors) {
|
|
|
739
744
|
title,
|
|
740
745
|
prompt,
|
|
741
746
|
...agent ? { agent } : {},
|
|
747
|
+
...maxToolCalls === undefined ? {} : { maxToolCalls },
|
|
742
748
|
permissions,
|
|
743
749
|
requires,
|
|
744
750
|
transitions,
|
|
@@ -6393,6 +6399,9 @@ var POLICY_KEYS = new Set([
|
|
|
6393
6399
|
"outcomes",
|
|
6394
6400
|
"pauseOutcomes",
|
|
6395
6401
|
"summaryMaxChars",
|
|
6402
|
+
"maxToolCalls",
|
|
6403
|
+
"handoffReserve",
|
|
6404
|
+
"totalToolCalls",
|
|
6396
6405
|
"gateSubmitOutcome",
|
|
6397
6406
|
"workspace"
|
|
6398
6407
|
]);
|
|
@@ -6411,6 +6420,29 @@ var rejectUnknownProperties = (value) => {
|
|
|
6411
6420
|
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
6412
6421
|
}
|
|
6413
6422
|
};
|
|
6423
|
+
var parseToolBudget = (value) => {
|
|
6424
|
+
const fields2 = [
|
|
6425
|
+
value.maxToolCalls,
|
|
6426
|
+
value.handoffReserve,
|
|
6427
|
+
value.totalToolCalls
|
|
6428
|
+
];
|
|
6429
|
+
if (fields2.every((field) => field === undefined))
|
|
6430
|
+
return {};
|
|
6431
|
+
if (fields2.some((field) => field === undefined)) {
|
|
6432
|
+
throw new Error("child policy tool budget fields must be provided together");
|
|
6433
|
+
}
|
|
6434
|
+
const [maxToolCalls, handoffReserve, totalToolCalls] = fields2;
|
|
6435
|
+
if (typeof maxToolCalls !== "number" || !Number.isInteger(maxToolCalls) || maxToolCalls < 1 || maxToolCalls > 1e5) {
|
|
6436
|
+
throw new Error("child policy maxToolCalls is invalid");
|
|
6437
|
+
}
|
|
6438
|
+
if (handoffReserve !== 2) {
|
|
6439
|
+
throw new Error("child policy handoffReserve is invalid");
|
|
6440
|
+
}
|
|
6441
|
+
if (typeof totalToolCalls !== "number" || !Number.isInteger(totalToolCalls) || totalToolCalls !== maxToolCalls + handoffReserve) {
|
|
6442
|
+
throw new Error("child policy totalToolCalls is invalid");
|
|
6443
|
+
}
|
|
6444
|
+
return { maxToolCalls, handoffReserve, totalToolCalls };
|
|
6445
|
+
};
|
|
6414
6446
|
var parseIdentityAndPaths = (value, environment) => {
|
|
6415
6447
|
const requestId = requiredString(value, "requestId");
|
|
6416
6448
|
const agent = requiredString(value, "agent");
|
|
@@ -6467,7 +6499,8 @@ var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) =
|
|
|
6467
6499
|
rejectUnknownProperties(value);
|
|
6468
6500
|
return {
|
|
6469
6501
|
...parseIdentityAndPaths(value, environment),
|
|
6470
|
-
...parseChildPolicySections(value)
|
|
6502
|
+
...parseChildPolicySections(value),
|
|
6503
|
+
...parseToolBudget(value)
|
|
6471
6504
|
};
|
|
6472
6505
|
};
|
|
6473
6506
|
|
|
@@ -6564,6 +6597,7 @@ var parseDelegatedStepResult = (value, policy) => {
|
|
|
6564
6597
|
}
|
|
6565
6598
|
};
|
|
6566
6599
|
// src/harness/delegation-plan.ts
|
|
6600
|
+
var HANDOFF_RESERVE = 2;
|
|
6567
6601
|
function createDelegationPlan(input, dependencies) {
|
|
6568
6602
|
const { workflow, run, step, latestContext } = input;
|
|
6569
6603
|
const agent = step.agent?.name;
|
|
@@ -6658,6 +6692,11 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6658
6692
|
const workspace = dependencies.createDelegationWorkspace();
|
|
6659
6693
|
const outcomes = allowedOutcomes(workflow, run);
|
|
6660
6694
|
const outcomeSet = new Set(outcomes);
|
|
6695
|
+
const toolBudget = step.maxToolCalls === undefined ? {} : {
|
|
6696
|
+
maxToolCalls: step.maxToolCalls,
|
|
6697
|
+
handoffReserve: HANDOFF_RESERVE,
|
|
6698
|
+
totalToolCalls: step.maxToolCalls + HANDOFF_RESERVE
|
|
6699
|
+
};
|
|
6661
6700
|
const policyDigest = digest({
|
|
6662
6701
|
version: 1,
|
|
6663
6702
|
requestId,
|
|
@@ -6670,6 +6709,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6670
6709
|
resultPath: workspace.resultPath,
|
|
6671
6710
|
permissions: step.permissions,
|
|
6672
6711
|
outcomes,
|
|
6712
|
+
...toolBudget,
|
|
6673
6713
|
...step.workspace ? { workspace: step.workspace } : {}
|
|
6674
6714
|
});
|
|
6675
6715
|
const policy = {
|
|
@@ -6689,6 +6729,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6689
6729
|
outcomes,
|
|
6690
6730
|
pauseOutcomes: Object.entries(step.transitions).filter(([outcome, target]) => target === "$pause" && outcomeSet.has(outcome)).map(([outcome]) => outcome),
|
|
6691
6731
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
6732
|
+
...toolBudget,
|
|
6692
6733
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
6693
6734
|
...step.workspace ? { workspace: structuredClone(step.workspace) } : {}
|
|
6694
6735
|
};
|
|
@@ -7951,6 +7992,24 @@ var COMPLETION_REPAIR_PROMPT = [
|
|
|
7951
7992
|
"Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields."
|
|
7952
7993
|
].join(`
|
|
7953
7994
|
`);
|
|
7995
|
+
var toolBudgetWarningPrompt = ({
|
|
7996
|
+
productiveCalls,
|
|
7997
|
+
productiveRemaining,
|
|
7998
|
+
handoffReserve
|
|
7999
|
+
}) => [
|
|
8000
|
+
"Tool-call budget warning.",
|
|
8001
|
+
`Productive calls used: ${productiveCalls}.`,
|
|
8002
|
+
`Productive calls remaining: ${productiveRemaining}.`,
|
|
8003
|
+
`Handoff reserve: ${handoffReserve} calls.`,
|
|
8004
|
+
"Begin preparing a compact handoff now."
|
|
8005
|
+
].join(`
|
|
8006
|
+
`);
|
|
8007
|
+
var TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
8008
|
+
"The productive tool-call budget is exhausted.",
|
|
8009
|
+
"Work tools are locked; do not execute further work.",
|
|
8010
|
+
"Call `structured_output` exactly once, alone, with one configured outcome and a compact handoff."
|
|
8011
|
+
].join(`
|
|
8012
|
+
`);
|
|
7954
8013
|
var needsCompletionRepair = ({
|
|
7955
8014
|
policy,
|
|
7956
8015
|
dependencies
|
|
@@ -8040,6 +8099,16 @@ var childSystemPrompt = (policy) => {
|
|
|
8040
8099
|
`Valid outcomes: ${policy.outcomes.join(", ")}`,
|
|
8041
8100
|
`Pause outcomes: ${policy.pauseOutcomes.join(", ") || "(none)"}`,
|
|
8042
8101
|
`Summary limit: ${policy.summaryMaxChars} characters`,
|
|
8102
|
+
...policy.maxToolCalls === undefined ? [] : [
|
|
8103
|
+
"",
|
|
8104
|
+
"## Tool-call budget",
|
|
8105
|
+
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
8106
|
+
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
8107
|
+
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
8108
|
+
"At 2 productive calls remaining, begin handoff.",
|
|
8109
|
+
"Work tools are locked when the productive budget is exhausted.",
|
|
8110
|
+
"Continue with `handoff` using the reserved calls."
|
|
8111
|
+
],
|
|
8043
8112
|
...policy.gateSubmitOutcome ? [
|
|
8044
8113
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`
|
|
8045
8114
|
] : [],
|
|
@@ -8060,7 +8129,9 @@ var INITIAL_STATE = {
|
|
|
8060
8129
|
policyError: undefined,
|
|
8061
8130
|
invalidCompletionCalls: new Set,
|
|
8062
8131
|
effectiveTools: new Set,
|
|
8063
|
-
|
|
8132
|
+
productiveToolCallIds: new Set,
|
|
8133
|
+
repairRequested: false,
|
|
8134
|
+
runtimeMode: "working"
|
|
8064
8135
|
};
|
|
8065
8136
|
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
8066
8137
|
var invalidPolicyInput = (pi, policyError, images) => {
|
|
@@ -8127,7 +8198,9 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8127
8198
|
activePolicy: extracted.policy,
|
|
8128
8199
|
policyError: undefined,
|
|
8129
8200
|
effectiveTools,
|
|
8130
|
-
|
|
8201
|
+
productiveToolCallIds: new Set,
|
|
8202
|
+
repairRequested: false,
|
|
8203
|
+
runtimeMode: "working"
|
|
8131
8204
|
};
|
|
8132
8205
|
} catch (error) {
|
|
8133
8206
|
const policyError = errorMessage2(error);
|
|
@@ -8160,6 +8233,38 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8160
8233
|
pi.on("turn_start", () => {
|
|
8161
8234
|
state = { ...state, invalidCompletionCalls: new Set };
|
|
8162
8235
|
});
|
|
8236
|
+
pi.on("tool_execution_end", (event) => {
|
|
8237
|
+
const policy = state.activePolicy;
|
|
8238
|
+
const handoffReserve = policy?.handoffReserve;
|
|
8239
|
+
if (!policy || policy.maxToolCalls === undefined || handoffReserve === undefined || event.toolName === CHILD_COMPLETION_TOOL || state.runtimeMode === "handoff" || state.productiveToolCallIds.has(event.toolCallId)) {
|
|
8240
|
+
return;
|
|
8241
|
+
}
|
|
8242
|
+
const productiveToolCallIds = new Set(state.productiveToolCallIds);
|
|
8243
|
+
productiveToolCallIds.add(event.toolCallId);
|
|
8244
|
+
const productiveCalls = productiveToolCallIds.size;
|
|
8245
|
+
if (productiveCalls >= policy.maxToolCalls) {
|
|
8246
|
+
state = {
|
|
8247
|
+
...state,
|
|
8248
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL]),
|
|
8249
|
+
productiveToolCallIds,
|
|
8250
|
+
runtimeMode: "handoff"
|
|
8251
|
+
};
|
|
8252
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
8253
|
+
pi.sendUserMessage(TOOL_BUDGET_HANDOFF_PROMPT, {
|
|
8254
|
+
deliverAs: "followUp"
|
|
8255
|
+
});
|
|
8256
|
+
return;
|
|
8257
|
+
}
|
|
8258
|
+
state = { ...state, productiveToolCallIds };
|
|
8259
|
+
const productiveRemaining = policy.maxToolCalls - productiveCalls;
|
|
8260
|
+
if (productiveRemaining <= handoffReserve) {
|
|
8261
|
+
pi.sendUserMessage(toolBudgetWarningPrompt({
|
|
8262
|
+
productiveCalls,
|
|
8263
|
+
productiveRemaining,
|
|
8264
|
+
handoffReserve
|
|
8265
|
+
}), { deliverAs: "followUp" });
|
|
8266
|
+
}
|
|
8267
|
+
});
|
|
8163
8268
|
pi.on("agent_settled", () => {
|
|
8164
8269
|
const policy = state.activePolicy;
|
|
8165
8270
|
if (!policy || state.repairRequested || !needsCompletionRepair({ policy, dependencies })) {
|
|
@@ -8223,6 +8328,12 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8223
8328
|
};
|
|
8224
8329
|
}
|
|
8225
8330
|
}
|
|
8331
|
+
if (state.runtimeMode === "handoff") {
|
|
8332
|
+
return {
|
|
8333
|
+
block: true,
|
|
8334
|
+
reason: "Productive tool-call budget is exhausted; only structured_output is available for handoff"
|
|
8335
|
+
};
|
|
8336
|
+
}
|
|
8226
8337
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
|
8227
8338
|
return {
|
|
8228
8339
|
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`,
|
|
@@ -445,6 +453,7 @@ export function parseWorkflowStep(
|
|
|
445
453
|
title,
|
|
446
454
|
prompt,
|
|
447
455
|
...(agent ? { agent } : {}),
|
|
456
|
+
...(maxToolCalls === undefined ? {} : { maxToolCalls }),
|
|
448
457
|
permissions,
|
|
449
458
|
requires,
|
|
450
459
|
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,14 @@ 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
|
+
};
|
|
159
169
|
const policyDigest = digest({
|
|
160
170
|
version: 1,
|
|
161
171
|
requestId,
|
|
@@ -168,6 +178,7 @@ export function createDelegationPlan(
|
|
|
168
178
|
resultPath: workspace.resultPath,
|
|
169
179
|
permissions: step.permissions,
|
|
170
180
|
outcomes,
|
|
181
|
+
...toolBudget,
|
|
171
182
|
...(step.workspace ? { workspace: step.workspace } : {}),
|
|
172
183
|
});
|
|
173
184
|
const policy: ChildStepPolicy = {
|
|
@@ -191,6 +202,7 @@ export function createDelegationPlan(
|
|
|
191
202
|
)
|
|
192
203
|
.map(([outcome]) => outcome),
|
|
193
204
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
205
|
+
...toolBudget,
|
|
194
206
|
...(step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {}),
|
|
195
207
|
...(step.workspace ? { workspace: structuredClone(step.workspace) } : {}),
|
|
196
208
|
};
|
|
@@ -22,6 +22,12 @@ 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;
|
|
25
31
|
readonly gateSubmitOutcome?: string;
|
|
26
32
|
readonly workspace?: StepWorkspaceBinding;
|
|
27
33
|
};
|
|
@@ -28,6 +28,9 @@ const POLICY_KEYS: ReadonlySet<string> = new Set([
|
|
|
28
28
|
'outcomes',
|
|
29
29
|
'pauseOutcomes',
|
|
30
30
|
'summaryMaxChars',
|
|
31
|
+
'maxToolCalls',
|
|
32
|
+
'handoffReserve',
|
|
33
|
+
'totalToolCalls',
|
|
31
34
|
'gateSubmitOutcome',
|
|
32
35
|
'workspace',
|
|
33
36
|
]);
|
|
@@ -75,6 +78,48 @@ const rejectUnknownProperties = (
|
|
|
75
78
|
}
|
|
76
79
|
};
|
|
77
80
|
|
|
81
|
+
type ToolBudget = Pick<
|
|
82
|
+
ChildStepPolicy,
|
|
83
|
+
'maxToolCalls' | 'handoffReserve' | 'totalToolCalls'
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
const parseToolBudget = (
|
|
87
|
+
value: Readonly<Record<string, unknown>>,
|
|
88
|
+
): ToolBudget => {
|
|
89
|
+
const fields = [
|
|
90
|
+
value.maxToolCalls,
|
|
91
|
+
value.handoffReserve,
|
|
92
|
+
value.totalToolCalls,
|
|
93
|
+
];
|
|
94
|
+
if (fields.every((field) => field === undefined)) return {};
|
|
95
|
+
if (fields.some((field) => field === undefined)) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'child policy tool budget fields must be provided together',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const [maxToolCalls, handoffReserve, totalToolCalls] = fields;
|
|
102
|
+
if (
|
|
103
|
+
typeof maxToolCalls !== 'number' ||
|
|
104
|
+
!Number.isInteger(maxToolCalls) ||
|
|
105
|
+
maxToolCalls < 1 ||
|
|
106
|
+
maxToolCalls > 100_000
|
|
107
|
+
) {
|
|
108
|
+
throw new Error('child policy maxToolCalls is invalid');
|
|
109
|
+
}
|
|
110
|
+
if (handoffReserve !== 2) {
|
|
111
|
+
throw new Error('child policy handoffReserve is invalid');
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
typeof totalToolCalls !== 'number' ||
|
|
115
|
+
!Number.isInteger(totalToolCalls) ||
|
|
116
|
+
totalToolCalls !== maxToolCalls + handoffReserve
|
|
117
|
+
) {
|
|
118
|
+
throw new Error('child policy totalToolCalls is invalid');
|
|
119
|
+
}
|
|
120
|
+
return { maxToolCalls, handoffReserve, totalToolCalls };
|
|
121
|
+
};
|
|
122
|
+
|
|
78
123
|
type IdentityAndPaths = Pick<
|
|
79
124
|
ChildStepPolicy,
|
|
80
125
|
| 'version'
|
|
@@ -164,5 +209,6 @@ export const parseChildPolicy = (
|
|
|
164
209
|
return {
|
|
165
210
|
...parseIdentityAndPaths(value, environment),
|
|
166
211
|
...parseChildPolicySections(value),
|
|
212
|
+
...parseToolBudget(value),
|
|
167
213
|
};
|
|
168
214
|
};
|
|
@@ -34,6 +34,18 @@ export const childSystemPrompt = (policy: ChildStepPolicy): string => {
|
|
|
34
34
|
`Valid outcomes: ${policy.outcomes.join(', ')}`,
|
|
35
35
|
`Pause outcomes: ${policy.pauseOutcomes.join(', ') || '(none)'}`,
|
|
36
36
|
`Summary limit: ${policy.summaryMaxChars} characters`,
|
|
37
|
+
...(policy.maxToolCalls === undefined
|
|
38
|
+
? []
|
|
39
|
+
: [
|
|
40
|
+
'',
|
|
41
|
+
'## Tool-call budget',
|
|
42
|
+
`Productive tool-call budget: ${policy.maxToolCalls} calls.`,
|
|
43
|
+
`Handoff reserve: ${policy.handoffReserve} calls.`,
|
|
44
|
+
`Total tool-call budget: ${policy.totalToolCalls} calls.`,
|
|
45
|
+
'At 2 productive calls remaining, begin handoff.',
|
|
46
|
+
'Work tools are locked when the productive budget is exhausted.',
|
|
47
|
+
'Continue with `handoff` using the reserved calls.',
|
|
48
|
+
]),
|
|
37
49
|
...(policy.gateSubmitOutcome
|
|
38
50
|
? [
|
|
39
51
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`,
|
|
@@ -7,6 +7,29 @@ export const COMPLETION_REPAIR_PROMPT = [
|
|
|
7
7
|
'Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields.',
|
|
8
8
|
].join('\n');
|
|
9
9
|
|
|
10
|
+
export const toolBudgetWarningPrompt = ({
|
|
11
|
+
productiveCalls,
|
|
12
|
+
productiveRemaining,
|
|
13
|
+
handoffReserve,
|
|
14
|
+
}: {
|
|
15
|
+
readonly productiveCalls: number;
|
|
16
|
+
readonly productiveRemaining: number;
|
|
17
|
+
readonly handoffReserve: number;
|
|
18
|
+
}): string =>
|
|
19
|
+
[
|
|
20
|
+
'Tool-call budget warning.',
|
|
21
|
+
`Productive calls used: ${productiveCalls}.`,
|
|
22
|
+
`Productive calls remaining: ${productiveRemaining}.`,
|
|
23
|
+
`Handoff reserve: ${handoffReserve} calls.`,
|
|
24
|
+
'Begin preparing a compact handoff now.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
export const TOOL_BUDGET_HANDOFF_PROMPT = [
|
|
28
|
+
'The productive tool-call budget is exhausted.',
|
|
29
|
+
'Work tools are locked; do not execute further work.',
|
|
30
|
+
'Call `structured_output` exactly once, alone, with one configured outcome and a compact handoff.',
|
|
31
|
+
].join('\n');
|
|
32
|
+
|
|
10
33
|
/** Returns whether a same-child completion repair may be requested safely. */
|
|
11
34
|
export const needsCompletionRepair = ({
|
|
12
35
|
policy,
|
|
@@ -16,6 +16,8 @@ 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
|
+
toolBudgetWarningPrompt,
|
|
19
21
|
} from './child-runtime-repair.ts';
|
|
20
22
|
import {
|
|
21
23
|
verifyChildCapability,
|
|
@@ -43,7 +45,9 @@ type ChildRuntimeState = {
|
|
|
43
45
|
readonly policyError: string | undefined;
|
|
44
46
|
readonly invalidCompletionCalls: ReadonlySet<string>;
|
|
45
47
|
readonly effectiveTools: ReadonlySet<string>;
|
|
48
|
+
readonly productiveToolCallIds: ReadonlySet<string>;
|
|
46
49
|
readonly repairRequested: boolean;
|
|
50
|
+
readonly runtimeMode: 'working' | 'handoff';
|
|
47
51
|
};
|
|
48
52
|
|
|
49
53
|
const INITIAL_STATE: ChildRuntimeState = {
|
|
@@ -51,7 +55,9 @@ const INITIAL_STATE: ChildRuntimeState = {
|
|
|
51
55
|
policyError: undefined,
|
|
52
56
|
invalidCompletionCalls: new Set(),
|
|
53
57
|
effectiveTools: new Set(),
|
|
58
|
+
productiveToolCallIds: new Set(),
|
|
54
59
|
repairRequested: false,
|
|
60
|
+
runtimeMode: 'working',
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
const errorMessage = (error: unknown): string =>
|
|
@@ -148,7 +154,9 @@ export const registerSubagentChildRuntime = (
|
|
|
148
154
|
activePolicy: extracted.policy,
|
|
149
155
|
policyError: undefined,
|
|
150
156
|
effectiveTools,
|
|
157
|
+
productiveToolCallIds: new Set(),
|
|
151
158
|
repairRequested: false,
|
|
159
|
+
runtimeMode: 'working',
|
|
152
160
|
};
|
|
153
161
|
} catch (error) {
|
|
154
162
|
const policyError = errorMessage(error);
|
|
@@ -182,6 +190,51 @@ export const registerSubagentChildRuntime = (
|
|
|
182
190
|
state = { ...state, invalidCompletionCalls: new Set() };
|
|
183
191
|
});
|
|
184
192
|
|
|
193
|
+
pi.on('tool_execution_end', (event) => {
|
|
194
|
+
const policy = state.activePolicy;
|
|
195
|
+
const handoffReserve = policy?.handoffReserve;
|
|
196
|
+
if (
|
|
197
|
+
!policy ||
|
|
198
|
+
policy.maxToolCalls === undefined ||
|
|
199
|
+
handoffReserve === undefined ||
|
|
200
|
+
event.toolName === CHILD_COMPLETION_TOOL ||
|
|
201
|
+
state.runtimeMode === 'handoff' ||
|
|
202
|
+
state.productiveToolCallIds.has(event.toolCallId)
|
|
203
|
+
) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const productiveToolCallIds = new Set(state.productiveToolCallIds);
|
|
208
|
+
productiveToolCallIds.add(event.toolCallId);
|
|
209
|
+
const productiveCalls = productiveToolCallIds.size;
|
|
210
|
+
if (productiveCalls >= policy.maxToolCalls) {
|
|
211
|
+
state = {
|
|
212
|
+
...state,
|
|
213
|
+
effectiveTools: new Set([CHILD_COMPLETION_TOOL]),
|
|
214
|
+
productiveToolCallIds,
|
|
215
|
+
runtimeMode: 'handoff',
|
|
216
|
+
};
|
|
217
|
+
pi.setActiveTools([CHILD_COMPLETION_TOOL]);
|
|
218
|
+
pi.sendUserMessage(TOOL_BUDGET_HANDOFF_PROMPT, {
|
|
219
|
+
deliverAs: 'followUp',
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
state = { ...state, productiveToolCallIds };
|
|
225
|
+
const productiveRemaining = policy.maxToolCalls - productiveCalls;
|
|
226
|
+
if (productiveRemaining <= handoffReserve) {
|
|
227
|
+
pi.sendUserMessage(
|
|
228
|
+
toolBudgetWarningPrompt({
|
|
229
|
+
productiveCalls,
|
|
230
|
+
productiveRemaining,
|
|
231
|
+
handoffReserve,
|
|
232
|
+
}),
|
|
233
|
+
{ deliverAs: 'followUp' },
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
185
238
|
pi.on('agent_settled', () => {
|
|
186
239
|
const policy = state.activePolicy;
|
|
187
240
|
if (
|
|
@@ -252,6 +305,13 @@ export const registerSubagentChildRuntime = (
|
|
|
252
305
|
};
|
|
253
306
|
}
|
|
254
307
|
}
|
|
308
|
+
if (state.runtimeMode === 'handoff') {
|
|
309
|
+
return {
|
|
310
|
+
block: true,
|
|
311
|
+
reason:
|
|
312
|
+
'Productive tool-call budget is exhausted; only structured_output is available for handoff',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
255
315
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
|
256
316
|
return {
|
|
257
317
|
block: true,
|