@gotgenes/pi-subagents 6.3.0 → 6.4.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/CHANGELOG.md +23 -0
- package/docs/architecture/architecture.md +234 -263
- package/docs/plans/0108-extract-agent-type-registry.md +322 -0
- package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +36 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +5 -0
- package/src/agent-runner.ts +4 -0
- package/src/agent-types.ts +108 -91
- package/src/index.ts +16 -21
- package/src/session-config.ts +5 -4
- package/src/tools/agent-tool.ts +8 -8
- package/src/tools/get-result-tool.ts +3 -1
- package/src/types.ts +0 -3
- package/src/ui/agent-menu.ts +23 -25
- package/src/ui/agent-widget.ts +10 -9
- package/src/ui/conversation-viewer.ts +4 -2
package/src/agent-types.ts
CHANGED
|
@@ -8,79 +8,132 @@
|
|
|
8
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
9
9
|
import type { AgentConfig } from "./types.js";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
11
|
+
// ── AgentConfigLookup interface ──────────────────────────────────────────────
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Narrow registry interface for consumers that only need config resolution.
|
|
15
|
+
* Prefer this over the full `AgentTypeRegistry` in function signatures (ISP).
|
|
16
|
+
*/
|
|
17
|
+
export interface AgentConfigLookup {
|
|
18
|
+
resolveAgentConfig(type: string): AgentConfig;
|
|
19
|
+
getToolNamesForType(type: string): string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── AgentTypeRegistry class ──────────────────────────────────────────────────
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
25
|
+
* Injectable registry of all agent configurations (defaults + user-defined).
|
|
26
|
+
*
|
|
27
|
+
* Replaces the module-scoped `agents` Map and its companion free functions.
|
|
28
|
+
* The constructor accepts a `loadUserAgents` callback to defer disk I/O to the
|
|
29
|
+
* call site, keeping this class side-effect-free and easy to test.
|
|
21
30
|
*/
|
|
22
|
-
export
|
|
23
|
-
agents
|
|
31
|
+
export class AgentTypeRegistry implements AgentConfigLookup {
|
|
32
|
+
private agents = new Map<string, AgentConfig>();
|
|
24
33
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
/** The three embedded default agent names. */
|
|
35
|
+
static readonly DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
36
|
+
|
|
37
|
+
constructor(private loadUserAgents: () => Map<string, AgentConfig>) {
|
|
38
|
+
this.reload();
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Re-scan user agents and rebuild the registry.
|
|
43
|
+
* Starts with DEFAULT_AGENTS, then overlays whatever `loadUserAgents()` returns.
|
|
44
|
+
*/
|
|
45
|
+
reload(): void {
|
|
46
|
+
this.agents.clear();
|
|
47
|
+
for (const [name, config] of DEFAULT_AGENTS) {
|
|
48
|
+
this.agents.set(name, config);
|
|
49
|
+
}
|
|
50
|
+
for (const [name, config] of this.loadUserAgents()) {
|
|
51
|
+
this.agents.set(name, config);
|
|
52
|
+
}
|
|
33
53
|
}
|
|
34
|
-
}
|
|
35
54
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const lower = name.toLowerCase();
|
|
40
|
-
for (const key of agents.keys()) {
|
|
41
|
-
if (key.toLowerCase() === lower) return key;
|
|
55
|
+
/** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
|
|
56
|
+
resolveType(name: string): string | undefined {
|
|
57
|
+
return this.resolveKey(name);
|
|
42
58
|
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
59
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
/** Get all enabled type names (for spawning and tool descriptions). */
|
|
61
|
+
getAvailableTypes(): string[] {
|
|
62
|
+
return [...this.agents.entries()]
|
|
63
|
+
.filter(([_, config]) => config.enabled !== false)
|
|
64
|
+
.map(([name]) => name);
|
|
65
|
+
}
|
|
50
66
|
|
|
51
|
-
/** Get all
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.map(([name]) => name);
|
|
56
|
-
}
|
|
67
|
+
/** Get all type names including disabled (for UI listing). */
|
|
68
|
+
getAllTypes(): string[] {
|
|
69
|
+
return [...this.agents.keys()];
|
|
70
|
+
}
|
|
57
71
|
|
|
58
|
-
/** Get
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
/** Get names of default agents currently in the registry. */
|
|
73
|
+
getDefaultAgentNames(): string[] {
|
|
74
|
+
return [...this.agents.entries()]
|
|
75
|
+
.filter(([_, config]) => config.isDefault === true)
|
|
76
|
+
.map(([name]) => name);
|
|
77
|
+
}
|
|
62
78
|
|
|
63
|
-
/** Get names of
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
79
|
+
/** Get names of user-defined agents (non-defaults) currently in the registry. */
|
|
80
|
+
getUserAgentNames(): string[] {
|
|
81
|
+
return [...this.agents.entries()]
|
|
82
|
+
.filter(([_, config]) => config.isDefault !== true)
|
|
83
|
+
.map(([name]) => name);
|
|
84
|
+
}
|
|
69
85
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
}
|
|
86
|
+
/** Check if a type is valid and enabled (case-insensitive). */
|
|
87
|
+
isValidType(type: string): boolean {
|
|
88
|
+
const key = this.resolveKey(type);
|
|
89
|
+
if (!key) return false;
|
|
90
|
+
return this.agents.get(key)?.enabled !== false;
|
|
91
|
+
}
|
|
76
92
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
94
|
+
getToolNamesForType(type: string): string[] {
|
|
95
|
+
const key = this.resolveKey(type);
|
|
96
|
+
const raw = key ? this.agents.get(key) : undefined;
|
|
97
|
+
const config = raw?.enabled !== false ? raw : undefined;
|
|
98
|
+
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
99
|
+
return names;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
103
|
+
resolveAgentConfig(type: string): AgentConfig {
|
|
104
|
+
const key = this.resolveKey(type);
|
|
105
|
+
const config = key ? this.agents.get(key) : undefined;
|
|
106
|
+
if (config) return config;
|
|
107
|
+
|
|
108
|
+
const gp = this.agents.get("general-purpose");
|
|
109
|
+
if (gp) return gp;
|
|
110
|
+
|
|
111
|
+
// Absolute fallback (should never happen in practice)
|
|
112
|
+
return {
|
|
113
|
+
name: type,
|
|
114
|
+
displayName: "Agent",
|
|
115
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
116
|
+
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
117
|
+
extensions: true,
|
|
118
|
+
skills: true,
|
|
119
|
+
systemPrompt: "",
|
|
120
|
+
promptMode: "append",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private resolveKey(name: string): string | undefined {
|
|
125
|
+
if (this.agents.has(name)) return name;
|
|
126
|
+
const lower = name.toLowerCase();
|
|
127
|
+
for (const key of this.agents.keys()) {
|
|
128
|
+
if (key.toLowerCase() === lower) return key;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
82
132
|
}
|
|
83
133
|
|
|
134
|
+
/** All known built-in tool names. */
|
|
135
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
136
|
+
|
|
84
137
|
/** Tool names required for memory management. */
|
|
85
138
|
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
86
139
|
|
|
@@ -100,39 +153,3 @@ const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
|
100
153
|
export function getReadOnlyMemoryToolNames(existingToolNames: Set<string>): string[] {
|
|
101
154
|
return READONLY_MEMORY_TOOL_NAMES.filter(n => !existingToolNames.has(n));
|
|
102
155
|
}
|
|
103
|
-
|
|
104
|
-
/** Get built-in tool names for a type (case-insensitive). */
|
|
105
|
-
export function getToolNamesForType(type: string): string[] {
|
|
106
|
-
const key = resolveKey(type);
|
|
107
|
-
const raw = key ? agents.get(key) : undefined;
|
|
108
|
-
const config = raw?.enabled !== false ? raw : undefined;
|
|
109
|
-
const names = config?.builtinToolNames?.length ? config.builtinToolNames : [...BUILTIN_TOOL_NAMES];
|
|
110
|
-
return names;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Resolve agent config with guaranteed non-null return. Falls back: unknown → general-purpose → absolute fallback. */
|
|
114
|
-
export function resolveAgentConfig(type: string): AgentConfig {
|
|
115
|
-
const key = resolveKey(type);
|
|
116
|
-
const config = key ? agents.get(key) : undefined;
|
|
117
|
-
if (config) {
|
|
118
|
-
return config;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Fallback to general-purpose for unknown types
|
|
122
|
-
const gp = agents.get("general-purpose");
|
|
123
|
-
if (gp) {
|
|
124
|
-
return gp;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Absolute fallback (should never happen in practice)
|
|
128
|
-
return {
|
|
129
|
-
name: type,
|
|
130
|
-
displayName: "Agent",
|
|
131
|
-
description: "General-purpose agent for complex, multi-step tasks",
|
|
132
|
-
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
133
|
-
extensions: true,
|
|
134
|
-
skills: true,
|
|
135
|
-
systemPrompt: "",
|
|
136
|
-
promptMode: "append",
|
|
137
|
-
};
|
|
138
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { AgentManager } from "./agent-manager.js";
|
|
16
16
|
import { getAgentConversation, normalizeMaxTurns, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
|
|
17
|
-
import {
|
|
17
|
+
import { AgentTypeRegistry } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
|
|
20
20
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
@@ -40,14 +40,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
40
40
|
// ---- Register custom notification renderer ----
|
|
41
41
|
pi.registerMessageRenderer<NotificationDetails>("subagent-notification", createNotificationRenderer());
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
const reloadCustomAgents = () => {
|
|
45
|
-
const userAgents = loadCustomAgents(process.cwd());
|
|
46
|
-
registerAgents(userAgents);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Initial load
|
|
50
|
-
reloadCustomAgents();
|
|
43
|
+
const registry = new AgentTypeRegistry(() => loadCustomAgents(process.cwd()));
|
|
51
44
|
|
|
52
45
|
// ---- Runtime: all mutable extension state in one place ----
|
|
53
46
|
const runtime = createSubagentRuntime();
|
|
@@ -67,6 +60,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
60
|
runner: { run: runAgent, resume: resumeAgent },
|
|
68
61
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
69
62
|
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
63
|
+
registry,
|
|
70
64
|
onComplete: (record) => {
|
|
71
65
|
// Emit lifecycle event based on terminal status
|
|
72
66
|
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
@@ -137,7 +131,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
137
131
|
pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
|
|
138
132
|
|
|
139
133
|
// Live widget: show running agents above editor
|
|
140
|
-
runtime.widget = new AgentWidget(manager, runtime.agentActivity);
|
|
134
|
+
runtime.widget = new AgentWidget(manager, runtime.agentActivity, registry);
|
|
141
135
|
|
|
142
136
|
// Grab UI context from first tool execution + clear lingering widget on new turn
|
|
143
137
|
const toolStart = new ToolStartHandler(runtime);
|
|
@@ -145,17 +139,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
139
|
|
|
146
140
|
/** Build the full type list text dynamically from the unified registry. */
|
|
147
141
|
const buildTypeListText = () => {
|
|
148
|
-
const defaultNames = getDefaultAgentNames();
|
|
149
|
-
const userNames = getUserAgentNames();
|
|
142
|
+
const defaultNames = registry.getDefaultAgentNames();
|
|
143
|
+
const userNames = registry.getUserAgentNames();
|
|
150
144
|
|
|
151
145
|
const defaultDescs = defaultNames.map((name) => {
|
|
152
|
-
const cfg = resolveAgentConfig(name);
|
|
146
|
+
const cfg = registry.resolveAgentConfig(name);
|
|
153
147
|
const modelSuffix = cfg.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
154
148
|
return `- ${name}: ${cfg.description}${modelSuffix}`;
|
|
155
149
|
});
|
|
156
150
|
|
|
157
151
|
const customDescs = userNames.map((name) => {
|
|
158
|
-
const cfg = resolveAgentConfig(name);
|
|
152
|
+
const cfg = registry.resolveAgentConfig(name);
|
|
159
153
|
return `- ${name}: ${cfg.description}`;
|
|
160
154
|
});
|
|
161
155
|
|
|
@@ -201,9 +195,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
201
195
|
},
|
|
202
196
|
agentActivity: runtime.agentActivity,
|
|
203
197
|
emitEvent: (name, data) => pi.events.emit(name, data),
|
|
204
|
-
|
|
198
|
+
registry,
|
|
205
199
|
typeListText,
|
|
206
|
-
availableTypesText: getAvailableTypes().join(", "),
|
|
200
|
+
availableTypesText: registry.getAvailableTypes().join(", "),
|
|
207
201
|
agentDir: getAgentDir(),
|
|
208
202
|
getDefaultMaxTurns: () => runtime.defaultMaxTurns,
|
|
209
203
|
})));
|
|
@@ -214,6 +208,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
214
208
|
getRecord: (id) => manager.getRecord(id),
|
|
215
209
|
cancelNudge: (key) => notifications.cancelNudge(key),
|
|
216
210
|
getConversation: (session) => getAgentConversation(session),
|
|
211
|
+
registry,
|
|
217
212
|
})));
|
|
218
213
|
|
|
219
214
|
// ---- steer_subagent tool ----
|
|
@@ -234,13 +229,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
234
229
|
getMaxConcurrent: () => manager.getMaxConcurrent(),
|
|
235
230
|
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
236
231
|
},
|
|
237
|
-
|
|
232
|
+
registry,
|
|
238
233
|
agentActivity: runtime.agentActivity,
|
|
239
|
-
getModelLabel: (type,
|
|
240
|
-
const cfg = resolveAgentConfig(type);
|
|
234
|
+
getModelLabel: (type, modelRegistry) => {
|
|
235
|
+
const cfg = registry.resolveAgentConfig(type);
|
|
241
236
|
if (!cfg.model) return 'inherit';
|
|
242
|
-
if (
|
|
243
|
-
const resolved = resolveModel(cfg.model,
|
|
237
|
+
if (modelRegistry) {
|
|
238
|
+
const resolved = resolveModel(cfg.model, modelRegistry);
|
|
244
239
|
if (typeof resolved === 'string') return 'inherit';
|
|
245
240
|
}
|
|
246
241
|
return getModelLabelFromConfig(cfg.model);
|
package/src/session-config.ts
CHANGED
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
+
type AgentConfigLookup,
|
|
14
15
|
getMemoryToolNames,
|
|
15
16
|
getReadOnlyMemoryToolNames,
|
|
16
|
-
getToolNamesForType,
|
|
17
|
-
resolveAgentConfig,
|
|
18
17
|
} from "./agent-types.js";
|
|
19
18
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
20
19
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
@@ -140,14 +139,16 @@ function resolveDefaultModel(
|
|
|
140
139
|
* @param ctx Narrow context from the parent session.
|
|
141
140
|
* @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
|
|
142
141
|
* @param env Pre-resolved environment info from `detectEnv()`.
|
|
142
|
+
* @param registry Agent config lookup — provides resolveAgentConfig and getToolNamesForType.
|
|
143
143
|
*/
|
|
144
144
|
export function assembleSessionConfig(
|
|
145
145
|
type: SubagentType,
|
|
146
146
|
ctx: AssemblerContext,
|
|
147
147
|
options: AssemblerOptions,
|
|
148
148
|
env: EnvInfo,
|
|
149
|
+
registry: AgentConfigLookup,
|
|
149
150
|
): SessionConfig {
|
|
150
|
-
const agentConfig = resolveAgentConfig(type);
|
|
151
|
+
const agentConfig = registry.resolveAgentConfig(type);
|
|
151
152
|
|
|
152
153
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
153
154
|
|
|
@@ -166,7 +167,7 @@ export function assembleSessionConfig(
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
let toolNames = getToolNamesForType(type);
|
|
170
|
+
let toolNames = registry.getToolNamesForType(type);
|
|
170
171
|
|
|
171
172
|
// Persistent memory: detect write capability and branch accordingly.
|
|
172
173
|
// Account for disallowedTools — a tool in the base set but on the denylist
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import type { SpawnOptions } from "../agent-manager.js";
|
|
5
5
|
import { normalizeMaxTurns } from "../agent-runner.js";
|
|
6
|
-
import {
|
|
6
|
+
import { AgentTypeRegistry } from "../agent-types.js";
|
|
7
7
|
import { resolveAgentInvocationConfig } from "../invocation-config.js";
|
|
8
8
|
import { resolveInvocationModel } from "../model-resolver.js";
|
|
9
9
|
|
|
@@ -108,7 +108,7 @@ export interface AgentToolDeps {
|
|
|
108
108
|
widget: AgentToolWidget;
|
|
109
109
|
agentActivity: Map<string, AgentActivity>;
|
|
110
110
|
emitEvent: (name: string, data: unknown) => void;
|
|
111
|
-
|
|
111
|
+
registry: AgentTypeRegistry;
|
|
112
112
|
typeListText: string;
|
|
113
113
|
availableTypesText: string;
|
|
114
114
|
agentDir: string;
|
|
@@ -207,7 +207,7 @@ Guidelines:
|
|
|
207
207
|
|
|
208
208
|
renderCall(args: Record<string, unknown>, theme: any) {
|
|
209
209
|
const displayName = args.subagent_type
|
|
210
|
-
? getDisplayName(args.subagent_type as string)
|
|
210
|
+
? getDisplayName(args.subagent_type as string, deps.registry)
|
|
211
211
|
: "Agent";
|
|
212
212
|
const desc = (args.description as string) ?? "";
|
|
213
213
|
return new Text(
|
|
@@ -327,17 +327,17 @@ Guidelines:
|
|
|
327
327
|
deps.widget.setUICtx(ctx.ui as UICtx);
|
|
328
328
|
|
|
329
329
|
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
330
|
-
deps.
|
|
330
|
+
deps.registry.reload();
|
|
331
331
|
|
|
332
332
|
const rawType = params.subagent_type as SubagentType;
|
|
333
|
-
const resolved = resolveType(rawType);
|
|
333
|
+
const resolved = deps.registry.resolveType(rawType);
|
|
334
334
|
const subagentType = resolved ?? "general-purpose";
|
|
335
335
|
const fellBack = resolved === undefined;
|
|
336
336
|
|
|
337
|
-
const displayName = getDisplayName(subagentType);
|
|
337
|
+
const displayName = getDisplayName(subagentType, deps.registry);
|
|
338
338
|
|
|
339
339
|
// Get agent config for invocation resolution
|
|
340
|
-
const customConfig = resolveAgentConfig(subagentType);
|
|
340
|
+
const customConfig = deps.registry.resolveAgentConfig(subagentType);
|
|
341
341
|
|
|
342
342
|
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
343
343
|
|
|
@@ -375,7 +375,7 @@ Guidelines:
|
|
|
375
375
|
runInBackground,
|
|
376
376
|
isolation,
|
|
377
377
|
};
|
|
378
|
-
const modeLabel = getPromptModeLabel(subagentType);
|
|
378
|
+
const modeLabel = getPromptModeLabel(subagentType, deps.registry);
|
|
379
379
|
const { tags: invocationTags } = buildInvocationTags(agentInvocation);
|
|
380
380
|
const agentTags = modeLabel ? [modeLabel, ...invocationTags] : invocationTags;
|
|
381
381
|
const detailBase = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { AgentConfigLookup } from "../agent-types.js";
|
|
3
4
|
import type { AgentRecord } from "../types.js";
|
|
4
5
|
import { formatDuration, getDisplayName } from "../ui/agent-widget.js";
|
|
5
6
|
import { getSessionContextPercent } from "../usage.js";
|
|
@@ -10,6 +11,7 @@ export interface GetResultDeps {
|
|
|
10
11
|
getRecord: (id: string) => AgentRecord | undefined;
|
|
11
12
|
cancelNudge: (key: string) => void;
|
|
12
13
|
getConversation: (session: AgentSession) => string | undefined;
|
|
14
|
+
registry: AgentConfigLookup;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
|
|
@@ -57,7 +59,7 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
57
59
|
await record.promise;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
const displayName = getDisplayName(record.type);
|
|
62
|
+
const displayName = getDisplayName(record.type, deps.registry);
|
|
61
63
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
62
64
|
const tokens = formatLifetimeTokens(record);
|
|
63
65
|
const contextPercent = getSessionContextPercent(record.session);
|
package/src/types.ts
CHANGED
|
@@ -12,9 +12,6 @@ export type { ThinkingLevel };
|
|
|
12
12
|
/** Agent type: any string name (built-in defaults or user-defined). */
|
|
13
13
|
export type SubagentType = string;
|
|
14
14
|
|
|
15
|
-
/** Names of the three embedded default agents. */
|
|
16
|
-
export const DEFAULT_AGENT_NAMES = ["general-purpose", "Explore", "Plan"] as const;
|
|
17
|
-
|
|
18
15
|
/** Memory scope for persistent agent memory. */
|
|
19
16
|
export type MemoryScope = "user" | "project" | "local";
|
|
20
17
|
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -4,10 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { SpawnOptions } from "../agent-manager.js";
|
|
6
6
|
import {
|
|
7
|
+
AgentTypeRegistry,
|
|
7
8
|
BUILTIN_TOOL_NAMES,
|
|
8
|
-
getAllTypes,
|
|
9
|
-
resolveAgentConfig,
|
|
10
|
-
resolveType,
|
|
11
9
|
} from "../agent-types.js";
|
|
12
10
|
import type { ModelRegistry } from "../model-resolver.js";
|
|
13
11
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
@@ -28,7 +26,7 @@ export interface AgentMenuManager {
|
|
|
28
26
|
|
|
29
27
|
export interface AgentMenuDeps {
|
|
30
28
|
manager: AgentMenuManager;
|
|
31
|
-
|
|
29
|
+
registry: AgentTypeRegistry;
|
|
32
30
|
agentActivity: Map<string, AgentActivity>;
|
|
33
31
|
/** Resolve model label for a given agent type + registry. */
|
|
34
32
|
getModelLabel: (type: string, registry?: ModelRegistry) => string;
|
|
@@ -72,8 +70,8 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
async function showAgentsMenu(ctx: ExtensionContext) {
|
|
75
|
-
deps.
|
|
76
|
-
const allNames = getAllTypes();
|
|
73
|
+
deps.registry.reload();
|
|
74
|
+
const allNames = deps.registry.getAllTypes();
|
|
77
75
|
|
|
78
76
|
const options: string[] = [];
|
|
79
77
|
|
|
@@ -126,7 +124,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
async function showAllAgentsList(ctx: ExtensionContext) {
|
|
129
|
-
const allNames = getAllTypes();
|
|
127
|
+
const allNames = deps.registry.getAllTypes();
|
|
130
128
|
if (allNames.length === 0) {
|
|
131
129
|
ctx.ui.notify("No agents.", "info");
|
|
132
130
|
return;
|
|
@@ -141,7 +139,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
141
139
|
};
|
|
142
140
|
|
|
143
141
|
const entries = allNames.map((name) => {
|
|
144
|
-
const cfg = resolveAgentConfig(name);
|
|
142
|
+
const cfg = deps.registry.resolveAgentConfig(name);
|
|
145
143
|
const disabled = cfg.enabled === false;
|
|
146
144
|
const model = deps.getModelLabel(name, ctx.modelRegistry);
|
|
147
145
|
const indicator = sourceIndicator(cfg);
|
|
@@ -152,10 +150,10 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
152
150
|
const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
|
|
153
151
|
|
|
154
152
|
const hasCustom = allNames.some((n) => {
|
|
155
|
-
const c = resolveAgentConfig(n);
|
|
153
|
+
const c = deps.registry.resolveAgentConfig(n);
|
|
156
154
|
return !c.isDefault && c.enabled !== false;
|
|
157
155
|
});
|
|
158
|
-
const hasDisabled = allNames.some((n) => resolveAgentConfig(n).enabled === false);
|
|
156
|
+
const hasDisabled = allNames.some((n) => deps.registry.resolveAgentConfig(n).enabled === false);
|
|
159
157
|
const legendParts: string[] = [];
|
|
160
158
|
if (hasCustom) legendParts.push("• = project ◦ = global");
|
|
161
159
|
if (hasDisabled) legendParts.push("✕ = disabled");
|
|
@@ -173,7 +171,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
173
171
|
.split(" · ")[0]
|
|
174
172
|
.replace(/^[•◦✕\s]+/, "")
|
|
175
173
|
.trim();
|
|
176
|
-
if (resolveType(agentName) != null) {
|
|
174
|
+
if (deps.registry.resolveType(agentName) != null) {
|
|
177
175
|
await showAgentDetail(ctx, agentName);
|
|
178
176
|
await showAllAgentsList(ctx);
|
|
179
177
|
}
|
|
@@ -187,7 +185,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
const options = agents.map((a) => {
|
|
190
|
-
const dn = getDisplayName(a.type);
|
|
188
|
+
const dn = getDisplayName(a.type, deps.registry);
|
|
191
189
|
const dur = formatDuration(a.startedAt, a.completedAt);
|
|
192
190
|
return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
|
|
193
191
|
});
|
|
@@ -220,7 +218,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
220
218
|
|
|
221
219
|
await ctx.ui.custom<undefined>(
|
|
222
220
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
223
|
-
return new ConversationViewer(tui, session, record, activity, theme, done);
|
|
221
|
+
return new ConversationViewer(tui, session, record, activity, theme, done, deps.registry);
|
|
224
222
|
},
|
|
225
223
|
{
|
|
226
224
|
overlay: true,
|
|
@@ -234,11 +232,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
234
232
|
}
|
|
235
233
|
|
|
236
234
|
async function showAgentDetail(ctx: ExtensionContext, name: string) {
|
|
237
|
-
if (resolveType(name) == null) {
|
|
235
|
+
if (deps.registry.resolveType(name) == null) {
|
|
238
236
|
ctx.ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
239
237
|
return;
|
|
240
238
|
}
|
|
241
|
-
const cfg = resolveAgentConfig(name);
|
|
239
|
+
const cfg = deps.registry.resolveAgentConfig(name);
|
|
242
240
|
|
|
243
241
|
const file = findAgentFile(name);
|
|
244
242
|
const isDefault = cfg.isDefault === true;
|
|
@@ -266,7 +264,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
266
264
|
if (edited !== undefined && edited !== content) {
|
|
267
265
|
const { writeFileSync } = await import("node:fs");
|
|
268
266
|
writeFileSync(file.path, edited, "utf-8");
|
|
269
|
-
deps.
|
|
267
|
+
deps.registry.reload();
|
|
270
268
|
ctx.ui.notify(`Updated ${file.path}`, "info");
|
|
271
269
|
}
|
|
272
270
|
} else if (choice === "Delete") {
|
|
@@ -277,7 +275,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
277
275
|
);
|
|
278
276
|
if (confirmed) {
|
|
279
277
|
unlinkSync(file.path);
|
|
280
|
-
deps.
|
|
278
|
+
deps.registry.reload();
|
|
281
279
|
ctx.ui.notify(`Deleted ${file.path}`, "info");
|
|
282
280
|
}
|
|
283
281
|
}
|
|
@@ -288,7 +286,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
288
286
|
);
|
|
289
287
|
if (confirmed) {
|
|
290
288
|
unlinkSync(file.path);
|
|
291
|
-
deps.
|
|
289
|
+
deps.registry.reload();
|
|
292
290
|
ctx.ui.notify(`Restored default ${name}`, "info");
|
|
293
291
|
}
|
|
294
292
|
} else if (choice.startsWith("Eject")) {
|
|
@@ -347,7 +345,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
347
345
|
|
|
348
346
|
const { writeFileSync } = await import("node:fs");
|
|
349
347
|
writeFileSync(targetPath, content, "utf-8");
|
|
350
|
-
deps.
|
|
348
|
+
deps.registry.reload();
|
|
351
349
|
ctx.ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
352
350
|
}
|
|
353
351
|
|
|
@@ -362,7 +360,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
362
360
|
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
363
361
|
const { writeFileSync } = await import("node:fs");
|
|
364
362
|
writeFileSync(file.path, updated, "utf-8");
|
|
365
|
-
deps.
|
|
363
|
+
deps.registry.reload();
|
|
366
364
|
ctx.ui.notify(`Disabled ${name} (${file.path})`, "info");
|
|
367
365
|
return;
|
|
368
366
|
}
|
|
@@ -381,7 +379,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
381
379
|
const targetPath = join(targetDir, `${name}.md`);
|
|
382
380
|
const { writeFileSync } = await import("node:fs");
|
|
383
381
|
writeFileSync(targetPath, "---\nenabled: false\n---\n", "utf-8");
|
|
384
|
-
deps.
|
|
382
|
+
deps.registry.reload();
|
|
385
383
|
ctx.ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
386
384
|
}
|
|
387
385
|
|
|
@@ -395,11 +393,11 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
395
393
|
|
|
396
394
|
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
397
395
|
unlinkSync(file.path);
|
|
398
|
-
deps.
|
|
396
|
+
deps.registry.reload();
|
|
399
397
|
ctx.ui.notify(`Enabled ${name} (removed ${file.path})`, "info");
|
|
400
398
|
} else {
|
|
401
399
|
writeFileSync(file.path, updated, "utf-8");
|
|
402
|
-
deps.
|
|
400
|
+
deps.registry.reload();
|
|
403
401
|
ctx.ui.notify(`Enabled ${name} (${file.path})`, "info");
|
|
404
402
|
}
|
|
405
403
|
}
|
|
@@ -501,7 +499,7 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
501
499
|
return;
|
|
502
500
|
}
|
|
503
501
|
|
|
504
|
-
deps.
|
|
502
|
+
deps.registry.reload();
|
|
505
503
|
|
|
506
504
|
if (existsSync(targetPath)) {
|
|
507
505
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
@@ -604,7 +602,7 @@ ${systemPrompt}
|
|
|
604
602
|
|
|
605
603
|
const { writeFileSync } = await import("node:fs");
|
|
606
604
|
writeFileSync(targetPath, content, "utf-8");
|
|
607
|
-
deps.
|
|
605
|
+
deps.registry.reload();
|
|
608
606
|
ctx.ui.notify(`Created ${targetPath}`, "info");
|
|
609
607
|
}
|
|
610
608
|
|