pi-subagents-lite 1.2.0 → 1.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/README.md +184 -225
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
- package/src/agents/agent-status.ts +50 -0
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +61 -223
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -271
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-status.ts — AgentStatus tool implementation.
|
|
3
|
+
*
|
|
4
|
+
* A lightweight informational tool that lists all agents (running, queued,
|
|
5
|
+
* completed, stopped, error) from the manager and returns a clear message
|
|
6
|
+
* about not polling for status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { AgentRecord } from "../types.js";
|
|
11
|
+
import { SHORT_ID_LENGTH } from "../types.js";
|
|
12
|
+
import { getManager } from "../shell.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format a single agent record as "type·short_id·status".
|
|
16
|
+
*/
|
|
17
|
+
function formatAgent(record: AgentRecord): string {
|
|
18
|
+
const shortId = record.id.slice(0, SHORT_ID_LENGTH);
|
|
19
|
+
return `${record.display.type}·${shortId}·${record.lifecycle.status}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute the AgentStatus tool.
|
|
24
|
+
*
|
|
25
|
+
* Returns a formatted list of all agents with their type, short ID, and status,
|
|
26
|
+
* followed by a nudge message telling the model not to poll.
|
|
27
|
+
*/
|
|
28
|
+
export async function executeAgentStatusTool(
|
|
29
|
+
_toolCallId: string,
|
|
30
|
+
_params: Record<string, unknown>,
|
|
31
|
+
_signal: AbortSignal | undefined,
|
|
32
|
+
_onUpdate: ((update: any) => void) | undefined,
|
|
33
|
+
_ctx: ExtensionContext,
|
|
34
|
+
): Promise<any> {
|
|
35
|
+
const manager = getManager()!;
|
|
36
|
+
const agents = manager.listAgents();
|
|
37
|
+
|
|
38
|
+
const nudge = "Don't poll — you'll receive notifications when agents complete.";
|
|
39
|
+
|
|
40
|
+
if (agents.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `No agents running or completed.\n\n${nudge}` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatted = agents.map(formatAgent).join(", ");
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: "text", text: `${formatted}\n\n${nudge}` }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-types.ts — Unified agent type registry.
|
|
3
|
+
*
|
|
4
|
+
* Merges embedded default agents with user-defined agents from .pi/agents/*.md.
|
|
5
|
+
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
|
|
9
|
+
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
10
|
+
import type { AgentConfig } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* All tool names that Pi can provide to a session.
|
|
14
|
+
*
|
|
15
|
+
* Note: only `read`, `bash`, `edit`, `write` are active by default.
|
|
16
|
+
* `find` and `grep` must be explicitly activated via setActiveToolsByName().
|
|
17
|
+
* `ls` was removed — it's a thin wrapper over bash that adds ~180 tokens/turn
|
|
18
|
+
* with no real benefit.
|
|
19
|
+
*/
|
|
20
|
+
export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find"];
|
|
21
|
+
|
|
22
|
+
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
23
|
+
const agents = new Map<string, AgentConfig>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Directories to scan for agent .md files at startup and on-demand.
|
|
27
|
+
* Set by setAgentScanDirs() during session_start.
|
|
28
|
+
*/
|
|
29
|
+
let userAgentDir = "";
|
|
30
|
+
let projectAgentDir = "";
|
|
31
|
+
|
|
32
|
+
/** Options for registerAgents. */
|
|
33
|
+
export interface RegisterAgentsOptions {
|
|
34
|
+
/** When true, skip built-in DEFAULT_AGENTS. */
|
|
35
|
+
disableDefaultAgents?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register agents into the unified registry.
|
|
40
|
+
* Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
|
|
41
|
+
* When options.disableDefaultAgents is true, DEFAULT_AGENTS are skipped.
|
|
42
|
+
* Hidden agents (hidden === true) are kept in the registry but excluded from spawning.
|
|
43
|
+
*/
|
|
44
|
+
export function registerAgents(userAgents: Map<string, AgentConfig>, options?: RegisterAgentsOptions): void {
|
|
45
|
+
agents.clear();
|
|
46
|
+
|
|
47
|
+
// Start with defaults (unless disabled)
|
|
48
|
+
if (!options?.disableDefaultAgents) {
|
|
49
|
+
for (const [name, config] of DEFAULT_AGENTS) {
|
|
50
|
+
agents.set(name, config);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Overlay user agents (overrides defaults with same name)
|
|
55
|
+
for (const [name, config] of userAgents) {
|
|
56
|
+
agents.set(name, config);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set the agent scan directories for on-demand discovery.
|
|
62
|
+
* Called during session_start alongside scanAndRegisterAgents.
|
|
63
|
+
*/
|
|
64
|
+
export function setAgentScanDirs(userDir: string, projectDir: string): void {
|
|
65
|
+
userAgentDir = userDir;
|
|
66
|
+
projectAgentDir = projectDir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scan the known agent directories and register any newly discovered agents
|
|
71
|
+
* that aren't already in the registry. Returns the number of new agents added.
|
|
72
|
+
*
|
|
73
|
+
* @param worktreeDir - Optional absolute path to a worktree's `.pi/agents/` directory.
|
|
74
|
+
* When set, agents from this directory are also scanned and added to the registry.
|
|
75
|
+
* Worktree-local types use "project" source attribution and follow the same
|
|
76
|
+
* parsing and name-uniqueness rules as the parent's project scan.
|
|
77
|
+
* @param options - Optional settings. disableDefaultAgents skips DEFAULT_AGENTS in the merge.
|
|
78
|
+
*/
|
|
79
|
+
export async function discoverNewAgents(worktreeDir?: string, options?: { disableDefaultAgents?: boolean }): Promise<number> {
|
|
80
|
+
const [userAgents, projectAgents] = await Promise.all([
|
|
81
|
+
scanAgentFilesInDir(userAgentDir, "user"),
|
|
82
|
+
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const defaults = options?.disableDefaultAgents ? new Map<string, AgentConfig>() : DEFAULT_AGENTS;
|
|
86
|
+
const merged = mergeAgents(defaults, userAgents, projectAgents);
|
|
87
|
+
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (const [name, config] of merged) {
|
|
90
|
+
if (!agents.has(name)) {
|
|
91
|
+
agents.set(name, config);
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Scan worktree-local agents (only when worktreeDir is provided)
|
|
97
|
+
if (worktreeDir) {
|
|
98
|
+
const worktreeAgents = await scanAgentFilesInDir(worktreeDir, "project");
|
|
99
|
+
// Use mergeAgents to convert AgentConfigFromMd to AgentConfig (applies fromMd
|
|
100
|
+
// and BASE_DEFAULTS), then add only names not already in the registry.
|
|
101
|
+
const wtMerged = mergeAgents(new Map(), [], worktreeAgents);
|
|
102
|
+
for (const [name, config] of wtMerged) {
|
|
103
|
+
if (!agents.has(name)) {
|
|
104
|
+
agents.set(name, config);
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve a type name case-insensitively. Also matches displayName. Returns the canonical key or undefined. */
|
|
114
|
+
export function resolveType(name: string): string | undefined {
|
|
115
|
+
if (!name) return undefined;
|
|
116
|
+
if (agents.has(name)) return name;
|
|
117
|
+
const lower = name.toLowerCase();
|
|
118
|
+
for (const [key, config] of agents.entries()) {
|
|
119
|
+
if (key.toLowerCase() === lower) return key;
|
|
120
|
+
if ((config.displayName ?? '').toLowerCase() === lower) return key;
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Get the agent config for a type (case-insensitive). */
|
|
126
|
+
export function getAgentConfig(name: string): AgentConfig | undefined {
|
|
127
|
+
const key = resolveType(name);
|
|
128
|
+
return key ? agents.get(key) : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Get all visible type names (for spawning and tool descriptions). */
|
|
132
|
+
export function getAvailableTypes(): string[] {
|
|
133
|
+
return [...agents.entries()]
|
|
134
|
+
.filter(([_, config]) => config.hidden !== true)
|
|
135
|
+
.map(([name]) => name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Get all type names including hidden (for UI listing). */
|
|
139
|
+
export function getAllTypes(): string[] {
|
|
140
|
+
return [...agents.keys()];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Names of tools that subagents must NOT inherit (no sub-subagent policy, ADR 0001). */
|
|
144
|
+
export const EXCLUDED_TOOL_NAMES = ["Agent"];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve tool entries (with ext/* syntax) into concrete tool names.
|
|
148
|
+
* Supports:
|
|
149
|
+
* - bare tool names: "read" → "read"
|
|
150
|
+
* - ext/* syntax: "tavily/*" → all tools from the tavily extension
|
|
151
|
+
* - ext/tool syntax: "tavily/web_search" → "web_search"
|
|
152
|
+
*/
|
|
153
|
+
function resolveToolEntries(
|
|
154
|
+
entries: string[],
|
|
155
|
+
extToolMap: Map<string, string[]> | undefined,
|
|
156
|
+
notify?: (msg: string) => void,
|
|
157
|
+
): Set<string> {
|
|
158
|
+
const resolved = new Set<string>();
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
const slashIdx = entry.indexOf("/");
|
|
162
|
+
if (slashIdx !== -1) {
|
|
163
|
+
// ext/* or ext/tool syntax
|
|
164
|
+
const extName = entry.slice(0, slashIdx);
|
|
165
|
+
const toolPart = entry.slice(slashIdx + 1);
|
|
166
|
+
if (toolPart === "*") {
|
|
167
|
+
const extTools = extToolMap?.get(extName);
|
|
168
|
+
if (extTools && extTools.length > 0) {
|
|
169
|
+
for (const t of extTools) resolved.add(t);
|
|
170
|
+
} else {
|
|
171
|
+
notify?.(`extension "${extName}" is not loaded, "${entry}" will have no effect`);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// ext/tool syntax: e.g. "tavily/web_search"
|
|
175
|
+
resolved.add(toolPart);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
// Bare tool name
|
|
179
|
+
resolved.add(entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return resolved;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the visible tool set for an agent type from its config.
|
|
188
|
+
*
|
|
189
|
+
* Single owner of tool visibility policy. Handles:
|
|
190
|
+
* - `tools: true` → all active tools (minus excluded)
|
|
191
|
+
* - `tools: string[]` → allowlist (minus excluded, with ext/* expansion)
|
|
192
|
+
* - `tools: false` → no tools
|
|
193
|
+
* - `tools: undefined` + `excludeTools` → denylist (minus excluded, with ext/* expansion)
|
|
194
|
+
* - `tools: undefined` → all active tools (minus EXCLUDED_TOOL_NAMES if any are present)
|
|
195
|
+
*
|
|
196
|
+
* `tools` and `excludeTools` are mutually exclusive. If both set, `tools` wins.
|
|
197
|
+
*
|
|
198
|
+
* Returns null when no filtering is needed, otherwise the filtered tool list.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveVisibleTools(opts: {
|
|
201
|
+
activeTools: string[];
|
|
202
|
+
tools?: true | string[] | false;
|
|
203
|
+
excludeTools?: string[];
|
|
204
|
+
extToolMap?: Map<string, string[]>;
|
|
205
|
+
notify?: (msg: string) => void;
|
|
206
|
+
}): string[] | null {
|
|
207
|
+
const { activeTools, tools, excludeTools, extToolMap, notify } = opts;
|
|
208
|
+
|
|
209
|
+
// Blacklist mode: excludeTools set and tools not set as whitelist
|
|
210
|
+
if (excludeTools && !Array.isArray(tools)) {
|
|
211
|
+
const excludeSet = resolveToolEntries(excludeTools, extToolMap, notify);
|
|
212
|
+
const filtered = activeTools.filter(t =>
|
|
213
|
+
!EXCLUDED_TOOL_NAMES.includes(t) && !excludeSet.has(t)
|
|
214
|
+
);
|
|
215
|
+
return filtered.length !== activeTools.length ? filtered : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (Array.isArray(tools)) {
|
|
219
|
+
// Whitelist mode: resolve entries with ext/* expansion
|
|
220
|
+
const allBuiltinSet = new Set(BUILTIN_TOOL_NAMES);
|
|
221
|
+
const allowedTools = resolveToolEntries(tools, extToolMap, notify);
|
|
222
|
+
|
|
223
|
+
// Warn about unknown entries
|
|
224
|
+
for (const entry of tools) {
|
|
225
|
+
const slashIdx = entry.indexOf("/");
|
|
226
|
+
if (slashIdx === -1 && !allBuiltinSet.has(entry)) {
|
|
227
|
+
// Bare name, not a known built-in — check if it's an extension tool
|
|
228
|
+
let foundInExt = false;
|
|
229
|
+
for (const [, extToolNames] of extToolMap ?? []) {
|
|
230
|
+
if (extToolNames.includes(entry)) { foundInExt = true; break; }
|
|
231
|
+
}
|
|
232
|
+
if (!foundInExt) {
|
|
233
|
+
notify?.(`tool "${entry}" not found in any loaded extension`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const visibleSet = new Set<string>();
|
|
239
|
+
for (const t of activeTools) {
|
|
240
|
+
if (EXCLUDED_TOOL_NAMES.includes(t)) continue;
|
|
241
|
+
if (allowedTools.has(t)) {
|
|
242
|
+
visibleSet.add(t);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Warn if a loaded extension has none of its tools in `tools`
|
|
247
|
+
if (extToolMap) {
|
|
248
|
+
for (const [extName, extTools] of extToolMap) {
|
|
249
|
+
const hasAny = extTools.some(t => allowedTools.has(t));
|
|
250
|
+
if (!hasAny) {
|
|
251
|
+
notify?.(`extension "${extName}" is loaded but none of its tools are in tools: [${tools.join(", ")}]`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [...visibleSet];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (tools === false) {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// tools: true or undefined — all tools visible (except excluded)
|
|
264
|
+
const hasExcluded = activeTools.some(t => EXCLUDED_TOOL_NAMES.includes(t));
|
|
265
|
+
if (!hasExcluded) return null;
|
|
266
|
+
return activeTools.filter(t => !EXCLUDED_TOOL_NAMES.includes(t));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Get built-in tool names for a type (case-insensitive). */
|
|
270
|
+
export function getToolNamesForType(type: string): string[] {
|
|
271
|
+
const config = getAgentConfig(type);
|
|
272
|
+
return config?.registeredTools?.length
|
|
273
|
+
? config.registeredTools
|
|
274
|
+
: [...BUILTIN_TOOL_NAMES];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Resolved config shape returned by getConfig. */
|
|
278
|
+
export interface ResolvedAgentConfig {
|
|
279
|
+
displayName: string;
|
|
280
|
+
description: string;
|
|
281
|
+
registeredTools: string[];
|
|
282
|
+
/** Controls tool schema visibility. true = all, string[] = listed, false = none. */
|
|
283
|
+
tools?: true | string[] | false;
|
|
284
|
+
extensions: true | string[] | false;
|
|
285
|
+
skills: true | string[] | false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Apply global implicit defaults to skills/extensions.
|
|
290
|
+
* undefined means "not explicitly set" → resolve from global default.
|
|
291
|
+
* Concrete values (true, false, string[]) pass through unchanged.
|
|
292
|
+
*/
|
|
293
|
+
function applyGlobalDefaults(
|
|
294
|
+
skills: true | string[] | false | undefined,
|
|
295
|
+
extensions: true | string[] | false | undefined,
|
|
296
|
+
loadSkillsImplicitly: boolean,
|
|
297
|
+
loadExtensionsImplicitly: boolean,
|
|
298
|
+
): { skills: true | string[] | false; extensions: true | string[] | false } {
|
|
299
|
+
return {
|
|
300
|
+
skills: skills === undefined ? loadSkillsImplicitly : skills,
|
|
301
|
+
extensions: extensions === undefined ? loadExtensionsImplicitly : extensions,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Get config for a type (case-insensitive). Falls back to general-purpose. */
|
|
306
|
+
export function getConfig(
|
|
307
|
+
type: string,
|
|
308
|
+
loadSkillsImplicitly: boolean = true,
|
|
309
|
+
loadExtensionsImplicitly: boolean = true,
|
|
310
|
+
): ResolvedAgentConfig {
|
|
311
|
+
const resolvedKey = resolveType(type);
|
|
312
|
+
const config = resolvedKey ? agents.get(resolvedKey) : undefined;
|
|
313
|
+
|
|
314
|
+
// If config exists and is not hidden, use it; otherwise fall back to general-purpose
|
|
315
|
+
const activeConfig = config?.hidden !== true
|
|
316
|
+
? config
|
|
317
|
+
: agents.get("general-purpose");
|
|
318
|
+
|
|
319
|
+
if (activeConfig && activeConfig.hidden !== true) {
|
|
320
|
+
const { skills, extensions, ...rest } = activeConfig;
|
|
321
|
+
const defaults = applyGlobalDefaults(skills, extensions, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
322
|
+
return {
|
|
323
|
+
displayName: rest.displayName ?? rest.name,
|
|
324
|
+
description: rest.description,
|
|
325
|
+
registeredTools: rest.registeredTools ?? BUILTIN_TOOL_NAMES,
|
|
326
|
+
tools: rest.tools,
|
|
327
|
+
...defaults,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Absolute fallback — general-purpose was hidden or missing
|
|
332
|
+
const defaults = applyGlobalDefaults(undefined, undefined, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
333
|
+
return {
|
|
334
|
+
displayName: "Agent",
|
|
335
|
+
description: "General-purpose agent for complex, multi-step tasks",
|
|
336
|
+
registeredTools: BUILTIN_TOOL_NAMES,
|
|
337
|
+
...defaults,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -17,8 +17,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
17
17
|
displayName: "Agent",
|
|
18
18
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
19
19
|
// registeredTools omitted — means "all available tools" (resolved at lookup time)
|
|
20
|
-
extensions
|
|
21
|
-
skills: true,
|
|
20
|
+
// extensions and skills intentionally omitted — resolved by global default
|
|
22
21
|
systemPrompt: "",
|
|
23
22
|
isDefault: true,
|
|
24
23
|
},
|
|
@@ -30,9 +29,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
30
29
|
displayName: "Explore",
|
|
31
30
|
description: "Fast codebase exploration agent (read-only)",
|
|
32
31
|
registeredTools: READ_ONLY_TOOLS,
|
|
33
|
-
extensions
|
|
34
|
-
skills: true,
|
|
35
|
-
model: "anthropic/claude-haiku-4-5-20251001",
|
|
32
|
+
// extensions and skills intentionally omitted — resolved by global default,
|
|
36
33
|
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
37
34
|
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
38
35
|
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
|
|
@@ -10,7 +10,7 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { formatTokens } from "./usage.js";
|
|
13
|
-
import { summarizeToolArgs } from "
|
|
13
|
+
import { summarizeToolArgs } from "../ui/format.js";
|
|
14
14
|
|
|
15
15
|
/** Max content length for full tool result display — longer results get a summary line. */
|
|
16
16
|
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
@@ -193,3 +193,70 @@ export function streamToOutputFile(
|
|
|
193
193
|
unsubscribe();
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// AgentOutputLog — lifecycle wrapper for per-agent output streaming
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/** Final stats written to the DONE line at agent completion. */
|
|
202
|
+
export interface OutputFinalStats {
|
|
203
|
+
turnCount: number;
|
|
204
|
+
toolUseCount: number;
|
|
205
|
+
totalTokens: number;
|
|
206
|
+
cost: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Manages a single agent's output log lifecycle: create path → write initial
|
|
211
|
+
* entry → attach session stream → finalize with stats → close.
|
|
212
|
+
*
|
|
213
|
+
* The manager holds one instance per agent. At spawn time the constructor
|
|
214
|
+
* creates the file and writes the [USER] entry. When the session is ready,
|
|
215
|
+
* `attach()` subscribes to streaming events. At completion, `finalize()`
|
|
216
|
+
* flushes remaining messages, writes the [DONE] line, and unsubscribes.
|
|
217
|
+
*/
|
|
218
|
+
export class AgentOutputLog {
|
|
219
|
+
readonly path: string;
|
|
220
|
+
private cleanup?: () => void;
|
|
221
|
+
private statsRef?: OutputFinalStats;
|
|
222
|
+
|
|
223
|
+
constructor(agentId: string, prompt: string, baseDir?: string) {
|
|
224
|
+
this.path = createOutputFilePath(agentId, baseDir);
|
|
225
|
+
writeInitialEntry(this.path, prompt);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Subscribe to session events so messages stream to the output file.
|
|
230
|
+
* Internally passes a mutable stats reference that `finalize()` populates
|
|
231
|
+
* before the DONE line is written.
|
|
232
|
+
*/
|
|
233
|
+
attach(session: AgentSession): void {
|
|
234
|
+
this.statsRef = { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
|
|
235
|
+
this.cleanup = streamToOutputFile(session, this.path, this.statsRef);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Flush remaining messages, write the [DONE] line with final stats,
|
|
240
|
+
* and unsubscribe from session events.
|
|
241
|
+
*
|
|
242
|
+
* Safe to call without a prior `attach()` — writes the DONE line only.
|
|
243
|
+
*/
|
|
244
|
+
finalize(stats: OutputFinalStats): void {
|
|
245
|
+
if (this.cleanup && this.statsRef) {
|
|
246
|
+
// Populate the mutable stats ref so streamToOutputFile's cleanup
|
|
247
|
+
// writes the actual final values to the DONE line.
|
|
248
|
+
this.statsRef.turnCount = stats.turnCount;
|
|
249
|
+
this.statsRef.toolUseCount = stats.toolUseCount;
|
|
250
|
+
this.statsRef.totalTokens = stats.totalTokens;
|
|
251
|
+
this.statsRef.cost = stats.cost;
|
|
252
|
+
this.cleanup();
|
|
253
|
+
this.cleanup = undefined;
|
|
254
|
+
this.statsRef = undefined;
|
|
255
|
+
} else {
|
|
256
|
+
// No attach was called — write DONE directly
|
|
257
|
+
const tokensStr = `${formatTokens(stats.totalTokens)} tokens`;
|
|
258
|
+
const costStr = `$${stats.cost.toFixed(3)}`;
|
|
259
|
+
safeAppend(this.path, `${timestamp()} [DONE] ${stats.turnCount} turns, ${stats.toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|