@narumitw/pi-subagents 0.25.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.
@@ -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: inProcessPolicy(agentConfig),
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: inProcessPolicy(agentConfig),
159
+ policy,
159
160
  };
160
161
  }
161
162
  return {
162
163
  output: output.text,
163
164
  exitCode: 0,
164
165
  truncated,
165
- policy: inProcessPolicy(agentConfig),
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: inProcessPolicy(agentConfig),
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: inProcessPolicy(agentConfig),
182
+ policy,
182
183
  };
183
184
  case "aborted":
184
185
  return {
185
186
  ...interruptedOutcome(output.text),
186
187
  truncated,
187
- policy: inProcessPolicy(agentConfig),
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(agent: AgentConfig): NonNullable<TurnOutcome["policy"]> {
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
- ...(agent.model ? ["model"] : []),
630
- ...(agent.thinkingLevel ? ["thinkingLevel"] : []),
631
- ...(agent.tools ? ["tools"] : []),
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
  };
@@ -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) && record.children.every((id) => typeof id === "string"))) &&
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<{ agent: ManagedAgent; task: string; resolve: (agent: ManagedAgent) => void }> = [];
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(transport: SubagentTransport | AgentTurnRunner, private readonly options: AgentRegistryOptions = {}) {
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") throw new Error(`Cannot message closed agent ${recipient.id}`);
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") throw new Error(`Closed agent ${sender.id} cannot send messages`);
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") throw new Error(`Agent ${id} is not running`);
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(agent: ManagedAgent, task: string, resolveQueued: (agent: ManagedAgent) => void): void {
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.runTurn(this.copy(agent), task, controller.signal)
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 ? "interrupted" : outcome.exitCode === 0 ? "completed" : "failed";
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);