@narumitw/pi-subagents 0.49.2 → 0.51.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.
Files changed (81) hide show
  1. package/README.md +313 -53
  2. package/package.json +11 -8
  3. package/src/adaptive-scheduler.ts +196 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +848 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +296 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +78 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +772 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +172 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +17 -0
  77. package/src/work-item-ledger.ts +682 -0
  78. package/src/work-item-persistence.ts +218 -0
  79. package/src/workflow-planning.ts +150 -0
  80. package/src/workflow-ui.ts +61 -0
  81. 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("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`,
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("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`;
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
+ }