@narumitw/pi-subagents 0.20.0 → 0.26.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 +94 -50
- package/package.json +1 -1
- package/src/agents.ts +8 -4
- package/src/config-ui.ts +566 -205
- package/src/in-process-transport.ts +15 -10
- package/src/persistence.ts +4 -1
- package/src/registry.ts +33 -15
- package/src/settings.ts +113 -0
- package/src/stateful.ts +398 -254
- package/src/subagents.ts +20 -19
- package/src/subprocess-transport.ts +10 -9
- package/src/orchestration.ts +0 -164
|
@@ -139,6 +139,7 @@ export class InProcessTransport implements SubagentTransport {
|
|
|
139
139
|
const final = latestAssistant(record.session.messages.slice(startingMessageCount));
|
|
140
140
|
const output = truncateUtf8(final.output || record.lastOutput, DEFAULT_MAX_OUTPUT_BYTES);
|
|
141
141
|
const truncated = output.truncated || agent.contextTruncated;
|
|
142
|
+
const policy = inProcessPolicy(agentConfig, agent);
|
|
142
143
|
|
|
143
144
|
switch (settlement.kind) {
|
|
144
145
|
case "completed":
|
|
@@ -148,21 +149,21 @@ export class InProcessTransport implements SubagentTransport {
|
|
|
148
149
|
exitCode: 1,
|
|
149
150
|
truncated,
|
|
150
151
|
error: final.error || "In-process subagent returned an error",
|
|
151
|
-
policy
|
|
152
|
+
policy,
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
if (final.stopReason === "aborted") {
|
|
155
156
|
return {
|
|
156
157
|
...interruptedOutcome(output.text),
|
|
157
158
|
truncated,
|
|
158
|
-
policy
|
|
159
|
+
policy,
|
|
159
160
|
};
|
|
160
161
|
}
|
|
161
162
|
return {
|
|
162
163
|
output: output.text,
|
|
163
164
|
exitCode: 0,
|
|
164
165
|
truncated,
|
|
165
|
-
policy
|
|
166
|
+
policy,
|
|
166
167
|
};
|
|
167
168
|
case "failed":
|
|
168
169
|
return {
|
|
@@ -170,7 +171,7 @@ export class InProcessTransport implements SubagentTransport {
|
|
|
170
171
|
exitCode: 1,
|
|
171
172
|
truncated,
|
|
172
173
|
error: errorMessage(settlement.error),
|
|
173
|
-
policy
|
|
174
|
+
policy,
|
|
174
175
|
};
|
|
175
176
|
case "timeout":
|
|
176
177
|
return {
|
|
@@ -178,13 +179,13 @@ export class InProcessTransport implements SubagentTransport {
|
|
|
178
179
|
exitCode: 124,
|
|
179
180
|
truncated,
|
|
180
181
|
error: `In-process subagent timed out after ${timeoutMs}ms`,
|
|
181
|
-
policy
|
|
182
|
+
policy,
|
|
182
183
|
};
|
|
183
184
|
case "aborted":
|
|
184
185
|
return {
|
|
185
186
|
...interruptedOutcome(output.text),
|
|
186
187
|
truncated,
|
|
187
|
-
policy
|
|
188
|
+
policy,
|
|
188
189
|
};
|
|
189
190
|
}
|
|
190
191
|
}
|
|
@@ -448,6 +449,7 @@ export async function resolveChildModel(options: ChildSessionCreateOptions): Pro
|
|
|
448
449
|
return {
|
|
449
450
|
model,
|
|
450
451
|
thinkingLevel:
|
|
452
|
+
options.agent.thinkingLevel ??
|
|
451
453
|
options.agentConfig.thinkingLevel ??
|
|
452
454
|
modelThinkingLevel ??
|
|
453
455
|
options.parentRuntime.thinkingLevel,
|
|
@@ -622,13 +624,16 @@ function interruptedOutcome(output: string): TurnOutcome {
|
|
|
622
624
|
return { output, exitCode: 130, aborted: true, error: "In-process subagent was aborted" };
|
|
623
625
|
}
|
|
624
626
|
|
|
625
|
-
function inProcessPolicy(
|
|
627
|
+
function inProcessPolicy(
|
|
628
|
+
agentConfig: AgentConfig,
|
|
629
|
+
managedAgent: ManagedAgent,
|
|
630
|
+
): NonNullable<TurnOutcome["policy"]> {
|
|
626
631
|
return {
|
|
627
632
|
inherited: ["modelRegistry", "authentication", "cwdResources"],
|
|
628
633
|
overridden: [
|
|
629
|
-
...(
|
|
630
|
-
...(
|
|
631
|
-
...(
|
|
634
|
+
...(agentConfig.model ? ["model"] : []),
|
|
635
|
+
...(managedAgent.thinkingLevel || agentConfig.thinkingLevel ? ["thinkingLevel"] : []),
|
|
636
|
+
...(agentConfig.tools ? ["tools"] : []),
|
|
632
637
|
],
|
|
633
638
|
unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders", "extensionState"],
|
|
634
639
|
};
|
package/src/persistence.ts
CHANGED
|
@@ -2,6 +2,7 @@ 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 { isThinkingLevel } from "./agents.js";
|
|
5
6
|
import { redactPrivateText } from "./context.js";
|
|
6
7
|
import type { ManagedAgent } from "./registry.js";
|
|
7
8
|
|
|
@@ -165,8 +166,10 @@ function isStoredState(value: unknown): value is StoredState {
|
|
|
165
166
|
typeof record.updatedAt === "number" &&
|
|
166
167
|
Number.isFinite(record.updatedAt) &&
|
|
167
168
|
(record.parentId === undefined || typeof record.parentId === "string") &&
|
|
169
|
+
(record.thinkingLevel === undefined || isThinkingLevel(record.thinkingLevel)) &&
|
|
168
170
|
(record.children === undefined ||
|
|
169
|
-
(Array.isArray(record.children) &&
|
|
171
|
+
(Array.isArray(record.children) &&
|
|
172
|
+
record.children.every((id) => typeof id === "string"))) &&
|
|
170
173
|
Array.isArray(record.history) &&
|
|
171
174
|
record.history.every(isAgentTurn) &&
|
|
172
175
|
(record.mailbox === undefined ||
|
package/src/registry.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { SubagentThinkingLevel } from "./agents.js";
|
|
2
3
|
import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
3
|
-
import {
|
|
4
|
-
type AgentTurnRunner,
|
|
5
|
-
normalizeTransport,
|
|
6
|
-
type SubagentTransport,
|
|
7
|
-
} from "./transport.js";
|
|
4
|
+
import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
|
|
8
5
|
|
|
9
6
|
export type AgentLifecycleState =
|
|
10
7
|
| "starting"
|
|
@@ -46,6 +43,7 @@ export interface ManagedAgent {
|
|
|
46
43
|
updatedAt: number;
|
|
47
44
|
cwd: string;
|
|
48
45
|
agentScope?: "user" | "project" | "both";
|
|
46
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
49
47
|
currentTask?: string;
|
|
50
48
|
history: AgentTurn[];
|
|
51
49
|
error?: string;
|
|
@@ -113,7 +111,11 @@ export class AgentRegistry {
|
|
|
113
111
|
private readonly agents = new Map<string, ManagedAgent>();
|
|
114
112
|
private readonly controllers = new Map<string, AbortController>();
|
|
115
113
|
private readonly running = new Map<string, Promise<ManagedAgent>>();
|
|
116
|
-
private readonly queue: Array<{
|
|
114
|
+
private readonly queue: Array<{
|
|
115
|
+
agent: ManagedAgent;
|
|
116
|
+
task: string;
|
|
117
|
+
resolve: (agent: ManagedAgent) => void;
|
|
118
|
+
}> = [];
|
|
117
119
|
private changeQueue: Promise<void> = Promise.resolve();
|
|
118
120
|
private readonly maxAgents: number;
|
|
119
121
|
private readonly maxActiveTurns: number;
|
|
@@ -128,7 +130,10 @@ export class AgentRegistry {
|
|
|
128
130
|
private readonly transport: SubagentTransport;
|
|
129
131
|
private readonly now: () => number;
|
|
130
132
|
|
|
131
|
-
constructor(
|
|
133
|
+
constructor(
|
|
134
|
+
transport: SubagentTransport | AgentTurnRunner,
|
|
135
|
+
private readonly options: AgentRegistryOptions = {},
|
|
136
|
+
) {
|
|
132
137
|
this.transport = normalizeTransport(transport);
|
|
133
138
|
this.maxAgents = positiveInteger(options.maxAgents ?? 16, "maxAgents");
|
|
134
139
|
this.maxActiveTurns = positiveInteger(options.maxActiveTurns ?? 4, "maxActiveTurns");
|
|
@@ -210,6 +215,7 @@ export class AgentRegistry {
|
|
|
210
215
|
task: string;
|
|
211
216
|
cwd: string;
|
|
212
217
|
agentScope?: "user" | "project" | "both";
|
|
218
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
213
219
|
parentId?: string;
|
|
214
220
|
context?: string;
|
|
215
221
|
contextSourceIds?: string[];
|
|
@@ -250,6 +256,7 @@ export class AgentRegistry {
|
|
|
250
256
|
updatedAt: now,
|
|
251
257
|
cwd: input.cwd,
|
|
252
258
|
agentScope: input.agentScope,
|
|
259
|
+
thinkingLevel: input.thinkingLevel,
|
|
253
260
|
currentTask: task,
|
|
254
261
|
history: [],
|
|
255
262
|
mailbox: [],
|
|
@@ -293,10 +300,12 @@ export class AgentRegistry {
|
|
|
293
300
|
throw new Error("Subagent mailbox deduplication keys cannot exceed 256 characters");
|
|
294
301
|
}
|
|
295
302
|
const recipient = this.require(recipientId);
|
|
296
|
-
if (recipient.state === "closed")
|
|
303
|
+
if (recipient.state === "closed")
|
|
304
|
+
throw new Error(`Cannot message closed agent ${recipient.id}`);
|
|
297
305
|
if (senderId !== "root") {
|
|
298
306
|
const sender = this.require(senderId);
|
|
299
|
-
if (sender.state === "closed")
|
|
307
|
+
if (sender.state === "closed")
|
|
308
|
+
throw new Error(`Closed agent ${sender.id} cannot send messages`);
|
|
300
309
|
if (sender.rootId !== recipient.rootId) {
|
|
301
310
|
throw new Error("Subagent mailbox messages cannot cross agent trees");
|
|
302
311
|
}
|
|
@@ -367,7 +376,8 @@ export class AgentRegistry {
|
|
|
367
376
|
|
|
368
377
|
async interrupt(id: string): Promise<ManagedAgent> {
|
|
369
378
|
const agent = this.require(id);
|
|
370
|
-
if (agent.state !== "running" && agent.state !== "starting")
|
|
379
|
+
if (agent.state !== "running" && agent.state !== "starting")
|
|
380
|
+
throw new Error(`Agent ${id} is not running`);
|
|
371
381
|
if (agent.state === "starting") {
|
|
372
382
|
const index = this.queue.findIndex((entry) => entry.agent.id === id);
|
|
373
383
|
if (index >= 0) {
|
|
@@ -539,7 +549,11 @@ export class AgentRegistry {
|
|
|
539
549
|
}
|
|
540
550
|
}
|
|
541
551
|
|
|
542
|
-
private runQueuedTurn(
|
|
552
|
+
private runQueuedTurn(
|
|
553
|
+
agent: ManagedAgent,
|
|
554
|
+
task: string,
|
|
555
|
+
resolveQueued: (agent: ManagedAgent) => void,
|
|
556
|
+
): void {
|
|
543
557
|
const controller = new AbortController();
|
|
544
558
|
this.controllers.set(agent.id, controller);
|
|
545
559
|
agent.state = "running";
|
|
@@ -549,7 +563,8 @@ export class AgentRegistry {
|
|
|
549
563
|
let completionContent = "";
|
|
550
564
|
let completionOutput = "";
|
|
551
565
|
let completionError: string | undefined;
|
|
552
|
-
void this.transport
|
|
566
|
+
void this.transport
|
|
567
|
+
.runTurn(this.copy(agent), task, controller.signal)
|
|
553
568
|
.then(async (outcome) => {
|
|
554
569
|
const output = truncateUtf8(outcome.output, this.maxTurnOutputBytes).text;
|
|
555
570
|
const error = outcome.error
|
|
@@ -564,7 +579,11 @@ export class AgentRegistry {
|
|
|
564
579
|
truncated: outcome.truncated,
|
|
565
580
|
});
|
|
566
581
|
agent.history = agent.history.slice(-this.maxHistoryTurns);
|
|
567
|
-
agent.state = outcome.aborted
|
|
582
|
+
agent.state = outcome.aborted
|
|
583
|
+
? "interrupted"
|
|
584
|
+
: outcome.exitCode === 0
|
|
585
|
+
? "completed"
|
|
586
|
+
: "failed";
|
|
568
587
|
agent.error = error;
|
|
569
588
|
agent.policy = outcome.policy;
|
|
570
589
|
completionOutput = output;
|
|
@@ -623,8 +642,7 @@ export class AgentRegistry {
|
|
|
623
642
|
): AgentMailboxMessage {
|
|
624
643
|
if (deduplicationKey) {
|
|
625
644
|
const existing = recipient.mailbox.find(
|
|
626
|
-
(message) =>
|
|
627
|
-
message.deduplicationKey === deduplicationKey && message.senderId === senderId,
|
|
645
|
+
(message) => message.deduplicationKey === deduplicationKey && message.senderId === senderId,
|
|
628
646
|
);
|
|
629
647
|
if (existing) return existing;
|
|
630
648
|
}
|
package/src/settings.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import {
|
|
6
6
|
type AgentConfig,
|
|
7
|
+
type CompletionDelivery,
|
|
7
8
|
isThinkingLevel,
|
|
8
9
|
type SubagentAgentConfig,
|
|
9
10
|
type SubagentSettings,
|
|
@@ -88,6 +89,15 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
88
89
|
}
|
|
89
90
|
runtime.transport = value.stateful.transport;
|
|
90
91
|
}
|
|
92
|
+
if (hasOwn(value.stateful, "completionDelivery")) {
|
|
93
|
+
if (
|
|
94
|
+
value.stateful.completionDelivery !== "next-turn" &&
|
|
95
|
+
value.stateful.completionDelivery !== "auto-resume"
|
|
96
|
+
) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
runtime.completionDelivery = value.stateful.completionDelivery;
|
|
100
|
+
}
|
|
91
101
|
for (const key of [
|
|
92
102
|
"maxAgents",
|
|
93
103
|
"maxActiveTurns",
|
|
@@ -121,6 +131,7 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
121
131
|
|
|
122
132
|
const SETTINGS_FILE = "pi-subagents.json";
|
|
123
133
|
const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
|
|
134
|
+
const DEFAULT_COMPLETION_DELIVERY: CompletionDelivery = "next-turn";
|
|
124
135
|
let pendingSettingsNotice: string | undefined;
|
|
125
136
|
|
|
126
137
|
export function readSubagentSettings(): SubagentSettings | undefined {
|
|
@@ -227,6 +238,108 @@ export function consumeSubagentSettingsNotice() {
|
|
|
227
238
|
}
|
|
228
239
|
|
|
229
240
|
export function saveSubagentConfig(settings: SubagentSettings): void {
|
|
241
|
+
writeSettingsObject(settings);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface CompletionDeliverySettingsSnapshot {
|
|
245
|
+
path: string;
|
|
246
|
+
value: CompletionDelivery;
|
|
247
|
+
source: "default" | "user settings";
|
|
248
|
+
error?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function subagentSettingsFilePath(): string {
|
|
252
|
+
return path.join(getAgentDir(), SETTINGS_FILE);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsSnapshot {
|
|
256
|
+
const configPath = subagentSettingsFilePath();
|
|
257
|
+
if (!fs.existsSync(configPath)) {
|
|
258
|
+
return { path: configPath, value: DEFAULT_COMPLETION_DELIVERY, source: "default" };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
262
|
+
const settings = normalizeSubagentSettings(raw);
|
|
263
|
+
if (!settings) throw new Error(`${SETTINGS_FILE} is not a valid settings object`);
|
|
264
|
+
const explicit =
|
|
265
|
+
isPlainObject(raw.stateful) && hasOwn(raw.stateful, "completionDelivery");
|
|
266
|
+
return {
|
|
267
|
+
path: configPath,
|
|
268
|
+
value: settings.stateful?.completionDelivery ?? DEFAULT_COMPLETION_DELIVERY,
|
|
269
|
+
source: explicit ? "user settings" : "default",
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return {
|
|
273
|
+
path: configPath,
|
|
274
|
+
value: DEFAULT_COMPLETION_DELIVERY,
|
|
275
|
+
source: "default",
|
|
276
|
+
error: formatError(error),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function updateCompletionDeliverySetting(value: CompletionDelivery): void {
|
|
282
|
+
const raw = readSettingsObjectForUpdate();
|
|
283
|
+
const stateful = raw.stateful;
|
|
284
|
+
if (stateful !== undefined && !isPlainObject(stateful)) {
|
|
285
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} stateful settings`);
|
|
286
|
+
}
|
|
287
|
+
writeSettingsObject({
|
|
288
|
+
...raw,
|
|
289
|
+
stateful: {
|
|
290
|
+
...(stateful ?? {}),
|
|
291
|
+
completionDelivery: value,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
|
|
297
|
+
const raw = readSettingsObjectForUpdate();
|
|
298
|
+
const rawAgents = raw.agents;
|
|
299
|
+
if (rawAgents !== undefined && !isPlainObject(rawAgents)) {
|
|
300
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} agent settings`);
|
|
301
|
+
}
|
|
302
|
+
const agents = { ...(rawAgents ?? {}) };
|
|
303
|
+
const rawAgent = hasOwn(agents, name) ? agents[name] : undefined;
|
|
304
|
+
if (rawAgent !== undefined && !isPlainObject(rawAgent)) {
|
|
305
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} settings for ${name}`);
|
|
306
|
+
}
|
|
307
|
+
const agent = { ...(rawAgent ?? {}) };
|
|
308
|
+
if (tools === undefined) delete agent.tools;
|
|
309
|
+
else agent.tools = tools;
|
|
310
|
+
if (Object.keys(agent).length > 0) {
|
|
311
|
+
Object.defineProperty(agents, name, {
|
|
312
|
+
value: agent,
|
|
313
|
+
enumerable: true,
|
|
314
|
+
configurable: true,
|
|
315
|
+
writable: true,
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
delete agents[name];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const updated = { ...raw };
|
|
322
|
+
if (Object.keys(agents).length > 0) updated.agents = agents;
|
|
323
|
+
else delete updated.agents;
|
|
324
|
+
writeSettingsObject(updated);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function readSettingsObjectForUpdate(): Record<string, unknown> {
|
|
328
|
+
const configPath = subagentSettingsFilePath();
|
|
329
|
+
if (!fs.existsSync(configPath)) return {};
|
|
330
|
+
let parsed: unknown;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
333
|
+
} catch (error) {
|
|
334
|
+
throw new Error(`Cannot update malformed ${SETTINGS_FILE}: ${formatError(error)}`);
|
|
335
|
+
}
|
|
336
|
+
if (!isPlainObject(parsed) || !normalizeSubagentSettings(parsed)) {
|
|
337
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE}`);
|
|
338
|
+
}
|
|
339
|
+
return parsed;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function writeSettingsObject(settings: object): void {
|
|
230
343
|
const agentDir = getAgentDir();
|
|
231
344
|
fs.mkdirSync(agentDir, { recursive: true });
|
|
232
345
|
const configPath = path.join(agentDir, SETTINGS_FILE);
|