pi-subagents 0.25.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +175 -19
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +60 -17
- package/src/agents/agent-management.ts +71 -15
- package/src/agents/agent-serializer.ts +13 -2
- package/src/agents/agents.ts +88 -17
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +2 -0
- package/src/extension/index.ts +5 -2
- package/src/extension/schemas.ts +132 -6
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +88 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +665 -39
- package/src/runs/foreground/chain-execution.ts +369 -118
- package/src/runs/foreground/execution.ts +392 -19
- package/src/runs/foreground/subagent-executor.ts +126 -3
- package/src/runs/shared/acceptance-contract.ts +318 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +173 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/parallel-utils.ts +33 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- package/src/runs/shared/workflow-graph.ts +210 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +265 -1
- package/src/shared/utils.ts +7 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +178 -45
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AcceptanceFinalizationTurn,
|
|
3
|
+
AcceptanceLedger,
|
|
4
|
+
ResolvedAcceptanceConfig,
|
|
5
|
+
} from "../../shared/types.ts";
|
|
6
|
+
import { acceptanceFailureMessage } from "./acceptance-evaluation.ts";
|
|
7
|
+
import { formatEvidenceReportFieldMapping } from "./acceptance-contract.ts";
|
|
8
|
+
import { stripAcceptanceReport } from "./acceptance-reports.ts";
|
|
9
|
+
|
|
10
|
+
const INITIAL_OUTPUT_LIMIT = 8_000;
|
|
11
|
+
|
|
12
|
+
function truncateForPrompt(value: string): string {
|
|
13
|
+
const trimmed = stripAcceptanceReport(value).trim();
|
|
14
|
+
if (trimmed.length <= INITIAL_OUTPUT_LIMIT) return trimmed || "(initial output was empty after removing acceptance-report)";
|
|
15
|
+
return `${trimmed.slice(0, INITIAL_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatReportForPrompt(ledger: AcceptanceLedger): string {
|
|
19
|
+
if (ledger.childReport) return JSON.stringify(ledger.childReport, null, 2);
|
|
20
|
+
return `Missing or malformed acceptance report: ${ledger.childReportParseError ?? "no parse detail"}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatAcceptanceFinalizationPrompt(input: {
|
|
24
|
+
acceptance: ResolvedAcceptanceConfig;
|
|
25
|
+
initialOutput: string;
|
|
26
|
+
initialLedger: AcceptanceLedger;
|
|
27
|
+
turn: number;
|
|
28
|
+
maxTurns: number;
|
|
29
|
+
previousFailure?: string;
|
|
30
|
+
}): string {
|
|
31
|
+
const lines = [
|
|
32
|
+
"## Acceptance Finalization",
|
|
33
|
+
"You are continuing the same subagent session. Before this run can be accepted, compare the current work to the acceptance contract and the evidence below.",
|
|
34
|
+
`This is finalization turn ${input.turn} of ${input.maxTurns}. The run will be rejected if the contract is still not satisfied after turn ${input.maxTurns}.`,
|
|
35
|
+
"",
|
|
36
|
+
"If a criterion is incomplete and fixable in this session, keep working now before returning the final report.",
|
|
37
|
+
"If a criterion cannot be satisfied in this session, report it as not-satisfied, explain the blocker in residualRisks, and say what input would unblock progress.",
|
|
38
|
+
"Do not claim a criterion is satisfied unless the current work has concrete evidence from files, commands, validation output, or other inspectable artifacts.",
|
|
39
|
+
"",
|
|
40
|
+
"## Acceptance Contract",
|
|
41
|
+
"Criteria:",
|
|
42
|
+
...(input.acceptance.criteria.length ? input.acceptance.criteria.map((criterion) => `- ${criterion.id}: ${criterion.must}`) : ["- No explicit criteria were configured; satisfy the requested task and required evidence/checks."]),
|
|
43
|
+
"",
|
|
44
|
+
`Required evidence: ${input.acceptance.evidence.join(", ") || "none explicitly requested"}`,
|
|
45
|
+
];
|
|
46
|
+
if (input.acceptance.evidence.length > 0) {
|
|
47
|
+
lines.push(
|
|
48
|
+
"",
|
|
49
|
+
"Structured evidence must be present in the final `acceptance-report` JSON fields. Markdown sections in the visible answer do not satisfy required evidence by themselves. If the previous visible output already included the evidence, copy or summarize it into the matching JSON field.",
|
|
50
|
+
"Evidence field mapping:",
|
|
51
|
+
...formatEvidenceReportFieldMapping(input.acceptance.evidence),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (input.acceptance.verify.length > 0) {
|
|
55
|
+
lines.push("", "Runtime verification commands that must pass:", ...input.acceptance.verify.map((command) => `- ${command.id}: ${command.command}`));
|
|
56
|
+
}
|
|
57
|
+
if (input.acceptance.review) {
|
|
58
|
+
lines.push("", `Independent review gate after self-review: ${input.acceptance.review.required === false ? "optional" : "required"}${input.acceptance.review.agent ? ` by ${input.acceptance.review.agent}` : ""}.`);
|
|
59
|
+
}
|
|
60
|
+
if (input.acceptance.stopRules.length > 0) {
|
|
61
|
+
lines.push("", "Stop rules are hard constraints while deciding whether to continue, stop as blocked, or report success:", ...input.acceptance.stopRules.map((rule) => `- ${rule}`));
|
|
62
|
+
}
|
|
63
|
+
lines.push(
|
|
64
|
+
"",
|
|
65
|
+
"Initial visible output:",
|
|
66
|
+
truncateForPrompt(input.initialOutput),
|
|
67
|
+
"",
|
|
68
|
+
"Initial acceptance report:",
|
|
69
|
+
formatReportForPrompt(input.initialLedger),
|
|
70
|
+
);
|
|
71
|
+
if (input.previousFailure) {
|
|
72
|
+
lines.push("", "Previous finalization failure to address:", input.previousFailure);
|
|
73
|
+
}
|
|
74
|
+
lines.push(
|
|
75
|
+
"",
|
|
76
|
+
"Now do the self-check. If work was missing and you repaired it, report the repaired final state. Finish with exactly one fenced JSON block tagged `acceptance-report`.",
|
|
77
|
+
"```acceptance-report",
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof from the final state" }],
|
|
80
|
+
changedFiles: [],
|
|
81
|
+
testsAddedOrUpdated: [],
|
|
82
|
+
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
83
|
+
validationOutput: [],
|
|
84
|
+
residualRisks: [],
|
|
85
|
+
noStagedFiles: true,
|
|
86
|
+
diffSummary: "concise summary of changed behavior and important files",
|
|
87
|
+
reviewFindings: [],
|
|
88
|
+
manualNotes: "manual notes or external evidence, if any",
|
|
89
|
+
notes: "final self-review summary",
|
|
90
|
+
}, null, 2),
|
|
91
|
+
"```",
|
|
92
|
+
);
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createFinalizationTurn(input: {
|
|
97
|
+
turn: number;
|
|
98
|
+
prompt: string;
|
|
99
|
+
rawOutput: string;
|
|
100
|
+
ledger: AcceptanceLedger;
|
|
101
|
+
}): AcceptanceFinalizationTurn {
|
|
102
|
+
const failureMessage = acceptanceFailureMessage(input.ledger);
|
|
103
|
+
return {
|
|
104
|
+
turn: input.turn,
|
|
105
|
+
prompt: input.prompt,
|
|
106
|
+
status: input.ledger.status,
|
|
107
|
+
rawOutput: input.rawOutput,
|
|
108
|
+
...(input.ledger.childReport ? { report: input.ledger.childReport } : {}),
|
|
109
|
+
...(input.ledger.childReportParseError ? { parseError: input.ledger.childReportParseError } : {}),
|
|
110
|
+
runtimeChecks: input.ledger.runtimeChecks,
|
|
111
|
+
verifyRuns: input.ledger.verifyRuns,
|
|
112
|
+
...(failureMessage ? { failureMessage } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createFinalizationProcessFailureTurn(input: {
|
|
117
|
+
turn: number;
|
|
118
|
+
prompt: string;
|
|
119
|
+
rawOutput?: string;
|
|
120
|
+
message: string;
|
|
121
|
+
}): AcceptanceFinalizationTurn {
|
|
122
|
+
return {
|
|
123
|
+
turn: input.turn,
|
|
124
|
+
prompt: input.prompt,
|
|
125
|
+
status: "rejected",
|
|
126
|
+
...(input.rawOutput ? { rawOutput: input.rawOutput } : {}),
|
|
127
|
+
runtimeChecks: [{ id: "finalization-process", status: "failed", message: input.message }],
|
|
128
|
+
verifyRuns: [],
|
|
129
|
+
failureMessage: `Acceptance rejected: ${input.message}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function attachFinalizationToLedger(input: {
|
|
134
|
+
initialLedger: AcceptanceLedger;
|
|
135
|
+
authoritativeLedger: AcceptanceLedger;
|
|
136
|
+
turns: AcceptanceFinalizationTurn[];
|
|
137
|
+
status: "completed" | "failed";
|
|
138
|
+
maxTurns: number;
|
|
139
|
+
}): AcceptanceLedger {
|
|
140
|
+
return {
|
|
141
|
+
...input.authoritativeLedger,
|
|
142
|
+
...(input.initialLedger.childReport ? { initialChildReport: input.initialLedger.childReport } : {}),
|
|
143
|
+
...(input.initialLedger.childReportParseError ? { initialChildReportParseError: input.initialLedger.childReportParseError } : {}),
|
|
144
|
+
finalization: {
|
|
145
|
+
mode: "self-review-loop",
|
|
146
|
+
status: input.status,
|
|
147
|
+
maxTurns: input.maxTurns,
|
|
148
|
+
turns: input.turns,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildFinalizationProcessFailureLedger(input: {
|
|
154
|
+
initialLedger: AcceptanceLedger;
|
|
155
|
+
turns: AcceptanceFinalizationTurn[];
|
|
156
|
+
maxTurns: number;
|
|
157
|
+
message: string;
|
|
158
|
+
}): AcceptanceLedger {
|
|
159
|
+
return attachFinalizationToLedger({
|
|
160
|
+
initialLedger: input.initialLedger,
|
|
161
|
+
authoritativeLedger: {
|
|
162
|
+
...input.initialLedger,
|
|
163
|
+
status: "rejected",
|
|
164
|
+
runtimeChecks: [
|
|
165
|
+
...input.initialLedger.runtimeChecks,
|
|
166
|
+
{ id: "finalization-process", status: "failed", message: input.message },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
turns: input.turns,
|
|
170
|
+
status: "failed",
|
|
171
|
+
maxTurns: input.maxTurns,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AcceptanceReport,
|
|
3
|
+
} from "../../shared/types.ts";
|
|
4
|
+
|
|
5
|
+
function extractBalancedJson(text: string, start: number): string | undefined {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let inString = false;
|
|
8
|
+
let escaped = false;
|
|
9
|
+
for (let i = start; i < text.length; i++) {
|
|
10
|
+
const char = text[i]!;
|
|
11
|
+
if (inString) {
|
|
12
|
+
if (escaped) escaped = false;
|
|
13
|
+
else if (char === "\\") escaped = true;
|
|
14
|
+
else if (char === "\"") inString = false;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (char === "\"") {
|
|
18
|
+
inString = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === "{") depth++;
|
|
22
|
+
if (char === "}") {
|
|
23
|
+
depth--;
|
|
24
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
31
|
+
const fenced = [...output.matchAll(/```acceptance-report\s*\n([\s\S]*?)```/gi)]
|
|
32
|
+
.map((match) => match[1]?.trim())
|
|
33
|
+
.filter((value): value is string => Boolean(value));
|
|
34
|
+
const parseErrors: string[] = [];
|
|
35
|
+
for (const body of fenced) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(body) as unknown;
|
|
38
|
+
const report = (parsed && typeof parsed === "object" && "acceptance" in parsed)
|
|
39
|
+
? (parsed as { acceptance?: unknown }).acceptance
|
|
40
|
+
: parsed;
|
|
41
|
+
if (isAcceptanceReport(report)) return { report };
|
|
42
|
+
parseErrors.push("acceptance-report block does not contain a valid acceptance report");
|
|
43
|
+
} catch (error) {
|
|
44
|
+
parseErrors.push(error instanceof Error ? error.message : String(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
48
|
+
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
49
|
+
if (markerIndex !== -1) {
|
|
50
|
+
const jsonStart = output.indexOf("{", markerIndex);
|
|
51
|
+
if (jsonStart !== -1) {
|
|
52
|
+
const json = extractBalancedJson(output, jsonStart);
|
|
53
|
+
if (json) {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(json) as unknown;
|
|
56
|
+
if (isAcceptanceReport(parsed)) return { report: parsed };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { error: "Structured acceptance report not found." };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stripAcceptanceReport(output: string): string {
|
|
67
|
+
return output
|
|
68
|
+
.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
|
|
69
|
+
.replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
|
|
70
|
+
.trimEnd();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isStringArray(value: unknown): value is string[] {
|
|
74
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCriterionReport(value: unknown): value is NonNullable<AcceptanceReport["criteriaSatisfied"]>[number] {
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
79
|
+
const criterion = value as { id?: unknown; status?: unknown; evidence?: unknown };
|
|
80
|
+
if (criterion.id !== undefined && typeof criterion.id !== "string") return false;
|
|
81
|
+
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") return false;
|
|
82
|
+
return typeof criterion.evidence === "string" && criterion.evidence.trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isCommandReport(value: unknown): value is NonNullable<AcceptanceReport["commandsRun"]>[number] {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
87
|
+
const command = value as { command?: unknown; result?: unknown; summary?: unknown };
|
|
88
|
+
return typeof command.command === "string"
|
|
89
|
+
&& (command.result === "passed" || command.result === "failed" || command.result === "not-run")
|
|
90
|
+
&& typeof command.summary === "string";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isAcceptanceReport(value: unknown): value is AcceptanceReport {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
95
|
+
const report = value as {
|
|
96
|
+
criteriaSatisfied?: unknown;
|
|
97
|
+
changedFiles?: unknown;
|
|
98
|
+
testsAddedOrUpdated?: unknown;
|
|
99
|
+
commandsRun?: unknown;
|
|
100
|
+
validationOutput?: unknown;
|
|
101
|
+
residualRisks?: unknown;
|
|
102
|
+
noStagedFiles?: unknown;
|
|
103
|
+
diffSummary?: unknown;
|
|
104
|
+
reviewFindings?: unknown;
|
|
105
|
+
manualNotes?: unknown;
|
|
106
|
+
notes?: unknown;
|
|
107
|
+
};
|
|
108
|
+
if (report.criteriaSatisfied !== undefined && (!Array.isArray(report.criteriaSatisfied) || !report.criteriaSatisfied.every(isCriterionReport))) return false;
|
|
109
|
+
if (report.changedFiles !== undefined && !isStringArray(report.changedFiles)) return false;
|
|
110
|
+
if (report.testsAddedOrUpdated !== undefined && !isStringArray(report.testsAddedOrUpdated)) return false;
|
|
111
|
+
if (report.commandsRun !== undefined && (!Array.isArray(report.commandsRun) || !report.commandsRun.every(isCommandReport))) return false;
|
|
112
|
+
if (report.validationOutput !== undefined && !isStringArray(report.validationOutput)) return false;
|
|
113
|
+
if (report.residualRisks !== undefined && !isStringArray(report.residualRisks)) return false;
|
|
114
|
+
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") return false;
|
|
115
|
+
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") return false;
|
|
116
|
+
if (report.reviewFindings !== undefined && !isStringArray(report.reviewFindings)) return false;
|
|
117
|
+
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") return false;
|
|
118
|
+
if (report.notes !== undefined && typeof report.notes !== "string") return false;
|
|
119
|
+
return report.criteriaSatisfied !== undefined
|
|
120
|
+
|| report.changedFiles !== undefined
|
|
121
|
+
|| report.testsAddedOrUpdated !== undefined
|
|
122
|
+
|| report.commandsRun !== undefined
|
|
123
|
+
|| report.residualRisks !== undefined
|
|
124
|
+
|| report.manualNotes !== undefined
|
|
125
|
+
|| report.reviewFindings !== undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
acceptanceSelfReviewConfig,
|
|
3
|
+
formatAcceptancePrompt,
|
|
4
|
+
resolveEffectiveAcceptance,
|
|
5
|
+
shouldRunAcceptanceFinalization,
|
|
6
|
+
validateAcceptanceInput,
|
|
7
|
+
} from "./acceptance-contract.ts";
|
|
8
|
+
export {
|
|
9
|
+
parseAcceptanceReport,
|
|
10
|
+
stripAcceptanceReport,
|
|
11
|
+
} from "./acceptance-reports.ts";
|
|
12
|
+
export {
|
|
13
|
+
acceptanceFailureMessage,
|
|
14
|
+
evaluateAcceptance,
|
|
15
|
+
} from "./acceptance-evaluation.ts";
|
|
16
|
+
export {
|
|
17
|
+
attachFinalizationToLedger,
|
|
18
|
+
buildFinalizationProcessFailureLedger,
|
|
19
|
+
createFinalizationProcessFailureTurn,
|
|
20
|
+
createFinalizationTurn,
|
|
21
|
+
formatAcceptanceFinalizationPrompt,
|
|
22
|
+
} from "./acceptance-finalization.ts";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
|
|
2
|
+
import type { ChainOutputMap, ChainOutputMapEntry, SingleResult } from "../../shared/types.ts";
|
|
3
|
+
import { getSingleResultOutput } from "../../shared/utils.ts";
|
|
4
|
+
import { DynamicFanoutError, hasDynamicFanoutFields, type DynamicFanoutConfig, validateDynamicStepShape } from "./dynamic-fanout.ts";
|
|
5
|
+
|
|
6
|
+
const OUTPUT_REF_PATTERN = /\{outputs\.([^}]*)\}/g;
|
|
7
|
+
const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
+
|
|
9
|
+
export class ChainOutputValidationError extends Error {}
|
|
10
|
+
|
|
11
|
+
function outputNamesForStep(step: ChainStep): string[] {
|
|
12
|
+
if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
|
|
13
|
+
if (isDynamicParallelStep(step)) return [step.collect.as];
|
|
14
|
+
const name = (step as SequentialStep).as;
|
|
15
|
+
return name ? [name] : [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function taskTemplatesForStep(step: ChainStep): string[] {
|
|
19
|
+
if (isParallelStep(step)) return step.parallel.map((task) => task.task ?? "{previous}");
|
|
20
|
+
if (isDynamicParallelStep(step)) return [step.parallel.task ?? "{previous}", step.parallel.label ?? ""].filter(Boolean);
|
|
21
|
+
return [(step as SequentialStep).task ?? "{previous}"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
25
|
+
const available = new Set<string>();
|
|
26
|
+
const seen = new Set<string>();
|
|
27
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
28
|
+
const step = steps[stepIndex]!;
|
|
29
|
+
if (hasDynamicFanoutFields(step)) {
|
|
30
|
+
if (!isDynamicParallelStep(step)) {
|
|
31
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${stepIndex + 1} requires expand, a single parallel template object, and collect; dynamic expand/collect cannot be mixed with static parallel arrays.`);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
validateDynamicStepShape(step, stepIndex, dynamicFanoutConfig);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error instanceof DynamicFanoutError) throw new ChainOutputValidationError(error.message);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
if (!available.has(step.expand.from.output)) {
|
|
40
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${stepIndex + 1} references unknown output '${step.expand.from.output}'. Named outputs are only available after producing step/group completes.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const name of outputNamesForStep(step)) {
|
|
44
|
+
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
45
|
+
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${stepIndex + 1}. Use /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
46
|
+
}
|
|
47
|
+
if (seen.has(name)) {
|
|
48
|
+
throw new ChainOutputValidationError(`Duplicate chain output name '${name}'. Each as name must be unique.`);
|
|
49
|
+
}
|
|
50
|
+
seen.add(name);
|
|
51
|
+
}
|
|
52
|
+
for (const template of taskTemplatesForStep(step)) {
|
|
53
|
+
for (const match of template.matchAll(OUTPUT_REF_PATTERN)) {
|
|
54
|
+
const rawReference = match[0];
|
|
55
|
+
const name = match[1]!;
|
|
56
|
+
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
57
|
+
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${stepIndex + 1}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
58
|
+
}
|
|
59
|
+
if (!available.has(name)) {
|
|
60
|
+
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${stepIndex + 1}. Named outputs are only available after producing step/group completes.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const name of outputNamesForStep(step)) {
|
|
65
|
+
available.add(name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveOutputReferences(template: string, outputs: ChainOutputMap): string {
|
|
71
|
+
return template.replace(OUTPUT_REF_PATTERN, (rawReference, name: string) => {
|
|
72
|
+
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
73
|
+
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}'. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
74
|
+
}
|
|
75
|
+
const entry = outputs[name];
|
|
76
|
+
if (!entry) throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}'.`);
|
|
77
|
+
return entry.text;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function compactStructuredText(value: unknown): string {
|
|
82
|
+
return JSON.stringify(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function outputEntryFromResult(result: SingleResult, stepIndex: number): ChainOutputMapEntry {
|
|
86
|
+
return {
|
|
87
|
+
text: result.structuredOutput !== undefined ? compactStructuredText(result.structuredOutput) : getSingleResultOutput(result),
|
|
88
|
+
...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
89
|
+
agent: result.agent,
|
|
90
|
+
stepIndex,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function outputEntryFromAsyncResult(result: { agent: string; output: string; structuredOutput?: unknown }, stepIndex: number): ChainOutputMapEntry {
|
|
95
|
+
return {
|
|
96
|
+
text: result.structuredOutput !== undefined ? compactStructuredText(result.structuredOutput) : result.output,
|
|
97
|
+
...(result.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
98
|
+
agent: result.agent,
|
|
99
|
+
stepIndex,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -66,6 +66,17 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
66
66
|
"contact_supervisor",
|
|
67
67
|
]);
|
|
68
68
|
|
|
69
|
+
export type CompletionPolicy = "none" | "mutation-guard" | "acceptance-contract";
|
|
70
|
+
|
|
71
|
+
interface CompletionPolicyInput {
|
|
72
|
+
agent: string;
|
|
73
|
+
task: string;
|
|
74
|
+
completionGuardEnabled: boolean;
|
|
75
|
+
usesAcceptanceContract: boolean;
|
|
76
|
+
tools?: string[];
|
|
77
|
+
mcpDirectTools?: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
interface CompletionMutationGuardInput {
|
|
70
81
|
agent: string;
|
|
71
82
|
task: string;
|
|
@@ -134,10 +145,22 @@ export function hasMutationToolCall(messages: Message[]): boolean {
|
|
|
134
145
|
return false;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
export function resolveCompletionPolicy(input: CompletionPolicyInput): CompletionPolicy {
|
|
149
|
+
if (input.usesAcceptanceContract) return "acceptance-contract";
|
|
150
|
+
if (!input.completionGuardEnabled) return "none";
|
|
151
|
+
if (declaresOnlyReadOnlyTools(input.tools, input.mcpDirectTools)) return "none";
|
|
152
|
+
return expectsImplementationMutation(input.agent, input.task) ? "mutation-guard" : "none";
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
|
|
138
|
-
const expectedMutation =
|
|
139
|
-
|
|
140
|
-
:
|
|
156
|
+
const expectedMutation = resolveCompletionPolicy({
|
|
157
|
+
agent: input.agent,
|
|
158
|
+
task: input.task,
|
|
159
|
+
completionGuardEnabled: true,
|
|
160
|
+
usesAcceptanceContract: false,
|
|
161
|
+
tools: input.tools,
|
|
162
|
+
mcpDirectTools: input.mcpDirectTools,
|
|
163
|
+
}) === "mutation-guard";
|
|
141
164
|
const attemptedMutation = hasMutationToolCall(input.messages);
|
|
142
165
|
return {
|
|
143
166
|
expectedMutation,
|