@draht/coding-agent 2026.3.3 → 2026.3.5
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 +144 -0
- package/README.md +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -12
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +14 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +167 -49
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +2 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/builtins/subagent.d.ts +14 -0
- package/dist/core/builtins/subagent.d.ts.map +1 -0
- package/dist/core/builtins/subagent.js +492 -0
- package/dist/core/builtins/subagent.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +4 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +6 -0
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +19 -8
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +1 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +9 -6
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +35 -5
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +1 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +4 -2
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resource-loader.d.ts +2 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +5 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +4 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +34 -12
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/gsd/domain-validator.d.ts +18 -0
- package/dist/gsd/domain-validator.d.ts.map +1 -0
- package/dist/gsd/domain-validator.js +61 -0
- package/dist/gsd/domain-validator.js.map +1 -0
- package/dist/gsd/domain.d.ts +12 -0
- package/dist/gsd/domain.d.ts.map +1 -0
- package/dist/gsd/domain.js +113 -0
- package/dist/gsd/domain.js.map +1 -0
- package/dist/gsd/git.d.ts +20 -0
- package/dist/gsd/git.d.ts.map +1 -0
- package/dist/gsd/git.js +59 -0
- package/dist/gsd/git.js.map +1 -0
- package/dist/gsd/hook-utils.d.ts +22 -0
- package/dist/gsd/hook-utils.d.ts.map +1 -0
- package/dist/gsd/hook-utils.js +100 -0
- package/dist/gsd/hook-utils.js.map +1 -0
- package/dist/gsd/index.d.ts +9 -0
- package/dist/gsd/index.d.ts.map +1 -0
- package/dist/gsd/index.js +8 -0
- package/dist/gsd/index.js.map +1 -0
- package/dist/gsd/planning.d.ts +20 -0
- package/dist/gsd/planning.d.ts.map +1 -0
- package/dist/gsd/planning.js +167 -0
- package/dist/gsd/planning.js.map +1 -0
- package/dist/hooks/gsd/draht-post-task.js +44 -11
- package/dist/hooks/gsd/draht-quality-gate.js +99 -57
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -5
- package/dist/main.js.map +1 -1
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/armin.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/daxnuts.js.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +9 -1
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +1 -1
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +1 -1
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +1 -1
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +28 -3
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +11 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +29 -28
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/prompts/agents/build.md +5 -1
- package/dist/prompts/agents/plan.md +5 -1
- package/dist/prompts/agents/verify.md +5 -1
- package/dist/prompts/commands/atomic-commit.md +8 -16
- package/dist/prompts/commands/discuss-phase.md +9 -3
- package/dist/prompts/commands/execute-phase.md +15 -8
- package/dist/prompts/commands/fix.md +35 -0
- package/dist/prompts/commands/init-project.md +55 -0
- package/dist/prompts/commands/map-codebase.md +7 -1
- package/dist/prompts/commands/new-project.md +8 -2
- package/dist/prompts/commands/next-milestone.md +48 -0
- package/dist/prompts/commands/pause-work.md +4 -0
- package/dist/prompts/commands/plan-phase.md +11 -5
- package/dist/prompts/commands/progress.md +4 -0
- package/dist/prompts/commands/quick.md +8 -2
- package/dist/prompts/commands/resume-work.md +4 -0
- package/dist/prompts/commands/review.md +32 -0
- package/dist/prompts/commands/verify-work.md +10 -4
- package/docs/custom-provider.md +10 -2
- package/docs/extensions.md +20 -1
- package/docs/providers.md +3 -1
- package/docs/settings.md +1 -0
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/antigravity-image-gen.ts +3 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/dynamic-tools.ts +74 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/hooks/gsd/draht-post-task.js +44 -11
- package/hooks/gsd/draht-quality-gate.js +99 -57
- package/package.json +9 -8
- package/prompts/agents/build.md +5 -1
- package/prompts/agents/plan.md +5 -1
- package/prompts/agents/verify.md +5 -1
- package/prompts/commands/atomic-commit.md +8 -16
- package/prompts/commands/discuss-phase.md +9 -3
- package/prompts/commands/execute-phase.md +15 -8
- package/prompts/commands/fix.md +35 -0
- package/prompts/commands/init-project.md +55 -0
- package/prompts/commands/map-codebase.md +7 -1
- package/prompts/commands/new-project.md +8 -2
- package/prompts/commands/next-milestone.md +48 -0
- package/prompts/commands/pause-work.md +4 -0
- package/prompts/commands/plan-phase.md +11 -5
- package/prompts/commands/progress.md +4 -0
- package/prompts/commands/quick.md +8 -2
- package/prompts/commands/resume-work.md +4 -0
- package/prompts/commands/review.md +32 -0
- package/prompts/commands/verify-work.md +10 -4
- package/dist/extensions/gsd-commands.ts +0 -403
- package/dist/extensions/subagent.ts +0 -515
- package/extensions/gsd-commands.ts +0 -403
- package/extensions/subagent.ts +0 -515
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subagent Tool for Draht — adapted from the pi subagent example
|
|
3
|
-
*
|
|
4
|
-
* Spawns isolated draht processes for delegated tasks.
|
|
5
|
-
* Agents are defined in .draht/agents/*.md (project) or ~/.draht/agent/agents/*.md (global).
|
|
6
|
-
*
|
|
7
|
-
* Modes:
|
|
8
|
-
* single — { agent, task }
|
|
9
|
-
* parallel — { tasks: [{ agent, task }] } (max 8, concurrency 4)
|
|
10
|
-
* chain — { chain: [{ agent, task }] } (sequential, {previous} placeholder)
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import * as fs from "node:fs";
|
|
15
|
-
import * as os from "node:os";
|
|
16
|
-
import * as path from "node:path";
|
|
17
|
-
import type { Message } from "@draht/ai";
|
|
18
|
-
import { StringEnum } from "@draht/ai";
|
|
19
|
-
import { type ExtensionAPI, getAgentDir, getPackageDir, isBunBinary, parseFrontmatter } from "@draht/coding-agent";
|
|
20
|
-
import { Text } from "@draht/tui";
|
|
21
|
-
import { Type } from "@sinclair/typebox";
|
|
22
|
-
|
|
23
|
-
const MAX_PARALLEL = 8;
|
|
24
|
-
const MAX_CONCURRENCY = 4;
|
|
25
|
-
|
|
26
|
-
// Build the command to spawn a subagent process.
|
|
27
|
-
// Compiled Bun binary: process.execPath IS the CLI binary, args go directly.
|
|
28
|
-
// Source/dev mode: process.execPath is the runtime (bun/node), process.argv[1] is the CLI script.
|
|
29
|
-
const DRAHT_BIN = process.execPath;
|
|
30
|
-
const DRAHT_ARGS_PREFIX: string[] = isBunBinary ? [] : [process.argv[1]];
|
|
31
|
-
|
|
32
|
-
// ─── Agent discovery ────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
interface AgentConfig {
|
|
35
|
-
name: string;
|
|
36
|
-
description: string;
|
|
37
|
-
tools?: string[];
|
|
38
|
-
model?: string;
|
|
39
|
-
systemPrompt: string;
|
|
40
|
-
source: "user" | "project";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
44
|
-
if (!fs.existsSync(dir)) return [];
|
|
45
|
-
let entries: fs.Dirent[];
|
|
46
|
-
try {
|
|
47
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
-
} catch {
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
const agents: AgentConfig[] = [];
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
54
|
-
try {
|
|
55
|
-
const content = fs.readFileSync(path.join(dir, entry.name), "utf-8");
|
|
56
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
57
|
-
if (!frontmatter.name || !frontmatter.description) continue;
|
|
58
|
-
const tools = frontmatter.tools?.split(",").map((t: string) => t.trim()).filter(Boolean);
|
|
59
|
-
agents.push({
|
|
60
|
-
name: frontmatter.name,
|
|
61
|
-
description: frontmatter.description,
|
|
62
|
-
tools: tools?.length ? tools : undefined,
|
|
63
|
-
model: frontmatter.model,
|
|
64
|
-
systemPrompt: body,
|
|
65
|
-
source,
|
|
66
|
-
});
|
|
67
|
-
} catch {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return agents;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function findProjectAgentsDir(cwd: string): string | null {
|
|
75
|
-
let dir = cwd;
|
|
76
|
-
while (true) {
|
|
77
|
-
const candidate = path.join(dir, ".draht", "agents");
|
|
78
|
-
try {
|
|
79
|
-
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
80
|
-
} catch {}
|
|
81
|
-
const parent = path.dirname(dir);
|
|
82
|
-
if (parent === dir) return null;
|
|
83
|
-
dir = parent;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type AgentScope = "user" | "project" | "both";
|
|
88
|
-
|
|
89
|
-
function discoverAgents(cwd: string, scope: AgentScope): AgentConfig[] {
|
|
90
|
-
// Shipped agents (bundled with the package) — lowest priority
|
|
91
|
-
const shippedDir = path.join(getPackageDir(), "agents");
|
|
92
|
-
const shippedAgents = loadAgentsFromDir(shippedDir, "user");
|
|
93
|
-
|
|
94
|
-
const userDir = path.join(getAgentDir(), "agents");
|
|
95
|
-
const projectDir = findProjectAgentsDir(cwd);
|
|
96
|
-
const userAgents = scope !== "project" ? loadAgentsFromDir(userDir, "user") : [];
|
|
97
|
-
const projectAgents = scope !== "user" && projectDir ? loadAgentsFromDir(projectDir, "project") : [];
|
|
98
|
-
|
|
99
|
-
// Priority: shipped < user < project
|
|
100
|
-
const map = new Map<string, AgentConfig>();
|
|
101
|
-
for (const a of shippedAgents) map.set(a.name, a);
|
|
102
|
-
for (const a of userAgents) map.set(a.name, a);
|
|
103
|
-
for (const a of projectAgents) map.set(a.name, a);
|
|
104
|
-
return Array.from(map.values());
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ─── Runner ─────────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
interface RunResult {
|
|
110
|
-
agent: string;
|
|
111
|
-
task: string;
|
|
112
|
-
exitCode: number;
|
|
113
|
-
output: string;
|
|
114
|
-
stderr: string;
|
|
115
|
-
step?: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function writeTemp(name: string, content: string): { file: string; dir: string } {
|
|
119
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "draht-subagent-"));
|
|
120
|
-
const file = path.join(dir, `${name.replace(/[^\w.-]/g, "_")}.md`);
|
|
121
|
-
fs.writeFileSync(file, content, { encoding: "utf-8", mode: 0o600 });
|
|
122
|
-
return { file, dir };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function cleanTemp(file: string, dir: string) {
|
|
126
|
-
try { fs.unlinkSync(file); } catch {}
|
|
127
|
-
try { fs.rmdirSync(dir); } catch {}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function getFinalText(messages: Message[]): string {
|
|
131
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
132
|
-
const msg = messages[i];
|
|
133
|
-
if (msg.role === "assistant") {
|
|
134
|
-
for (const part of msg.content) {
|
|
135
|
-
if (part.type === "text") return part.text;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return "";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
type ProgressFn = (activity: string) => void;
|
|
143
|
-
|
|
144
|
-
async function runAgent(
|
|
145
|
-
cwd: string,
|
|
146
|
-
agent: AgentConfig,
|
|
147
|
-
task: string,
|
|
148
|
-
signal?: AbortSignal,
|
|
149
|
-
step?: number,
|
|
150
|
-
onProgress?: ProgressFn,
|
|
151
|
-
): Promise<RunResult> {
|
|
152
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
153
|
-
if (agent.model) args.push("--model", agent.model);
|
|
154
|
-
if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
|
|
155
|
-
|
|
156
|
-
let tmpFile: string | null = null;
|
|
157
|
-
let tmpDir: string | null = null;
|
|
158
|
-
|
|
159
|
-
if (agent.systemPrompt.trim()) {
|
|
160
|
-
const tmp = writeTemp(agent.name, agent.systemPrompt);
|
|
161
|
-
tmpFile = tmp.file;
|
|
162
|
-
tmpDir = tmp.dir;
|
|
163
|
-
args.push("--append-system-prompt", tmpFile);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
args.push(`Task: ${task}`);
|
|
167
|
-
|
|
168
|
-
const messages: Message[] = [];
|
|
169
|
-
let stderr = "";
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const exitCode = await new Promise<number>((resolve) => {
|
|
173
|
-
const proc = spawn(DRAHT_BIN, [...DRAHT_ARGS_PREFIX, ...args], { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
174
|
-
let buf = "";
|
|
175
|
-
|
|
176
|
-
const processLine = (line: string) => {
|
|
177
|
-
if (!line.trim()) return;
|
|
178
|
-
try {
|
|
179
|
-
const event = JSON.parse(line);
|
|
180
|
-
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
181
|
-
messages.push(event.message as Message);
|
|
182
|
-
}
|
|
183
|
-
// Stream activity updates to the parent
|
|
184
|
-
if (onProgress) {
|
|
185
|
-
if (event.type === "tool_execution_start") {
|
|
186
|
-
const toolArgs = event.args ?? {};
|
|
187
|
-
const detail = toolArgs.command || toolArgs.path || toolArgs.file_path || toolArgs.pattern || "";
|
|
188
|
-
const short = typeof detail === "string" && detail.length > 60 ? `${detail.slice(0, 60)}...` : detail;
|
|
189
|
-
onProgress(short ? `${event.toolName} ${short}` : event.toolName);
|
|
190
|
-
} else if (event.type === "text_delta") {
|
|
191
|
-
// Show first line of streaming text as activity
|
|
192
|
-
const text = event.text?.trim();
|
|
193
|
-
if (text) {
|
|
194
|
-
const firstLine = text.split("\n")[0];
|
|
195
|
-
const short = firstLine.length > 80 ? `${firstLine.slice(0, 80)}...` : firstLine;
|
|
196
|
-
onProgress(short);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch {}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
proc.stdout.on("data", (d) => {
|
|
204
|
-
buf += d.toString();
|
|
205
|
-
const lines = buf.split("\n");
|
|
206
|
-
buf = lines.pop() || "";
|
|
207
|
-
for (const l of lines) processLine(l);
|
|
208
|
-
});
|
|
209
|
-
proc.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
210
|
-
proc.on("close", (code) => {
|
|
211
|
-
if (buf.trim()) processLine(buf);
|
|
212
|
-
resolve(code ?? 0);
|
|
213
|
-
});
|
|
214
|
-
proc.on("error", () => resolve(1));
|
|
215
|
-
|
|
216
|
-
if (signal) {
|
|
217
|
-
const kill = () => { proc.kill("SIGTERM"); setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000); };
|
|
218
|
-
if (signal.aborted) kill();
|
|
219
|
-
else signal.addEventListener("abort", kill, { once: true });
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
return { agent: agent.name, task, exitCode, output: getFinalText(messages), stderr, step };
|
|
224
|
-
} finally {
|
|
225
|
-
if (tmpFile && tmpDir) cleanTemp(tmpFile, tmpDir);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function runParallel<T>(
|
|
230
|
-
items: T[],
|
|
231
|
-
concurrency: number,
|
|
232
|
-
fn: (item: T, i: number) => Promise<T extends unknown ? RunResult : never>,
|
|
233
|
-
): Promise<RunResult[]> {
|
|
234
|
-
const results: RunResult[] = new Array(items.length);
|
|
235
|
-
let next = 0;
|
|
236
|
-
await Promise.all(
|
|
237
|
-
Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
238
|
-
while (true) {
|
|
239
|
-
const i = next++;
|
|
240
|
-
if (i >= items.length) return;
|
|
241
|
-
results[i] = await (fn as (item: T, i: number) => Promise<RunResult>)(items[i], i);
|
|
242
|
-
}
|
|
243
|
-
}),
|
|
244
|
-
);
|
|
245
|
-
return results;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ─── Extension ──────────────────────────────────────────────────────────────
|
|
249
|
-
|
|
250
|
-
const TaskItem = Type.Object({
|
|
251
|
-
agent: Type.String({ description: "Agent name" }),
|
|
252
|
-
task: Type.String({ description: "Task description" }),
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const ChainItem = Type.Object({
|
|
256
|
-
agent: Type.String({ description: "Agent name" }),
|
|
257
|
-
task: Type.String({ description: "Task, optionally using {previous} placeholder" }),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const Params = Type.Object({
|
|
261
|
-
agent: Type.Optional(Type.String()),
|
|
262
|
-
task: Type.Optional(Type.String()),
|
|
263
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "Parallel tasks" })),
|
|
264
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "Chained tasks" })),
|
|
265
|
-
agentScope: Type.Optional(
|
|
266
|
-
StringEnum(["user", "project", "both"] as const, { default: "both" }),
|
|
267
|
-
),
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
// ─── Rendering helpers ──────────────────────────────────────────────────────
|
|
271
|
-
|
|
272
|
-
function truncateTask(task: string, maxLen = 120): string {
|
|
273
|
-
const oneLine = task.replace(/\n/g, " ").trim();
|
|
274
|
-
return oneLine.length > maxLen ? `${oneLine.slice(0, maxLen)}...` : oneLine;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
interface SubagentDetails {
|
|
278
|
-
status?: string;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export default function (pi: ExtensionAPI) {
|
|
282
|
-
pi.registerTool({
|
|
283
|
-
name: "subagent",
|
|
284
|
-
label: "Subagent",
|
|
285
|
-
description:
|
|
286
|
-
"Delegate to specialized agents. single: {agent,task} | parallel: {tasks:[]} | chain: {chain:[]} with {previous} placeholder. agentScope: 'both' (default) uses project .draht/agents/ + global.",
|
|
287
|
-
parameters: Params,
|
|
288
|
-
|
|
289
|
-
renderCall(args, theme) {
|
|
290
|
-
const lines: string[] = [];
|
|
291
|
-
|
|
292
|
-
if (args.chain?.length) {
|
|
293
|
-
const agents = args.chain.map((s: { agent: string }) => s.agent).join(" -> ");
|
|
294
|
-
lines.push(theme.fg("toolTitle", theme.bold(`subagent chain`)) + " " + theme.fg("accent", agents));
|
|
295
|
-
for (let i = 0; i < args.chain.length; i++) {
|
|
296
|
-
const step = args.chain[i];
|
|
297
|
-
const prefix = theme.fg("muted", ` ${i + 1}.`);
|
|
298
|
-
lines.push(`${prefix} ${theme.fg("accent", step.agent)} ${theme.fg("toolOutput", truncateTask(step.task))}`);
|
|
299
|
-
}
|
|
300
|
-
} else if (args.tasks?.length) {
|
|
301
|
-
lines.push(theme.fg("toolTitle", theme.bold(`subagent parallel`)) + theme.fg("muted", ` (${args.tasks.length} tasks)`));
|
|
302
|
-
for (const t of args.tasks) {
|
|
303
|
-
lines.push(` ${theme.fg("accent", t.agent)} ${theme.fg("toolOutput", truncateTask(t.task))}`);
|
|
304
|
-
}
|
|
305
|
-
} else if (args.agent) {
|
|
306
|
-
lines.push(
|
|
307
|
-
theme.fg("toolTitle", theme.bold("subagent")) +
|
|
308
|
-
" " +
|
|
309
|
-
theme.fg("accent", args.agent) +
|
|
310
|
-
(args.task ? " " + theme.fg("toolOutput", truncateTask(args.task)) : ""),
|
|
311
|
-
);
|
|
312
|
-
} else {
|
|
313
|
-
lines.push(theme.fg("toolTitle", theme.bold("subagent")));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
renderResult(result, options, theme) {
|
|
320
|
-
const status = result.details?.status;
|
|
321
|
-
const output = result.content
|
|
322
|
-
?.filter((c) => c.type === "text")
|
|
323
|
-
.map((c) => ("text" in c ? c.text : ""))
|
|
324
|
-
.join("\n") || "";
|
|
325
|
-
|
|
326
|
-
const lines: string[] = [];
|
|
327
|
-
|
|
328
|
-
if (status) {
|
|
329
|
-
lines.push(theme.fg("muted", status));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (output.trim()) {
|
|
333
|
-
const trimmed = output.trim();
|
|
334
|
-
if (options.expanded || options.isPartial) {
|
|
335
|
-
// When expanded or during execution, show full activity log
|
|
336
|
-
for (const line of trimmed.split("\n")) {
|
|
337
|
-
lines.push(theme.fg("toolOutput", line));
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
// Collapsed final result — show preview
|
|
341
|
-
const allLines = trimmed.split("\n");
|
|
342
|
-
const previewLines = allLines.slice(0, 8);
|
|
343
|
-
lines.push(theme.fg("toolOutput", previewLines.join("\n")));
|
|
344
|
-
if (allLines.length > 8) {
|
|
345
|
-
lines.push(theme.fg("muted", `... (${allLines.length - 8} more lines)`));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (lines.length === 0) return new Text("", 0, 0);
|
|
351
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
352
|
-
},
|
|
353
|
-
|
|
354
|
-
async execute(_id, params, signal, onUpdate, ctx) {
|
|
355
|
-
const scope: AgentScope = (params.agentScope as AgentScope) ?? "both";
|
|
356
|
-
const agents = discoverAgents(ctx.cwd, scope);
|
|
357
|
-
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
358
|
-
|
|
359
|
-
const find = (name: string) => agents.find((a) => a.name === name);
|
|
360
|
-
const notFound = (name: string) => ({
|
|
361
|
-
content: [{ type: "text" as const, text: `Unknown agent "${name}". Available: ${available}` }],
|
|
362
|
-
isError: true,
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// Stream live activity from the subagent process
|
|
366
|
-
const activityLines: string[] = [];
|
|
367
|
-
const emitProgress = (agentName: string, activity?: string) => {
|
|
368
|
-
const status = activity ? `${agentName}: ${activity}` : `${agentName} working...`;
|
|
369
|
-
onUpdate?.({
|
|
370
|
-
content: [{ type: "text" as const, text: activityLines.join("\n") }],
|
|
371
|
-
details: { status },
|
|
372
|
-
});
|
|
373
|
-
};
|
|
374
|
-
const makeProgressFn = (agentName: string): ProgressFn => (activity) => {
|
|
375
|
-
// Keep a rolling log of recent activities
|
|
376
|
-
activityLines.push(`${agentName}: ${activity}`);
|
|
377
|
-
if (activityLines.length > 50) activityLines.splice(0, activityLines.length - 50);
|
|
378
|
-
emitProgress(agentName, activity);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// ── Chain mode ──
|
|
382
|
-
if (params.chain?.length) {
|
|
383
|
-
let previous = "";
|
|
384
|
-
const results: RunResult[] = [];
|
|
385
|
-
for (let i = 0; i < params.chain.length; i++) {
|
|
386
|
-
const step = params.chain[i];
|
|
387
|
-
const agent = find(step.agent);
|
|
388
|
-
if (!agent) return notFound(step.agent);
|
|
389
|
-
activityLines.length = 0;
|
|
390
|
-
emitProgress(step.agent, `step ${i + 1}/${params.chain.length}`);
|
|
391
|
-
const task = step.task.replace(/\{previous\}/g, previous);
|
|
392
|
-
const result = await runAgent(ctx.cwd, agent, task, signal, i + 1, makeProgressFn(step.agent));
|
|
393
|
-
results.push(result);
|
|
394
|
-
if (result.exitCode !== 0) {
|
|
395
|
-
return {
|
|
396
|
-
content: [{ type: "text" as const, text: `Chain failed at step ${i + 1} (${step.agent}):\n${result.output || result.stderr}` }],
|
|
397
|
-
isError: true,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
previous = result.output;
|
|
401
|
-
}
|
|
402
|
-
return { content: [{ type: "text" as const, text: results[results.length - 1].output || "(no output)" }] };
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ── Parallel mode ──
|
|
406
|
-
if (params.tasks?.length) {
|
|
407
|
-
if (params.tasks.length > MAX_PARALLEL) {
|
|
408
|
-
return { content: [{ type: "text" as const, text: `Too many tasks (max ${MAX_PARALLEL})` }], isError: true };
|
|
409
|
-
}
|
|
410
|
-
for (const t of params.tasks) { if (!find(t.agent)) return notFound(t.agent); }
|
|
411
|
-
|
|
412
|
-
const agentNames = params.tasks.map((t: { agent: string }) => t.agent).join(", ");
|
|
413
|
-
emitProgress(agentNames);
|
|
414
|
-
|
|
415
|
-
const results = await runParallel(params.tasks, MAX_CONCURRENCY, async (t, i) => {
|
|
416
|
-
return runAgent(ctx.cwd, find(t.agent)!, t.task, signal, undefined, makeProgressFn(t.agent));
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
const ok = results.filter((r) => r.exitCode === 0).length;
|
|
420
|
-
const summary = results
|
|
421
|
-
.map((r) => `[${r.agent}] ${r.exitCode === 0 ? "ok" : "fail"} ${r.output.slice(0, 200)}`)
|
|
422
|
-
.join("\n\n");
|
|
423
|
-
return { content: [{ type: "text" as const, text: `Parallel: ${ok}/${results.length} succeeded\n\n${summary}` }] };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ── Single mode ──
|
|
427
|
-
if (params.agent && params.task) {
|
|
428
|
-
const agent = find(params.agent);
|
|
429
|
-
if (!agent) return notFound(params.agent);
|
|
430
|
-
emitProgress(params.agent);
|
|
431
|
-
const result = await runAgent(ctx.cwd, agent, params.task, signal, undefined, makeProgressFn(params.agent));
|
|
432
|
-
const isError = result.exitCode !== 0;
|
|
433
|
-
return {
|
|
434
|
-
content: [{ type: "text" as const, text: result.output || result.stderr || "(no output)" }],
|
|
435
|
-
...(isError ? { isError: true } : {}),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return { content: [{ type: "text" as const, text: `Provide exactly one mode. Available agents: ${available}` }], isError: true };
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// ── Agent selection for user prompts ─────────────────────────────────────
|
|
444
|
-
|
|
445
|
-
let selectedAgent: string | undefined;
|
|
446
|
-
|
|
447
|
-
function updateAgentStatus(ctx: { ui: { setStatus: (key: string, text: string | undefined) => void } }) {
|
|
448
|
-
ctx.ui.setStatus("agent", selectedAgent ? `agent: ${selectedAgent}` : undefined);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// /agent command — select an agent or clear selection
|
|
452
|
-
pi.registerCommand("agent", {
|
|
453
|
-
description: "Select an agent to handle your next prompts, or clear selection. Usage: /agent [name]",
|
|
454
|
-
handler: async (args, ctx) => {
|
|
455
|
-
const agents = discoverAgents(ctx.cwd, "both");
|
|
456
|
-
|
|
457
|
-
if (args.trim()) {
|
|
458
|
-
// Direct selection: /agent architect
|
|
459
|
-
const name = args.trim();
|
|
460
|
-
if (name === "none" || name === "off" || name === "clear") {
|
|
461
|
-
selectedAgent = undefined;
|
|
462
|
-
updateAgentStatus(ctx);
|
|
463
|
-
ctx.ui.notify("Agent cleared — prompts go to default model", "info");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const agent = agents.find((a) => a.name === name);
|
|
467
|
-
if (!agent) {
|
|
468
|
-
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
469
|
-
ctx.ui.notify(`Unknown agent "${name}". Available: ${available}`, "warning");
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
selectedAgent = name;
|
|
473
|
-
updateAgentStatus(ctx);
|
|
474
|
-
ctx.ui.notify(`Agent set to "${name}" — your prompts will be handled by this agent`, "info");
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Interactive selection
|
|
479
|
-
if (!ctx.hasUI) {
|
|
480
|
-
ctx.ui.notify("Usage: /agent <name> or /agent none", "warning");
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const options = ["(none — default model)", ...agents.map((a) => `${a.name} — ${a.description}`)];
|
|
485
|
-
const choice = await ctx.ui.select("Select agent for your prompts", options);
|
|
486
|
-
if (choice === undefined) return; // cancelled
|
|
487
|
-
|
|
488
|
-
if (choice === options[0]) {
|
|
489
|
-
selectedAgent = undefined;
|
|
490
|
-
updateAgentStatus(ctx);
|
|
491
|
-
ctx.ui.notify("Agent cleared", "info");
|
|
492
|
-
} else {
|
|
493
|
-
const name = choice.split(" — ")[0];
|
|
494
|
-
selectedAgent = name;
|
|
495
|
-
updateAgentStatus(ctx);
|
|
496
|
-
ctx.ui.notify(`Agent set to "${name}"`, "info");
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
getArgumentCompletions: (partial) => {
|
|
500
|
-
const agents = discoverAgents(process.cwd(), "both");
|
|
501
|
-
const names = ["none", ...agents.map((a) => a.name)];
|
|
502
|
-
return names.filter((n) => n.startsWith(partial));
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// Intercept user input when an agent is selected
|
|
507
|
-
pi.on("input", (event) => {
|
|
508
|
-
if (!selectedAgent) return { action: "continue" as const };
|
|
509
|
-
// Don't intercept slash commands
|
|
510
|
-
if (event.text.startsWith("/") || event.text.startsWith("!")) return { action: "continue" as const };
|
|
511
|
-
|
|
512
|
-
const wrapped = `Use the subagent tool to delegate to the "${selectedAgent}" agent with this task:\n\n"${event.text}"\n\nSet agentScope to "both".`;
|
|
513
|
-
return { action: "transform" as const, text: wrapped, images: event.images };
|
|
514
|
-
});
|
|
515
|
-
}
|