@narumitw/pi-subagents 0.49.3 → 0.52.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/README.md +362 -53
- package/package.json +10 -7
- package/src/adaptive-scheduler.ts +224 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +1098 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +321 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +109 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +770 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +179 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +67 -0
- package/src/work-item-ledger.ts +931 -0
- package/src/work-item-persistence.ts +223 -0
- package/src/workflow-planning.ts +162 -0
- package/src/workflow-tree-identity.ts +289 -0
- package/src/workflow-ui.ts +61 -0
- package/src/workflow-verification.ts +296 -0
- package/src/workspace.ts +69 -12
package/src/render.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
11
|
+
import { renderPanelCall, renderPanelResult } from "./panel-render.js";
|
|
11
12
|
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
12
13
|
import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
|
|
13
14
|
import {
|
|
@@ -137,6 +138,12 @@ function sanitizeSingleResultForRender(result: SingleResult): SingleResult {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
function renderResultStatus(result: SingleResult, isPartial: boolean): string {
|
|
141
|
+
if (result.outcome && !["completed", "partial"].includes(result.outcome.status)) {
|
|
142
|
+
return result.outcome.status
|
|
143
|
+
.split("-")
|
|
144
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
145
|
+
.join(" ");
|
|
146
|
+
}
|
|
140
147
|
if (isResultError(result)) {
|
|
141
148
|
return result.stopReason === "aborted" || result.aborted ? "Cancelled" : "Failed";
|
|
142
149
|
}
|
|
@@ -153,6 +160,31 @@ function coloredResultStatus(theme: Theme, result: SingleResult, isPartial: bool
|
|
|
153
160
|
return coloredStatus(theme, renderResultStatus(result, isPartial));
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
function formatResultIntelligence(result: SingleResult): string {
|
|
164
|
+
const structured =
|
|
165
|
+
result.structuredResult?.version === "pi-subagents:result:v2"
|
|
166
|
+
? result.structuredResult
|
|
167
|
+
: undefined;
|
|
168
|
+
return [
|
|
169
|
+
result.contract ? `contract: ${result.contract.level} · ${result.contract.taskId}` : undefined,
|
|
170
|
+
result.outcome
|
|
171
|
+
? `outcome: ${result.outcome.status}${result.outcome.reasonCode ? ` · ${result.outcome.reasonCode}` : ""}`
|
|
172
|
+
: undefined,
|
|
173
|
+
structured
|
|
174
|
+
? `evidence: ${structured.claims.length} claims · ${structured.artifacts.length} artifacts · ${structured.verification.length} verification items · ${structured.limitations.length} limitations`
|
|
175
|
+
: undefined,
|
|
176
|
+
result.resultContractInvalid ? "contract warning: invalid structured result" : undefined,
|
|
177
|
+
result.executionPlan
|
|
178
|
+
? `plan: ${result.executionPlan.id.slice(0, 12)} · generation ${result.executionPlan.taskGeneration} · admission ${result.executionPlan.admission.recommendation}`
|
|
179
|
+
: undefined,
|
|
180
|
+
result.capabilityGrant
|
|
181
|
+
? `grant: ${result.capabilityGrant.state} · expires ${result.capabilityGrant.expiresAt}`
|
|
182
|
+
: undefined,
|
|
183
|
+
]
|
|
184
|
+
.filter((line): line is string => Boolean(line))
|
|
185
|
+
.join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
156
188
|
function formatResultPolicy(result: SingleResult): string {
|
|
157
189
|
if (!result.policy) return "";
|
|
158
190
|
return [
|
|
@@ -164,11 +196,34 @@ function formatResultPolicy(result: SingleResult): string {
|
|
|
164
196
|
|
|
165
197
|
export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
166
198
|
const scope: AgentScope = args.agentScope ?? "user";
|
|
199
|
+
const limits = [
|
|
200
|
+
typeof args.timeoutMs === "number" ? `timeout:${args.timeoutMs}ms` : undefined,
|
|
201
|
+
typeof args.totalTimeoutMs === "number" ? `total:${args.totalTimeoutMs}ms` : undefined,
|
|
202
|
+
typeof args.idleTimeoutMs === "number" ? `idle:${args.idleTimeoutMs}ms` : undefined,
|
|
203
|
+
typeof args.maxTurns === "number" ? `turns:${args.maxTurns}` : undefined,
|
|
204
|
+
typeof args.maxToolCalls === "number" ? `tools:${args.maxToolCalls}` : undefined,
|
|
205
|
+
].filter((value): value is string => Boolean(value));
|
|
206
|
+
const limitText = limits.length > 0 ? ` · ${limits.join(" · ")}` : "";
|
|
207
|
+
const panelCall = renderPanelCall(args, theme);
|
|
208
|
+
if (panelCall) return panelCall;
|
|
209
|
+
if (args.workflow && args.workflow.tasks.length > 0) {
|
|
210
|
+
let text =
|
|
211
|
+
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
212
|
+
theme.fg("accent", `workflow (${args.workflow.tasks.length} tasks)`) +
|
|
213
|
+
theme.fg("muted", ` [${scope}]${limitText}`);
|
|
214
|
+
for (const task of args.workflow.tasks.slice(0, 3)) {
|
|
215
|
+
text += `\n ${theme.fg("muted", `${task.id}:`)} ${theme.fg("accent", previewAgent(task.agent))}${theme.fg("dim", ` ${previewTask(task.task)}`)}`;
|
|
216
|
+
}
|
|
217
|
+
if (args.workflow.tasks.length > 3) {
|
|
218
|
+
text += `\n ${theme.fg("muted", `... +${args.workflow.tasks.length - 3} more`)}`;
|
|
219
|
+
}
|
|
220
|
+
return new Text(text, 0, 0);
|
|
221
|
+
}
|
|
167
222
|
if (args.chain && args.chain.length > 0) {
|
|
168
223
|
let text =
|
|
169
224
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
170
225
|
theme.fg("accent", `chain (${args.chain.length} steps)`) +
|
|
171
|
-
theme.fg("muted", ` [${scope}]`);
|
|
226
|
+
theme.fg("muted", ` [${scope}]${limitText}`);
|
|
172
227
|
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
173
228
|
const step = args.chain[i] as { agent?: unknown; task?: unknown } | undefined;
|
|
174
229
|
// Clean up {previous} placeholder for display
|
|
@@ -189,7 +244,7 @@ export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
|
189
244
|
let text =
|
|
190
245
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
191
246
|
theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
192
|
-
theme.fg("muted", ` [${scope}]`);
|
|
247
|
+
theme.fg("muted", ` [${scope}]${limitText}`);
|
|
193
248
|
for (const task of args.tasks.slice(0, 3)) {
|
|
194
249
|
const item = task as { agent?: unknown; task?: unknown } | undefined;
|
|
195
250
|
text += `\n ${theme.fg("accent", previewAgent(item?.agent))}${theme.fg("dim", ` ${previewTask(item?.task)}`)}`;
|
|
@@ -210,7 +265,7 @@ export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
|
210
265
|
let text =
|
|
211
266
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
212
267
|
theme.fg("accent", agentName) +
|
|
213
|
-
theme.fg("muted", ` [${scope}]`);
|
|
268
|
+
theme.fg("muted", ` [${scope}]${limitText}`);
|
|
214
269
|
text += `\n ${theme.fg("dim", preview)}`;
|
|
215
270
|
return new Text(text, 0, 0);
|
|
216
271
|
}
|
|
@@ -221,6 +276,10 @@ export function renderSubagentResult(
|
|
|
221
276
|
theme: Theme,
|
|
222
277
|
) {
|
|
223
278
|
const rawDetails = result.details as SubagentDetails | undefined;
|
|
279
|
+
if (rawDetails?.mode === "panel") {
|
|
280
|
+
const panelResult = renderPanelResult(rawDetails, expanded, isPartial, theme);
|
|
281
|
+
if (panelResult) return panelResult;
|
|
282
|
+
}
|
|
224
283
|
if (!rawDetails || rawDetails.results.length === 0) {
|
|
225
284
|
const text = result.content[0];
|
|
226
285
|
return new Text(
|
|
@@ -277,6 +336,12 @@ export function renderSubagentResult(
|
|
|
277
336
|
container.addChild(new Spacer(1));
|
|
278
337
|
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
279
338
|
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
339
|
+
const intelligence = formatResultIntelligence(r);
|
|
340
|
+
if (intelligence) {
|
|
341
|
+
container.addChild(new Spacer(1));
|
|
342
|
+
container.addChild(new Text(theme.fg("muted", "─── Delegation ───"), 0, 0));
|
|
343
|
+
container.addChild(new Text(theme.fg("dim", intelligence), 0, 0));
|
|
344
|
+
}
|
|
280
345
|
const policy = formatResultPolicy(r);
|
|
281
346
|
if (policy) {
|
|
282
347
|
container.addChild(new Spacer(1));
|
|
@@ -467,7 +532,8 @@ export function renderSubagentResult(
|
|
|
467
532
|
return new Text(text, 0, 0);
|
|
468
533
|
}
|
|
469
534
|
|
|
470
|
-
if (details.mode === "parallel") {
|
|
535
|
+
if (details.mode === "parallel" || details.mode === "workflow") {
|
|
536
|
+
const modeLabel = details.mode === "workflow" ? "workflow" : "parallel";
|
|
471
537
|
const resultIsRunning = (result: SingleResult) =>
|
|
472
538
|
result.exitCode === -1 && !isResultError(result);
|
|
473
539
|
const running = details.results.filter(resultIsRunning).length;
|
|
@@ -507,7 +573,7 @@ export function renderSubagentResult(
|
|
|
507
573
|
const container = new Container();
|
|
508
574
|
container.addChild(
|
|
509
575
|
new Text(
|
|
510
|
-
`${icon} ${theme.fg("toolTitle", theme.bold(
|
|
576
|
+
`${icon} ${theme.fg("toolTitle", theme.bold(`${modeLabel} `))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`,
|
|
511
577
|
0,
|
|
512
578
|
0,
|
|
513
579
|
),
|
|
@@ -623,7 +689,7 @@ export function renderSubagentResult(
|
|
|
623
689
|
}
|
|
624
690
|
|
|
625
691
|
// Collapsed view (or still running)
|
|
626
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(
|
|
692
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(`${modeLabel} `))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`;
|
|
627
693
|
for (const r of details.results) {
|
|
628
694
|
const rFailed = isResultError(r);
|
|
629
695
|
const rRunning = resultIsRunning(r);
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { redactPrivateText } from "./context.js";
|
|
2
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
3
|
+
|
|
4
|
+
export const SUBAGENT_RESULT_FORMATS = ["text", "structured-v1", "structured-v2"] as const;
|
|
5
|
+
export type SubagentResultFormat = (typeof SUBAGENT_RESULT_FORMATS)[number];
|
|
6
|
+
|
|
7
|
+
export interface StructuredSubagentResult {
|
|
8
|
+
version: "pi-subagents:result:v1";
|
|
9
|
+
summary: string;
|
|
10
|
+
evidence: string[];
|
|
11
|
+
changes: string[];
|
|
12
|
+
verification: string[];
|
|
13
|
+
risks: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SUBAGENT_OUTCOME_STATUSES = [
|
|
17
|
+
"completed",
|
|
18
|
+
"partial",
|
|
19
|
+
"blocked",
|
|
20
|
+
"needs-input",
|
|
21
|
+
"abstained",
|
|
22
|
+
"failed",
|
|
23
|
+
"interrupted",
|
|
24
|
+
"stale",
|
|
25
|
+
"contract-invalid",
|
|
26
|
+
] as const;
|
|
27
|
+
export type SubagentOutcomeStatus = (typeof SUBAGENT_OUTCOME_STATUSES)[number];
|
|
28
|
+
|
|
29
|
+
export const SUBAGENT_CLAIM_CLASSIFICATIONS = ["observed", "inferred", "unverified"] as const;
|
|
30
|
+
export type SubagentClaimClassification = (typeof SUBAGENT_CLAIM_CLASSIFICATIONS)[number];
|
|
31
|
+
|
|
32
|
+
export interface EvidenceBackedClaim {
|
|
33
|
+
claim: string;
|
|
34
|
+
classification: SubagentClaimClassification;
|
|
35
|
+
evidence: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SubagentArtifactReference {
|
|
39
|
+
id: string;
|
|
40
|
+
kind: string;
|
|
41
|
+
version?: string;
|
|
42
|
+
location?: string;
|
|
43
|
+
digest?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SubagentChangeReference {
|
|
47
|
+
path: string;
|
|
48
|
+
summary: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SubagentVerificationReference {
|
|
52
|
+
status: "passed" | "failed" | "not-run";
|
|
53
|
+
summary: string;
|
|
54
|
+
command?: string;
|
|
55
|
+
evidence?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SubagentResultProvenance {
|
|
59
|
+
taskId?: string;
|
|
60
|
+
taskGeneration?: number;
|
|
61
|
+
executionPlanId?: string;
|
|
62
|
+
cancellationLineage?: string[];
|
|
63
|
+
inputArtifacts?: string[];
|
|
64
|
+
repositoryGeneration?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface StructuredSubagentResultV2 {
|
|
68
|
+
version: "pi-subagents:result:v2";
|
|
69
|
+
status: SubagentOutcomeStatus;
|
|
70
|
+
reasonCode?: string;
|
|
71
|
+
summary: string;
|
|
72
|
+
claims: EvidenceBackedClaim[];
|
|
73
|
+
artifacts: SubagentArtifactReference[];
|
|
74
|
+
changes: SubagentChangeReference[];
|
|
75
|
+
verification: SubagentVerificationReference[];
|
|
76
|
+
limitations: string[];
|
|
77
|
+
unresolvedDependencies: string[];
|
|
78
|
+
provenance?: SubagentResultProvenance;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type AnyStructuredSubagentResult = StructuredSubagentResult | StructuredSubagentResultV2;
|
|
82
|
+
|
|
83
|
+
export interface ResultContractRuntimeMetadata {
|
|
84
|
+
truncated?: boolean;
|
|
85
|
+
usage?: {
|
|
86
|
+
input: number;
|
|
87
|
+
output: number;
|
|
88
|
+
cost: number;
|
|
89
|
+
turns: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ResultContractEnvelope<T extends AnyStructuredSubagentResult> {
|
|
94
|
+
result: T;
|
|
95
|
+
truncated?: boolean;
|
|
96
|
+
usage?: ResultContractRuntimeMetadata["usage"];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const MAX_FIELD_BYTES = 8 * 1024;
|
|
100
|
+
const MAX_ITEMS = 50;
|
|
101
|
+
const MAX_REASON_CODE_BYTES = 256;
|
|
102
|
+
const MAX_ARTIFACT_IDENTIFIER_BYTES = 256;
|
|
103
|
+
|
|
104
|
+
export function structuredResultInstruction(format: SubagentResultFormat | undefined): string {
|
|
105
|
+
if (format === "structured-v1") {
|
|
106
|
+
return [
|
|
107
|
+
"Return the final answer as one JSON object and no surrounding prose.",
|
|
108
|
+
'Use exactly version "pi-subagents:result:v1" and fields summary, evidence, changes, verification, and risks.',
|
|
109
|
+
"summary must be a string and the other fields must be arrays of strings.",
|
|
110
|
+
].join(" ");
|
|
111
|
+
}
|
|
112
|
+
if (format === "structured-v2") {
|
|
113
|
+
return [
|
|
114
|
+
"Return the final answer as one JSON object and no surrounding prose.",
|
|
115
|
+
'Use exactly version "pi-subagents:result:v2".',
|
|
116
|
+
"Required fields are status, summary, claims, artifacts, changes, verification, limitations, and unresolvedDependencies.",
|
|
117
|
+
`status must be one of ${SUBAGENT_OUTCOME_STATUSES.join(", ")}.`,
|
|
118
|
+
"Each claim must include claim, classification (observed, inferred, or unverified), and an evidence string array.",
|
|
119
|
+
"Each artifact must include id and kind; each change must include path and summary; each verification item must include status (passed, failed, or not-run) and summary.",
|
|
120
|
+
"Use optional reasonCode for non-completed outcomes and optional provenance for taskId, inputArtifacts, and repositoryGeneration; executor-owned generation and plan identity are stamped after parsing.",
|
|
121
|
+
].join(" ");
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function appendResultInstruction(
|
|
127
|
+
prompt: string,
|
|
128
|
+
format: SubagentResultFormat | undefined,
|
|
129
|
+
maxBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
130
|
+
): string {
|
|
131
|
+
const instruction = structuredResultInstruction(format);
|
|
132
|
+
if (!instruction) return prompt;
|
|
133
|
+
const suffix = `\n\nResult contract:\n${instruction}`;
|
|
134
|
+
const suffixBytes = Buffer.byteLength(suffix, "utf8");
|
|
135
|
+
const boundedPrompt = truncateUtf8(prompt, Math.max(0, maxBytes - suffixBytes)).text;
|
|
136
|
+
return `${boundedPrompt}${suffix}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function parseStructuredSubagentResult(text: string): StructuredSubagentResult | undefined {
|
|
140
|
+
const value = parseJsonObject(text);
|
|
141
|
+
if (
|
|
142
|
+
value?.version !== "pi-subagents:result:v1" ||
|
|
143
|
+
!hasOnlyKeys(value, ["version", "summary", "evidence", "changes", "verification", "risks"]) ||
|
|
144
|
+
typeof value.summary !== "string"
|
|
145
|
+
) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const evidence = stringArray(value.evidence);
|
|
149
|
+
const changes = stringArray(value.changes);
|
|
150
|
+
const verification = stringArray(value.verification);
|
|
151
|
+
const risks = stringArray(value.risks);
|
|
152
|
+
if (!evidence || !changes || !verification || !risks) return undefined;
|
|
153
|
+
return {
|
|
154
|
+
version: "pi-subagents:result:v1",
|
|
155
|
+
summary: bounded(value.summary),
|
|
156
|
+
evidence,
|
|
157
|
+
changes,
|
|
158
|
+
verification,
|
|
159
|
+
risks,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function parseStructuredSubagentResultV2(
|
|
164
|
+
text: string,
|
|
165
|
+
): StructuredSubagentResultV2 | undefined {
|
|
166
|
+
const value = parseJsonObject(text);
|
|
167
|
+
if (
|
|
168
|
+
value?.version !== "pi-subagents:result:v2" ||
|
|
169
|
+
!hasOnlyKeys(value, [
|
|
170
|
+
"version",
|
|
171
|
+
"status",
|
|
172
|
+
"reasonCode",
|
|
173
|
+
"summary",
|
|
174
|
+
"claims",
|
|
175
|
+
"artifacts",
|
|
176
|
+
"changes",
|
|
177
|
+
"verification",
|
|
178
|
+
"limitations",
|
|
179
|
+
"unresolvedDependencies",
|
|
180
|
+
"provenance",
|
|
181
|
+
]) ||
|
|
182
|
+
typeof value.status !== "string" ||
|
|
183
|
+
!SUBAGENT_OUTCOME_STATUSES.includes(value.status as SubagentOutcomeStatus) ||
|
|
184
|
+
typeof value.summary !== "string"
|
|
185
|
+
) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const claims = objectArray(value.claims, parseClaim);
|
|
189
|
+
const artifacts = objectArray(value.artifacts, parseArtifact);
|
|
190
|
+
const changes = objectArray(value.changes, parseChange);
|
|
191
|
+
const verification = objectArray(value.verification, parseVerification);
|
|
192
|
+
const limitations = stringArray(value.limitations);
|
|
193
|
+
const unresolvedDependencies = stringArray(value.unresolvedDependencies);
|
|
194
|
+
if (
|
|
195
|
+
!claims ||
|
|
196
|
+
!artifacts ||
|
|
197
|
+
new Set(artifacts.map((artifact) => artifact.id)).size !== artifacts.length ||
|
|
198
|
+
!changes ||
|
|
199
|
+
!verification ||
|
|
200
|
+
!limitations ||
|
|
201
|
+
!unresolvedDependencies
|
|
202
|
+
) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const reasonCode = optionalBoundedString(value.reasonCode, MAX_REASON_CODE_BYTES);
|
|
206
|
+
if (reasonCode === false) return undefined;
|
|
207
|
+
const provenance = parseProvenance(value.provenance);
|
|
208
|
+
if (provenance === false) return undefined;
|
|
209
|
+
return {
|
|
210
|
+
version: "pi-subagents:result:v2",
|
|
211
|
+
status: value.status as SubagentOutcomeStatus,
|
|
212
|
+
...(reasonCode === undefined ? {} : { reasonCode }),
|
|
213
|
+
summary: bounded(value.summary),
|
|
214
|
+
claims,
|
|
215
|
+
artifacts,
|
|
216
|
+
changes,
|
|
217
|
+
verification,
|
|
218
|
+
limitations,
|
|
219
|
+
unresolvedDependencies,
|
|
220
|
+
...(provenance === undefined ? {} : { provenance }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function parseAnyStructuredSubagentResult(
|
|
225
|
+
text: string,
|
|
226
|
+
format: SubagentResultFormat | undefined,
|
|
227
|
+
): AnyStructuredSubagentResult | undefined {
|
|
228
|
+
if (format === "structured-v2") return parseStructuredSubagentResultV2(text);
|
|
229
|
+
if (format === "structured-v1") return parseStructuredSubagentResult(text);
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function resultContractEnvelope<T extends AnyStructuredSubagentResult>(
|
|
234
|
+
result: T,
|
|
235
|
+
metadata: ResultContractRuntimeMetadata = {},
|
|
236
|
+
): ResultContractEnvelope<T> {
|
|
237
|
+
return {
|
|
238
|
+
result,
|
|
239
|
+
...(metadata.truncated === undefined ? {} : { truncated: metadata.truncated }),
|
|
240
|
+
...(metadata.usage === undefined ? {} : { usage: { ...metadata.usage } }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseClaim(value: Record<string, unknown>): EvidenceBackedClaim | undefined {
|
|
245
|
+
if (
|
|
246
|
+
typeof value.claim !== "string" ||
|
|
247
|
+
typeof value.classification !== "string" ||
|
|
248
|
+
!SUBAGENT_CLAIM_CLASSIFICATIONS.includes(value.classification as SubagentClaimClassification)
|
|
249
|
+
) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const evidence = stringArray(value.evidence);
|
|
253
|
+
if (!evidence) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
claim: bounded(value.claim),
|
|
256
|
+
classification: value.classification as SubagentClaimClassification,
|
|
257
|
+
evidence,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseArtifact(value: Record<string, unknown>): SubagentArtifactReference | undefined {
|
|
262
|
+
if (typeof value.id !== "string" || typeof value.kind !== "string") return undefined;
|
|
263
|
+
const id = truncateUtf8(redactPrivateText(value.id), MAX_ARTIFACT_IDENTIFIER_BYTES).text.trim();
|
|
264
|
+
const kind = truncateUtf8(
|
|
265
|
+
redactPrivateText(value.kind),
|
|
266
|
+
MAX_ARTIFACT_IDENTIFIER_BYTES,
|
|
267
|
+
).text.trim();
|
|
268
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(id) || !kind) return undefined;
|
|
269
|
+
const version = optionalTrimmedBoundedString(value.version, MAX_ARTIFACT_IDENTIFIER_BYTES);
|
|
270
|
+
const location = optionalTrimmedBoundedString(value.location);
|
|
271
|
+
const digest = optionalTrimmedBoundedString(value.digest);
|
|
272
|
+
if (version === false || location === false || digest === false) return undefined;
|
|
273
|
+
return {
|
|
274
|
+
id,
|
|
275
|
+
kind,
|
|
276
|
+
...(version === undefined ? {} : { version }),
|
|
277
|
+
...(location === undefined ? {} : { location }),
|
|
278
|
+
...(digest === undefined ? {} : { digest }),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseChange(value: Record<string, unknown>): SubagentChangeReference | undefined {
|
|
283
|
+
if (typeof value.path !== "string" || typeof value.summary !== "string") return undefined;
|
|
284
|
+
return { path: bounded(value.path), summary: bounded(value.summary) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseVerification(
|
|
288
|
+
value: Record<string, unknown>,
|
|
289
|
+
): SubagentVerificationReference | undefined {
|
|
290
|
+
if (
|
|
291
|
+
typeof value.status !== "string" ||
|
|
292
|
+
!(["passed", "failed", "not-run"] as const).includes(
|
|
293
|
+
value.status as SubagentVerificationReference["status"],
|
|
294
|
+
) ||
|
|
295
|
+
typeof value.summary !== "string"
|
|
296
|
+
) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const command = optionalBoundedString(value.command);
|
|
300
|
+
const evidence = value.evidence === undefined ? undefined : stringArray(value.evidence);
|
|
301
|
+
if (command === false || (evidence === undefined && value.evidence !== undefined))
|
|
302
|
+
return undefined;
|
|
303
|
+
return {
|
|
304
|
+
status: value.status as SubagentVerificationReference["status"],
|
|
305
|
+
summary: bounded(value.summary),
|
|
306
|
+
...(command === undefined ? {} : { command }),
|
|
307
|
+
...(evidence === undefined ? {} : { evidence }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseProvenance(value: unknown): SubagentResultProvenance | undefined | false {
|
|
312
|
+
if (value === undefined) return undefined;
|
|
313
|
+
if (!isPlainObject(value)) return false;
|
|
314
|
+
const taskId = optionalBoundedString(value.taskId);
|
|
315
|
+
const repositoryGeneration = optionalBoundedString(value.repositoryGeneration);
|
|
316
|
+
const taskGeneration =
|
|
317
|
+
value.taskGeneration === undefined
|
|
318
|
+
? undefined
|
|
319
|
+
: Number.isSafeInteger(value.taskGeneration) && Number(value.taskGeneration) >= 0
|
|
320
|
+
? Number(value.taskGeneration)
|
|
321
|
+
: false;
|
|
322
|
+
const executionPlanId = optionalBoundedString(value.executionPlanId);
|
|
323
|
+
const cancellationLineage =
|
|
324
|
+
value.cancellationLineage === undefined ? undefined : stringArray(value.cancellationLineage);
|
|
325
|
+
const inputArtifacts =
|
|
326
|
+
value.inputArtifacts === undefined ? undefined : stringArray(value.inputArtifacts);
|
|
327
|
+
if (
|
|
328
|
+
taskId === false ||
|
|
329
|
+
repositoryGeneration === false ||
|
|
330
|
+
taskGeneration === false ||
|
|
331
|
+
executionPlanId === false ||
|
|
332
|
+
(cancellationLineage === undefined && value.cancellationLineage !== undefined) ||
|
|
333
|
+
(inputArtifacts === undefined && value.inputArtifacts !== undefined)
|
|
334
|
+
) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
...(taskId === undefined ? {} : { taskId }),
|
|
339
|
+
...(taskGeneration === undefined ? {} : { taskGeneration }),
|
|
340
|
+
...(executionPlanId === undefined ? {} : { executionPlanId }),
|
|
341
|
+
...(cancellationLineage === undefined ? {} : { cancellationLineage }),
|
|
342
|
+
...(inputArtifacts === undefined ? {} : { inputArtifacts }),
|
|
343
|
+
...(repositoryGeneration === undefined ? {} : { repositoryGeneration }),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseJsonObject(text: string): Record<string, unknown> | undefined {
|
|
348
|
+
if (Buffer.byteLength(text, "utf8") > DEFAULT_MAX_OUTPUT_BYTES) return undefined;
|
|
349
|
+
const source = unwrapJsonFence(text).trim();
|
|
350
|
+
if (!source.startsWith("{") || !source.endsWith("}")) return undefined;
|
|
351
|
+
let parsed: unknown;
|
|
352
|
+
try {
|
|
353
|
+
parsed = JSON.parse(source);
|
|
354
|
+
} catch {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
return isPlainObject(parsed) ? parsed : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function objectArray<T>(
|
|
361
|
+
value: unknown,
|
|
362
|
+
parse: (entry: Record<string, unknown>) => T | undefined,
|
|
363
|
+
): T[] | undefined {
|
|
364
|
+
if (!Array.isArray(value) || value.length > MAX_ITEMS) return undefined;
|
|
365
|
+
const result: T[] = [];
|
|
366
|
+
for (const item of value) {
|
|
367
|
+
if (!isPlainObject(item)) return undefined;
|
|
368
|
+
const parsed = parse(item);
|
|
369
|
+
if (!parsed) return undefined;
|
|
370
|
+
result.push(parsed);
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
376
|
+
if (!Array.isArray(value) || value.length > MAX_ITEMS) return undefined;
|
|
377
|
+
if (!value.every((item) => typeof item === "string")) return undefined;
|
|
378
|
+
return value.map((item) => bounded(item));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function optionalTrimmedBoundedString(
|
|
382
|
+
value: unknown,
|
|
383
|
+
maxBytes = MAX_FIELD_BYTES,
|
|
384
|
+
): string | undefined | false {
|
|
385
|
+
const boundedValue = optionalBoundedString(value, maxBytes);
|
|
386
|
+
if (boundedValue === undefined || boundedValue === false) return boundedValue;
|
|
387
|
+
const trimmed = boundedValue.trim();
|
|
388
|
+
return trimmed || false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function optionalBoundedString(
|
|
392
|
+
value: unknown,
|
|
393
|
+
maxBytes = MAX_FIELD_BYTES,
|
|
394
|
+
): string | undefined | false {
|
|
395
|
+
if (value === undefined) return undefined;
|
|
396
|
+
if (typeof value !== "string") return false;
|
|
397
|
+
return truncateUtf8(redactPrivateText(value), maxBytes).text;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function bounded(value: string): string {
|
|
401
|
+
return truncateUtf8(redactPrivateText(value), MAX_FIELD_BYTES).text;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
405
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
409
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function unwrapJsonFence(value: string): string {
|
|
413
|
+
const trimmed = value.trim();
|
|
414
|
+
const match = /^```(?:json)?\s*\n([\s\S]*?)\n```$/iu.exec(trimmed);
|
|
415
|
+
return match?.[1] ?? trimmed;
|
|
416
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AgentConfig, SubagentThinkingLevel, SubagentTransportKind } from "./agents.js";
|
|
4
|
+
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
5
|
+
import type { DelegationContract } from "./delegation-contract.js";
|
|
6
|
+
import {
|
|
7
|
+
acknowledgeExecutionPlan,
|
|
8
|
+
createExecutionPlan,
|
|
9
|
+
type ExecutionPlan,
|
|
10
|
+
resolveContractTools,
|
|
11
|
+
} from "./execution-plan.js";
|
|
12
|
+
import type { SubagentResultFormat } from "./result-contract.js";
|
|
13
|
+
import {
|
|
14
|
+
captureRepositoryGeneration,
|
|
15
|
+
captureSemanticResourceGeneration,
|
|
16
|
+
captureSemanticSnapshot,
|
|
17
|
+
type SemanticSnapshot,
|
|
18
|
+
} from "./semantic-snapshot.js";
|
|
19
|
+
|
|
20
|
+
export interface RetainedSemanticStateInput {
|
|
21
|
+
agent: AgentConfig;
|
|
22
|
+
contract?: DelegationContract;
|
|
23
|
+
target: TargetPolicyAudit;
|
|
24
|
+
cwd: string;
|
|
25
|
+
workspaceMode: "shared" | "worktree";
|
|
26
|
+
transport: SubagentTransportKind;
|
|
27
|
+
resultFormat: SubagentResultFormat;
|
|
28
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
taskGeneration?: number;
|
|
31
|
+
cancellationLineage?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RetainedSemanticState {
|
|
35
|
+
executionPlan: ExecutionPlan;
|
|
36
|
+
semanticSnapshot: SemanticSnapshot;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function buildRetainedSemanticState(
|
|
40
|
+
input: RetainedSemanticStateInput,
|
|
41
|
+
): Promise<RetainedSemanticState> {
|
|
42
|
+
const executionPlan = createExecutionPlan({
|
|
43
|
+
contract: input.contract,
|
|
44
|
+
agent: input.agent,
|
|
45
|
+
effectiveTools: resolveContractTools(input.agent.tools, input.contract),
|
|
46
|
+
target: input.target,
|
|
47
|
+
workspaceMode: input.workspaceMode,
|
|
48
|
+
transport: input.transport,
|
|
49
|
+
resultFormat: input.resultFormat,
|
|
50
|
+
model: input.agent.model,
|
|
51
|
+
thinkingLevel: input.thinkingLevel ?? input.agent.thinkingLevel,
|
|
52
|
+
timeoutMs: input.timeoutMs ?? input.agent.timeoutMs,
|
|
53
|
+
taskGeneration: input.taskGeneration,
|
|
54
|
+
cancellationLineage: input.cancellationLineage,
|
|
55
|
+
});
|
|
56
|
+
const acknowledgement = acknowledgeExecutionPlan(executionPlan);
|
|
57
|
+
if (acknowledgement.status === "rejected") {
|
|
58
|
+
throw new Error(`Execution plan rejected: ${JSON.stringify(acknowledgement)}`);
|
|
59
|
+
}
|
|
60
|
+
const agentDir = getAgentDir();
|
|
61
|
+
const [repository, resourceGeneration] = await Promise.all([
|
|
62
|
+
captureRepositoryGeneration(input.cwd),
|
|
63
|
+
captureSemanticResourceGeneration([
|
|
64
|
+
path.join(agentDir, "skills"),
|
|
65
|
+
path.join(agentDir, "prompts"),
|
|
66
|
+
path.join(agentDir, "SYSTEM.md"),
|
|
67
|
+
path.join(agentDir, "APPEND_SYSTEM.md"),
|
|
68
|
+
path.join(input.cwd, ".pi", "skills"),
|
|
69
|
+
path.join(input.cwd, ".pi", "prompts"),
|
|
70
|
+
path.join(input.cwd, "AGENTS.md"),
|
|
71
|
+
path.join(input.cwd, "CLAUDE.md"),
|
|
72
|
+
path.join(input.cwd, "SYSTEM.md"),
|
|
73
|
+
path.join(input.cwd, "APPEND_SYSTEM.md"),
|
|
74
|
+
]),
|
|
75
|
+
]);
|
|
76
|
+
return {
|
|
77
|
+
executionPlan,
|
|
78
|
+
semanticSnapshot: captureSemanticSnapshot({
|
|
79
|
+
agentName: input.agent.name,
|
|
80
|
+
agentManifest: {
|
|
81
|
+
manifest: input.agent.capabilityManifest,
|
|
82
|
+
delegationContract: input.contract,
|
|
83
|
+
resourceGeneration,
|
|
84
|
+
},
|
|
85
|
+
rolePrompt: input.agent.systemPrompt,
|
|
86
|
+
tools: executionPlan.effectiveTools,
|
|
87
|
+
model: executionPlan.model,
|
|
88
|
+
thinkingLevel: executionPlan.thinkingLevel,
|
|
89
|
+
transport: input.transport,
|
|
90
|
+
trust: {
|
|
91
|
+
kind: input.target.trust.kind,
|
|
92
|
+
projectTrusted: input.target.trust.projectTrusted,
|
|
93
|
+
},
|
|
94
|
+
repository,
|
|
95
|
+
artifacts: {},
|
|
96
|
+
workflowGeneration: 0,
|
|
97
|
+
schedulerPolicy: "retained-fifo-v1",
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
}
|