@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.
- package/CHANGELOG.md +16 -0
- package/ROADMAP.md +28 -72
- package/package.json +1 -1
- package/src/abortable.ts +14 -0
- package/src/agent-manager.ts +83 -5
- package/src/agent-recovery.ts +7 -0
- package/src/agent-runner.ts +69 -17
- package/src/agent-types.ts +88 -15
- package/src/child-context.ts +12 -0
- package/src/custom-agents.ts +12 -0
- package/src/index.ts +38 -7
- package/src/model-scope.ts +35 -0
- package/src/nested-tools.ts +368 -0
- package/src/settings.ts +18 -0
- package/src/types.ts +22 -0
- package/src/ui/agent-widget.ts +148 -63
- package/src/ui/conversation-viewer.ts +7 -3
package/src/agent-types.ts
CHANGED
|
@@ -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
|
|
44
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
72
|
-
if (
|
|
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
|
|
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
|
+
}
|
package/src/custom-agents.ts
CHANGED
|
@@ -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,
|
|
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
|
|
1151
|
-
|
|
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
|
+
}
|