pi-subagents 0.7.0 → 0.8.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/README.md +67 -1
- package/agent-management.ts +502 -0
- package/index.ts +32 -3
- package/package.json +1 -1
- package/schemas.ts +13 -1
- package/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.8.0] - 2026-02-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Management mode for `subagent` tool** via `action` field — the LLM can now discover, create, modify, and delete agent/chain definitions at runtime without manual file editing or restarts. Five actions:
|
|
9
|
+
- `list` — discover agents and chains with scope + description
|
|
10
|
+
- `get` — full detail for agent or chain, including path and system prompt/steps
|
|
11
|
+
- `create` — create agent (`.md`) or chain (`.chain.md`) definitions from `config`; immediately usable
|
|
12
|
+
- `update` — merge-update agent or chain fields, including rename with chain reference warnings
|
|
13
|
+
- `delete` — remove agent or chain definitions with dangling reference warnings
|
|
14
|
+
- **New `agent-management.ts` module** with all management handlers, validation, and serialization helpers
|
|
15
|
+
- **New management params** in tool schema: `action`, `chainName`, `config`
|
|
16
|
+
- **Agent/chain CRUD safeguards**
|
|
17
|
+
- Name sanitization (lowercase-hyphenated) for create/rename
|
|
18
|
+
- Scope-aware uniqueness checks across agents and chains
|
|
19
|
+
- File-path collision checks to prevent overwriting non-agent markdown files
|
|
20
|
+
- Scope disambiguation for update/delete when names exist in both user and project scope
|
|
21
|
+
- Not-found errors include available names for fast self-correction
|
|
22
|
+
- Per-step validation warnings for model registry and skill availability
|
|
23
|
+
- Validate-then-mutate ordering — all validation completes before any filesystem mutations
|
|
24
|
+
- **Config field mapping**: `tools` (comma-separated with `mcp:` prefix support), `reads` -> `defaultReads`, `progress` -> `defaultProgress`
|
|
25
|
+
- **Uniform field clearing** — all optional string fields accept both `false` and `""` to clear
|
|
26
|
+
- **JSON string parsing for `config` param** — handles `Type.Any()` delivering objects as JSON strings through the tool framework
|
|
27
|
+
|
|
5
28
|
## [0.7.0] - 2026-02-09
|
|
6
29
|
|
|
7
30
|
### Added
|
package/README.md
CHANGED
|
@@ -218,6 +218,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
218
218
|
|
|
219
219
|
- **Slash Commands**: `/run`, `/chain`, `/parallel` with tab-completion and live progress
|
|
220
220
|
- **Agents Manager Overlay**: Browse, view, edit, create, delete, and launch agents/chains from a TUI (`Ctrl+Shift+A`)
|
|
221
|
+
- **Management Actions**: LLM can list, inspect, create, update, and delete agent/chain definitions via `action` field
|
|
221
222
|
- **Chain Files**: Reusable `.chain.md` files with per-step config, saveable from the clarify TUI
|
|
222
223
|
- **Multi-select & Parallel**: Select agents in the overlay, launch as chain or parallel
|
|
223
224
|
- **Run History**: Per-agent JSONL recording of task, exit code, duration; shown on detail screen
|
|
@@ -399,14 +400,78 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
399
400
|
{ dir: "/tmp/pi-async-subagent-runs/a53ebe46-..." }
|
|
400
401
|
```
|
|
401
402
|
|
|
403
|
+
## Management Actions
|
|
404
|
+
|
|
405
|
+
Agent definitions are not loaded into LLM context by default. Management actions let the LLM discover, inspect, create, and modify agent and chain definitions at runtime through the `subagent` tool — no manual file editing or restart required. Newly created agents are immediately usable in the same session. Set `action` and omit execution payloads (`task`, `chain`, `tasks`).
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Discover all agents and chains (management defaults to both scopes)
|
|
409
|
+
{ action: "list" }
|
|
410
|
+
{ action: "list", agentScope: "project" }
|
|
411
|
+
|
|
412
|
+
// Inspect one agent or chain (searches both scopes)
|
|
413
|
+
{ action: "get", agent: "scout" }
|
|
414
|
+
{ action: "get", chainName: "review-pipeline" }
|
|
415
|
+
|
|
416
|
+
// Create agent
|
|
417
|
+
{ action: "create", config: {
|
|
418
|
+
name: "Code Scout",
|
|
419
|
+
description: "Scans codebases for patterns and issues",
|
|
420
|
+
scope: "user",
|
|
421
|
+
systemPrompt: "You are a code scout...",
|
|
422
|
+
model: "anthropic/claude-sonnet-4",
|
|
423
|
+
tools: "read, bash, mcp:github/search_repositories",
|
|
424
|
+
skills: "parallel-scout",
|
|
425
|
+
thinking: "high",
|
|
426
|
+
output: "context.md",
|
|
427
|
+
reads: "shared-context.md",
|
|
428
|
+
progress: true
|
|
429
|
+
}}
|
|
430
|
+
|
|
431
|
+
// Create chain (presence of steps creates .chain.md)
|
|
432
|
+
{ action: "create", config: {
|
|
433
|
+
name: "review-pipeline",
|
|
434
|
+
description: "Scout then review",
|
|
435
|
+
scope: "project",
|
|
436
|
+
steps: [
|
|
437
|
+
{ agent: "scout", task: "Scan {task}", output: "context.md" },
|
|
438
|
+
{ agent: "reviewer", task: "Review {previous}", reads: ["context.md"] }
|
|
439
|
+
]
|
|
440
|
+
}}
|
|
441
|
+
|
|
442
|
+
// Update agent fields (merge semantics)
|
|
443
|
+
{ action: "update", agent: "scout", config: { model: "openai/gpt-4o" } }
|
|
444
|
+
{ action: "update", agent: "scout", config: { output: false, skills: "" } } // clear optional fields
|
|
445
|
+
{ action: "update", chainName: "review-pipeline", config: {
|
|
446
|
+
steps: [
|
|
447
|
+
{ agent: "scout", task: "Scan {task}", output: "context.md" },
|
|
448
|
+
{ agent: "reviewer", task: "Improved review of {previous}", reads: ["context.md"] }
|
|
449
|
+
]
|
|
450
|
+
}}
|
|
451
|
+
|
|
452
|
+
// Delete definitions
|
|
453
|
+
{ action: "delete", agent: "scout" }
|
|
454
|
+
{ action: "delete", chainName: "review-pipeline" }
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Notes:
|
|
458
|
+
- `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
|
|
459
|
+
- `update`/`delete` use `agentScope` only for scope disambiguation when the same name exists in both scopes.
|
|
460
|
+
- Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, and `tools` supports `mcp:` entries that map to direct MCP tools.
|
|
461
|
+
- To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
|
|
462
|
+
|
|
402
463
|
## Parameters
|
|
403
464
|
|
|
404
465
|
| Param | Type | Default | Description |
|
|
405
466
|
|-------|------|---------|-------------|
|
|
406
|
-
| `agent` | string | - | Agent name (single mode) |
|
|
467
|
+
| `agent` | string | - | Agent name (single mode) or target for management get/update/delete |
|
|
407
468
|
| `task` | string | - | Task string (single mode) |
|
|
469
|
+
| `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
|
|
470
|
+
| `chainName` | string | - | Chain name for management get/update/delete |
|
|
471
|
+
| `config` | object | - | Agent or chain config for management create/update |
|
|
408
472
|
| `output` | `string \| false` | agent default | Override output file for single agent |
|
|
409
473
|
| `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
|
|
474
|
+
| `model` | string | agent default | Override model for single agent |
|
|
410
475
|
| `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
|
|
411
476
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
412
477
|
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
@@ -584,6 +649,7 @@ Legacy events (still emitted):
|
|
|
584
649
|
├── agent-manager-edit.ts # Edit screen (pickers, prompt editor)
|
|
585
650
|
├── agent-manager-parallel.ts # Parallel builder screen (slot management, agent picker)
|
|
586
651
|
├── agent-manager-chain-detail.ts # Chain detail screen (flow visualization)
|
|
652
|
+
├── agent-management.ts # Management action handlers (list, get, create, update, delete)
|
|
587
653
|
├── agent-serializer.ts # Serialize agents to markdown frontmatter
|
|
588
654
|
├── agent-templates.ts # Agent/chain creation templates
|
|
589
655
|
├── render-helpers.ts # Shared pad/row/header/footer helpers
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
4
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
type AgentConfig,
|
|
7
|
+
type AgentScope,
|
|
8
|
+
type ChainConfig,
|
|
9
|
+
type ChainStepConfig,
|
|
10
|
+
discoverAgentsAll,
|
|
11
|
+
} from "./agents.js";
|
|
12
|
+
import { serializeAgent } from "./agent-serializer.js";
|
|
13
|
+
import { serializeChain } from "./chain-serializer.js";
|
|
14
|
+
import { discoverAvailableSkills } from "./skills.js";
|
|
15
|
+
import type { Details } from "./types.js";
|
|
16
|
+
|
|
17
|
+
type ManagementAction = "list" | "get" | "create" | "update" | "delete";
|
|
18
|
+
type ManagementScope = "user" | "project";
|
|
19
|
+
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry">;
|
|
20
|
+
|
|
21
|
+
interface ManagementParams {
|
|
22
|
+
action?: string;
|
|
23
|
+
agent?: string;
|
|
24
|
+
chainName?: string;
|
|
25
|
+
agentScope?: string;
|
|
26
|
+
config?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
30
|
+
return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [] } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseCsv(value: string): string[] {
|
|
34
|
+
return [...new Set(value.split(",").map((v) => v.trim()).filter(Boolean))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configObject(config: unknown): Record<string, unknown> | undefined {
|
|
38
|
+
let val = config;
|
|
39
|
+
if (typeof val === "string") {
|
|
40
|
+
try { val = JSON.parse(val); } catch { return undefined; }
|
|
41
|
+
}
|
|
42
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) return undefined;
|
|
43
|
+
return val as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasKey(obj: Record<string, unknown>, key: string): boolean {
|
|
47
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function asDisambiguationScope(scope: unknown): ManagementScope | undefined {
|
|
51
|
+
if (scope === "user" || scope === "project") return scope;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeListScope(scope: unknown): AgentScope | undefined {
|
|
56
|
+
if (scope === undefined) return "both";
|
|
57
|
+
if (scope === "user" || scope === "project" || scope === "both") return scope;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function sanitizeName(name: string): string {
|
|
62
|
+
return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function availableNames(cwd: string, kind: "agent" | "chain"): string[] {
|
|
66
|
+
const d = discoverAgentsAll(cwd);
|
|
67
|
+
const items = kind === "agent" ? [...d.user, ...d.project] : d.chains;
|
|
68
|
+
return [...new Set(items.map((x) => x.name))].sort((a, b) => a.localeCompare(b));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findAgents(name: string, cwd: string, scope: AgentScope = "both"): AgentConfig[] {
|
|
72
|
+
const d = discoverAgentsAll(cwd);
|
|
73
|
+
const raw = name.trim();
|
|
74
|
+
const sanitized = sanitizeName(raw);
|
|
75
|
+
return [...d.user, ...d.project]
|
|
76
|
+
.filter((a) => (scope === "both" || a.source === scope) && (a.name === raw || a.name === sanitized))
|
|
77
|
+
.sort((a, b) => a.source.localeCompare(b.source));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function findChains(name: string, cwd: string, scope: AgentScope = "both"): ChainConfig[] {
|
|
81
|
+
const raw = name.trim();
|
|
82
|
+
const sanitized = sanitizeName(raw);
|
|
83
|
+
return discoverAgentsAll(cwd).chains
|
|
84
|
+
.filter((c) => (scope === "both" || c.source === scope) && (c.name === raw || c.name === sanitized))
|
|
85
|
+
.sort((a, b) => a.source.localeCompare(b.source));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, excludePath?: string): boolean {
|
|
89
|
+
const d = discoverAgentsAll(cwd);
|
|
90
|
+
for (const a of scope === "user" ? d.user : d.project) {
|
|
91
|
+
if (a.name === name && a.filePath !== excludePath) return true;
|
|
92
|
+
}
|
|
93
|
+
for (const c of d.chains) {
|
|
94
|
+
if (c.source === scope && c.name === name && c.filePath !== excludePath) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
|
|
100
|
+
const d = discoverAgentsAll(cwd);
|
|
101
|
+
const known = new Set([...d.user, ...d.project].map((a) => a.name));
|
|
102
|
+
return [...new Set(steps.map((s) => s.agent).filter((a) => !known.has(a)))].sort((a, b) => a.localeCompare(b));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function chainStepWarnings(ctx: ManagementContext, steps: ChainStepConfig[]): string[] {
|
|
106
|
+
const warnings: string[] = [];
|
|
107
|
+
const available = new Set(discoverAvailableSkills(ctx.cwd).map((s) => s.name));
|
|
108
|
+
for (let i = 0; i < steps.length; i++) {
|
|
109
|
+
const s = steps[i]!;
|
|
110
|
+
if (s.model) {
|
|
111
|
+
const found = ctx.modelRegistry.getAvailable().some((m) => `${m.provider}/${m.id}` === s.model || m.id === s.model);
|
|
112
|
+
if (!found) warnings.push(`Warning: step ${i + 1} (${s.agent}): model '${s.model}' is not in the current model registry.`);
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(s.skills) && s.skills.length > 0) {
|
|
115
|
+
const missing = s.skills.filter((sk) => !available.has(sk));
|
|
116
|
+
if (missing.length) warnings.push(`Warning: step ${i + 1} (${s.agent}): skills not found: ${missing.join(", ")}.`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return warnings;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function modelWarning(ctx: ManagementContext, model: string | undefined): string | undefined {
|
|
123
|
+
if (!model) return undefined;
|
|
124
|
+
const found = ctx.modelRegistry.getAvailable().some((m) => `${m.provider}/${m.id}` === model || m.id === model);
|
|
125
|
+
return found ? undefined : `Warning: model '${model}' is not in the current model registry.`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function skillsWarning(cwd: string, skills: string[] | undefined): string | undefined {
|
|
129
|
+
if (!skills || skills.length === 0) return undefined;
|
|
130
|
+
const available = new Set(discoverAvailableSkills(cwd).map((s) => s.name));
|
|
131
|
+
const missing = skills.filter((s) => !available.has(s));
|
|
132
|
+
return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: string } {
|
|
136
|
+
if (!Array.isArray(raw)) return { error: "config.steps must be an array." };
|
|
137
|
+
if (raw.length === 0) return { error: "config.steps must include at least one step." };
|
|
138
|
+
const steps: ChainStepConfig[] = [];
|
|
139
|
+
for (let i = 0; i < raw.length; i++) {
|
|
140
|
+
const item = raw[i];
|
|
141
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return { error: `config.steps[${i}] must be an object.` };
|
|
142
|
+
const s = item as Record<string, unknown>;
|
|
143
|
+
if (typeof s.agent !== "string" || !s.agent.trim()) return { error: `config.steps[${i}].agent must be a non-empty string.` };
|
|
144
|
+
const step: ChainStepConfig = { agent: s.agent.trim(), task: typeof s.task === "string" ? s.task : "" };
|
|
145
|
+
if (hasKey(s, "output")) {
|
|
146
|
+
if (s.output === false) step.output = false;
|
|
147
|
+
else if (typeof s.output === "string") step.output = s.output;
|
|
148
|
+
else return { error: `config.steps[${i}].output must be a string or false.` };
|
|
149
|
+
}
|
|
150
|
+
if (hasKey(s, "reads")) {
|
|
151
|
+
if (s.reads === false) step.reads = false;
|
|
152
|
+
else if (Array.isArray(s.reads)) step.reads = s.reads.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
|
|
153
|
+
else return { error: `config.steps[${i}].reads must be an array or false.` };
|
|
154
|
+
}
|
|
155
|
+
if (hasKey(s, "model")) {
|
|
156
|
+
if (typeof s.model === "string") step.model = s.model;
|
|
157
|
+
else return { error: `config.steps[${i}].model must be a string.` };
|
|
158
|
+
}
|
|
159
|
+
if (hasKey(s, "skills")) {
|
|
160
|
+
if (s.skills === false) step.skills = false;
|
|
161
|
+
else if (Array.isArray(s.skills)) step.skills = s.skills.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
|
|
162
|
+
else return { error: `config.steps[${i}].skills must be an array or false.` };
|
|
163
|
+
}
|
|
164
|
+
if (hasKey(s, "progress")) {
|
|
165
|
+
if (typeof s.progress === "boolean") step.progress = s.progress;
|
|
166
|
+
else return { error: `config.steps[${i}].progress must be a boolean.` };
|
|
167
|
+
}
|
|
168
|
+
steps.push(step);
|
|
169
|
+
}
|
|
170
|
+
return { steps };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
174
|
+
const tools: string[] = [];
|
|
175
|
+
const mcpDirectTools: string[] = [];
|
|
176
|
+
for (const item of parseCsv(raw)) {
|
|
177
|
+
if (item.startsWith("mcp:")) {
|
|
178
|
+
const direct = item.slice(4).trim();
|
|
179
|
+
if (direct) mcpDirectTools.push(direct);
|
|
180
|
+
} else tools.push(item);
|
|
181
|
+
}
|
|
182
|
+
return { tools: tools.length ? tools : undefined, mcpDirectTools: mcpDirectTools.length ? mcpDirectTools : undefined };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
|
|
186
|
+
if (hasKey(cfg, "systemPrompt")) {
|
|
187
|
+
if (cfg.systemPrompt === false || cfg.systemPrompt === "") target.systemPrompt = "";
|
|
188
|
+
else if (typeof cfg.systemPrompt === "string") target.systemPrompt = cfg.systemPrompt;
|
|
189
|
+
else return "config.systemPrompt must be a string or false when provided.";
|
|
190
|
+
}
|
|
191
|
+
if (hasKey(cfg, "model")) {
|
|
192
|
+
if (cfg.model === false || cfg.model === "") target.model = undefined;
|
|
193
|
+
else if (typeof cfg.model === "string") target.model = cfg.model.trim() || undefined;
|
|
194
|
+
else return "config.model must be a string or false when provided.";
|
|
195
|
+
}
|
|
196
|
+
if (hasKey(cfg, "tools")) {
|
|
197
|
+
if (cfg.tools === false || cfg.tools === "") { target.tools = undefined; target.mcpDirectTools = undefined; }
|
|
198
|
+
else if (typeof cfg.tools === "string") { const parsed = parseTools(cfg.tools); target.tools = parsed.tools; target.mcpDirectTools = parsed.mcpDirectTools; }
|
|
199
|
+
else return "config.tools must be a comma-separated string or false when provided.";
|
|
200
|
+
}
|
|
201
|
+
if (hasKey(cfg, "skills")) {
|
|
202
|
+
if (cfg.skills === false || cfg.skills === "") target.skills = undefined;
|
|
203
|
+
else if (typeof cfg.skills === "string") { const skills = parseCsv(cfg.skills); target.skills = skills.length ? skills : undefined; }
|
|
204
|
+
else return "config.skills must be a comma-separated string or false when provided.";
|
|
205
|
+
}
|
|
206
|
+
if (hasKey(cfg, "thinking")) {
|
|
207
|
+
if (cfg.thinking === false || cfg.thinking === "") target.thinking = undefined;
|
|
208
|
+
else if (typeof cfg.thinking === "string") target.thinking = cfg.thinking.trim() || undefined;
|
|
209
|
+
else return "config.thinking must be a string or false when provided.";
|
|
210
|
+
}
|
|
211
|
+
if (hasKey(cfg, "output")) {
|
|
212
|
+
if (cfg.output === false || cfg.output === "") target.output = undefined;
|
|
213
|
+
else if (typeof cfg.output === "string") target.output = cfg.output;
|
|
214
|
+
else return "config.output must be a string or false when provided.";
|
|
215
|
+
}
|
|
216
|
+
if (hasKey(cfg, "reads")) {
|
|
217
|
+
if (cfg.reads === false || cfg.reads === "") target.defaultReads = undefined;
|
|
218
|
+
else if (typeof cfg.reads === "string") {
|
|
219
|
+
const reads = parseCsv(cfg.reads);
|
|
220
|
+
target.defaultReads = reads.length ? reads : undefined;
|
|
221
|
+
} else return "config.reads must be a comma-separated string or false when provided.";
|
|
222
|
+
}
|
|
223
|
+
if (hasKey(cfg, "progress")) {
|
|
224
|
+
if (typeof cfg.progress !== "boolean") return "config.progress must be a boolean when provided.";
|
|
225
|
+
target.defaultProgress = cfg.progress;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveTarget<T extends { source: "user" | "project"; filePath: string }>(
|
|
231
|
+
kind: "agent" | "chain",
|
|
232
|
+
name: string,
|
|
233
|
+
matches: T[],
|
|
234
|
+
cwd: string,
|
|
235
|
+
scopeHint?: string,
|
|
236
|
+
): T | AgentToolResult<Details> {
|
|
237
|
+
if (matches.length === 0) {
|
|
238
|
+
const available = availableNames(cwd, kind);
|
|
239
|
+
return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found. Available: ${available.join(", ") || "none"}.`, true);
|
|
240
|
+
}
|
|
241
|
+
if (matches.length === 1) return matches[0]!;
|
|
242
|
+
const scope = asDisambiguationScope(scopeHint);
|
|
243
|
+
if (!scope) {
|
|
244
|
+
const paths = matches.map((m) => `${m.source}: ${m.filePath}`).join("\n");
|
|
245
|
+
return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' exists in both scopes. Specify agentScope: 'user' or 'project'.\n${paths}`, true);
|
|
246
|
+
}
|
|
247
|
+
const scoped = matches.filter((m) => m.source === scope);
|
|
248
|
+
if (scoped.length === 0) return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found in scope '${scope}'.`, true);
|
|
249
|
+
if (scoped.length > 1) return result(`Multiple ${kind}s named '${name}' found in scope '${scope}': ${scoped.map((m) => m.filePath).join(", ")}`, true);
|
|
250
|
+
return scoped[0]!;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renamePath(
|
|
254
|
+
kind: "agent" | "chain",
|
|
255
|
+
currentPath: string,
|
|
256
|
+
newName: string,
|
|
257
|
+
scope: ManagementScope,
|
|
258
|
+
cwd: string,
|
|
259
|
+
): { filePath?: string; error?: string } {
|
|
260
|
+
if (nameExistsInScope(cwd, scope, newName, currentPath)) return { error: `Name '${newName}' already exists in ${scope} scope.` };
|
|
261
|
+
const ext = kind === "agent" ? ".md" : ".chain.md";
|
|
262
|
+
const filePath = path.join(path.dirname(currentPath), `${newName}${ext}`);
|
|
263
|
+
if (fs.existsSync(filePath) && filePath !== currentPath) {
|
|
264
|
+
return { error: `File already exists at ${filePath} but is not a valid ${kind} definition. Remove or rename it first.` };
|
|
265
|
+
}
|
|
266
|
+
fs.renameSync(currentPath, filePath);
|
|
267
|
+
return { filePath };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function formatAgentDetail(agent: AgentConfig): string {
|
|
271
|
+
const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
|
|
272
|
+
const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
|
|
273
|
+
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
274
|
+
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
275
|
+
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
276
|
+
if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
|
|
277
|
+
if (agent.output) lines.push(`Output: ${agent.output}`);
|
|
278
|
+
if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
|
|
279
|
+
if (agent.defaultProgress) lines.push("Progress: true");
|
|
280
|
+
if (agent.systemPrompt.trim()) lines.push("", "System Prompt:", agent.systemPrompt);
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function formatChainDetail(chain: ChainConfig): string {
|
|
285
|
+
const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}`, "", "Steps:"];
|
|
286
|
+
for (let i = 0; i < chain.steps.length; i++) {
|
|
287
|
+
const s = chain.steps[i]!;
|
|
288
|
+
lines.push(`${i + 1}. ${s.agent}`);
|
|
289
|
+
if (s.task.trim()) lines.push(` Task: ${s.task}`);
|
|
290
|
+
if (s.output === false) lines.push(" Output: false");
|
|
291
|
+
else if (s.output) lines.push(` Output: ${s.output}`);
|
|
292
|
+
if (s.reads === false) lines.push(" Reads: false");
|
|
293
|
+
else if (Array.isArray(s.reads) && s.reads.length > 0) lines.push(` Reads: ${s.reads.join(", ")}`);
|
|
294
|
+
if (s.model) lines.push(` Model: ${s.model}`);
|
|
295
|
+
if (s.skills === false) lines.push(" Skills: false");
|
|
296
|
+
else if (Array.isArray(s.skills) && s.skills.length > 0) lines.push(` Skills: ${s.skills.join(", ")}`);
|
|
297
|
+
if (s.progress !== undefined) lines.push(` Progress: ${s.progress ? "true" : "false"}`);
|
|
298
|
+
}
|
|
299
|
+
return lines.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
303
|
+
const scope = normalizeListScope(params.agentScope) ?? "both";
|
|
304
|
+
const d = discoverAgentsAll(ctx.cwd);
|
|
305
|
+
const agents = [...d.user, ...d.project].filter((a) => scope === "both" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
306
|
+
const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
307
|
+
const lines = ["Agents:", ...(agents.length ? agents.map((a) => `- ${a.name} (${a.source}): ${a.description}`) : ["- (none)"]), "", "Chains:", ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"])];
|
|
308
|
+
return result(lines.join("\n"));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
312
|
+
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
|
|
313
|
+
const hasBoth = Boolean(params.agent && params.chainName);
|
|
314
|
+
const blocks: string[] = [];
|
|
315
|
+
let anyFound = false;
|
|
316
|
+
if (params.agent) {
|
|
317
|
+
const matches = findAgents(params.agent, ctx.cwd, "both");
|
|
318
|
+
if (!matches.length) {
|
|
319
|
+
const msg = `Agent '${params.agent}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`;
|
|
320
|
+
if (!hasBoth) return result(msg, true);
|
|
321
|
+
blocks.push(msg);
|
|
322
|
+
} else {
|
|
323
|
+
anyFound = true;
|
|
324
|
+
blocks.push(...matches.map(formatAgentDetail));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (params.chainName) {
|
|
328
|
+
const matches = findChains(params.chainName, ctx.cwd, "both");
|
|
329
|
+
if (!matches.length) {
|
|
330
|
+
const msg = `Chain '${params.chainName}' not found. Available: ${availableNames(ctx.cwd, "chain").join(", ") || "none"}.`;
|
|
331
|
+
if (!hasBoth) return result(msg, true);
|
|
332
|
+
blocks.push(msg);
|
|
333
|
+
} else {
|
|
334
|
+
anyFound = true;
|
|
335
|
+
blocks.push(...matches.map(formatChainDetail));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return result(blocks.join("\n\n"), !anyFound);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function handleCreate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
342
|
+
const cfg = configObject(params.config);
|
|
343
|
+
if (!cfg) return result("config required for create.", true);
|
|
344
|
+
if (typeof cfg.name !== "string" || !cfg.name.trim()) return result("config.name is required and must be a non-empty string.", true);
|
|
345
|
+
if (typeof cfg.description !== "string" || !cfg.description.trim()) return result("config.description is required and must be a non-empty string.", true);
|
|
346
|
+
const name = sanitizeName(cfg.name);
|
|
347
|
+
if (!name) return result("config.name is invalid after sanitization. Use letters, numbers, spaces, or hyphens.", true);
|
|
348
|
+
const scopeRaw = cfg.scope ?? "user";
|
|
349
|
+
if (scopeRaw !== "user" && scopeRaw !== "project") return result("config.scope must be 'user' or 'project'.", true);
|
|
350
|
+
const scope = scopeRaw as ManagementScope;
|
|
351
|
+
const isChain = hasKey(cfg, "steps");
|
|
352
|
+
const d = discoverAgentsAll(ctx.cwd);
|
|
353
|
+
const targetDir = scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
|
|
354
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
355
|
+
if (nameExistsInScope(ctx.cwd, scope, name)) return result(`Name '${name}' already exists in ${scope} scope. Use update instead.`, true);
|
|
356
|
+
const targetPath = path.join(targetDir, isChain ? `${name}.chain.md` : `${name}.md`);
|
|
357
|
+
if (fs.existsSync(targetPath)) return result(`File already exists at ${targetPath} but is not a valid ${isChain ? "chain" : "agent"} definition. Remove or rename it first.`, true);
|
|
358
|
+
const warnings: string[] = [];
|
|
359
|
+
if (isChain) {
|
|
360
|
+
const parsed = parseStepList(cfg.steps);
|
|
361
|
+
if (parsed.error) return result(parsed.error, true);
|
|
362
|
+
const chain: ChainConfig = { name, description: cfg.description.trim(), source: scope, filePath: targetPath, steps: parsed.steps! };
|
|
363
|
+
fs.writeFileSync(targetPath, serializeChain(chain), "utf-8");
|
|
364
|
+
const missing = unknownChainAgents(ctx.cwd, chain.steps);
|
|
365
|
+
if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
|
|
366
|
+
warnings.push(...chainStepWarnings(ctx, chain.steps));
|
|
367
|
+
return result([`Created chain '${name}' at ${targetPath}.`, ...warnings].join("\n"));
|
|
368
|
+
}
|
|
369
|
+
const agent: AgentConfig = { name, description: cfg.description.trim(), source: scope, filePath: targetPath, systemPrompt: "" };
|
|
370
|
+
const applyError = applyAgentConfig(agent, cfg);
|
|
371
|
+
if (applyError) return result(applyError, true);
|
|
372
|
+
const mw = modelWarning(ctx, agent.model);
|
|
373
|
+
if (mw) warnings.push(mw);
|
|
374
|
+
const sw = skillsWarning(ctx.cwd, agent.skills);
|
|
375
|
+
if (sw) warnings.push(sw);
|
|
376
|
+
fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
|
|
377
|
+
return result([`Created agent '${name}' at ${targetPath}.`, ...warnings].join("\n"));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
381
|
+
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for update.", true);
|
|
382
|
+
if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
|
|
383
|
+
const cfg = configObject(params.config);
|
|
384
|
+
if (!cfg) return result("config required for update.", true);
|
|
385
|
+
const warnings: string[] = [];
|
|
386
|
+
if (params.agent) {
|
|
387
|
+
const scopeHint = asDisambiguationScope(params.agentScope);
|
|
388
|
+
const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
|
|
389
|
+
if ("content" in targetOrError) return targetOrError;
|
|
390
|
+
const target = targetOrError;
|
|
391
|
+
const updated: AgentConfig = { ...target };
|
|
392
|
+
const oldName = target.name;
|
|
393
|
+
// Validate all fields before any filesystem mutation
|
|
394
|
+
if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
|
|
395
|
+
if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
|
|
396
|
+
let newName: string | undefined;
|
|
397
|
+
if (hasKey(cfg, "name")) {
|
|
398
|
+
newName = sanitizeName(cfg.name as string);
|
|
399
|
+
if (!newName) return result("config.name is invalid after sanitization.", true);
|
|
400
|
+
}
|
|
401
|
+
const applyError = applyAgentConfig(updated, cfg);
|
|
402
|
+
if (applyError) return result(applyError, true);
|
|
403
|
+
// Apply name/description (validated above)
|
|
404
|
+
if (newName !== undefined) updated.name = newName;
|
|
405
|
+
if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
|
|
406
|
+
if (hasKey(cfg, "model")) {
|
|
407
|
+
const mw = modelWarning(ctx, updated.model);
|
|
408
|
+
if (mw) warnings.push(mw);
|
|
409
|
+
}
|
|
410
|
+
if (hasKey(cfg, "skills")) {
|
|
411
|
+
const sw = skillsWarning(ctx.cwd, updated.skills);
|
|
412
|
+
if (sw) warnings.push(sw);
|
|
413
|
+
}
|
|
414
|
+
// Filesystem mutations last
|
|
415
|
+
if (updated.name !== oldName) {
|
|
416
|
+
const renamed = renamePath("agent", target.filePath, updated.name, target.source, ctx.cwd);
|
|
417
|
+
if (renamed.error) return result(renamed.error, true);
|
|
418
|
+
updated.filePath = renamed.filePath!;
|
|
419
|
+
}
|
|
420
|
+
fs.writeFileSync(updated.filePath, serializeAgent(updated), "utf-8");
|
|
421
|
+
if (updated.name !== oldName) {
|
|
422
|
+
const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === oldName)).map((c) => `${c.name} (${c.source})`);
|
|
423
|
+
if (refs.length) warnings.push(`Warning: chains still reference '${oldName}': ${refs.join(", ")}.`);
|
|
424
|
+
}
|
|
425
|
+
const headline = updated.name === oldName
|
|
426
|
+
? `Updated agent '${updated.name}' at ${updated.filePath}.`
|
|
427
|
+
: `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
|
|
428
|
+
return result([headline, ...warnings].join("\n"));
|
|
429
|
+
}
|
|
430
|
+
const scopeHint = asDisambiguationScope(params.agentScope);
|
|
431
|
+
const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
|
|
432
|
+
if ("content" in targetOrError) return targetOrError;
|
|
433
|
+
const target = targetOrError;
|
|
434
|
+
const updated: ChainConfig = { ...target, steps: [...target.steps] };
|
|
435
|
+
const oldName = target.name;
|
|
436
|
+
// Validate all fields before any filesystem mutation
|
|
437
|
+
if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
|
|
438
|
+
if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
|
|
439
|
+
let newName: string | undefined;
|
|
440
|
+
if (hasKey(cfg, "name")) {
|
|
441
|
+
newName = sanitizeName(cfg.name as string);
|
|
442
|
+
if (!newName) return result("config.name is invalid after sanitization.", true);
|
|
443
|
+
}
|
|
444
|
+
let parsedSteps: ChainStepConfig[] | undefined;
|
|
445
|
+
if (hasKey(cfg, "steps")) {
|
|
446
|
+
const parsed = parseStepList(cfg.steps);
|
|
447
|
+
if (parsed.error) return result(parsed.error, true);
|
|
448
|
+
parsedSteps = parsed.steps!;
|
|
449
|
+
}
|
|
450
|
+
// Apply validated changes to in-memory object
|
|
451
|
+
if (newName !== undefined) updated.name = newName;
|
|
452
|
+
if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
|
|
453
|
+
if (parsedSteps) {
|
|
454
|
+
updated.steps = parsedSteps;
|
|
455
|
+
const missing = unknownChainAgents(ctx.cwd, updated.steps);
|
|
456
|
+
if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
|
|
457
|
+
warnings.push(...chainStepWarnings(ctx, updated.steps));
|
|
458
|
+
}
|
|
459
|
+
// Filesystem mutations last
|
|
460
|
+
if (updated.name !== oldName) {
|
|
461
|
+
const renamed = renamePath("chain", target.filePath, updated.name, target.source, ctx.cwd);
|
|
462
|
+
if (renamed.error) return result(renamed.error, true);
|
|
463
|
+
updated.filePath = renamed.filePath!;
|
|
464
|
+
}
|
|
465
|
+
fs.writeFileSync(updated.filePath, serializeChain(updated), "utf-8");
|
|
466
|
+
const headline = updated.name === oldName
|
|
467
|
+
? `Updated chain '${updated.name}' at ${updated.filePath}.`
|
|
468
|
+
: `Updated chain '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
|
|
469
|
+
return result([headline, ...warnings].join("\n"));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
473
|
+
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for delete.", true);
|
|
474
|
+
if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
|
|
475
|
+
const scopeHint = asDisambiguationScope(params.agentScope);
|
|
476
|
+
if (params.agent) {
|
|
477
|
+
const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
|
|
478
|
+
if ("content" in targetOrError) return targetOrError;
|
|
479
|
+
const target = targetOrError;
|
|
480
|
+
fs.unlinkSync(target.filePath);
|
|
481
|
+
const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === target.name)).map((c) => `${c.name} (${c.source})`);
|
|
482
|
+
const lines = [`Deleted agent '${target.name}' at ${target.filePath}.`];
|
|
483
|
+
if (refs.length) lines.push(`Warning: chains reference deleted agent '${target.name}': ${refs.join(", ")}.`);
|
|
484
|
+
return result(lines.join("\n"));
|
|
485
|
+
}
|
|
486
|
+
const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
|
|
487
|
+
if ("content" in targetOrError) return targetOrError;
|
|
488
|
+
const target = targetOrError;
|
|
489
|
+
fs.unlinkSync(target.filePath);
|
|
490
|
+
return result(`Deleted chain '${target.name}' at ${target.filePath}.`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function handleManagementAction(action: string, params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
494
|
+
switch (action as ManagementAction) {
|
|
495
|
+
case "list": return handleList(params, ctx);
|
|
496
|
+
case "get": return handleGet(params, ctx);
|
|
497
|
+
case "create": return handleCreate(params, ctx);
|
|
498
|
+
case "update": return handleUpdate(params, ctx);
|
|
499
|
+
case "delete": return handleDelete(params, ctx);
|
|
500
|
+
default: return result(`Unknown action: ${action}`, true);
|
|
501
|
+
}
|
|
502
|
+
}
|
package/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import { isAsyncAvailable, executeAsyncChain, executeAsyncSingle } from "./async
|
|
|
48
48
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
|
|
49
49
|
import { AgentManagerComponent, type ManagerResult } from "./agent-manager.js";
|
|
50
50
|
import { recordRun } from "./run-history.js";
|
|
51
|
+
import { handleManagementAction } from "./agent-management.js";
|
|
51
52
|
|
|
52
53
|
// ExtensionConfig is now imported from ./types.js
|
|
53
54
|
|
|
@@ -147,7 +148,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
147
148
|
const tool: ToolDefinition<typeof SubagentParams, Details> = {
|
|
148
149
|
name: "subagent",
|
|
149
150
|
label: "Subagent",
|
|
150
|
-
description: `Delegate to subagents
|
|
151
|
+
description: `Delegate to subagents or manage agent definitions.
|
|
152
|
+
|
|
153
|
+
EXECUTION (use exactly ONE mode):
|
|
151
154
|
• SINGLE: { agent, task } - one task
|
|
152
155
|
• CHAIN: { chain: [{agent:"scout"}, {agent:"planner"}] } - sequential pipeline
|
|
153
156
|
• PARALLEL: { tasks: [{agent,task}, ...] } - concurrent execution
|
|
@@ -162,12 +165,31 @@ CHAIN DATA FLOW:
|
|
|
162
165
|
2. Steps can also write files to {chain_dir} (via agent's "output" config)
|
|
163
166
|
3. Later steps can read those files (via agent's "reads" config)
|
|
164
167
|
|
|
165
|
-
Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
|
|
168
|
+
Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
|
|
169
|
+
|
|
170
|
+
MANAGEMENT (use action field — omit agent/task/chain/tasks):
|
|
171
|
+
• { action: "list" } - discover available agents and chains
|
|
172
|
+
• { action: "get", agent: "name" } - full agent detail with system prompt
|
|
173
|
+
• { action: "create", config: { name, description, systemPrompt, ... } } - create agent/chain
|
|
174
|
+
• { action: "update", agent: "name", config: { ... } } - modify fields (merge)
|
|
175
|
+
• { action: "delete", agent: "name" } - remove definition
|
|
176
|
+
• Use chainName instead of agent for chain operations`,
|
|
166
177
|
parameters: SubagentParams,
|
|
167
178
|
|
|
168
179
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
169
|
-
const scope: AgentScope = params.agentScope ?? "user";
|
|
170
180
|
baseCwd = ctx.cwd;
|
|
181
|
+
if (params.action) {
|
|
182
|
+
const validActions = ["list", "get", "create", "update", "delete"];
|
|
183
|
+
if (!validActions.includes(params.action)) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
|
|
186
|
+
isError: true,
|
|
187
|
+
details: { mode: "management" as const, results: [] },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return handleManagementAction(params.action, params, ctx);
|
|
191
|
+
}
|
|
192
|
+
const scope: AgentScope = params.agentScope ?? "user";
|
|
171
193
|
currentSessionId = ctx.sessionManager.getSessionFile() ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
172
194
|
const agents = discoverAgents(ctx.cwd, scope).agents;
|
|
173
195
|
const runId = randomUUID().slice(0, 8);
|
|
@@ -630,6 +652,13 @@ Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", tas
|
|
|
630
652
|
},
|
|
631
653
|
|
|
632
654
|
renderCall(args, theme) {
|
|
655
|
+
if (args.action) {
|
|
656
|
+
const target = args.agent || args.chainName || "";
|
|
657
|
+
return new Text(
|
|
658
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${args.action}${target ? ` ${theme.fg("accent", target)}` : ""}`,
|
|
659
|
+
0, 0,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
633
662
|
const isParallel = (args.tasks?.length ?? 0) > 0;
|
|
634
663
|
const asyncLabel = args.async === true && !isParallel ? theme.fg("warning", " [async]") : "";
|
|
635
664
|
if (args.chain?.length)
|
package/package.json
CHANGED
package/schemas.ts
CHANGED
|
@@ -59,8 +59,20 @@ export const MaxOutputSchema = Type.Optional(
|
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
export const SubagentParams = Type.Object({
|
|
62
|
-
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode)" })),
|
|
62
|
+
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
63
63
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
64
|
+
// Management action (when present, tool operates in management mode)
|
|
65
|
+
action: Type.Optional(Type.String({
|
|
66
|
+
description: "Management action: 'list' (discover agents/chains), 'get' (full detail), 'create', 'update', 'delete'. Omit for execution mode."
|
|
67
|
+
})),
|
|
68
|
+
// Chain identifier for management (can't reuse 'chain' — that's the execution array)
|
|
69
|
+
chainName: Type.Optional(Type.String({
|
|
70
|
+
description: "Chain name for get/update/delete management actions"
|
|
71
|
+
})),
|
|
72
|
+
// Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
|
|
73
|
+
config: Type.Optional(Type.Any({
|
|
74
|
+
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, model, tools (comma-separated), skills (comma-separated), thinking, output, reads, progress. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
|
|
75
|
+
})),
|
|
64
76
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task}, ...]" })),
|
|
65
77
|
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. Use {task}, {previous}, {chain_dir} in task templates." })),
|
|
66
78
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
package/types.ts
CHANGED