@esso0428/pi-subagents 0.17.31 → 0.17.33

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.
@@ -28,6 +28,11 @@ const agents = new Map<string, AgentConfig>();
28
28
 
29
29
  /** When true, DEFAULT_AGENTS are skipped during registration. */
30
30
  let disableDefaults = false;
31
+ /** Optional top-level fallback; nested resolution never consults this value. */
32
+ let fallbackSubagent: string | undefined;
33
+ export const NO_FALLBACK = "none";
34
+ export function getFallbackSubagent(): string | undefined { return fallbackSubagent; }
35
+ export function setFallbackSubagent(value: string | undefined): void { fallbackSubagent = value; }
31
36
 
32
37
  /** Check whether default agents are disabled. */
33
38
  export function isDefaultsDisabled(): boolean { return disableDefaults; }
@@ -40,20 +45,18 @@ export function setDefaultsDisabled(b: boolean): void { disableDefaults = b; }
40
45
  * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
41
46
  * Disabled agents (enabled === false) are kept in the registry but excluded from spawning.
42
47
  */
43
- export function registerAgents(userAgents: Map<string, AgentConfig>): void {
44
- agents.clear();
45
-
46
- // Start with defaults (unless disabled via settings)
48
+ export function buildAgentRegistry(userAgents: Map<string, AgentConfig>): Map<string, AgentConfig> {
49
+ const registry = new Map<string, AgentConfig>();
47
50
  if (!disableDefaults) {
48
- for (const [name, config] of DEFAULT_AGENTS) {
49
- agents.set(name, config);
50
- }
51
+ for (const [name, config] of DEFAULT_AGENTS) registry.set(name, config);
51
52
  }
53
+ for (const [name, config] of userAgents) registry.set(name, config);
54
+ return registry;
55
+ }
52
56
 
53
- // Overlay user agents (overrides defaults with same name)
54
- for (const [name, config] of userAgents) {
55
- agents.set(name, config);
56
- }
57
+ export function registerAgents(userAgents: Map<string, AgentConfig>): void {
58
+ agents.clear();
59
+ for (const [name, config] of buildAgentRegistry(userAgents)) agents.set(name, config);
57
60
  }
58
61
 
59
62
  /**
@@ -67,16 +70,86 @@ export function applyNicoOverrides(): void {
67
70
  applyNicoOverridesToMap(agents, overrides, defaultModel);
68
71
  }
69
72
 
70
- /** Case-insensitive key resolution. */
71
- function resolveKey(name: string): string | undefined {
72
- if (agents.has(name)) return name;
73
+ /** Case-insensitive key resolution within a registry. */
74
+ function resolveKeyIn(registry: Map<string, AgentConfig>, name: string): string | undefined {
75
+ if (registry.has(name)) return name;
73
76
  const lower = name.toLowerCase();
74
- for (const key of agents.keys()) {
77
+ for (const key of registry.keys()) {
75
78
  if (key.toLowerCase() === lower) return key;
76
79
  }
77
80
  return undefined;
78
81
  }
79
82
 
83
+ /** Case-insensitive key resolution. */
84
+ function resolveKey(name: string): string | undefined {
85
+ return resolveKeyIn(agents, name);
86
+ }
87
+
88
+ /** Resolve a type name case-insensitively within a private registry. */
89
+ export function resolveTypeIn(registry: Map<string, AgentConfig>, name: string): string | undefined {
90
+ return resolveKeyIn(registry, name);
91
+ }
92
+
93
+ /** Get an agent config from a private registry. */
94
+ export function getAgentConfigIn(registry: Map<string, AgentConfig>, name: string): AgentConfig | undefined {
95
+ const key = resolveKeyIn(registry, name);
96
+ return key ? registry.get(key) : undefined;
97
+ }
98
+
99
+ /** Get all enabled agent types from a private registry. */
100
+ export function getAvailableTypesIn(registry: Map<string, AgentConfig>): string[] {
101
+ return [...registry.entries()]
102
+ .filter(([_, config]) => config.enabled !== false)
103
+ .map(([name]) => name);
104
+ }
105
+
106
+ /** Check whether a type resolves to one enabled registry entry. */
107
+ export function isValidTypeIn(registry: Map<string, AgentConfig>, type: string): boolean {
108
+ return resolveEnabledTypeIn(registry, type) !== undefined;
109
+ }
110
+
111
+ /** Resolve exactly one enabled type without applying the top-level fallback policy. */
112
+ export function resolveEnabledTypeIn(
113
+ registry: Map<string, AgentConfig>,
114
+ requested: unknown,
115
+ ): string | undefined {
116
+ const raw = typeof requested === "string" ? requested.trim() : "";
117
+ if (!raw) return undefined;
118
+ const exact = registry.has(raw) ? raw : undefined;
119
+ if (exact !== undefined) return registry.get(exact)?.enabled === false ? undefined : exact;
120
+ const matches = [...registry.keys()].filter(key => key.toLowerCase() === raw.toLowerCase());
121
+ return matches.length === 1 && registry.get(matches[0])?.enabled !== false ? matches[0] : undefined;
122
+ }
123
+
124
+ export type SpawnTypeResolution =
125
+ | { ok: true; type: string; fellBackFrom?: string }
126
+ | { ok: false; message: string };
127
+
128
+ /** Resolve a top-level caller type, retaining the historical general-purpose fallback. */
129
+ export function resolveSpawnTypeIn(registry: Map<string, AgentConfig>, requested: unknown): SpawnTypeResolution {
130
+ const raw = typeof requested === "string" ? requested.trim() : "";
131
+ const available = () => getAvailableTypesIn(registry).join(", ") || "(none)";
132
+ const key = resolveEnabledTypeIn(registry, raw);
133
+ if (key !== undefined) return { ok: true, type: key };
134
+ const reason = raw ? `Unknown or disabled agent type: "${raw}".` : "No agent type given.";
135
+ const configured = fallbackSubagent?.trim();
136
+ if (configured?.toLowerCase() === NO_FALLBACK) {
137
+ return { ok: false, message: `${reason} Available: ${available()}.` };
138
+ }
139
+ if (configured) {
140
+ const fallback = resolveEnabledTypeIn(registry, configured);
141
+ if (fallback === undefined) {
142
+ return { ok: false, message: `${reason} The configured fallbackSubagent "${configured}" is itself unknown or disabled. Available: ${available()}.` };
143
+ }
144
+ return { ok: true, type: fallback, fellBackFrom: raw };
145
+ }
146
+ return { ok: true, type: "general-purpose", fellBackFrom: raw };
147
+ }
148
+
149
+ export function resolveSpawnType(requested: unknown): SpawnTypeResolution {
150
+ return resolveSpawnTypeIn(agents, requested);
151
+ }
152
+
80
153
  /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
81
154
  export function resolveType(name: string): string | undefined {
82
155
  return resolveKey(name);
@@ -0,0 +1,12 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /** Marks resource loading/session construction performed for a child session. */
4
+ const childSessionContext = new AsyncLocalStorage<boolean>();
5
+
6
+ export function inChildSessionContext(): boolean {
7
+ return childSessionContext.getStore() === true;
8
+ }
9
+
10
+ export function runInChildSessionContext<T>(fn: () => Promise<T>): Promise<T> {
11
+ return childSessionContext.run(true, fn);
12
+ }
@@ -73,6 +73,7 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
73
73
  persistSession: fm.persist_session != null ? fm.persist_session === true : undefined,
74
74
  outputTranscript: fm.output_transcript != null ? fm.output_transcript !== false : undefined,
75
75
  sessionDir: str(fm.session_dir),
76
+ allowedSubagents: parseAllowedSubagents(fm.allowed_subagents),
76
77
  systemPrompt: body.trim(),
77
78
  promptMode: fm.prompt_mode === "append" ? "append" : "replace",
78
79
  inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
@@ -146,6 +147,17 @@ function csvListOptional(val: unknown): string[] | undefined {
146
147
  return parseCsvField(val);
147
148
  }
148
149
 
150
+ /**
151
+ * Parse the nested-delegation allowlist. Omitted/empty/"none"/false disables
152
+ * nested tools; true, "all", and "*" allow every enabled agent type.
153
+ */
154
+ function parseAllowedSubagents(val: unknown): "all" | string[] | undefined {
155
+ if (typeof val === "boolean") return val ? "all" : undefined;
156
+ const items = parseCsvField(val);
157
+ if (!items) return undefined;
158
+ return items.some(item => item === "*" || item.toLowerCase() === "all") ? "all" : items;
159
+ }
160
+
149
161
  /**
150
162
  * Parse a memory scope field.
151
163
  * omitted → undefined; "user"/"project"/"local" → MemoryScope.
package/src/index.ts CHANGED
@@ -20,13 +20,15 @@ import { agentHistoryLocator, createAgentHistoryPath, readAgentHistory, readAgen
20
20
  import { buildAgentStatusMenuEntries, canOpenActiveAgent, canOpenAgentHistory, formatAgentHistoryOption, splitAgentRecords } from "./agent-history-list.js";
21
21
  import { AgentManager } from "./agent-manager.js";
22
22
  import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
23
- import { applyNicoOverrides, BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
23
+ import { applyNicoOverrides, BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveSpawnType, setDefaultsDisabled, setFallbackSubagent } from "./agent-types.js";
24
24
  import { type RpcHandle, registerRpcHandlers } from "./cross-extension-rpc.js";
25
25
  import { loadCustomAgents } from "./custom-agents.js";
26
26
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
27
27
  import { GroupJoinManager } from "./group-join.js";
28
28
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
29
29
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
30
+ import { isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
31
+ import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js";
30
32
  import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
31
33
  import { SubagentScheduler } from "./schedule.js";
32
34
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
@@ -425,6 +427,13 @@ export default function (pi: ExtensionAPI) {
425
427
 
426
428
  // Background completion: route through group join or send individual nudge
427
429
  const manager = new AgentManager((record) => {
430
+ // Nested children report through their owner: they remain visible in the
431
+ // widget but never emit a second top-level event, history entry, or nudge.
432
+ if (record.parentAgentId) {
433
+ widget.markFinished(record.id);
434
+ widget.update();
435
+ return;
436
+ }
428
437
  // Emit lifecycle event based on terminal status
429
438
  const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
430
439
  const eventData = buildEventData(record);
@@ -484,6 +493,12 @@ export default function (pi: ExtensionAPI) {
484
493
  // 'delivered' → group callback already fired
485
494
  widget.update();
486
495
  }, undefined, (record) => {
496
+ // Nested children are already represented under their owner in the widget.
497
+ if (record.parentAgentId) {
498
+ widget.ensureTimer();
499
+ widget.update();
500
+ return;
501
+ }
487
502
  // Emit started event when agent transitions to running (including from queue)
488
503
  pi.events.emit("subagents:started", {
489
504
  id: record.id,
@@ -493,6 +508,7 @@ export default function (pi: ExtensionAPI) {
493
508
  widget.ensureTimer();
494
509
  widget.update();
495
510
  }, (record, info) => {
511
+ if (record.parentAgentId) return;
496
512
  // Emit compacted event when agent's session compacts (preserves count on record).
497
513
  pi.events.emit("subagents:compacted", {
498
514
  id: record.id,
@@ -701,9 +717,6 @@ export default function (pi: ExtensionAPI) {
701
717
  // Off by default; opt-in via `/agents → Settings`. See docstring on
702
718
  // SubagentsSettings.scopeModels for the hard-error vs warn-and-proceed
703
719
  // policy and its rationale.
704
- let scopeModelsEnabled = false;
705
- function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
706
- function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
707
720
 
708
721
  // ---- Disable default agents configuration ----
709
722
  // When enabled, the three hardcoded default agents (general-purpose, Explore,
@@ -823,6 +836,8 @@ export default function (pi: ExtensionAPI) {
823
836
  applyAndEmitLoaded(
824
837
  {
825
838
  setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
839
+ setMaxSubagentDepth,
840
+ setFallbackSubagent,
826
841
  setDefaultMaxTurns,
827
842
  setGraceTurns,
828
843
  setDefaultJoinMode,
@@ -1147,8 +1162,9 @@ Terse command-style prompts produce shallow, generic work.
1147
1162
  reloadCustomAgents();
1148
1163
 
1149
1164
  const rawType = params.subagent_type as SubagentType;
1150
- const resolved = resolveType(rawType);
1151
- const subagentType = resolved ?? "general-purpose";
1165
+ const resolution = resolveSpawnType(rawType);
1166
+ if (!resolution.ok) return textResult(resolution.message);
1167
+ const subagentType = resolution.type;
1152
1168
 
1153
1169
  const displayName = getDisplayName(subagentType);
1154
1170
 
@@ -2336,6 +2352,7 @@ ${systemPrompt}
2336
2352
  function snapshotSettings(): SubagentsSettings {
2337
2353
  return {
2338
2354
  maxConcurrent: manager.getMaxConcurrent(),
2355
+ maxSubagentDepth: getMaxSubagentDepth(),
2339
2356
  // 0 = unlimited — per SubagentsSettings.defaultMaxTurns docstring and
2340
2357
  // normalizeMaxTurns() in agent-runner.ts (which maps 0 → undefined).
2341
2358
  defaultMaxTurns: getDefaultMaxTurns() ?? 0,
@@ -2350,11 +2367,12 @@ ${systemPrompt}
2350
2367
  };
2351
2368
  }
2352
2369
 
2353
- const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
2370
+ const NUMERIC_IDS = new Set(["maxConcurrent", "maxSubagentDepth", "defaultMaxTurns", "graceTurns"]);
2354
2371
 
2355
2372
  async function showSettings(ctx: ExtensionCommandContext) {
2356
2373
  function buildItems(): SettingItem[] {
2357
2374
  const mc = manager.getMaxConcurrent();
2375
+ const depth = getMaxSubagentDepth();
2358
2376
  const dmt = getDefaultMaxTurns() ?? 0;
2359
2377
  const gt = getGraceTurns();
2360
2378
 
@@ -2366,6 +2384,13 @@ ${systemPrompt}
2366
2384
  currentValue: String(mc),
2367
2385
  values: [String(mc)],
2368
2386
  },
2387
+ {
2388
+ id: "maxSubagentDepth",
2389
+ label: "Max nested depth",
2390
+ description: "Maximum nested delegation depth (0 = disabled, Enter to type)",
2391
+ currentValue: String(depth),
2392
+ values: [String(depth)],
2393
+ },
2369
2394
  {
2370
2395
  id: "defaultMaxTurns",
2371
2396
  label: "Default max turns",
@@ -2439,6 +2464,12 @@ ${systemPrompt}
2439
2464
  manager.setMaxConcurrent(n);
2440
2465
  notifyApplied(ctx, `Max concurrency set to ${n}`);
2441
2466
  }
2467
+ } else if (id === "maxSubagentDepth") {
2468
+ const n = parseInt(value, 10);
2469
+ if (n >= 0) {
2470
+ setMaxSubagentDepth(n);
2471
+ notifyApplied(ctx, `Max nested depth set to ${n}`);
2472
+ }
2442
2473
  } else if (id === "defaultMaxTurns") {
2443
2474
  const n = parseInt(value, 10);
2444
2475
  if (n === 0) {
@@ -0,0 +1,35 @@
1
+ /** Shared model-scope policy for top-level and nested spawns. */
2
+
3
+ import { isModelInScope, type ModelRegistryRef, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
4
+
5
+ let scopeModelsEnabled = false;
6
+
7
+ export function isScopeModelsEnabled(): boolean { return scopeModelsEnabled; }
8
+ export function setScopeModelsEnabled(enabled: boolean): void { scopeModelsEnabled = enabled; }
9
+
10
+ export type ModelScopeVerdict =
11
+ | { kind: "ok" }
12
+ | { kind: "error"; message: string }
13
+ | { kind: "warn"; message: string };
14
+
15
+ export function checkModelScope(args: {
16
+ model: { provider: string; id: string } | undefined;
17
+ cwd: string;
18
+ modelRegistry: ModelRegistryRef;
19
+ callerSupplied: boolean;
20
+ agentLabel: string;
21
+ modelInput?: string;
22
+ }): ModelScopeVerdict {
23
+ const { model, cwd, modelRegistry, callerSupplied, agentLabel, modelInput } = args;
24
+ if (!scopeModelsEnabled || !model) return { kind: "ok" };
25
+ const allowed = resolveEnabledModels(readEnabledModels(cwd), modelRegistry, cwd);
26
+ if (!allowed || isModelInScope(model, allowed)) return { kind: "ok" };
27
+ if (callerSupplied) {
28
+ const list = [...allowed].sort().map(item => ` ${item}`).join("\n");
29
+ return { kind: "error", message: `Model not in scope: "${modelInput}".\n\nAllowed models (from enabledModels):\n${list}` };
30
+ }
31
+ return {
32
+ kind: "warn",
33
+ message: `Agent "${agentLabel}" using out-of-scope model "${modelInput ?? `${model.provider}/${model.id}`}"`,
34
+ };
35
+ }