@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.
- package/README.md +313 -53
- package/package.json +11 -8
- package/src/adaptive-scheduler.ts +196 -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 +848 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +296 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +78 -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 +772 -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 +172 -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 +17 -0
- package/src/work-item-ledger.ts +682 -0
- package/src/work-item-persistence.ts +218 -0
- package/src/workflow-planning.ts +150 -0
- package/src/workflow-ui.ts +61 -0
- 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 {
|
|
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
|
-
"
|
|
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, {
|
|
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>;
|
package/src/persistence.ts
CHANGED
|
@@ -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 ??
|
|
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
|
|
58
|
-
.filter((agent) => agent.updatedAt >= cutoff && agent.state !== "closed")
|
|
59
|
-
|
|
60
|
-
|
|
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 =
|
|
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: "
|
|
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
|
+
}
|