@kinqs/brainrouter-cli 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/changelog/0.2.0.md +15 -0
- package/changelog/0.3.0.md +20 -0
- package/changelog/0.3.1.md +22 -0
- package/changelog/0.3.2.md +15 -0
- package/changelog/0.3.3.md +19 -0
- package/changelog/0.3.4.md +20 -0
- package/changelog/0.3.5.md +9 -0
- package/changelog/0.3.6.md +9 -0
- package/changelog/0.3.7.md +20 -0
- package/changelog/0.3.8.md +30 -0
- package/changelog/README.md +41 -0
- package/dist/agent/agent.d.ts +34 -1
- package/dist/agent/agent.js +372 -79
- package/dist/agent/toolCallRecovery.d.ts +57 -0
- package/dist/agent/toolCallRecovery.js +130 -0
- package/dist/agent/toolSafety.d.ts +17 -0
- package/dist/agent/toolSafety.js +102 -0
- package/dist/cli/banner.d.ts +20 -0
- package/dist/cli/banner.js +47 -14
- package/dist/cli/cliPrompt.d.ts +40 -3
- package/dist/cli/cliPrompt.js +117 -25
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +13 -11
- package/dist/cli/commands/mcp.js +261 -74
- package/dist/cli/commands/mcpInstall.d.ts +20 -0
- package/dist/cli/commands/mcpInstall.js +87 -0
- package/dist/cli/commands/orchestration.js +51 -0
- package/dist/cli/commands/releaseNotes.d.ts +24 -0
- package/dist/cli/commands/releaseNotes.js +109 -0
- package/dist/cli/commands/schedule.d.ts +18 -0
- package/dist/cli/commands/schedule.js +189 -0
- package/dist/cli/commands/ui.js +119 -60
- package/dist/cli/commands/workflow.d.ts +2 -0
- package/dist/cli/commands/workflow.js +54 -8
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +71 -0
- package/dist/cli/ink/Picker.js +168 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +682 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +75 -0
- package/dist/cli/ink/toolFormat.js +206 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +52 -714
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +13 -1
- package/dist/config/config.js +45 -3
- package/dist/index.js +157 -206
- package/dist/memory/briefing.d.ts +1 -1
- package/dist/memory/briefing.js +4 -4
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +105 -3
- package/dist/orchestration/tools.js +167 -8
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.js +7 -2
- package/dist/runtime/anthropicAdapter.d.ts +100 -0
- package/dist/runtime/anthropicAdapter.js +293 -0
- package/dist/runtime/cronParser.d.ts +23 -0
- package/dist/runtime/cronParser.js +122 -0
- package/dist/runtime/mcpClient.js +14 -11
- package/dist/runtime/mcpPool.d.ts +170 -0
- package/dist/runtime/mcpPool.js +442 -0
- package/dist/runtime/mcpUtils.d.ts +17 -1
- package/dist/runtime/mcpUtils.js +23 -0
- package/dist/runtime/scheduleTicker.d.ts +33 -0
- package/dist/runtime/scheduleTicker.js +99 -0
- package/dist/runtime/vendorSnippets.d.ts +45 -0
- package/dist/runtime/vendorSnippets.js +153 -0
- package/dist/state/scheduleStore.d.ts +37 -0
- package/dist/state/scheduleStore.js +64 -0
- package/package.json +14 -5
- package/.env.example +0 -116
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type Tier = 'chat' | 'reasoning' | 'worker';
|
|
2
|
+
export type AccessMode = 'read' | 'write' | 'shell';
|
|
3
|
+
export interface AgentDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
whenToUse: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
model: string | null;
|
|
9
|
+
effort: string | null;
|
|
10
|
+
defaultAccess: AccessMode;
|
|
11
|
+
toolScope: {
|
|
12
|
+
local: string[];
|
|
13
|
+
mcp: string[];
|
|
14
|
+
};
|
|
15
|
+
disallowedTools: string[];
|
|
16
|
+
maxIterations: number;
|
|
17
|
+
timeoutMs: number;
|
|
18
|
+
maxResultChars: number;
|
|
19
|
+
subagents: string[];
|
|
20
|
+
delegateName: string;
|
|
21
|
+
tier: Tier;
|
|
22
|
+
outputContract: unknown;
|
|
23
|
+
}
|
|
24
|
+
export type DefinitionSource = 'builtin' | 'user' | 'workspace';
|
|
25
|
+
export interface LoadedDefinition {
|
|
26
|
+
def: AgentDefinition;
|
|
27
|
+
source: DefinitionSource;
|
|
28
|
+
filePath: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load all agent definitions from three tiers (builtin → user-global → workspace).
|
|
32
|
+
* Same `id` from a higher-priority source wins; distinct ids coexist.
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadRegistry(workspaceRoot?: string): LoadedDefinition[];
|
|
35
|
+
export declare function findById(id: string, workspaceRoot?: string): LoadedDefinition | undefined;
|
|
36
|
+
export declare function listAll(workspaceRoot?: string): LoadedDefinition[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
// Resolved at import time from dist/orchestration/agentRegistry.js → ../../agents
|
|
6
|
+
const BUILTIN_AGENTS_DIR = fileURLToPath(new URL('../../agents', import.meta.url));
|
|
7
|
+
function getUserAgentsDir() {
|
|
8
|
+
const home = process.env.BRAINROUTER_HOME ?? path.join(os.homedir(), '.config', 'brainrouter');
|
|
9
|
+
return path.join(home, 'agents');
|
|
10
|
+
}
|
|
11
|
+
function getWorkspaceAgentsDir(workspaceRoot) {
|
|
12
|
+
return path.join(workspaceRoot, '.brainrouter', 'agents');
|
|
13
|
+
}
|
|
14
|
+
function loadFromDir(dir, source) {
|
|
15
|
+
if (!fs.existsSync(dir))
|
|
16
|
+
return [];
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const filePath = path.join(dir, entry);
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
29
|
+
const def = JSON.parse(raw);
|
|
30
|
+
if (!def.id || typeof def.id !== 'string') {
|
|
31
|
+
console.error(`[agentRegistry] Skipping ${filePath}: missing or invalid "id" field.`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
results.push({ def, source, filePath });
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.error(`[agentRegistry] Skipping ${filePath}: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Load all agent definitions from three tiers (builtin → user-global → workspace).
|
|
44
|
+
* Same `id` from a higher-priority source wins; distinct ids coexist.
|
|
45
|
+
*/
|
|
46
|
+
export function loadRegistry(workspaceRoot) {
|
|
47
|
+
const builtin = loadFromDir(BUILTIN_AGENTS_DIR, 'builtin');
|
|
48
|
+
const user = loadFromDir(getUserAgentsDir(), 'user');
|
|
49
|
+
const workspace = workspaceRoot
|
|
50
|
+
? loadFromDir(getWorkspaceAgentsDir(workspaceRoot), 'workspace')
|
|
51
|
+
: [];
|
|
52
|
+
// Precedence: builtin first (lowest), workspace last (highest).
|
|
53
|
+
const merged = new Map();
|
|
54
|
+
for (const loaded of [...builtin, ...user, ...workspace]) {
|
|
55
|
+
merged.set(loaded.def.id, loaded);
|
|
56
|
+
}
|
|
57
|
+
return Array.from(merged.values());
|
|
58
|
+
}
|
|
59
|
+
export function findById(id, workspaceRoot) {
|
|
60
|
+
return loadRegistry(workspaceRoot).find((l) => l.def.id === id);
|
|
61
|
+
}
|
|
62
|
+
export function listAll(workspaceRoot) {
|
|
63
|
+
return loadRegistry(workspaceRoot);
|
|
64
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type AccessMode } from './roles.js';
|
|
2
|
+
import type { Tier } from './agentRegistry.js';
|
|
2
3
|
export type ChildStatus = 'pending' | 'running' | 'completed' | 'failed' | 'stale' | 'closed';
|
|
3
4
|
export interface ChildSessionRecord {
|
|
4
5
|
id: string;
|
|
@@ -14,6 +15,10 @@ export interface ChildSessionRecord {
|
|
|
14
15
|
pid: number;
|
|
15
16
|
finalOutput?: string;
|
|
16
17
|
error?: string;
|
|
18
|
+
/** Agent tier from the definition (reasoning | worker). Undefined for legacy records. */
|
|
19
|
+
tier?: Tier;
|
|
20
|
+
/** Nesting depth in the spawn chain; 0 = direct child of the chat root. */
|
|
21
|
+
depth?: number;
|
|
17
22
|
/** LLM usage attributable to this child (filled when the child completes). */
|
|
18
23
|
usage?: {
|
|
19
24
|
promptTokens: number;
|
|
@@ -30,6 +35,8 @@ export declare function createSession(workspaceRoot: string, input: {
|
|
|
30
35
|
parentSessionKey: string;
|
|
31
36
|
access?: AccessMode;
|
|
32
37
|
label?: string;
|
|
38
|
+
tier?: Tier;
|
|
39
|
+
depth?: number;
|
|
33
40
|
}): ChildSessionRecord;
|
|
34
41
|
export declare function updateSession(workspaceRoot: string, id: string, patch: Partial<ChildSessionRecord>): ChildSessionRecord;
|
|
35
42
|
export declare function reconcileStale(workspaceRoot: string): number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { McpClientWrapper } from '../runtime/
|
|
1
|
+
import type { McpClientPool as McpClientWrapper } from '../runtime/mcpPool.js';
|
|
2
2
|
import type { LLMConfig } from '../config/config.js';
|
|
3
3
|
import { type AccessMode } from './roles.js';
|
|
4
|
+
import { type Tier } from './agentRegistry.js';
|
|
4
5
|
export interface OrchestrationContext {
|
|
5
6
|
workspaceRoot: string;
|
|
6
7
|
parentSessionKey: string;
|
|
@@ -21,18 +22,35 @@ export interface OrchestrationContext {
|
|
|
21
22
|
parentSpanId?: string;
|
|
22
23
|
/** Parent agent_id so children can be grouped via attribute even without trace links. */
|
|
23
24
|
parentAgentId?: string;
|
|
25
|
+
/** Parent agent tier — used for hierarchy checks (worker cannot spawn; reasoning can only spawn workers). */
|
|
26
|
+
parentTier?: Tier;
|
|
27
|
+
/** Current spawn-chain depth (0 = direct child of chat root). */
|
|
28
|
+
depth?: number;
|
|
24
29
|
mcpClient: McpClientWrapper;
|
|
25
30
|
llmConfig: LLMConfig;
|
|
26
31
|
launchCwd: string;
|
|
27
32
|
/** Called when a child output got offloaded — chars beyond preview that didn't land in parent context. */
|
|
28
33
|
recordOffload?: (charsAvoided: number) => void;
|
|
29
|
-
/**
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Paired child tool lifecycle callbacks. Fire from the child agent's
|
|
36
|
+
* onToolStart / onToolEnd so the parent's REPL can render explicit
|
|
37
|
+
* "child began X" / "child finished X" rows in the scrollback — without
|
|
38
|
+
* these, long child runs look like the parent has frozen (roadmap §3).
|
|
39
|
+
*/
|
|
40
|
+
onChildToolStart?: (event: {
|
|
41
|
+
childId: string;
|
|
42
|
+
role: string;
|
|
43
|
+
tool: string;
|
|
44
|
+
args: Record<string, any>;
|
|
45
|
+
}) => void;
|
|
46
|
+
onChildToolEnd?: (event: {
|
|
31
47
|
childId: string;
|
|
32
48
|
role: string;
|
|
33
49
|
tool: string;
|
|
34
50
|
ok: boolean;
|
|
35
51
|
summary: string;
|
|
52
|
+
preview?: string;
|
|
53
|
+
durationMs: number;
|
|
36
54
|
}) => void;
|
|
37
55
|
/**
|
|
38
56
|
* Called when a child agent's runTurn ends — success, fail, or empty answer.
|
|
@@ -76,6 +94,10 @@ export declare function createSpawnAgentTool(): {
|
|
|
76
94
|
type: string;
|
|
77
95
|
description: string;
|
|
78
96
|
};
|
|
97
|
+
agentId: {
|
|
98
|
+
type: string;
|
|
99
|
+
description: string;
|
|
100
|
+
};
|
|
79
101
|
prompt: {
|
|
80
102
|
type: string;
|
|
81
103
|
description: string;
|
|
@@ -108,6 +130,86 @@ export declare function createSpawnAgentTool(): {
|
|
|
108
130
|
required: string[];
|
|
109
131
|
};
|
|
110
132
|
};
|
|
133
|
+
export declare function createTaskAgentTool(): {
|
|
134
|
+
name: string;
|
|
135
|
+
description: string;
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: string;
|
|
138
|
+
properties: {
|
|
139
|
+
role: {
|
|
140
|
+
type: string;
|
|
141
|
+
description: string;
|
|
142
|
+
};
|
|
143
|
+
agentId: {
|
|
144
|
+
type: string;
|
|
145
|
+
description: string;
|
|
146
|
+
};
|
|
147
|
+
prompt: {
|
|
148
|
+
type: string;
|
|
149
|
+
description: string;
|
|
150
|
+
};
|
|
151
|
+
label: {
|
|
152
|
+
type: string;
|
|
153
|
+
description: string;
|
|
154
|
+
};
|
|
155
|
+
access: {
|
|
156
|
+
type: string;
|
|
157
|
+
enum: string[];
|
|
158
|
+
description: string;
|
|
159
|
+
};
|
|
160
|
+
timeoutMs: {
|
|
161
|
+
type: string;
|
|
162
|
+
description: string;
|
|
163
|
+
};
|
|
164
|
+
seedRecordIds: {
|
|
165
|
+
type: string;
|
|
166
|
+
items: {
|
|
167
|
+
type: string;
|
|
168
|
+
};
|
|
169
|
+
description: string;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
required: string[];
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
export declare function createDelegateAgentTool(): {
|
|
176
|
+
name: string;
|
|
177
|
+
description: string;
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: string;
|
|
180
|
+
properties: {
|
|
181
|
+
role: {
|
|
182
|
+
type: string;
|
|
183
|
+
description: string;
|
|
184
|
+
};
|
|
185
|
+
agentId: {
|
|
186
|
+
type: string;
|
|
187
|
+
description: string;
|
|
188
|
+
};
|
|
189
|
+
prompt: {
|
|
190
|
+
type: string;
|
|
191
|
+
description: string;
|
|
192
|
+
};
|
|
193
|
+
label: {
|
|
194
|
+
type: string;
|
|
195
|
+
description: string;
|
|
196
|
+
};
|
|
197
|
+
access: {
|
|
198
|
+
type: string;
|
|
199
|
+
enum: string[];
|
|
200
|
+
description: string;
|
|
201
|
+
};
|
|
202
|
+
seedRecordIds: {
|
|
203
|
+
type: string;
|
|
204
|
+
items: {
|
|
205
|
+
type: string;
|
|
206
|
+
};
|
|
207
|
+
description: string;
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
required: string[];
|
|
211
|
+
};
|
|
212
|
+
};
|
|
111
213
|
export declare function createListAgentsTool(): {
|
|
112
214
|
name: string;
|
|
113
215
|
description: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Agent } from '../agent/agent.js';
|
|
2
2
|
import { createSession, formatSessionSummary, getSession, listSessions, updateSession, } from './orchestrator.js';
|
|
3
3
|
import { buildRolePrompt, resolveRole } from './roles.js';
|
|
4
|
+
import { findById, listAll } from './agentRegistry.js';
|
|
4
5
|
import { buildSystemPrompt, loadWorkspaceInstructionSummary } from '../prompt/systemPrompt.js';
|
|
5
6
|
import { readTranscriptEntries } from '../state/sessionStore.js';
|
|
6
7
|
import { callMcpTool, childSessionKey } from '../runtime/mcpUtils.js';
|
|
@@ -54,7 +55,16 @@ export function extractChildPreview(output, maxChars) {
|
|
|
54
55
|
const tail = maxChars - head - 6; // 6 chars for the `\n...\n` divider
|
|
55
56
|
return output.slice(0, head) + '\n…\n' + output.slice(-tail);
|
|
56
57
|
}
|
|
58
|
+
// Default wait/timeout for foreground delegation. Mirrors wait_agent's
|
|
59
|
+
// historical 120 s default so task_agent and spawn_agent({ wait: true })
|
|
60
|
+
// behave identically when no explicit timeoutMs is passed. Only used inside
|
|
61
|
+
// handleTaskAgent (call-time); kept out of the schema-creator bodies to
|
|
62
|
+
// avoid the ESM circular-import TDZ between tools.ts and agent.ts (agent.ts
|
|
63
|
+
// constructs the LOCAL_TOOLS array eagerly at module load).
|
|
64
|
+
const DEFAULT_TASK_AGENT_TIMEOUT_MS = 120_000;
|
|
57
65
|
const ORCHESTRATION_TOOL_NAMES = new Set([
|
|
66
|
+
'task_agent',
|
|
67
|
+
'delegate_agent',
|
|
58
68
|
'spawn_agent',
|
|
59
69
|
'spawn_agents',
|
|
60
70
|
'list_agents',
|
|
@@ -100,11 +110,12 @@ export function trackedPromiseFor(id) {
|
|
|
100
110
|
export function createSpawnAgentTool() {
|
|
101
111
|
return {
|
|
102
112
|
name: 'spawn_agent',
|
|
103
|
-
description: 'Spawn a child agent
|
|
113
|
+
description: 'Spawn a child agent and a bounded prompt. Returns the child agent id immediately; the child runs in the background. Specify the agent via `role` (legacy: explorer/architect/reviewer/worker/verifier) or `agentId` (registry id, e.g. a custom workspace definition).',
|
|
104
114
|
inputSchema: {
|
|
105
115
|
type: 'object',
|
|
106
116
|
properties: {
|
|
107
|
-
role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier.' },
|
|
117
|
+
role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
|
|
118
|
+
agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
|
|
108
119
|
prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
|
|
109
120
|
label: { type: 'string', description: 'Optional short label for the child run.' },
|
|
110
121
|
access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
|
|
@@ -116,7 +127,55 @@ export function createSpawnAgentTool() {
|
|
|
116
127
|
description: 'Optional BrainRouter memory record IDs that the parent already recalled. The child agent is told to build on these instead of re-discovering them.',
|
|
117
128
|
},
|
|
118
129
|
},
|
|
119
|
-
required: ['
|
|
130
|
+
required: ['prompt'],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function createTaskAgentTool() {
|
|
135
|
+
return {
|
|
136
|
+
name: 'task_agent',
|
|
137
|
+
description: 'Run one foreground child agent for a bounded task and wait for its result. ' +
|
|
138
|
+
'Returns the completed child output, failure, or an explicit timeout envelope. Prefer this over spawn_agent({ wait: true }) for foreground delegation.',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
|
|
143
|
+
agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
|
|
144
|
+
prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
|
|
145
|
+
label: { type: 'string', description: 'Optional short label for the child run.' },
|
|
146
|
+
access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
|
|
147
|
+
timeoutMs: { type: 'integer', description: 'Optional timeout in milliseconds. Default 120000.' },
|
|
148
|
+
seedRecordIds: {
|
|
149
|
+
type: 'array',
|
|
150
|
+
items: { type: 'string' },
|
|
151
|
+
description: 'Optional BrainRouter memory record IDs that the parent already recalled.',
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
required: ['prompt'],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function createDelegateAgentTool() {
|
|
159
|
+
return {
|
|
160
|
+
name: 'delegate_agent',
|
|
161
|
+
description: 'Start one background child agent and keep working in the parent turn. ' +
|
|
162
|
+
'Non-blocking — there is no `timeoutMs`; the child runs until it finishes or is cancelled. ' +
|
|
163
|
+
'Returns a running child id plus a reminder to continue useful work; call wait_agent later when the result is needed.',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: {
|
|
167
|
+
role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
|
|
168
|
+
agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
|
|
169
|
+
prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
|
|
170
|
+
label: { type: 'string', description: 'Optional short label for the child run.' },
|
|
171
|
+
access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
|
|
172
|
+
seedRecordIds: {
|
|
173
|
+
type: 'array',
|
|
174
|
+
items: { type: 'string' },
|
|
175
|
+
description: 'Optional BrainRouter memory record IDs that the parent already recalled.',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['prompt'],
|
|
120
179
|
},
|
|
121
180
|
};
|
|
122
181
|
}
|
|
@@ -223,6 +282,10 @@ export function createRouteAgentTool() {
|
|
|
223
282
|
}
|
|
224
283
|
export async function executeOrchestrationTool(name, args, ctx) {
|
|
225
284
|
switch (name) {
|
|
285
|
+
case 'task_agent':
|
|
286
|
+
return await handleTaskAgent(args, ctx);
|
|
287
|
+
case 'delegate_agent':
|
|
288
|
+
return await handleDelegateAgent(args, ctx);
|
|
226
289
|
case 'spawn_agent':
|
|
227
290
|
return await handleSpawn(args, ctx);
|
|
228
291
|
case 'spawn_agents':
|
|
@@ -243,6 +306,31 @@ export async function executeOrchestrationTool(name, args, ctx) {
|
|
|
243
306
|
throw new Error(`Unknown orchestration tool: ${name}`);
|
|
244
307
|
}
|
|
245
308
|
}
|
|
309
|
+
async function handleTaskAgent(args, ctx) {
|
|
310
|
+
return await handleSpawn({ ...args, wait: true, timeoutMs: args?.timeoutMs ?? DEFAULT_TASK_AGENT_TIMEOUT_MS }, ctx);
|
|
311
|
+
}
|
|
312
|
+
async function handleDelegateAgent(args, ctx) {
|
|
313
|
+
const spawned = await handleSpawn({ ...args, wait: false }, ctx);
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
const value = JSON.parse(spawned);
|
|
317
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
318
|
+
parsed = value;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// not JSON; fall through to verbatim propagation
|
|
322
|
+
}
|
|
323
|
+
// If handleSpawn returned an error string or a non-object payload (no id to
|
|
324
|
+
// attach next-step semantics to), propagate it verbatim — wrapping it in
|
|
325
|
+
// { raw, nextAction } would hide the failure from the model and prevent the
|
|
326
|
+
// child-drain guardrail from finding a child id to wait on.
|
|
327
|
+
if (!parsed || typeof parsed.id !== 'string')
|
|
328
|
+
return spawned;
|
|
329
|
+
return JSON.stringify({
|
|
330
|
+
...parsed,
|
|
331
|
+
nextAction: 'continue working in the parent turn; call wait_agent when this child output is needed',
|
|
332
|
+
}, null, 2);
|
|
333
|
+
}
|
|
246
334
|
async function handleSpawnBatch(args, ctx) {
|
|
247
335
|
const list = Array.isArray(args?.agents) ? args.agents : [];
|
|
248
336
|
if (list.length === 0)
|
|
@@ -297,10 +385,47 @@ function explainRoute(task, role) {
|
|
|
297
385
|
}
|
|
298
386
|
}
|
|
299
387
|
async function handleSpawn(args, ctx) {
|
|
300
|
-
|
|
388
|
+
// Resolve agent definition via agentId (registry) or role (legacy).
|
|
389
|
+
let role;
|
|
390
|
+
let childTier;
|
|
391
|
+
if (typeof args.agentId === 'string' && args.agentId.trim()) {
|
|
392
|
+
const loaded = findById(args.agentId.trim(), ctx.workspaceRoot);
|
|
393
|
+
if (!loaded) {
|
|
394
|
+
const known = listAll(ctx.workspaceRoot).map((l) => l.def.id).join(', ');
|
|
395
|
+
throw new Error(`Unknown agentId "${args.agentId}". Known agents: ${known}.`);
|
|
396
|
+
}
|
|
397
|
+
role = {
|
|
398
|
+
name: loaded.def.id,
|
|
399
|
+
description: loaded.def.whenToUse,
|
|
400
|
+
defaultAccess: loaded.def.defaultAccess,
|
|
401
|
+
promptOverlay: loaded.def.prompt,
|
|
402
|
+
};
|
|
403
|
+
childTier = loaded.def.tier;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
const roleName = String(args.role ?? '');
|
|
407
|
+
if (!roleName.trim())
|
|
408
|
+
throw new Error('spawn_agent requires either "agentId" or "role".');
|
|
409
|
+
role = resolveRole(roleName);
|
|
410
|
+
childTier = findById(role.name, ctx.workspaceRoot)?.def.tier;
|
|
411
|
+
}
|
|
301
412
|
const prompt = String(args.prompt ?? '');
|
|
302
413
|
if (!prompt.trim())
|
|
303
414
|
throw new Error('spawn_agent requires a non-empty prompt.');
|
|
415
|
+
// P1.2 — spawn hierarchy checks.
|
|
416
|
+
const rawMaxDepth = parseInt(process.env.BRAINROUTER_MAX_SPAWN_DEPTH ?? '3', 10);
|
|
417
|
+
const maxDepth = Number.isFinite(rawMaxDepth) && rawMaxDepth > 0 ? rawMaxDepth : 3;
|
|
418
|
+
const currentDepth = ctx.depth ?? 0;
|
|
419
|
+
const parentTier = ctx.parentTier;
|
|
420
|
+
if (parentTier === 'worker') {
|
|
421
|
+
throw new Error('Tier "worker" cannot delegate — ask the parent agent to spawn instead.');
|
|
422
|
+
}
|
|
423
|
+
if (parentTier === 'reasoning' && childTier && (childTier === 'chat' || childTier === 'reasoning')) {
|
|
424
|
+
throw new Error(`Tier "reasoning" cannot spawn a "${childTier}" agent — only "worker" children are allowed.`);
|
|
425
|
+
}
|
|
426
|
+
if (currentDepth >= maxDepth) {
|
|
427
|
+
throw new Error(`Spawn depth cap reached (${currentDepth}/${maxDepth}). Reduce agent nesting or raise BRAINROUTER_MAX_SPAWN_DEPTH.`);
|
|
428
|
+
}
|
|
304
429
|
const requested = args.access ?? role.defaultAccess;
|
|
305
430
|
const access = clampAccess(ctx.parentAccessMode ?? 'shell', requested);
|
|
306
431
|
const record = createSession(ctx.workspaceRoot, {
|
|
@@ -309,6 +434,8 @@ async function handleSpawn(args, ctx) {
|
|
|
309
434
|
parentSessionKey: ctx.parentSessionKey,
|
|
310
435
|
access,
|
|
311
436
|
label: typeof args.label === 'string' ? args.label : undefined,
|
|
437
|
+
tier: childTier,
|
|
438
|
+
depth: currentDepth + 1,
|
|
312
439
|
});
|
|
313
440
|
const childKey = childSessionKey(ctx.parentSessionKey, record.id);
|
|
314
441
|
const seededIds = Array.isArray(args.seedRecordIds)
|
|
@@ -346,22 +473,41 @@ async function handleSpawn(args, ctx) {
|
|
|
346
473
|
// dispatching spawn_agent tool span instead of starting a fresh tree.
|
|
347
474
|
parentTraceId: ctx.parentTraceId,
|
|
348
475
|
parentSpanId: ctx.parentSpanId,
|
|
476
|
+
// Propagate tier and depth so grandchildren can enforce hierarchy caps.
|
|
477
|
+
tier: childTier,
|
|
478
|
+
agentDepth: currentDepth + 1,
|
|
349
479
|
});
|
|
350
480
|
if (ctx.parentAgentId)
|
|
351
481
|
childAgent.setParentAgentId(ctx.parentAgentId);
|
|
352
482
|
updateSession(ctx.workspaceRoot, record.id, { status: 'running' });
|
|
353
483
|
const promise = (async () => {
|
|
354
484
|
try {
|
|
485
|
+
// Track per-tool start times so the paired onChildToolEnd carries a
|
|
486
|
+
// real duration — the REPL renders this on the child's end row.
|
|
487
|
+
const childToolStarts = new Map();
|
|
355
488
|
const output = await childAgent.runTurn(prompt, {
|
|
356
489
|
onStatusUpdate: () => { },
|
|
357
|
-
onToolStart: () => {
|
|
490
|
+
onToolStart: (tool, args) => {
|
|
491
|
+
childToolStarts.set(tool, Date.now());
|
|
492
|
+
ctx.onChildToolStart?.({
|
|
493
|
+
childId: record.id,
|
|
494
|
+
role: role.name,
|
|
495
|
+
tool,
|
|
496
|
+
args: args ?? {},
|
|
497
|
+
});
|
|
498
|
+
},
|
|
358
499
|
onToolEnd: (tool, result) => {
|
|
359
|
-
|
|
500
|
+
const startedAt = childToolStarts.get(tool);
|
|
501
|
+
childToolStarts.delete(tool);
|
|
502
|
+
const durationMs = startedAt ? Date.now() - startedAt : 0;
|
|
503
|
+
ctx.onChildToolEnd?.({
|
|
360
504
|
childId: record.id,
|
|
361
505
|
role: role.name,
|
|
362
506
|
tool,
|
|
363
507
|
ok: result.success,
|
|
364
508
|
summary: result.summary,
|
|
509
|
+
preview: result.preview,
|
|
510
|
+
durationMs,
|
|
365
511
|
});
|
|
366
512
|
},
|
|
367
513
|
});
|
|
@@ -462,12 +608,25 @@ async function handleWait(args, ctx) {
|
|
|
462
608
|
const promise = runningPromises.get(id);
|
|
463
609
|
if (promise) {
|
|
464
610
|
let timedOut = false;
|
|
611
|
+
let timeout;
|
|
465
612
|
await Promise.race([
|
|
466
613
|
promise,
|
|
467
|
-
new Promise((resolve) =>
|
|
614
|
+
new Promise((resolve) => {
|
|
615
|
+
timeout = setTimeout(() => { timedOut = true; resolve(); }, timeoutMs);
|
|
616
|
+
}),
|
|
468
617
|
]);
|
|
618
|
+
if (timeout)
|
|
619
|
+
clearTimeout(timeout);
|
|
469
620
|
if (timedOut) {
|
|
470
|
-
|
|
621
|
+
const record = getSession(ctx.workspaceRoot, id);
|
|
622
|
+
return JSON.stringify({
|
|
623
|
+
id,
|
|
624
|
+
status: 'timeout',
|
|
625
|
+
childStatus: record?.status ?? 'unknown',
|
|
626
|
+
role: record?.role,
|
|
627
|
+
label: record?.label,
|
|
628
|
+
summary: record ? formatSessionSummary(record) : `No child session with id ${id}.`,
|
|
629
|
+
}, null, 2);
|
|
471
630
|
}
|
|
472
631
|
}
|
|
473
632
|
const record = getSession(ctx.workspaceRoot, id);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SkillListItem {
|
|
2
|
+
name: string;
|
|
3
|
+
scope?: string;
|
|
4
|
+
category?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
source?: 'mcp' | 'filesystem';
|
|
7
|
+
}
|
|
8
|
+
export declare function listFilesystemSkills(workspaceRoot: string): SkillListItem[];
|
|
9
|
+
export declare function mergeSkillLists(primary: SkillListItem[], fallback: SkillListItem[]): SkillListItem[];
|
|
10
|
+
export declare function sortSkills(a: SkillListItem, b: SkillListItem): number;
|
|
11
|
+
export declare function skillSearchRoots(workspaceRoot: string): string[];
|