@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/params.ts CHANGED
@@ -1,26 +1,117 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { type Static, Type } from "typebox";
3
3
  import { THINKING_LEVELS } from "./agents.js";
4
- import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
4
+ import { DelegationContractSchema } from "./delegation-contract.js";
5
+ import { MAX_CONFIGURABLE_PARALLEL_TASKS, MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
6
+ import { PANEL_PRESETS } from "./panel-planning.js";
7
+ import { SUBAGENT_RESULT_FORMATS } from "./result-contract.js";
8
+ import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
5
9
 
6
10
  const TimeoutMs = Type.Number({
7
11
  description:
8
- "Hard timeout in milliseconds for each subagent subprocess. Defaults to PI_SUBAGENT_TIMEOUT_MS or 600000.",
12
+ "Work deadline in milliseconds selected for the task difficulty. On expiry, Pi aborts the work and makes one separately bounded summary attempt. Defaults to PI_SUBAGENT_TIMEOUT_MS or 600000.",
9
13
  minimum: 1,
10
14
  maximum: MAX_SUBAGENT_TIMEOUT_MS,
11
15
  });
12
16
 
17
+ const TurnLimitFields = {
18
+ idleTimeoutMs: Type.Optional(
19
+ Type.Integer({
20
+ description:
21
+ "Maximum milliseconds without a completed assistant turn or tool result before Pi aborts work and preserves a checkpoint.",
22
+ minimum: 1,
23
+ maximum: MAX_SUBAGENT_TIMEOUT_MS,
24
+ }),
25
+ ),
26
+ maxTurns: Type.Optional(
27
+ Type.Integer({
28
+ description: "Maximum assistant turns before unfinished work is stopped and checkpointed.",
29
+ minimum: 1,
30
+ maximum: MAX_SUBAGENT_TURNS,
31
+ }),
32
+ ),
33
+ maxToolCalls: Type.Optional(
34
+ Type.Integer({
35
+ description: "Maximum tool calls before additional tool work is stopped and checkpointed.",
36
+ minimum: 1,
37
+ maximum: MAX_SUBAGENT_TOOL_CALLS,
38
+ }),
39
+ ),
40
+ };
41
+
13
42
  const ThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
14
43
  description:
15
44
  "Pi thinking level for the subagent process: off, minimal, low, medium, high, xhigh, or max.",
16
45
  });
17
46
 
47
+ const ResultFormatSchema = StringEnum(SUBAGENT_RESULT_FORMATS, {
48
+ description:
49
+ "Optional completion contract. Text preserves ordinary output; structured-v1 and structured-v2 request versioned JSON with bounded text fallback.",
50
+ });
51
+
52
+ const ContractFields = {
53
+ contract: Type.Optional(DelegationContractSchema),
54
+ resultFormat: Type.Optional(ResultFormatSchema),
55
+ retryPolicy: Type.Optional(
56
+ Type.Object(
57
+ {
58
+ maxAttempts: Type.Integer({ minimum: 1, maximum: 3 }),
59
+ backoffMs: Type.Optional(Type.Integer({ minimum: 0, maximum: 30_000 })),
60
+ },
61
+ { additionalProperties: false },
62
+ ),
63
+ ),
64
+ hedgeAfterMs: Type.Optional(Type.Integer({ minimum: 1, maximum: 30_000 })),
65
+ };
66
+
18
67
  const TaskItem = Type.Object({
19
68
  agent: Type.String({ description: "Name of the agent to invoke" }),
20
69
  task: Type.String({ description: "Task to delegate to the agent" }),
21
70
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
22
71
  timeoutMs: Type.Optional(TimeoutMs),
72
+ ...TurnLimitFields,
73
+ thinkingLevel: Type.Optional(ThinkingLevelSchema),
74
+ ...ContractFields,
75
+ });
76
+
77
+ const WorkflowTaskItem = Type.Object({
78
+ id: Type.String({ minLength: 1, maxLength: 256 }),
79
+ agent: Type.Optional(
80
+ Type.String({ description: "Optional explicit agent; omit to route by capability manifest" }),
81
+ ),
82
+ requiredCapabilities: Type.Optional(
83
+ Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 50 }),
84
+ ),
85
+ requiredTools: Type.Optional(
86
+ Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 50 }),
87
+ ),
88
+ requiredVerificationRole: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
89
+ preferredCostHint: Type.Optional(StringEnum(["low", "medium", "high"] as const)),
90
+ preferredLatencyHint: Type.Optional(StringEnum(["low", "medium", "high"] as const)),
91
+ task: Type.String({ description: "Task to delegate to the agent" }),
92
+ dependsOn: Type.Optional(
93
+ Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 64 }),
94
+ ),
95
+ inputArtifacts: Type.Optional(
96
+ Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 50 }),
97
+ ),
98
+ inputArtifactVersions: Type.Optional(
99
+ Type.Record(
100
+ Type.String({ minLength: 1, maxLength: 256 }),
101
+ Type.String({ minLength: 1, maxLength: 256 }),
102
+ ),
103
+ ),
104
+ readPaths: Type.Optional(Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 50 })),
105
+ writePaths: Type.Optional(Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 50 })),
106
+ ownershipKeys: Type.Optional(Type.Array(Type.String({ maxLength: 256 }), { maxItems: 50 })),
107
+ acceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 50 })),
108
+ integrationOwner: Type.Optional(Type.Boolean()),
109
+ verifierFor: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
110
+ cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
111
+ timeoutMs: Type.Optional(TimeoutMs),
112
+ ...TurnLimitFields,
23
113
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
114
+ ...ContractFields,
24
115
  });
