@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.
Files changed (83) hide show
  1. package/README.md +362 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +224 -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 +1098 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +321 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +109 -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 +770 -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 +179 -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 +67 -0
  77. package/src/work-item-ledger.ts +931 -0
  78. package/src/work-item-persistence.ts +223 -0
  79. package/src/workflow-planning.ts +162 -0
  80. package/src/workflow-tree-identity.ts +289 -0
  81. package/src/workflow-ui.ts +61 -0
  82. package/src/workflow-verification.ts +296 -0
  83. package/src/workspace.ts +69 -12
package/src/params.ts CHANGED
@@ -1,26 +1,124 @@
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(
110
+ Type.String({
111
+ minLength: 1,
112
+ maxLength: 256,
113
+ description:
114
+ "Target task ID for one distinct direct-dependent structured-v2 verifier. The executor gates target acceptance on a current exact-tree receipt.",
115
+ }),
116
+ ),
117
+ cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
118
+ timeoutMs: Type.Optional(TimeoutMs),
119
+ ...TurnLimitFields,
23
120
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
121
+ ...ContractFields,
24
122
  });
25
123
 
26
124
  const ChainItem = Type.Object({
@@ -28,7 +126,9 @@ const ChainItem = Type.Object({
28
126
  task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
29
127
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
30
128
  timeoutMs: Type.Optional(TimeoutMs),
129
+ ...TurnLimitFields,
31
130
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
131
+ ...ContractFields,
32
132
  });
33
133
 
34
134
  const AggregatorItem = Type.Object(
@@ -43,7 +143,9 @@ const AggregatorItem = Type.Object(
43
143
  Type.String({ description: "Working directory for the aggregator process" }),
44
144
  ),
45
145
  timeoutMs: Type.Optional(TimeoutMs),
146
+ ...TurnLimitFields,
46
147
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
148
+ ...ContractFields,
47
149
  },
48
150
  {
49
151
  description:
@@ -51,6 +153,48 @@ const AggregatorItem = Type.Object(
51
153
  },
52
154
  );
53
155
 
156
+ const PanelReviewerItem = Type.Object(
157
+ {
158
+ id: Type.String({ minLength: 1, maxLength: 256 }),
159
+ agent: Type.String({ minLength: 1 }),
160
+ focus: Type.Optional(Type.String({ maxLength: 8 * 1024 })),
161
+ timeoutMs: Type.Optional(TimeoutMs),
162
+ ...TurnLimitFields,
163
+ thinkingLevel: Type.Optional(ThinkingLevelSchema),
164
+ },
165
+ { additionalProperties: false },
166
+ );
167
+
168
+ const PanelSynthesizerItem = Type.Object(
169
+ {
170
+ agent: Type.String({ minLength: 1 }),
171
+ timeoutMs: Type.Optional(TimeoutMs),
172
+ ...TurnLimitFields,
173
+ thinkingLevel: Type.Optional(ThinkingLevelSchema),
174
+ },
175
+ { additionalProperties: false },
176
+ );
177
+
178
+ const PanelItem = Type.Object(
179
+ {
180
+ id: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
181
+ preset: Type.Optional(StringEnum(PANEL_PRESETS, { default: "custom" })),
182
+ task: Type.String({ minLength: 1, maxLength: 50 * 1024 }),
183
+ context: Type.Optional(Type.String({ maxLength: 50 * 1024 })),
184
+ reviewers: Type.Array(PanelReviewerItem, {
185
+ minItems: 2,
186
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
187
+ }),
188
+ synthesizer: PanelSynthesizerItem,
189
+ minValidReviews: Type.Optional(Type.Integer({ minimum: 2 })),
190
+ },
191
+ {
192
+ additionalProperties: false,
193
+ description:
194
+ "One bounded independent review round followed by one evidence-preserving synthesis when enough valid reviews remain.",
195
+ },
196
+ );
197
+
54
198
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
55
199
  description:
56
200
  '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 +207,34 @@ export const SubagentParams = Type.Object({
63
207
  ),
64
208
  task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
65
209
  tasks: Type.Optional(
66
- Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
210
+ Type.Array(TaskItem, {
211
+ description: "Array of {agent, task} for parallel execution",
212
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
213
+ }),
67
214
  ),
68
215
  chain: Type.Optional(
69
216
  Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" }),
70
217
  ),
71
218
  aggregator: Type.Optional(AggregatorItem),
219
+ panel: Type.Optional(PanelItem),
220
+ workflow: Type.Optional(
221
+ Type.Object(
222
+ {
223
+ id: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
224
+ honorAdmission: Type.Optional(
225
+ Type.Boolean({
226
+ description:
227
+ "Opt in to declining workflow tasks whose explicit audit metadata recommends parent-owned work or abstention.",
228
+ }),
229
+ ),
230
+ tasks: Type.Array(WorkflowTaskItem, {
231
+ minItems: 1,
232
+ maxItems: MAX_CONFIGURABLE_PARALLEL_TASKS,
233
+ }),
234
+ },
235
+ { additionalProperties: false },
236
+ ),
237
+ ),
72
238
  agentScope: Type.Optional(AgentScopeSchema),
73
239
  confirmProjectAgents: Type.Optional(
74
240
  Type.Boolean({
@@ -80,7 +246,17 @@ export const SubagentParams = Type.Object({
80
246
  Type.String({ description: "Working directory for the agent process (single mode)" }),
81
247
  ),
82
248
  timeoutMs: Type.Optional(TimeoutMs),
249
+ totalTimeoutMs: Type.Optional(
250
+ Type.Number({
251
+ description:
252
+ "Overall blocking-workflow deadline in milliseconds, including queued work, workflow tasks, panel phases, chain steps, and fan-in.",
253
+ minimum: 1,
254
+ maximum: MAX_SUBAGENT_TIMEOUT_MS,
255
+ }),
256
+ ),
257
+ ...TurnLimitFields,
83
258
  thinkingLevel: Type.Optional(ThinkingLevelSchema),
259
+ ...ContractFields,
84
260
  });
85
261
 
86
262
  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
+ }