feat: wire agent.delegate tool with sub-agent configs
- Export createAgentDelegateTool through builtin/index.ts → tools/index.ts - Register agent.delegate in routing.ts with lazy orchestrator pattern - Add agent.delegate + agents.list to messaging and coding policy profiles - Add group:agents tool group to policy.ts - Add research/code/comms agent config examples to default.yaml - Add research/code/comms agent configs to user config.yaml - Add 11 tests for agent-delegate tool (all pass) - Typecheck clean, no regressions
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
export type ExternalBackendName = 'claude_code' | 'opencode' | 'codex' | 'gemini';
|
||||
|
||||
export interface ExternalBackendRequest {
|
||||
prompt: string;
|
||||
history: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
}
|
||||
|
||||
export interface ExternalBackend {
|
||||
name: ExternalBackendName;
|
||||
process(input: ExternalBackendRequest): Promise<string>;
|
||||
}
|
||||
|
||||
interface ExternalCliBackendOptions {
|
||||
name: ExternalBackendName;
|
||||
command: string;
|
||||
args?: string[];
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 120_000;
|
||||
|
||||
function buildPrompt(request: ExternalBackendRequest): string {
|
||||
const lines: string[] = [];
|
||||
for (const item of request.history) {
|
||||
if (!item.content.trim()) {
|
||||
continue;
|
||||
}
|
||||
lines.push(`${item.role.toUpperCase()}: ${item.content}`);
|
||||
}
|
||||
lines.push(`USER: ${request.prompt}`);
|
||||
return lines.join('\n\n');
|
||||
}
|
||||
|
||||
function inferArgs(name: ExternalBackendName, prompt: string): string[] {
|
||||
if (name === 'claude_code') {
|
||||
return ['--print', prompt];
|
||||
}
|
||||
return ['-p', prompt];
|
||||
}
|
||||
|
||||
export class ExternalCliBackend implements ExternalBackend {
|
||||
readonly name: ExternalBackendName;
|
||||
private readonly command: string;
|
||||
private readonly args: string[];
|
||||
private readonly timeoutMs: number;
|
||||
|
||||
constructor(options: ExternalCliBackendOptions) {
|
||||
this.name = options.name;
|
||||
this.command = options.command;
|
||||
this.args = options.args ?? [];
|
||||
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
async process(input: ExternalBackendRequest): Promise<string> {
|
||||
const prompt = buildPrompt(input);
|
||||
const configuredArgs = this.args
|
||||
.map((arg) => arg.includes('{prompt}') ? arg.replaceAll('{prompt}', prompt) : arg);
|
||||
const hasPromptPlaceholder = this.args.some((arg) => arg.includes('{prompt}'));
|
||||
const args = hasPromptPlaceholder
|
||||
? configuredArgs
|
||||
: [...configuredArgs, ...inferArgs(this.name, prompt)];
|
||||
|
||||
const output = await execFileAsync(this.command, args, this.timeoutMs);
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`External backend "${this.name}" returned no output`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClaudeCodeBackend extends ExternalCliBackend {
|
||||
constructor(path?: string, args?: string[], timeoutMs?: number) {
|
||||
super({ name: 'claude_code', command: path ?? 'claude', args, timeoutMs });
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenCodeBackend extends ExternalCliBackend {
|
||||
constructor(path?: string, args?: string[], timeoutMs?: number) {
|
||||
super({ name: 'opencode', command: path ?? 'opencode', args, timeoutMs });
|
||||
}
|
||||
}
|
||||
|
||||
export class CodexBackend extends ExternalCliBackend {
|
||||
constructor(path?: string, args?: string[], timeoutMs?: number) {
|
||||
super({ name: 'codex', command: path ?? 'codex', args, timeoutMs });
|
||||
}
|
||||
}
|
||||
|
||||
export class GeminiBackend extends ExternalCliBackend {
|
||||
constructor(path?: string, args?: string[], timeoutMs?: number) {
|
||||
super({ name: 'gemini', command: path ?? 'gemini', args, timeoutMs });
|
||||
}
|
||||
}
|
||||
|
||||
function execFileAsync(command: string, args: string[], timeoutMs: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`${error.message}${stderr ? `\n${stderr}` : ''}`));
|
||||
return;
|
||||
}
|
||||
resolve(stdout || '');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user