@wichayutdew/pi-workflows 3.4.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 +122 -5
- 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/src/prompt/step-task.ts +2 -0
- package/src/workflow-status/render-step-detail.ts +11 -4
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,
|
|
@@ -3195,6 +3201,9 @@ function wrapPlain(value, width, theme, color = "text") {
|
|
|
3195
3201
|
});
|
|
3196
3202
|
return wrapped.length > 0 ? wrapped : [theme.fg(color, " ")];
|
|
3197
3203
|
}
|
|
3204
|
+
function summaryLines(theme, value, width) {
|
|
3205
|
+
return [theme.fg("muted", "summary"), ...wrapPlain(value, width, theme)];
|
|
3206
|
+
}
|
|
3198
3207
|
function boundedArtifact(value) {
|
|
3199
3208
|
return value.length <= MAX_DETAIL_ARTIFACT_CHARS ? { value, truncated: false } : {
|
|
3200
3209
|
value: value.slice(0, MAX_DETAIL_ARTIFACT_CHARS),
|
|
@@ -3210,7 +3219,8 @@ function renderResult(theme, attempt, width) {
|
|
|
3210
3219
|
"",
|
|
3211
3220
|
theme.bold(theme.fg("accent", "Submitted result")),
|
|
3212
3221
|
...keyValueLines(theme, "outcome", result.outcome, width, "success"),
|
|
3213
|
-
...
|
|
3222
|
+
...summaryLines(theme, `${result.summary}${result.summaryTruncated ? `
|
|
3223
|
+
… [trace truncated]` : ""}`, width),
|
|
3214
3224
|
...result.workspaceCwd ? keyValueLines(theme, "workspace", result.workspaceCwd, width) : [],
|
|
3215
3225
|
...artifact ? [
|
|
3216
3226
|
theme.fg("muted", "artifact"),
|
|
@@ -3364,7 +3374,7 @@ function renderStepDetail(theme, snapshot, selectedIndex, cache, width) {
|
|
|
3364
3374
|
] : [],
|
|
3365
3375
|
...history ? [
|
|
3366
3376
|
...keyValueLines(theme, "outcome", history.outcome, width, "success"),
|
|
3367
|
-
...
|
|
3377
|
+
...summaryLines(theme, history.summary, width)
|
|
3368
3378
|
] : [],
|
|
3369
3379
|
...currentReason ? keyValueLines(theme, "reason", currentReason, width, "warning") : [],
|
|
3370
3380
|
...entry.isCurrent && snapshot.run.gateFeedback ? keyValueLines(theme, "feedback", snapshot.run.gateFeedback, width, "warning") : [],
|
|
@@ -6122,6 +6132,8 @@ function buildStepTask(options) {
|
|
|
6122
6132
|
...contract.workspaceLines,
|
|
6123
6133
|
"",
|
|
6124
6134
|
"Put a self-contained compact handoff in `summary`; this is the only step context passed to the next fresh child.",
|
|
6135
|
+
"Format all human-facing output—including summaries, gate artifacts, Markdown plans, reports, comments, and replies—for scanning: short headings, then one distinct fact, action, or metadata value per bullet or paragraph. Never pack unrelated values into one line or dense prose.",
|
|
6136
|
+
"When a schema needs several related fields, use a bullet list with one `field`: `value` per row; use JSON only for machine-readable data under `## Machine-readable handoff` in a fenced valid `json` block. Keep prose outside that block.",
|
|
6125
6137
|
...buildNonSuccessSummaryInstructions(contract.outcomes),
|
|
6126
6138
|
...isDelegated ? buildDelegatedCompletionInstructions() : [],
|
|
6127
6139
|
"Do not call the completion tool alongside other tool calls."
|
|
@@ -6387,6 +6399,9 @@ var POLICY_KEYS = new Set([
|
|
|
6387
6399
|
"outcomes",
|
|
6388
6400
|
"pauseOutcomes",
|
|
6389
6401
|
"summaryMaxChars",
|
|
6402
|
+
"maxToolCalls",
|
|
6403
|
+
"handoffReserve",
|
|
6404
|
+
"totalToolCalls",
|
|
6390
6405
|
"gateSubmitOutcome",
|
|
6391
6406
|
"workspace"
|
|
6392
6407
|
]);
|
|
@@ -6405,6 +6420,29 @@ var rejectUnknownProperties = (value) => {
|
|
|
6405
6420
|
throw new Error(`child policy has unknown property "${unknownKey}"`);
|
|
6406
6421
|
}
|
|
6407
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
|
+
};
|
|
6408
6446
|
var parseIdentityAndPaths = (value, environment) => {
|
|
6409
6447
|
const requestId = requiredString(value, "requestId");
|
|
6410
6448
|
const agent = requiredString(value, "agent");
|
|
@@ -6461,7 +6499,8 @@ var parseChildPolicy = (value, environment = DEFAULT_CHILD_POLICY_ENVIRONMENT) =
|
|
|
6461
6499
|
rejectUnknownProperties(value);
|
|
6462
6500
|
return {
|
|
6463
6501
|
...parseIdentityAndPaths(value, environment),
|
|
6464
|
-
...parseChildPolicySections(value)
|
|
6502
|
+
...parseChildPolicySections(value),
|
|
6503
|
+
...parseToolBudget(value)
|
|
6465
6504
|
};
|
|
6466
6505
|
};
|
|
6467
6506
|
|
|
@@ -6558,6 +6597,7 @@ var parseDelegatedStepResult = (value, policy) => {
|
|
|
6558
6597
|
}
|
|
6559
6598
|
};
|
|
6560
6599
|
// src/harness/delegation-plan.ts
|
|
6600
|
+
var HANDOFF_RESERVE = 2;
|
|
6561
6601
|
function createDelegationPlan(input, dependencies) {
|
|
6562
6602
|
const { workflow, run, step, latestContext } = input;
|
|
6563
6603
|
const agent = step.agent?.name;
|
|
@@ -6652,6 +6692,11 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6652
6692
|
const workspace = dependencies.createDelegationWorkspace();
|
|
6653
6693
|
const outcomes = allowedOutcomes(workflow, run);
|
|
6654
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
|
+
};
|
|
6655
6700
|
const policyDigest = digest({
|
|
6656
6701
|
version: 1,
|
|
6657
6702
|
requestId,
|
|
@@ -6664,6 +6709,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6664
6709
|
resultPath: workspace.resultPath,
|
|
6665
6710
|
permissions: step.permissions,
|
|
6666
6711
|
outcomes,
|
|
6712
|
+
...toolBudget,
|
|
6667
6713
|
...step.workspace ? { workspace: step.workspace } : {}
|
|
6668
6714
|
});
|
|
6669
6715
|
const policy = {
|
|
@@ -6683,6 +6729,7 @@ function createDelegationPlan(input, dependencies) {
|
|
|
6683
6729
|
outcomes,
|
|
6684
6730
|
pauseOutcomes: Object.entries(step.transitions).filter(([outcome, target]) => target === "$pause" && outcomeSet.has(outcome)).map(([outcome]) => outcome),
|
|
6685
6731
|
summaryMaxChars: workflow.definition.summaryMaxChars,
|
|
6732
|
+
...toolBudget,
|
|
6686
6733
|
...step.gate ? { gateSubmitOutcome: step.gate.submitOutcome } : {},
|
|
6687
6734
|
...step.workspace ? { workspace: structuredClone(step.workspace) } : {}
|
|
6688
6735
|
};
|
|
@@ -7945,6 +7992,24 @@ var COMPLETION_REPAIR_PROMPT = [
|
|
|
7945
7992
|
"Call `structured_output` exactly once, alone, with one configured outcome and the required summary, artifact, and workspace fields."
|
|
7946
7993
|
].join(`
|
|
7947
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
|
+
`);
|
|
7948
8013
|
var needsCompletionRepair = ({
|
|
7949
8014
|
policy,
|
|
7950
8015
|
dependencies
|
|
@@ -8034,6 +8099,16 @@ var childSystemPrompt = (policy) => {
|
|
|
8034
8099
|
`Valid outcomes: ${policy.outcomes.join(", ")}`,
|
|
8035
8100
|
`Pause outcomes: ${policy.pauseOutcomes.join(", ") || "(none)"}`,
|
|
8036
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
|
+
],
|
|
8037
8112
|
...policy.gateSubmitOutcome ? [
|
|
8038
8113
|
`Outcome "${policy.gateSubmitOutcome}" requires the complete gate artifact.`
|
|
8039
8114
|
] : [],
|
|
@@ -8054,7 +8129,9 @@ var INITIAL_STATE = {
|
|
|
8054
8129
|
policyError: undefined,
|
|
8055
8130
|
invalidCompletionCalls: new Set,
|
|
8056
8131
|
effectiveTools: new Set,
|
|
8057
|
-
|
|
8132
|
+
productiveToolCallIds: new Set,
|
|
8133
|
+
repairRequested: false,
|
|
8134
|
+
runtimeMode: "working"
|
|
8058
8135
|
};
|
|
8059
8136
|
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
8060
8137
|
var invalidPolicyInput = (pi, policyError, images) => {
|
|
@@ -8121,7 +8198,9 @@ var registerSubagentChildRuntime = (pi, options = {}) => {
|
|
|
8121
8198
|
activePolicy: extracted.policy,
|
|
8122
8199
|
policyError: undefined,
|
|
8123
8200
|
effectiveTools,
|
|
8124
|
-
|
|
8201
|
+
productiveToolCallIds: new Set,
|
|
8202
|
+
repairRequested: false,
|
|
8203
|
+
runtimeMode: "working"
|
|
8125
8204
|
};
|
|
8126
8205
|
} catch (error) {
|
|
8127
8206
|
const policyError = errorMessage2(error);
|
|
@@ -8154,6 +8233,38 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8154
8233
|
pi.on("turn_start", () => {
|
|
8155
8234
|
state = { ...state, invalidCompletionCalls: new Set };
|
|
8156
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
|
+
});
|
|
8157
8268
|
pi.on("agent_settled", () => {
|
|
8158
8269
|
const policy = state.activePolicy;
|
|
8159
8270
|
if (!policy || state.repairRequested || !needsCompletionRepair({ policy, dependencies })) {
|
|
@@ -8217,6 +8328,12 @@ ${childSystemPrompt(state.activePolicy)}`
|
|
|
8217
8328
|
};
|
|
8218
8329
|
}
|
|
8219
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
|
+
}
|
|
8220
8337
|
if (CHILD_COORDINATION_TOOLS.has(event.toolName)) {
|
|
8221
8338
|
return {
|
|
8222
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,
|
package/src/prompt/step-task.ts
CHANGED
|
@@ -151,6 +151,8 @@ export function buildStepTask(options: BuildStepTaskOptions): string {
|
|
|
151
151
|
...contract.workspaceLines,
|
|
152
152
|
'',
|
|
153
153
|
'Put a self-contained compact handoff in `summary`; this is the only step context passed to the next fresh child.',
|
|
154
|
+
'Format all human-facing output—including summaries, gate artifacts, Markdown plans, reports, comments, and replies—for scanning: short headings, then one distinct fact, action, or metadata value per bullet or paragraph. Never pack unrelated values into one line or dense prose.',
|
|
155
|
+
'When a schema needs several related fields, use a bullet list with one `field`: `value` per row; use JSON only for machine-readable data under `## Machine-readable handoff` in a fenced valid `json` block. Keep prose outside that block.',
|
|
154
156
|
...buildNonSuccessSummaryInstructions(contract.outcomes),
|
|
155
157
|
...(isDelegated ? buildDelegatedCompletionInstructions() : []),
|
|
156
158
|
'Do not call the completion tool alongside other tool calls.',
|
|
@@ -74,6 +74,14 @@ function wrapPlain(
|
|
|
74
74
|
return wrapped.length > 0 ? wrapped : [theme.fg(color, ' ')];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function summaryLines(
|
|
78
|
+
theme: WorkflowStatusTheme,
|
|
79
|
+
value: string,
|
|
80
|
+
width: number,
|
|
81
|
+
): Array<string> {
|
|
82
|
+
return [theme.fg('muted', 'summary'), ...wrapPlain(value, width, theme)];
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function boundedArtifact(value: string): {
|
|
78
86
|
readonly value: string;
|
|
79
87
|
readonly truncated: boolean;
|
|
@@ -101,10 +109,9 @@ function renderResult(
|
|
|
101
109
|
'',
|
|
102
110
|
theme.bold(theme.fg('accent', 'Submitted result')),
|
|
103
111
|
...keyValueLines(theme, 'outcome', result.outcome, width, 'success'),
|
|
104
|
-
...
|
|
112
|
+
...summaryLines(
|
|
105
113
|
theme,
|
|
106
|
-
|
|
107
|
-
`${result.summary}${result.summaryTruncated ? ' … [trace truncated]' : ''}`,
|
|
114
|
+
`${result.summary}${result.summaryTruncated ? '\n… [trace truncated]' : ''}`,
|
|
108
115
|
width,
|
|
109
116
|
),
|
|
110
117
|
...(result.workspaceCwd
|
|
@@ -439,7 +446,7 @@ export function renderStepDetail(
|
|
|
439
446
|
...(history
|
|
440
447
|
? [
|
|
441
448
|
...keyValueLines(theme, 'outcome', history.outcome, width, 'success'),
|
|
442
|
-
...
|
|
449
|
+
...summaryLines(theme, history.summary, width),
|
|
443
450
|
]
|
|
444
451
|
: []),
|
|
445
452
|
...(currentReason
|