25
116
 
26
117
  const ChainItem = Type.Object({
@@ -28,7 +119,9 @@ const ChainItem = Type.Object({
28
119
  task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
29
120
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
30
121
  timeoutMs: Type.Optional(TimeoutMs),
122
+ ...TurnLimitFields,
31
123
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
124
+ ...ContractFields,
32
125
  });
33
126
 
34
127
  const AggregatorItem = Type.Object(
@@ -43,7 +136,9 @@ const AggregatorItem = Type.Object(
43
136
  Type.String({ description: "Working directory for the aggregator process" }),
44
137
  ),
45
138
  timeoutMs: Type.Optional(TimeoutMs),
139
+ ...TurnLimitFields,
46
140
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
141
+ ...ContractFields,
47
142
  },
48
143
  {
49
144
  description:
@@ -51,6 +146,48 @@ const AggregatorItem = Type.Object(
51
146
  },
52
147
  );
53
148
 
149
+ const PanelReviewerItem = Type.Object(
150
+ {
151
+ id: Type.String({ minLength: 1, maxLength: 256 }),
152
+ agent: Type.String({ minLength: 1 }),
153
+ focus: Type.Optional(Type.String({ maxLength: 8 * 1024 })),
154
+ timeoutMs: Type.Optional(TimeoutMs),
155
+ ...TurnLimitFields,
156
+ thinkingLevel: Type.Optional(ThinkingLevelSchema),
157
+ },
158
+ { additionalProperties: false },
159
+ );
160
+
161
+ const PanelSynthesizerItem = Type.Object(
162
+ {
163
+ agent: Type.String({ minLength: 1 }),
164
+ timeoutMs: Type.Optional(TimeoutMs),
165
+ ...TurnLimitFields,
166
+ thinkingLevel: Type.Optional(ThinkingLevelSchema),
167
+ },
168
+ { additionalProperties: false },
169
+ );
170
+
171
+ const PanelItem = Type.Object(
172
+ {
173
+ id: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
174
+ preset: Type.Optional(StringEnum(PANEL_PRESETS, { default: "custom" })),
175
+ task: Type.String({ minLength: 1, maxLength: 50 * 1024 }),
176
+ context: Type.Optional(Type.String({ maxLength: 50 * 1024 })),
177
+ reviewers: Type.Array(PanelReviewerItem, {
178
+ minItems: 2,
179
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
180
+ }),
181
+ synthesizer: PanelSynthesizerItem,
182
+ minValidReviews: Type.Optional(Type.Integer({ minimum: 2 })),
183
+ },
184
+ {
185
+ additionalProperties: false,
186
+ description:
187
+ "One bounded independent review round followed by one evidence-preserving synthesis when enough valid reviews remain.",
188
+ },
189
+ );
190
+
54
191
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
55
192
  description:
56
193
  'Per-invocation custom agent scope. Default: "user". Use "project" for project-local agents or "both" for user and project agents; this is a tool argument, not a pi-subagents.json setting.',
@@ -63,12 +200,34 @@ export const SubagentParams = Type.Object({
63
200
  ),
64
201
  task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
65
202
  tasks: Type.Optional(
66
- Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
203
+ Type.Array(TaskItem, {
204
+ description: "Array of {agent, task} for parallel execution",
205
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
206
+ }),
67
207
  ),
68
208
  chain: Type.Optional(
69
209
  Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" }),
70
210
  ),
71
211
  aggregator: Type.Optional(AggregatorItem),
212
+ panel: Type.Optional(PanelItem),
213
+ workflow: Type.Optional(
214
+ Type.Object(
215
+ {
216
+ id: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
217
+ honorAdmission: Type.Optional(
218
+ Type.Boolean({
219
+ description:
220
+ "Opt in to declining workflow tasks whose explicit audit metadata recommends parent-owned work or abstention.",
221
+ }),
222
+ ),
223
+ tasks: Type.Array(WorkflowTaskItem, {
224
+ minItems: 1,
225
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
226
+ }),
227
+ },
228
+ { additionalProperties: false },
229
+ ),
230
+ ),
72
231
  agentScope: Type.Optional(AgentScopeSchema),
73
232
  confirmProjectAgents: Type.Optional(
74
233
  Type.Boolean({
@@ -80,7 +239,17 @@ export const SubagentParams = Type.Object({
80
239
  Type.String({ description: "Working directory for the agent process (single mode)" }),
81
240
  ),
82
241
  timeoutMs: Type.Optional(TimeoutMs),
242
+ totalTimeoutMs: Type.Optional(
243
+ Type.Number({
244
+ description:
245
+ "Overall blocking-workflow deadline in milliseconds, including queued work, workflow tasks, panel phases, chain steps, and fan-in.",
246
+ minimum: 1,
247
+ maximum: MAX_SUBAGENT_TIMEOUT_MS,
248
+ }),
249
+ ),
250
+ ...TurnLimitFields,
83
251
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
252
+ ...ContractFields,
84
253
  });
85
254
 
86
255
  export type SubagentParams = Static<typeof SubagentParams>;
@@ -2,11 +2,22 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
5
+ import { projectAgentRecords } from "./agent-projection.js";
5
6
  import { isThinkingLevel } from "./agents.js";
7
+ import { isCapabilityGrant, revokeCapabilityGrant } from "./capability-grant.js";
6
8
  import { redactPrivateText } from "./context.js";
9
+ import { normalizeDelegationContract } from "./delegation-contract.js";
10
+ import { copyExecutionPlan, isExecutionPlan } from "./execution-plan.js";
11
+ import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
7
12
  import type { ManagedAgent } from "./registry.js";
13
+ import { parseAnyStructuredSubagentResult, SUBAGENT_RESULT_FORMATS } from "./result-contract.js";
14
+ import { isSemanticSnapshot } from "./semantic-snapshot.js";
15
+ import { resolveStatefulLimits } from "./stateful-limits.js";
16
+ import { copyTurnTerminationReport, type TurnTerminationReport } from "./timeout-checkpoint.js";
17
+ import { MAX_SUBAGENT_TOOL_CALLS, MAX_SUBAGENT_TURNS } from "./turn-budget.js";
8
18
 
9
19
  const STATE_VERSION = 2;
20
+ const DEFAULT_STATEFUL_LIMITS = resolveStatefulLimits();
10
21
  const MAX_STATE_BYTES = 1024 * 1024;
11
22
 
12
23
  interface StoredState {
@@ -35,7 +46,7 @@ export class AgentPersistence {
35
46
  if (!Number.isFinite(retentionMs)) {
36
47
  throw new Error("Subagent retentionDays is too large");
37
48
  }
38
- const maxStoredAgents = options.maxStoredAgents ?? 50;
49
+ const maxStoredAgents = options.maxStoredAgents ?? DEFAULT_STATEFUL_LIMITS.maxStoredAgents;
39
50
  if (!Number.isSafeInteger(maxStoredAgents) || maxStoredAgents < 1) {
40
51
  throw new Error("Subagent maxStoredAgents must be a positive safe integer");
41
52
  }
@@ -54,10 +65,10 @@ export class AgentPersistence {
54
65
  const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8")) as unknown;
55
66
  if (!isStoredState(parsed)) throw new Error("unsupported or malformed state");
56
67
  const cutoff = Date.now() - this.retentionMs;
57
- return parsed.agents
58
- .filter((agent) => agent.updatedAt >= cutoff && agent.state !== "closed")
59
- .slice(-this.maxStoredAgents)
60
- .map(sanitizeAgent);
68
+ return projectAgentRecords(
69
+ parsed.agents.filter((agent) => agent.updatedAt >= cutoff && agent.state !== "closed"),
70
+ { maxAgents: this.maxStoredAgents },
71
+ ).map(sanitizeAgent);
61
72
  } catch {
62
73
  this.quarantine();
63
74
  return [];
@@ -69,7 +80,9 @@ export class AgentPersistence {
69
80
  const eligible = agents.filter(
70
81
  (agent) => agent.state !== "closed" && agent.updatedAt >= cutoff,
71
82
  );
72
- const records = selectAgentsForPersistence(eligible, this.maxStoredAgents).map(sanitizeAgent);
83
+ const records = projectAgentRecords(eligible, {
84
+ maxAgents: this.maxStoredAgents,
85
+ }).map(sanitizeAgent);
73
86
  const state: StoredState = { version: STATE_VERSION, updatedAt: Date.now(), agents: records };
74
87
  let content = `${JSON.stringify(state, null, "\t")}\n`;
75
88
  while (Buffer.byteLength(content, "utf8") > MAX_STATE_BYTES && state.agents.length > 0) {
@@ -100,30 +113,6 @@ export class AgentPersistence {
100
113
  }
101
114
  }
102
115
 
103
- function selectAgentsForPersistence(
104
- agents: readonly ManagedAgent[],
105
- maxAgents: number,
106
- ): ManagedAgent[] {
107
- const byId = new Map(agents.map((agent) => [agent.id, agent]));
108
- const selected = new Map<string, ManagedAgent>();
109
- const newestFirst = [...agents].sort((left, right) => right.updatedAt - left.updatedAt);
110
- for (const agent of newestFirst) {
111
- const chain: ManagedAgent[] = [];
112
- let current: ManagedAgent | undefined = agent;
113
- const seen = new Set<string>();
114
- while (current && !seen.has(current.id)) {
115
- seen.add(current.id);
116
- chain.unshift(current);
117
- current = current.parentId ? byId.get(current.parentId) : undefined;
118
- }
119
- if (current || (agent.parentId && chain[0].parentId)) continue;
120
- const missing = chain.filter((candidate) => !selected.has(candidate.id));
121
- if (selected.size + missing.length > maxAgents) continue;
122
- for (const candidate of missing) selected.set(candidate.id, candidate);
123
- }
124
- return agents.filter((agent) => selected.has(agent.id));
125
- }
126
-
127
116
  function sanitizeAgent(agent: ManagedAgent): ManagedAgent {
128
117
  return {
129
118
  ...agent,
@@ -135,19 +124,63 @@ function sanitizeAgent(agent: ManagedAgent): ManagedAgent {
135
124
  recipientId: agent.id,
136
125
  content: redactPrivateText(message.content),
137
126
  })),
138
- state: "idle",
127
+ state: agent.state === "running" || agent.state === "starting" ? "interrupted" : agent.state,
139
128
  currentTask: undefined,
129
+ currentTimeoutMs: undefined,
130
+ currentIdleTimeoutMs: undefined,
131
+ currentMaxTurns: undefined,
132
+ currentMaxToolCalls: undefined,
140
133
  currentMailboxMessageIds: undefined,
134
+ telemetry: undefined,
135
+ contract: normalizeDelegationContract(agent.contract),
136
+ structuredResult:
137
+ agent.structuredResult && agent.resultFormat
138
+ ? parseAnyStructuredSubagentResult(
139
+ JSON.stringify(agent.structuredResult),
140
+ agent.resultFormat,
141
+ )
142
+ : undefined,
143
+ executionPlan: agent.executionPlan ? copyExecutionPlan(agent.executionPlan) : undefined,
144
+ capabilityGrant: agent.capabilityGrant
145
+ ? agent.capabilityGrant.state === "active"
146
+ ? revokeCapabilityGrant(agent.capabilityGrant, "persistence-boundary", Date.now())
147
+ : structuredClone(agent.capabilityGrant)
148
+ : undefined,
149
+ semanticSnapshot: agent.semanticSnapshot ? structuredClone(agent.semanticSnapshot) : undefined,
150
+ semanticCompatibility: agent.semanticCompatibility
151
+ ? structuredClone(agent.semanticCompatibility)
152
+ : undefined,
153
+ termination: agent.termination ? sanitizeTermination(agent.termination) : undefined,
141
154
  context: agent.context ? redactPrivateText(agent.context) : undefined,
142
155
  error: agent.error ? redactPrivateText(agent.error) : undefined,
143
156
  history: agent.history.map((turn) => ({
144
157
  ...turn,
145
158
  task: redactPrivateText(turn.task),
146
159
  output: redactPrivateText(turn.output),
160
+ termination: turn.termination ? sanitizeTermination(turn.termination) : undefined,
147
161
  })),
148
162
  };
149
163
  }
150
164
 
165
+ function sanitizeTermination(report: TurnTerminationReport): TurnTerminationReport {
166
+ const copy = copyTurnTerminationReport(report);
167
+ copy.checkpoint.task = redactPrivateText(copy.checkpoint.task);
168
+ copy.checkpoint.partialOutput = copy.checkpoint.partialOutput
169
+ ? redactPrivateText(copy.checkpoint.partialOutput)
170
+ : undefined;
171
+ copy.checkpoint.assistantNotes = copy.checkpoint.assistantNotes.map(redactPrivateText);
172
+ copy.checkpoint.completedTools = copy.checkpoint.completedTools.map((item) => ({
173
+ ...item,
174
+ toolName: redactPrivateText(item.toolName),
175
+ output: redactPrivateText(item.output),
176
+ }));
177
+ copy.checkpoint.changedFiles = copy.checkpoint.changedFiles.map(redactPrivateText);
178
+ copy.finalization.error = copy.finalization.error
179
+ ? redactPrivateText(copy.finalization.error)
180
+ : undefined;
181
+ return copy;
182
+ }
183
+
151
184
  function isStoredState(value: unknown): value is StoredState {
152
185
  if (!value || typeof value !== "object") return false;
153
186
  const state = value as { version?: unknown; agents?: unknown };
@@ -167,6 +200,34 @@ function isStoredState(value: unknown): value is StoredState {
167
200
  Number.isFinite(record.updatedAt) &&
168
201
  (record.parentId === undefined || typeof record.parentId === "string") &&
169
202
  (record.thinkingLevel === undefined || isThinkingLevel(record.thinkingLevel)) &&
203
+ (record.timeoutMs === undefined || isTurnTimeout(record.timeoutMs)) &&
204
+ (record.idleTimeoutMs === undefined || isTurnTimeout(record.idleTimeoutMs)) &&
205
+ (record.maxTurns === undefined || isPositiveBounded(record.maxTurns, MAX_SUBAGENT_TURNS)) &&
206
+ (record.maxToolCalls === undefined ||
207
+ isPositiveBounded(record.maxToolCalls, MAX_SUBAGENT_TOOL_CALLS)) &&
208
+ (record.termination === undefined || isTerminationReport(record.termination)) &&
209
+ (record.contextTurns === undefined || isNonNegativeInteger(record.contextTurns)) &&
210
+ (record.contextBytes === undefined || isNonNegativeInteger(record.contextBytes)) &&
211
+ (record.spawnIdempotencyKey === undefined ||
212
+ (typeof record.spawnIdempotencyKey === "string" &&
213
+ record.spawnIdempotencyKey.length > 0 &&
214
+ record.spawnIdempotencyKey.length <= 256)) &&
215
+ (record.spawnRequestHash === undefined || isSha256(record.spawnRequestHash)) &&
216
+ (record.contract === undefined ||
217
+ normalizeDelegationContract(record.contract) !== undefined) &&
218
+ (record.resultFormat === undefined ||
219
+ SUBAGENT_RESULT_FORMATS.includes(record.resultFormat)) &&
220
+ (record.structuredResult === undefined ||
221
+ (record.resultFormat !== undefined &&
222
+ parseAnyStructuredSubagentResult(
223
+ JSON.stringify(record.structuredResult),
224
+ record.resultFormat,
225
+ ) !== undefined)) &&
226
+ (record.executionPlan === undefined || isExecutionPlan(record.executionPlan)) &&
227
+ (record.capabilityGrant === undefined || isCapabilityGrant(record.capabilityGrant)) &&
228
+ (record.semanticSnapshot === undefined || isSemanticSnapshot(record.semanticSnapshot)) &&
229
+ (record.semanticCompatibility === undefined ||
230
+ isSemanticCompatibility(record.semanticCompatibility)) &&
170
231
  (record.workspaceMode === undefined || record.workspaceMode === "worktree") &&
171
232
  (record.target === undefined || isTargetPolicyAudit(record.target)) &&
172
233
  (record.children === undefined ||
@@ -180,6 +241,94 @@ function isStoredState(value: unknown): value is StoredState {
180
241
  });
181
242
  }
182
243
 
244
+ function isSemanticCompatibility(value: unknown): boolean {
245
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
246
+ const compatibility = value as Record<string, unknown>;
247
+ return (
248
+ ["compatible", "warning", "needs-revalidation", "rejected"].includes(
249
+ String(compatibility.status),
250
+ ) &&
251
+ Array.isArray(compatibility.changedComponents) &&
252
+ compatibility.changedComponents.every((item) => typeof item === "string")
253
+ );
254
+ }
255
+
256
+ function isNonNegativeInteger(value: unknown): value is number {
257
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
258
+ }
259
+
260
+ function isTurnTimeout(value: unknown): value is number {
261
+ return (
262
+ typeof value === "number" &&
263
+ Number.isSafeInteger(value) &&
264
+ value >= 1 &&
265
+ value <= MAX_SUBAGENT_TIMEOUT_MS
266
+ );
267
+ }
268
+
269
+ function isPositiveBounded(value: unknown, maximum: number): value is number {
270
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= maximum;
271
+ }
272
+
273
+ function isTerminationReport(value: unknown): value is TurnTerminationReport {
274
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
275
+ const report = value as Record<string, unknown>;
276
+ if (
277
+ report.version !== "pi-subagents:termination:v1" ||
278
+ ![
279
+ "work_timeout",
280
+ "idle_timeout",
281
+ "turn_limit",
282
+ "tool_call_limit",
283
+ "orchestration_timeout",
284
+ ].includes(String(report.reason)) ||
285
+ !isPositiveBounded(report.limit, MAX_SUBAGENT_TIMEOUT_MS)
286
+ ) {
287
+ return false;
288
+ }
289
+ const checkpoint = report.checkpoint;
290
+ const finalization = report.finalization;
291
+ if (!checkpoint || typeof checkpoint !== "object" || Array.isArray(checkpoint)) return false;
292
+ if (!finalization || typeof finalization !== "object" || Array.isArray(finalization))
293
+ return false;
294
+ const checkpointValue = checkpoint as Record<string, unknown>;
295
+ const finalizationValue = finalization as Record<string, unknown>;
296
+ return (
297
+ checkpointValue.version === "pi-subagents:checkpoint:v1" &&
298
+ typeof checkpointValue.task === "string" &&
299
+ (checkpointValue.partialOutput === undefined ||
300
+ typeof checkpointValue.partialOutput === "string") &&
301
+ Array.isArray(checkpointValue.assistantNotes) &&
302
+ checkpointValue.assistantNotes.every((item) => typeof item === "string") &&
303
+ Array.isArray(checkpointValue.completedTools) &&
304
+ checkpointValue.completedTools.every(isCompletedToolEvidence) &&
305
+ Array.isArray(checkpointValue.changedFiles) &&
306
+ checkpointValue.changedFiles.every((item) => typeof item === "string") &&
307
+ typeof checkpointValue.sideEffectsMayHaveOccurred === "boolean" &&
308
+ typeof checkpointValue.truncated === "boolean" &&
309
+ typeof finalizationValue.attempted === "boolean" &&
310
+ ["completed", "failed", "timed_out", "skipped"].includes(String(finalizationValue.status)) &&
311
+ typeof finalizationValue.durationMs === "number" &&
312
+ Number.isFinite(finalizationValue.durationMs) &&
313
+ finalizationValue.durationMs >= 0 &&
314
+ (finalizationValue.error === undefined || typeof finalizationValue.error === "string")
315
+ );
316
+ }
317
+
318
+ function isCompletedToolEvidence(value: unknown): boolean {
319
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
320
+ const item = value as Record<string, unknown>;
321
+ return (
322
+ typeof item.toolName === "string" &&
323
+ typeof item.output === "string" &&
324
+ typeof item.isError === "boolean"
325
+ );
326
+ }
327
+
328
+ function isSha256(value: unknown): value is string {
329
+ return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
330
+ }
331
+
183
332
  function isTargetPolicyAudit(value: unknown): boolean {
184
333
  if (!value || typeof value !== "object") return false;
185
334
  const target = value as Record<string, unknown>;
@@ -218,7 +367,8 @@ function isAgentTurn(value: unknown): boolean {
218
367
  typeof turn.completedAt === "number" &&
219
368
  Number.isFinite(turn.completedAt) &&
220
369
  typeof turn.exitCode === "number" &&
221
- Number.isFinite(turn.exitCode)
370
+ Number.isFinite(turn.exitCode) &&
371
+ (turn.termination === undefined || isTerminationReport(turn.termination))
222
372
  );
223
373
  }
224
374
 
@@ -0,0 +1,38 @@
1
+ import {
2
+ DefaultResourceLoader,
3
+ getAgentDir,
4
+ SettingsManager,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { assertPiPromptSourcesAreReadableFiles } from "./prompt-source-safety.js";
7
+
8
+ export interface PiPromptResources {
9
+ systemPrompt?: string;
10
+ appendSystemPromptPaths: string[];
11
+ }
12
+
13
+ /** Resolve Pi-owned prompt files without loading target packages, extensions, or settings. */
14
+ export async function resolvePiPromptResources(
15
+ cwd: string,
16
+ projectTrusted: boolean,
17
+ agentDir = getAgentDir(),
18
+ ): Promise<PiPromptResources> {
19
+ assertPiPromptSourcesAreReadableFiles(cwd, agentDir, projectTrusted, [
20
+ "SYSTEM.md",
21
+ "APPEND_SYSTEM.md",
22
+ ]);
23
+ const loader = new DefaultResourceLoader({
24
+ cwd,
25
+ agentDir,
26
+ settingsManager: SettingsManager.inMemory({}, { projectTrusted }),
27
+ noExtensions: true,
28
+ noSkills: true,
29
+ noPromptTemplates: true,
30
+ noThemes: true,
31
+ noContextFiles: true,
32
+ });
33
+ await loader.reload();
34
+ return {
35
+ systemPrompt: loader.getSystemPrompt(),
36
+ appendSystemPromptPaths: loader.getAppendSystemPromptSources().map((source) => source.path),
37
+ };
38
+ }