@agnishc/edb-subagents 0.10.9 → 0.12.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 +19 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +90 -0
- package/src/agent-runner.ts +111 -21
- package/src/custom-agents.ts +23 -1
- package/src/default-agents.ts +6 -0
- package/src/index.ts +244 -12
- package/src/prompts.ts +50 -0
- package/src/types.ts +10 -0
- package/src/ui/agent-widget.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.12.0] - 2026-05-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Add missing `unlinkSync` import
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- Bridge context threading and promptGuidelines on tools
|
|
10
|
+
|
|
3
11
|
## [0.10.9] - 2026-05-18
|
|
4
12
|
|
|
13
|
+
### Added
|
|
14
|
+
- **edb-bridge integration** — injects `<bridge_parent_session>`, `<bridge_agent_id>`, and `<task_store_path>` XML tags into sub-agent system prompts at spawn time
|
|
15
|
+
- **`bridgeContext` option** — new optional field on `SpawnOptions` and `RunOptions`; when present, bridge metadata is embedded into the sub-agent's prompt extras
|
|
16
|
+
- **`bridge:ready` listener** — captures the parent session's broker session ID when edb-bridge connects
|
|
17
|
+
- **`todo:store_path` listener** — captures the active edb-todo task store path for injection into sub-agents
|
|
18
|
+
- **`bridgeContext` threading** — passes bridge context through `manager.spawn()`, `manager.spawnAndWait()`, and the `sharedRunOptions` in `agent-manager.ts`
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `prompts.ts` `PromptExtras` gains optional `bridgeContext` field; when set, a `<bridge_context>` XML block is appended to the sub-agent system prompt
|
|
22
|
+
- `agent-runner.ts` reads `options.bridgeContext` and populates `extras.bridgeContext` before building the agent prompt
|
|
23
|
+
|
|
5
24
|
## [0.10.8] - 2026-05-18
|
|
6
25
|
|
|
7
26
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agnishc/edb-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Pi extension: Claude Code-style autonomous sub-agents with live widget, parallel execution, mid-run steering, and custom agent types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/agent-manager.ts
CHANGED
|
@@ -34,6 +34,8 @@ interface SpawnArgs {
|
|
|
34
34
|
|
|
35
35
|
interface SpawnOptions {
|
|
36
36
|
description: string;
|
|
37
|
+
/** Short memorable name (e.g. 'Quinn'). Shown in agent widget. Falls back to description if not set. */
|
|
38
|
+
agentName?: string;
|
|
37
39
|
model?: Model<any>;
|
|
38
40
|
maxTurns?: number;
|
|
39
41
|
isolated?: boolean;
|
|
@@ -54,6 +56,19 @@ interface SpawnOptions {
|
|
|
54
56
|
invocation?: AgentInvocation;
|
|
55
57
|
/** Parent abort signal — when aborted, the subagent is also stopped. */
|
|
56
58
|
signal?: AbortSignal;
|
|
59
|
+
/**
|
|
60
|
+
* edb-bridge context: injected into sub-agent system prompt so it can communicate back.
|
|
61
|
+
* Set by edb-subagents when bridgeSessionId is available.
|
|
62
|
+
*/
|
|
63
|
+
bridgeContext?: {
|
|
64
|
+
parentSessionId: string;
|
|
65
|
+
agentId: string;
|
|
66
|
+
storePath?: string;
|
|
67
|
+
taskId?: string;
|
|
68
|
+
agentIsSubtask?: boolean;
|
|
69
|
+
};
|
|
70
|
+
/** Called when the agent lifecycle changes: start, complete (success/steered/aborted), or failed (error/stopped). */
|
|
71
|
+
onTaskLifecycle?: (event: "start" | "complete" | "failed") => void;
|
|
57
72
|
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
58
73
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
59
74
|
/** Called on streaming text deltas from the assistant response. */
|
|
@@ -144,6 +159,8 @@ export class AgentManager {
|
|
|
144
159
|
id,
|
|
145
160
|
type,
|
|
146
161
|
description: options.description,
|
|
162
|
+
agentName: options.agentName,
|
|
163
|
+
taskId: options.bridgeContext?.taskId,
|
|
147
164
|
status: options.isBackground ? "queued" : "running",
|
|
148
165
|
toolUses: 0,
|
|
149
166
|
startedAt: Date.now(),
|
|
@@ -194,6 +211,11 @@ export class AgentManager {
|
|
|
194
211
|
record.status = "running";
|
|
195
212
|
record.startedAt = Date.now();
|
|
196
213
|
if (options.isBackground) this.runningBackground++;
|
|
214
|
+
try {
|
|
215
|
+
options.onTaskLifecycle?.("start");
|
|
216
|
+
} catch {
|
|
217
|
+
/* ignore */
|
|
218
|
+
}
|
|
197
219
|
this.onStart?.(record);
|
|
198
220
|
|
|
199
221
|
// Wire parent abort signal to stop the subagent when the parent is interrupted
|
|
@@ -218,6 +240,8 @@ export class AgentManager {
|
|
|
218
240
|
thinkingLevel: options.thinkingLevel,
|
|
219
241
|
cwd: worktreeCwd,
|
|
220
242
|
signal: record.abortController!.signal,
|
|
243
|
+
// Pass through edb-bridge context if provided
|
|
244
|
+
...(options.bridgeContext ? { bridgeContext: options.bridgeContext } : {}),
|
|
221
245
|
onToolActivity: (activity: ToolActivity) => {
|
|
222
246
|
if (activity.type === "end") record.toolUses++;
|
|
223
247
|
options.onToolActivity?.(activity);
|
|
@@ -303,6 +327,13 @@ export class AgentManager {
|
|
|
303
327
|
}
|
|
304
328
|
}
|
|
305
329
|
|
|
330
|
+
// Auto-lifecycle: notify task management regardless of foreground/background
|
|
331
|
+
try {
|
|
332
|
+
options.onTaskLifecycle?.("complete");
|
|
333
|
+
} catch {
|
|
334
|
+
/* ignore */
|
|
335
|
+
}
|
|
336
|
+
|
|
306
337
|
if (options.isBackground) {
|
|
307
338
|
this.runningBackground--;
|
|
308
339
|
try {
|
|
@@ -344,6 +375,13 @@ export class AgentManager {
|
|
|
344
375
|
}
|
|
345
376
|
}
|
|
346
377
|
|
|
378
|
+
// Auto-lifecycle: notify task management on failure
|
|
379
|
+
try {
|
|
380
|
+
options.onTaskLifecycle?.("failed");
|
|
381
|
+
} catch {
|
|
382
|
+
/* ignore */
|
|
383
|
+
}
|
|
384
|
+
|
|
347
385
|
if (options.isBackground) {
|
|
348
386
|
this.runningBackground--;
|
|
349
387
|
this.onComplete?.(record);
|
|
@@ -391,6 +429,58 @@ export class AgentManager {
|
|
|
391
429
|
return record;
|
|
392
430
|
}
|
|
393
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Resume an existing agent session with a new prompt in the background (non-blocking).
|
|
434
|
+
* Returns immediately; the record's promise resolves when done.
|
|
435
|
+
*/
|
|
436
|
+
resumeInBackground(id: string, prompt: string): AgentRecord | undefined {
|
|
437
|
+
const record = this.agents.get(id);
|
|
438
|
+
if (!record?.session) return undefined;
|
|
439
|
+
|
|
440
|
+
record.status = "running";
|
|
441
|
+
record.startedAt = Date.now();
|
|
442
|
+
record.completedAt = undefined;
|
|
443
|
+
record.result = undefined;
|
|
444
|
+
record.error = undefined;
|
|
445
|
+
|
|
446
|
+
record.promise = resumeAgent(record.session, prompt, {
|
|
447
|
+
onToolActivity: (activity) => {
|
|
448
|
+
if (activity.type === "end") record.toolUses++;
|
|
449
|
+
},
|
|
450
|
+
onAssistantUsage: (usage) => {
|
|
451
|
+
addUsage(record.lifetimeUsage, usage);
|
|
452
|
+
},
|
|
453
|
+
onCompaction: (info) => {
|
|
454
|
+
record.compactionCount++;
|
|
455
|
+
this.onCompact?.(record, info);
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
.then((responseText) => {
|
|
459
|
+
record.status = "completed";
|
|
460
|
+
record.result = responseText;
|
|
461
|
+
record.completedAt = Date.now();
|
|
462
|
+
try {
|
|
463
|
+
this.onComplete?.(record);
|
|
464
|
+
} catch {
|
|
465
|
+
/* ignore */
|
|
466
|
+
}
|
|
467
|
+
return responseText;
|
|
468
|
+
})
|
|
469
|
+
.catch((err) => {
|
|
470
|
+
record.status = "error";
|
|
471
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
472
|
+
record.completedAt = Date.now();
|
|
473
|
+
try {
|
|
474
|
+
this.onComplete?.(record);
|
|
475
|
+
} catch {
|
|
476
|
+
/* ignore */
|
|
477
|
+
}
|
|
478
|
+
return "";
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return record;
|
|
482
|
+
}
|
|
483
|
+
|
|
394
484
|
/**
|
|
395
485
|
* Resume an existing agent session with a new prompt.
|
|
396
486
|
*/
|
package/src/agent-runner.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
5
6
|
import type { Model } from "@earendil-works/pi-ai";
|
|
6
7
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import {
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
SettingsManager,
|
|
16
17
|
} from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import {
|
|
19
|
+
BUILTIN_TOOL_NAMES,
|
|
18
20
|
getAgentConfig,
|
|
19
21
|
getConfig,
|
|
20
22
|
getMemoryToolNames,
|
|
@@ -30,7 +32,40 @@ import { preloadSkills } from "./skill-loader.js";
|
|
|
30
32
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
31
33
|
|
|
32
34
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
33
|
-
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
35
|
+
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent", "send_to_agent"];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract parent session extension file paths from the parent's tool registry.
|
|
39
|
+
* These are passed to the sub-agent's DefaultResourceLoader as additionalExtensionPaths,
|
|
40
|
+
* so sub-agents get access to the same extension tools as the parent session
|
|
41
|
+
* (e.g., TaskCreate, TaskUpdate from edb-todo).
|
|
42
|
+
*
|
|
43
|
+
* Excludes:
|
|
44
|
+
* - Built-in / SDK tools (paths inside node_modules)
|
|
45
|
+
* - Synthetic paths (non-existent on disk)
|
|
46
|
+
* - Extensions where ALL registered tools are in the EXCLUDED_TOOL_NAMES list
|
|
47
|
+
* (i.e., edb-subagents itself — only has Agent, get_subagent_result, steer_subagent)
|
|
48
|
+
*/
|
|
49
|
+
function extractParentExtensionPaths(pi: ExtensionAPI): string[] {
|
|
50
|
+
const excluded = new Set(EXCLUDED_TOOL_NAMES);
|
|
51
|
+
const pathTools = new Map<string, string[]>();
|
|
52
|
+
|
|
53
|
+
for (const tool of pi.getAllTools()) {
|
|
54
|
+
const p = tool.sourceInfo?.path ?? "";
|
|
55
|
+
if (!p) continue;
|
|
56
|
+
if (!pathTools.has(p)) pathTools.set(p, []);
|
|
57
|
+
pathTools.get(p)!.push(tool.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result: string[] = [];
|
|
61
|
+
for (const [p, toolNames] of pathTools) {
|
|
62
|
+
if (p.includes("node_modules")) continue;
|
|
63
|
+
if (!existsSync(p)) continue;
|
|
64
|
+
if (toolNames.every((name) => excluded.has(name))) continue;
|
|
65
|
+
result.push(p);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
34
69
|
|
|
35
70
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
36
71
|
let defaultMaxTurns: number | undefined;
|
|
@@ -109,6 +144,17 @@ export interface RunOptions {
|
|
|
109
144
|
thinkingLevel?: ThinkingLevel;
|
|
110
145
|
/** Override working directory (e.g. for worktree isolation). */
|
|
111
146
|
cwd?: string;
|
|
147
|
+
/**
|
|
148
|
+
* edb-bridge context: injected into the sub-agent's system prompt so edb-bridge + edb-todo
|
|
149
|
+
* extensions in the sub-agent session can connect back to the orchestrator.
|
|
150
|
+
*/
|
|
151
|
+
bridgeContext?: {
|
|
152
|
+
parentSessionId: string;
|
|
153
|
+
agentId: string;
|
|
154
|
+
storePath?: string;
|
|
155
|
+
taskId?: string;
|
|
156
|
+
agentIsSubtask?: boolean;
|
|
157
|
+
};
|
|
112
158
|
/** Called on tool start/end with activity info. */
|
|
113
159
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
114
160
|
/** Called on streaming text deltas from the assistant response. */
|
|
@@ -197,10 +243,21 @@ export async function runAgent(
|
|
|
197
243
|
// Build prompt extras (memory, skill preloading)
|
|
198
244
|
const extras: PromptExtras = {};
|
|
199
245
|
|
|
246
|
+
// Inject edb-bridge context if provided (enables ask_supervisor, notify_parent, shared task store)
|
|
247
|
+
if (options.bridgeContext) {
|
|
248
|
+
extras.bridgeContext = options.bridgeContext;
|
|
249
|
+
}
|
|
250
|
+
|
|
200
251
|
// Resolve extensions/skills: isolated overrides to false
|
|
201
252
|
const extensions = options.isolated ? false : config.extensions;
|
|
202
253
|
const skills = options.isolated ? false : config.skills;
|
|
203
254
|
|
|
255
|
+
// Extract parent session extension paths so sub-agents inherit the same extension
|
|
256
|
+
// tools (e.g. TaskCreate/TaskUpdate from edb-todo). Without this, sub-agents
|
|
257
|
+
// running in a session started with `-ne -e <path>` would have no extension tools
|
|
258
|
+
// because the DefaultResourceLoader doesn't know about the parent's explicit -e paths.
|
|
259
|
+
const parentExtensionPaths = extensions !== false ? extractParentExtensionPaths(options.pi) : [];
|
|
260
|
+
|
|
204
261
|
// Skill preloading: when skills is string[], preload their content into prompt
|
|
205
262
|
if (Array.isArray(skills)) {
|
|
206
263
|
const loaded = preloadSkills(skills, effectiveCwd);
|
|
@@ -259,7 +316,12 @@ export async function runAgent(
|
|
|
259
316
|
const loader = new DefaultResourceLoader({
|
|
260
317
|
cwd: effectiveCwd,
|
|
261
318
|
agentDir,
|
|
262
|
-
|
|
319
|
+
// When we have explicit parent extension paths, suppress standard discovery so
|
|
320
|
+
// sub-agents don't accidentally pick up globally installed extensions the parent
|
|
321
|
+
// didn't opt into (e.g. when parent was started with -ne). Standard discovery
|
|
322
|
+
// is kept when there are no explicit paths (parent has no extensions).
|
|
323
|
+
noExtensions: extensions === false || parentExtensionPaths.length > 0,
|
|
324
|
+
additionalExtensionPaths: parentExtensionPaths,
|
|
263
325
|
noSkills,
|
|
264
326
|
noPromptTemplates: true,
|
|
265
327
|
noThemes: true,
|
|
@@ -282,7 +344,13 @@ export async function runAgent(
|
|
|
282
344
|
settingsManager: SettingsManager.create(effectiveCwd, agentDir),
|
|
283
345
|
modelRegistry: ctx.modelRegistry,
|
|
284
346
|
model,
|
|
285
|
-
tools: toolNames
|
|
347
|
+
// Do NOT pass tools: [...] or noTools here. Passing tools: toolNames creates a
|
|
348
|
+
// hard allowedToolNames restriction that permanently blocks extension tools from
|
|
349
|
+
// the registry even after bindExtensions() registers them (setActiveToolsByName
|
|
350
|
+
// silently ignores names not in the registry). Using noTools: "all" empties the
|
|
351
|
+
// registry entirely. Instead, let the session use its default (full registry:
|
|
352
|
+
// all built-ins + all extension tools from the loader), then restrict via
|
|
353
|
+
// setActiveToolsByName after bindExtensions().
|
|
286
354
|
resourceLoader: loader,
|
|
287
355
|
};
|
|
288
356
|
if (thinkingLevel) {
|
|
@@ -294,21 +362,56 @@ export async function runAgent(
|
|
|
294
362
|
const baseSessionName = agentConfig?.name ?? type;
|
|
295
363
|
session.setSessionName(options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName);
|
|
296
364
|
|
|
365
|
+
// Bind extensions FIRST so that extension tools (e.g. TaskCreate from edb-todo)
|
|
366
|
+
// are registered before we query the active tool set. This ensures extension
|
|
367
|
+
// tools are available for filtering (vs. being invisible at query time).
|
|
368
|
+
await session.bindExtensions({
|
|
369
|
+
onError: (err) => {
|
|
370
|
+
options.onToolActivity?.({
|
|
371
|
+
type: "end",
|
|
372
|
+
toolName: `extension-error:${err.extensionPath}`,
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
297
377
|
// Build disallowed tools set from agent config
|
|
298
378
|
const disallowedSet = agentConfig?.disallowedTools ? new Set(agentConfig.disallowedTools) : undefined;
|
|
299
379
|
|
|
300
380
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
301
|
-
// apply extension allowlist if specified, and apply disallowedTools denylist
|
|
381
|
+
// apply extension allowlist if specified, and apply disallowedTools denylist.
|
|
382
|
+
//
|
|
383
|
+
// SOURCE: Use extensionRunner.getAllRegisteredTools() for extension tools — this
|
|
384
|
+
// bypasses any session-level allowlist restriction and gives us ALL tools registered
|
|
385
|
+
// by extensions. Combined with BUILTIN_TOOL_NAMES we get a complete picture.
|
|
386
|
+
//
|
|
387
|
+
// createAgentSession is called without tools/noTools so the session uses its default
|
|
388
|
+
// (full registry, all built-ins + extension tools from the loader, no allowlist).
|
|
389
|
+
// setActiveToolsByName then restricts to exactly what this agent type should have.
|
|
390
|
+
//
|
|
391
|
+
// Built-in tools (bash, read, edit, write, grep, find, ls) are gated by the
|
|
392
|
+
// per-agent builtinToolNames config. Extension tools (TaskCreate, ask_supervisor…)
|
|
393
|
+
// are gated by the extensions config (true = all, string[] = allowlist, false = none).
|
|
302
394
|
if (extensions !== false) {
|
|
303
395
|
const builtinToolNameSet = new Set(toolNames);
|
|
304
|
-
const
|
|
396
|
+
const allBuiltins = new Set(BUILTIN_TOOL_NAMES);
|
|
397
|
+
// Get all extension tools from the extension runner (not filtered by any allowlist)
|
|
398
|
+
const registeredExtensionToolNames = session.extensionRunner
|
|
399
|
+
.getAllRegisteredTools()
|
|
400
|
+
.map((t) => t.definition.name);
|
|
401
|
+
// Combine: built-in tools + all registered extension tools
|
|
402
|
+
const allKnown = [...new Set([...BUILTIN_TOOL_NAMES, ...registeredExtensionToolNames])];
|
|
403
|
+
const activeTools = allKnown.filter((t) => {
|
|
305
404
|
if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
|
|
306
|
-
|
|
307
|
-
if (
|
|
405
|
+
// Built-in tools: respect the per-agent builtinToolNames allowlist
|
|
406
|
+
if (allBuiltins.has(t)) return builtinToolNameSet.has(t);
|
|
407
|
+
// Extension tools: whitelist (allowed_tools) wins over blacklist (disallowed_tools)
|
|
308
408
|
if (Array.isArray(extensions)) {
|
|
409
|
+
// Whitelist mode — disallowedTools is ignored
|
|
309
410
|
return extensions.some((ext) => t.startsWith(ext) || t.includes(ext));
|
|
310
411
|
}
|
|
311
|
-
|
|
412
|
+
// Blacklist mode — exclude disallowed tools
|
|
413
|
+
if (disallowedSet?.has(t)) return false;
|
|
414
|
+
return true; // extensions === true → include all extension tools
|
|
312
415
|
});
|
|
313
416
|
session.setActiveToolsByName(activeTools);
|
|
314
417
|
} else if (disallowedSet) {
|
|
@@ -317,19 +420,6 @@ export async function runAgent(
|
|
|
317
420
|
session.setActiveToolsByName(activeTools);
|
|
318
421
|
}
|
|
319
422
|
|
|
320
|
-
// Bind extensions so that session_start fires and extensions can initialize
|
|
321
|
-
// (e.g. loading credentials, setting up state). Placed after tool filtering
|
|
322
|
-
// so extension-provided skills/prompts from extendResourcesFromExtensions()
|
|
323
|
-
// respect the active tool set. All ExtensionBindings fields are optional.
|
|
324
|
-
await session.bindExtensions({
|
|
325
|
-
onError: (err) => {
|
|
326
|
-
options.onToolActivity?.({
|
|
327
|
-
type: "end",
|
|
328
|
-
toolName: `extension-error:${err.extensionPath}`,
|
|
329
|
-
});
|
|
330
|
-
},
|
|
331
|
-
});
|
|
332
|
-
|
|
333
423
|
options.onSessionCreated?.(session);
|
|
334
424
|
|
|
335
425
|
// Track turns for graceful max_turns enforcement
|
package/src/custom-agents.ts
CHANGED
|
@@ -54,9 +54,12 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
54
54
|
name,
|
|
55
55
|
displayName: str(fm.display_name),
|
|
56
56
|
description: str(fm.description) ?? name,
|
|
57
|
+
context: str(fm.context),
|
|
57
58
|
builtinToolNames: csvList(fm.tools, BUILTIN_TOOL_NAMES),
|
|
58
59
|
disallowedTools: csvListOptional(fm.disallowed_tools),
|
|
59
|
-
|
|
60
|
+
// allowed_tools: whitelist for extension tools (wins over disallowed_tools when both set)
|
|
61
|
+
// extensions: legacy field — string[] format is treated as allowed_tools whitelist
|
|
62
|
+
extensions: parseExtensions(fm.allowed_tools ?? fm.extensions),
|
|
60
63
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
61
64
|
model: str(fm.model),
|
|
62
65
|
fallbackModels: csvListOptional(fm.fallback_models),
|
|
@@ -138,3 +141,22 @@ function inheritField(val: unknown): true | string[] | false {
|
|
|
138
141
|
const items = csvList(val, []);
|
|
139
142
|
return items.length > 0 ? items : false;
|
|
140
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Parse the extensions/allowed_tools field.
|
|
147
|
+
* - undefined/null/true → all extension tools (true)
|
|
148
|
+
* - false/"none" → no extension tools (false)
|
|
149
|
+
* - "TaskCreate, ask_supervisor" → whitelist of tool name patterns (string[])
|
|
150
|
+
* - absolute path (legacy executor.md style) → treat as true (path-based extensions
|
|
151
|
+
* are handled by extractParentExtensionPaths; ignore the path here)
|
|
152
|
+
*/
|
|
153
|
+
function parseExtensions(val: unknown): true | string[] | false {
|
|
154
|
+
if (val === undefined || val === null || val === true) return true;
|
|
155
|
+
if (val === false || val === "none") return false;
|
|
156
|
+
// Legacy: absolute path string → treat as "all extensions enabled"
|
|
157
|
+
if (typeof val === "string" && (val.startsWith("/") || val.startsWith(".") || val.includes("node_modules"))) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const items = csvList(val, []);
|
|
161
|
+
return items.length > 0 ? items : true;
|
|
162
|
+
}
|
package/src/default-agents.ts
CHANGED
|
@@ -15,6 +15,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
15
15
|
name: "general-purpose",
|
|
16
16
|
displayName: "Agent",
|
|
17
17
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
18
|
+
context:
|
|
19
|
+
"Use for any multi-step task that requires reasoning, writing code, or making changes. The default choice when no specialized agent fits.",
|
|
18
20
|
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
19
21
|
// inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
|
|
20
22
|
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
@@ -31,6 +33,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
31
33
|
name: "Explore",
|
|
32
34
|
displayName: "Explore",
|
|
33
35
|
description: "Fast codebase exploration agent (read-only)",
|
|
36
|
+
context:
|
|
37
|
+
"Use when you need to understand an unfamiliar codebase, find where something is implemented, or answer 'where is X' / 'how does Y work' questions. Read-only, fast, and cheap.",
|
|
34
38
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
35
39
|
extensions: true,
|
|
36
40
|
skills: true,
|
|
@@ -73,6 +77,8 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
|
|
|
73
77
|
name: "Plan",
|
|
74
78
|
displayName: "Plan",
|
|
75
79
|
description: "Software architect for implementation planning (read-only)",
|
|
80
|
+
context:
|
|
81
|
+
"Use when you need a detailed implementation plan before writing code. Analyzes the codebase and produces a step-by-step plan without making any changes.",
|
|
76
82
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
77
83
|
extensions: true,
|
|
78
84
|
skills: true,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Agent — LLM-callable: spawn a sub-agent
|
|
6
6
|
* get_subagent_result — LLM-callable: check background agent status/result
|
|
7
7
|
* steer_subagent — LLM-callable: send a steering message to a running agent
|
|
8
|
+
* send_to_agent — LLM-callable: send a follow-up to a completed agent (context preserved)
|
|
8
9
|
*
|
|
9
10
|
* Commands:
|
|
10
11
|
* /agents — Interactive agent management menu
|
|
@@ -518,6 +519,63 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
519
|
// --- Cross-extension RPC via pi.events ---
|
|
519
520
|
let currentCtx: ExtensionContext | undefined;
|
|
520
521
|
|
|
522
|
+
// edb-bridge + edb-todo integration: track current session's bridge ID and todo store path
|
|
523
|
+
let bridgeSessionId: string | undefined;
|
|
524
|
+
let todoStorePath: string | undefined;
|
|
525
|
+
|
|
526
|
+
// Promise that resolves when bridge:ready fires (or immediately if already ready).
|
|
527
|
+
// Used to avoid a race where a sub-agent is spawned before the bridge connects.
|
|
528
|
+
let bridgeReadyResolve: (() => void) | undefined;
|
|
529
|
+
const bridgeReadyPromise = new Promise<void>((resolve) => {
|
|
530
|
+
bridgeReadyResolve = resolve;
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
pi.events.on("bridge:ready", (payload: unknown) => {
|
|
534
|
+
const p = payload as { sessionId?: string } | undefined;
|
|
535
|
+
if (p?.sessionId) {
|
|
536
|
+
bridgeSessionId = p.sessionId;
|
|
537
|
+
bridgeReadyResolve?.();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
pi.events.on("todo:store_path", (payload: unknown) => {
|
|
542
|
+
const p = payload as { path?: string } | undefined;
|
|
543
|
+
if (p?.path) todoStorePath = p.path;
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Read a task from the shared task store (synchronous, best-effort).
|
|
548
|
+
* Returns undefined if the store is not available or the task doesn't exist.
|
|
549
|
+
*/
|
|
550
|
+
function readTaskFromStore(
|
|
551
|
+
taskId: string,
|
|
552
|
+
): { parentId?: string; status?: string; blockQuestion?: string; blockMessageId?: string } | undefined {
|
|
553
|
+
if (!todoStorePath) return undefined;
|
|
554
|
+
try {
|
|
555
|
+
const raw = readFileSync(todoStorePath, "utf-8");
|
|
556
|
+
const data = JSON.parse(raw) as {
|
|
557
|
+
tasks?: Array<{
|
|
558
|
+
id: string;
|
|
559
|
+
parentId?: string;
|
|
560
|
+
status?: string;
|
|
561
|
+
blockQuestion?: string;
|
|
562
|
+
blockMessageId?: string;
|
|
563
|
+
}>;
|
|
564
|
+
};
|
|
565
|
+
return data.tasks?.find((t) => t.id === taskId);
|
|
566
|
+
} catch {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Update a task via pi.events — edb-todo handles the write.
|
|
573
|
+
* Fires "todo:update_task" which edb-todo listens for and applies synchronously.
|
|
574
|
+
*/
|
|
575
|
+
function updateTask(taskId: string, fields: { status?: string; owner?: string }): void {
|
|
576
|
+
pi.events.emit("todo:update_task", { taskId, fields });
|
|
577
|
+
}
|
|
578
|
+
|
|
521
579
|
// ---- Subagent scheduler ----
|
|
522
580
|
// Session-scoped: store is constructed inside session_start once sessionId
|
|
523
581
|
// is available. Mirrors pi-chonky-tasks's session-scoped task store —
|
|
@@ -660,21 +718,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
660
718
|
const defaultNames = getDefaultAgentNames().filter((n) => getAgentConfig(n)?.enabled !== false);
|
|
661
719
|
const userNames = getUserAgentNames().filter((n) => getAgentConfig(n)?.enabled !== false);
|
|
662
720
|
|
|
663
|
-
const
|
|
721
|
+
const formatAgent = (name: string, isDefault: boolean) => {
|
|
664
722
|
const cfg = getAgentConfig(name);
|
|
665
|
-
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return
|
|
672
|
-
}
|
|
723
|
+
const modelSuffix = isDefault && cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
724
|
+
const desc = `- ${name}: ${cfg?.description ?? name}${modelSuffix}`;
|
|
725
|
+
// Include context block if defined — tells the orchestrator when/why to use this agent
|
|
726
|
+
if (cfg?.context) {
|
|
727
|
+
return `${desc}\n When to use: ${cfg.context.trim()}`;
|
|
728
|
+
}
|
|
729
|
+
return desc;
|
|
730
|
+
};
|
|
673
731
|
|
|
674
732
|
return [
|
|
675
733
|
"Default agents:",
|
|
676
|
-
...
|
|
677
|
-
...(
|
|
734
|
+
...defaultNames.map((n) => formatAgent(n, true)),
|
|
735
|
+
...(userNames.length > 0 ? ["", "Custom agents:", ...userNames.map((n) => formatAgent(n, false))] : []),
|
|
678
736
|
"",
|
|
679
737
|
`Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.`,
|
|
680
738
|
].join("\n");
|
|
@@ -746,6 +804,7 @@ Guidelines:
|
|
|
746
804
|
- Use run_in_background for work you don't need immediately. You will be notified when it completes.
|
|
747
805
|
- Use resume with an agent ID to continue a previous agent's work.
|
|
748
806
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
807
|
+
- Use send_to_agent to follow up with a completed agent (preserves full conversation context).
|
|
749
808
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
750
809
|
- Use thinking to control extended thinking level.
|
|
751
810
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
@@ -810,6 +869,23 @@ Guidelines:
|
|
|
810
869
|
'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
811
870
|
}),
|
|
812
871
|
),
|
|
872
|
+
task_id: Type.Optional(
|
|
873
|
+
Type.String({
|
|
874
|
+
description:
|
|
875
|
+
"Optional task ID from the shared task store to associate with this agent. " +
|
|
876
|
+
"When set: the agent auto-updates its task status (in_progress on start, completed/failed on finish), " +
|
|
877
|
+
"the agent's system prompt is given task lifecycle instructions, " +
|
|
878
|
+
"and subtask creation follows the two-layer rule (top-level tasks can create subtasks; subtasks cannot).",
|
|
879
|
+
}),
|
|
880
|
+
),
|
|
881
|
+
agent_name: Type.Optional(
|
|
882
|
+
Type.String({
|
|
883
|
+
description:
|
|
884
|
+
"A short, memorable name for this agent (e.g. 'Quinn', 'Atlas', 'Hex'). " +
|
|
885
|
+
"Shown in the task widget as [name] when the agent owns a task. " +
|
|
886
|
+
"Separate from description (which is the task summary). Keep it 1-2 words, evocative of the role.",
|
|
887
|
+
}),
|
|
888
|
+
),
|
|
813
889
|
...scheduleParam,
|
|
814
890
|
}),
|
|
815
891
|
promptSnippet: "Launch a specialized subagent to autonomously handle a complex multi-step task",
|
|
@@ -911,9 +987,19 @@ Guidelines:
|
|
|
911
987
|
// ---- Execute ----
|
|
912
988
|
|
|
913
989
|
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
990
|
+
// Extract extended params — task_id and agent_name are optional Agent tool params
|
|
991
|
+
// not reflected in the TypeBox static type, accessed via explicit cast here.
|
|
992
|
+
const taskId = (params as any).task_id as string | undefined;
|
|
993
|
+
const agentName = (params as any).agent_name as string | undefined;
|
|
994
|
+
|
|
914
995
|
// Ensure we have UI context for widget rendering
|
|
915
996
|
widget.setUICtx(ctx.ui as UICtx);
|
|
916
997
|
|
|
998
|
+
// Wait for bridge:ready (up to 3s) to avoid race where bridge hasn't connected yet
|
|
999
|
+
if (!bridgeSessionId) {
|
|
1000
|
+
await Promise.race([bridgeReadyPromise, new Promise<void>((res) => setTimeout(res, 3000))]);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
917
1003
|
// Reload custom agents so new .pi/agents/*.md files are picked up without restart
|
|
918
1004
|
reloadCustomAgents();
|
|
919
1005
|
|
|
@@ -1065,6 +1151,7 @@ Guidelines:
|
|
|
1065
1151
|
try {
|
|
1066
1152
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1067
1153
|
description: params.description,
|
|
1154
|
+
agentName: agentName,
|
|
1068
1155
|
model,
|
|
1069
1156
|
fallbackModels: params.fallback_models as string[] | undefined,
|
|
1070
1157
|
maxTurns: effectiveMaxTurns,
|
|
@@ -1074,6 +1161,33 @@ Guidelines:
|
|
|
1074
1161
|
isBackground: true,
|
|
1075
1162
|
isolation,
|
|
1076
1163
|
invocation: agentInvocation,
|
|
1164
|
+
// edb-bridge + edb-todo: inject orchestrator context into sub-agent
|
|
1165
|
+
...(bridgeSessionId
|
|
1166
|
+
? {
|
|
1167
|
+
bridgeContext: {
|
|
1168
|
+
parentSessionId: bridgeSessionId,
|
|
1169
|
+
agentId: String(agentName ?? params.description ?? subagentType),
|
|
1170
|
+
storePath: todoStorePath,
|
|
1171
|
+
taskId: taskId,
|
|
1172
|
+
agentIsSubtask: taskId ? !!readTaskFromStore(taskId!)?.parentId : undefined,
|
|
1173
|
+
},
|
|
1174
|
+
}
|
|
1175
|
+
: {}),
|
|
1176
|
+
// Auto task lifecycle
|
|
1177
|
+
...(taskId
|
|
1178
|
+
? {
|
|
1179
|
+
onTaskLifecycle: (event: "start" | "complete" | "failed") => {
|
|
1180
|
+
const agentId = String(agentName ?? params.description ?? subagentType);
|
|
1181
|
+
if (event === "start") {
|
|
1182
|
+
updateTask(taskId, { status: "in_progress", owner: agentId });
|
|
1183
|
+
} else if (event === "complete") {
|
|
1184
|
+
updateTask(taskId, { status: "completed", owner: agentId });
|
|
1185
|
+
} else {
|
|
1186
|
+
updateTask(taskId, { status: "failed", owner: agentId });
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
}
|
|
1190
|
+
: {}),
|
|
1077
1191
|
...bgCallbacks,
|
|
1078
1192
|
});
|
|
1079
1193
|
} catch (err) {
|
|
@@ -1122,6 +1236,9 @@ Guidelines:
|
|
|
1122
1236
|
`Description: ${params.description}\n` +
|
|
1123
1237
|
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
1124
1238
|
(isQueued ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n` : "") +
|
|
1239
|
+
(!bridgeSessionId
|
|
1240
|
+
? `\nNote: edb-bridge not connected — ask_supervisor and notify_parent will not work.\n`
|
|
1241
|
+
: "") +
|
|
1125
1242
|
`\nYou will be notified when this agent completes.\n` +
|
|
1126
1243
|
`Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
|
|
1127
1244
|
`Do not duplicate this agent's work.`,
|
|
@@ -1180,6 +1297,7 @@ Guidelines:
|
|
|
1180
1297
|
try {
|
|
1181
1298
|
record = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
1182
1299
|
description: params.description,
|
|
1300
|
+
agentName: agentName,
|
|
1183
1301
|
model,
|
|
1184
1302
|
fallbackModels: params.fallback_models as string[] | undefined,
|
|
1185
1303
|
maxTurns: effectiveMaxTurns,
|
|
@@ -1189,6 +1307,33 @@ Guidelines:
|
|
|
1189
1307
|
isolation,
|
|
1190
1308
|
invocation: agentInvocation,
|
|
1191
1309
|
signal,
|
|
1310
|
+
// edb-bridge + edb-todo: inject orchestrator context into sub-agent
|
|
1311
|
+
...(bridgeSessionId
|
|
1312
|
+
? {
|
|
1313
|
+
bridgeContext: {
|
|
1314
|
+
parentSessionId: bridgeSessionId,
|
|
1315
|
+
agentId: String(agentName ?? params.description ?? "agent"),
|
|
1316
|
+
storePath: todoStorePath,
|
|
1317
|
+
taskId: taskId,
|
|
1318
|
+
agentIsSubtask: taskId ? !!readTaskFromStore(taskId!)?.parentId : undefined,
|
|
1319
|
+
},
|
|
1320
|
+
}
|
|
1321
|
+
: {}),
|
|
1322
|
+
// Auto task lifecycle
|
|
1323
|
+
...(taskId
|
|
1324
|
+
? {
|
|
1325
|
+
onTaskLifecycle: (event: "start" | "complete" | "failed") => {
|
|
1326
|
+
const agentId = String(agentName ?? params.description ?? "agent");
|
|
1327
|
+
if (event === "start") {
|
|
1328
|
+
updateTask(taskId, { status: "in_progress", owner: agentId });
|
|
1329
|
+
} else if (event === "complete") {
|
|
1330
|
+
updateTask(taskId, { status: "completed", owner: agentId });
|
|
1331
|
+
} else {
|
|
1332
|
+
updateTask(taskId, { status: "failed", owner: agentId });
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
}
|
|
1336
|
+
: {}),
|
|
1192
1337
|
...fgCallbacks,
|
|
1193
1338
|
});
|
|
1194
1339
|
} catch (err) {
|
|
@@ -1234,7 +1379,8 @@ Guidelines:
|
|
|
1234
1379
|
name: "get_subagent_result",
|
|
1235
1380
|
label: "Get Agent Result",
|
|
1236
1381
|
description:
|
|
1237
|
-
"Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background
|
|
1382
|
+
"Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.\n\n" +
|
|
1383
|
+
"If the agent has called ask_supervisor and is waiting for your answer, wait: true will detect this and return the question and message_id immediately instead of blocking.",
|
|
1238
1384
|
parameters: Type.Object({
|
|
1239
1385
|
agent_id: Type.String({
|
|
1240
1386
|
description: "The agent ID to check.",
|
|
@@ -1259,6 +1405,26 @@ Guidelines:
|
|
|
1259
1405
|
}
|
|
1260
1406
|
|
|
1261
1407
|
// Wait for completion if requested.
|
|
1408
|
+
// Check if the agent's linked task is blocked (waiting for answer_subagent).
|
|
1409
|
+
// If so, waiting would cause a deadlock — return immediately with the question.
|
|
1410
|
+
if (params.wait && record.status === "running" && record.taskId && todoStorePath) {
|
|
1411
|
+
try {
|
|
1412
|
+
const task = readTaskFromStore(record.taskId);
|
|
1413
|
+
if (task && (task as any).status === "blocked" && (task as any).blockQuestion) {
|
|
1414
|
+
const q = (task as any).blockQuestion as string;
|
|
1415
|
+
const msgId = (task as any).blockMessageId as string | undefined;
|
|
1416
|
+
return textResult(
|
|
1417
|
+
`Agent is blocked waiting for your answer.\n\n` +
|
|
1418
|
+
`Question: "${q}"\n\n` +
|
|
1419
|
+
(msgId ? `Call: answer_subagent({ message_id: "${msgId}", answer: "..." })\n\n` : "") +
|
|
1420
|
+
`After answering, call get_subagent_result again to wait for completion.`,
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
} catch {
|
|
1424
|
+
/* ignore — best-effort check */
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1262
1428
|
// Pre-mark resultConsumed BEFORE awaiting: onComplete fires inside .then()
|
|
1263
1429
|
// (attached earlier at spawn time) and always runs before this await resumes.
|
|
1264
1430
|
// Setting the flag here prevents a redundant follow-up notification.
|
|
@@ -1371,6 +1537,72 @@ Guidelines:
|
|
|
1371
1537
|
}),
|
|
1372
1538
|
);
|
|
1373
1539
|
|
|
1540
|
+
// ---- send_to_agent tool ----
|
|
1541
|
+
|
|
1542
|
+
pi.registerTool(
|
|
1543
|
+
defineTool({
|
|
1544
|
+
name: "send_to_agent",
|
|
1545
|
+
label: "Send to Agent",
|
|
1546
|
+
description:
|
|
1547
|
+
"Send a follow-up message to a completed (or running) agent, continuing its existing session. " +
|
|
1548
|
+
"The agent retains its full conversation context — no context is lost. " +
|
|
1549
|
+
"Use this instead of spawning a new agent when you want to build on prior work. " +
|
|
1550
|
+
"Use run_in_background: true to fire and check later via get_subagent_result.",
|
|
1551
|
+
parameters: Type.Object({
|
|
1552
|
+
agent_id: Type.String({
|
|
1553
|
+
description: "The agent ID to send the message to.",
|
|
1554
|
+
}),
|
|
1555
|
+
message: Type.String({
|
|
1556
|
+
description: "The follow-up message / next task for the agent.",
|
|
1557
|
+
}),
|
|
1558
|
+
run_in_background: Type.Optional(
|
|
1559
|
+
Type.Boolean({
|
|
1560
|
+
description:
|
|
1561
|
+
"If true, returns immediately with the agent ID. Use get_subagent_result to collect the response later. " +
|
|
1562
|
+
"Default: false (waits for the agent to finish and returns its response).",
|
|
1563
|
+
}),
|
|
1564
|
+
),
|
|
1565
|
+
}),
|
|
1566
|
+
promptSnippet: "Send a follow-up message to a previously completed agent (preserves full context)",
|
|
1567
|
+
execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
|
|
1568
|
+
const record = manager.getRecord(params.agent_id);
|
|
1569
|
+
if (!record) {
|
|
1570
|
+
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
1571
|
+
}
|
|
1572
|
+
if (!record.session) {
|
|
1573
|
+
return textResult(
|
|
1574
|
+
`Agent "${params.agent_id}" has no active session (status: ${record.status}). ` +
|
|
1575
|
+
"It may have been cleaned up after 10 minutes of inactivity.",
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
if (record.status === "running") {
|
|
1579
|
+
return textResult(
|
|
1580
|
+
`Agent "${params.agent_id}" is currently running. ` +
|
|
1581
|
+
"Use steer_subagent to redirect it mid-run, or wait for it to finish first.",
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
if (params.run_in_background) {
|
|
1586
|
+
const updated = manager.resumeInBackground(params.agent_id, params.message);
|
|
1587
|
+
if (!updated) {
|
|
1588
|
+
return textResult(`Failed to resume agent "${params.agent_id}" in background.`);
|
|
1589
|
+
}
|
|
1590
|
+
return textResult(
|
|
1591
|
+
`Message sent to agent ${params.agent_id} (running in background). ` +
|
|
1592
|
+
"Call get_subagent_result to collect the response when ready.",
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Foreground: wait for response
|
|
1597
|
+
const updated = await manager.resume(params.agent_id, params.message, signal);
|
|
1598
|
+
if (!updated) {
|
|
1599
|
+
return textResult(`Failed to resume agent "${params.agent_id}".`);
|
|
1600
|
+
}
|
|
1601
|
+
return textResult(updated.result?.trim() || updated.error?.trim() || "No output.");
|
|
1602
|
+
},
|
|
1603
|
+
}),
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1374
1606
|
// ---- /agents interactive menu ----
|
|
1375
1607
|
|
|
1376
1608
|
const projectAgentsDir = () => join(process.cwd(), ".pi", "agents");
|
package/src/prompts.ts
CHANGED
|
@@ -10,6 +10,17 @@ export interface PromptExtras {
|
|
|
10
10
|
memoryBlock?: string;
|
|
11
11
|
/** Preloaded skill contents to inject. */
|
|
12
12
|
skillBlocks?: { name: string; content: string }[];
|
|
13
|
+
/**
|
|
14
|
+
* edb-bridge context for sub-agent communication.
|
|
15
|
+
* Injected as XML tags so sub-agent's edb-bridge and edb-todo extensions can read it.
|
|
16
|
+
*/
|
|
17
|
+
bridgeContext?: {
|
|
18
|
+
parentSessionId: string; // broker session ID of the parent
|
|
19
|
+
agentId: string; // edb-subagents agent ID
|
|
20
|
+
storePath?: string; // parent's edb-todo task store file path
|
|
21
|
+
taskId?: string; // assigned task ID for this agent
|
|
22
|
+
agentIsSubtask?: boolean; // true = already a subtask (depth 1) — no further subtasks allowed
|
|
23
|
+
};
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
/**
|
|
@@ -50,6 +61,19 @@ Platform: ${env.platform}`;
|
|
|
50
61
|
extraSections.push(`\n# Preloaded Skill: ${skill.name}\n${skill.content}`);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
64
|
+
// edb-bridge context: inject XML tags so sub-agent extensions can read parent session info
|
|
65
|
+
if (extras?.bridgeContext) {
|
|
66
|
+
const { parentSessionId, agentId, storePath, taskId, agentIsSubtask } = extras.bridgeContext;
|
|
67
|
+
const storeTag = storePath ? `\n<task_store_path>${storePath}</task_store_path>` : "";
|
|
68
|
+
const taskTag = taskId ? `\n<assigned_task_id>${taskId}</assigned_task_id>` : "";
|
|
69
|
+
extraSections.push(
|
|
70
|
+
`<bridge_context>\n<bridge_parent_session>${parentSessionId}</bridge_parent_session>\n<bridge_agent_id>${agentId}</bridge_agent_id>${storeTag}${taskTag}\n</bridge_context>`,
|
|
71
|
+
);
|
|
72
|
+
// Task context instructions — only when task_id was provided
|
|
73
|
+
if (taskId) {
|
|
74
|
+
extraSections.push(buildTaskContextBlock(taskId, agentId, agentIsSubtask ?? false));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
53
77
|
const extrasSuffix = extraSections.length > 0 ? `\n\n${extraSections.join("\n")}` : "";
|
|
54
78
|
|
|
55
79
|
if (config.promptMode === "append") {
|
|
@@ -98,3 +122,29 @@ const genericBase = `# Role
|
|
|
98
122
|
You are a general-purpose coding agent for complex, multi-step tasks.
|
|
99
123
|
You have full access to read, write, edit files, and execute commands.
|
|
100
124
|
Do what has been asked; nothing more, nothing less.`;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build the task context block injected when a task_id is assigned at spawn time.
|
|
128
|
+
*
|
|
129
|
+
* Tells the sub-agent:
|
|
130
|
+
* - Its assigned task ID and how to update it
|
|
131
|
+
* - Whether it can create subtasks (depth-0 task: yes; depth-1 subtask: no)
|
|
132
|
+
* - That depth-1 agents must not create further subtasks (two-layer rule)
|
|
133
|
+
*/
|
|
134
|
+
function buildTaskContextBlock(taskId: string, agentId: string, agentIsSubtask: boolean): string {
|
|
135
|
+
const subtaskSection = agentIsSubtask
|
|
136
|
+
? `**Subtasks:** You are already a subtask (${taskId}). Do NOT create further subtasks (parentId is not allowed). You are at the maximum nesting depth. If you need internal tracking, you may use TaskCreate without parentId — those tasks will not appear in the orchestrator's widget.`
|
|
137
|
+
: `**Subtasks (optional):** You may break your work into subtasks visible to the orchestrator:\n TaskCreate({ content: "...", parentId: "${taskId}" })\n Update them as you work: TaskUpdate({ id: "<subtask-id>", status: "in_progress" })`;
|
|
138
|
+
|
|
139
|
+
return `## Your Assigned Task
|
|
140
|
+
You have been assigned task **${taskId}**.
|
|
141
|
+
|
|
142
|
+
**Lifecycle (required — the orchestrator tracks these):**
|
|
143
|
+
1. When you begin: \`TaskUpdate({ id: "${taskId}", status: "in_progress", owner: "${agentId}" })\`
|
|
144
|
+
2. Optionally set progress: \`TaskUpdate({ id: "${taskId}", activeForm: "Doing X..." })\`
|
|
145
|
+
3. When done: \`TaskUpdate({ id: "${taskId}", status: "completed", owner: "${agentId}" })\`
|
|
146
|
+
4. If you need clarification from the orchestrator: \`ask_supervisor({ question: "..." })\` — this will block you until answered.
|
|
147
|
+
5. For progress updates: \`notify_parent({ message: "..." })\` — task_id is auto-injected, updates the widget spinner text.
|
|
148
|
+
|
|
149
|
+
${subtaskSection}`;
|
|
150
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface AgentConfig {
|
|
|
55
55
|
enabled?: boolean;
|
|
56
56
|
/** Where this agent was loaded from */
|
|
57
57
|
source?: "default" | "project" | "global";
|
|
58
|
+
/**
|
|
59
|
+
* Usage context — when and why to use this agent.
|
|
60
|
+
* Injected into the main agent's system prompt so it knows which agent to spawn for what tasks.
|
|
61
|
+
* Example: "Use when exploring an unfamiliar codebase to find where something is implemented."
|
|
62
|
+
*/
|
|
63
|
+
context?: string;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export type JoinMode = "async" | "group" | "smart";
|
|
@@ -63,6 +69,10 @@ export interface AgentRecord {
|
|
|
63
69
|
id: string;
|
|
64
70
|
type: SubagentType;
|
|
65
71
|
description: string;
|
|
72
|
+
/** Short memorable name given by the orchestrator (e.g. 'Quinn'). Shown in widget alongside description. */
|
|
73
|
+
agentName?: string;
|
|
74
|
+
/** Linked task ID in the shared task store. Used for blocked-state check in get_subagent_result. */
|
|
75
|
+
taskId?: string;
|
|
66
76
|
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
67
77
|
result?: string;
|
|
68
78
|
error?: string;
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -273,6 +273,7 @@ export class AgentWidget {
|
|
|
273
273
|
type: SubagentType;
|
|
274
274
|
status: string;
|
|
275
275
|
description: string;
|
|
276
|
+
agentName?: string;
|
|
276
277
|
toolUses: number;
|
|
277
278
|
startedAt: number;
|
|
278
279
|
completedAt?: number;
|
|
@@ -312,7 +313,7 @@ export class AgentWidget {
|
|
|
312
313
|
parts.push(duration);
|
|
313
314
|
|
|
314
315
|
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
315
|
-
return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
316
|
+
return `${icon} ${theme.fg("dim", name)}${modeTag}${a.agentName ? ` ${theme.fg("dim", a.agentName)}` : ""} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
316
317
|
}
|
|
317
318
|
|
|
318
319
|
/**
|
|
@@ -373,7 +374,7 @@ export class AgentWidget {
|
|
|
373
374
|
runningLines.push([
|
|
374
375
|
truncate(
|
|
375
376
|
theme.fg("dim", "├─") +
|
|
376
|
-
` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`,
|
|
377
|
+
` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag}${a.agentName ? ` ${theme.fg("accent", theme.bold(a.agentName))}` : ""} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`,
|
|
377
378
|
),
|
|
378
379
|
truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
|
|
379
380
|
]);
|