@kinqs/brainrouter-cli 0.3.7 → 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/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 +22 -0
- package/dist/agent/agent.js +259 -82
- 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.js +2 -2
- package/dist/cli/cliPrompt.js +65 -0
- package/dist/cli/commands/config.js +1 -1
- package/dist/cli/commands/mcp.d.ts +1 -1
- package/dist/cli/commands/mcp.js +29 -7
- package/dist/cli/commands/mcpInstall.d.ts +20 -0
- package/dist/cli/commands/mcpInstall.js +87 -0
- package/dist/cli/commands/orchestration.js +33 -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 +2 -2
- package/dist/cli/ink/Picker.d.ts +6 -0
- package/dist/cli/ink/Picker.js +41 -6
- package/dist/cli/ink/runChat.js +112 -1
- package/dist/cli/ink/toolFormat.d.ts +11 -9
- package/dist/cli/ink/toolFormat.js +42 -16
- package/dist/cli/repl.d.ts +1 -1
- package/dist/cli/repl.js +9 -2
- package/dist/config/config.d.ts +1 -1
- package/dist/index.js +10 -1
- package/dist/memory/briefing.js +4 -4
- package/dist/orchestration/tools.d.ts +95 -2
- package/dist/orchestration/tools.js +119 -4
- package/dist/prompt/systemPrompt.js +5 -4
- 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 +1 -1
- package/dist/runtime/mcpPool.d.ts +8 -0
- package/dist/runtime/mcpPool.js +19 -0
- package/dist/runtime/mcpUtils.d.ts +14 -0
- 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 +7 -4
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* — one-line, identity-revealing, no JSON. These helpers do the same
|
|
13
13
|
* mapping for our built-in LOCAL_TOOLS (cli/../agent/agent.ts) + MCP
|
|
14
|
-
* tool names (which carry an `
|
|
14
|
+
* tool names (which carry an `mcp_<server>_` namespace prefix that
|
|
15
15
|
* the user doesn't care about).
|
|
16
16
|
*
|
|
17
17
|
* Reference for the convention: claude-code transcripts (see
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
* → "Bash(npm test)"
|
|
29
29
|
* formatToolCall('grep_search', { query: 'authenticate', path: '.' })
|
|
30
30
|
* → 'Grep("authenticate")'
|
|
31
|
-
* formatToolCall('
|
|
31
|
+
* formatToolCall('mcp_brainrouter_memory_search', { q: 'auth' })
|
|
32
32
|
* → 'MemorySearch("auth")'
|
|
33
33
|
* formatToolCall('spawn_agent', { role: 'researcher', prompt: '...' })
|
|
34
34
|
* → 'Spawn(researcher, "...")'
|
|
35
|
+
* formatToolCall('task_agent', { role: 'reviewer', prompt: '...' })
|
|
36
|
+
* → 'Task(reviewer, "...")'
|
|
35
37
|
*/
|
|
36
38
|
export function formatToolCall(name, args) {
|
|
37
39
|
const safeArgs = args ?? {};
|
|
@@ -69,6 +71,18 @@ export function formatToolCall(name, args) {
|
|
|
69
71
|
const task = truncateOneLine(safeArgs.prompt ?? '', 50);
|
|
70
72
|
return `Spawn(${role}${label}, "${task}")`;
|
|
71
73
|
}
|
|
74
|
+
case 'task_agent': {
|
|
75
|
+
const role = String(safeArgs.role ?? safeArgs.agentId ?? 'agent');
|
|
76
|
+
const label = safeArgs.label ? ` [${safeArgs.label}]` : '';
|
|
77
|
+
const task = truncateOneLine(safeArgs.prompt ?? '', 50);
|
|
78
|
+
return `Task(${role}${label}, "${task}")`;
|
|
79
|
+
}
|
|
80
|
+
case 'delegate_agent': {
|
|
81
|
+
const role = String(safeArgs.role ?? safeArgs.agentId ?? 'agent');
|
|
82
|
+
const label = safeArgs.label ? ` [${safeArgs.label}]` : '';
|
|
83
|
+
const task = truncateOneLine(safeArgs.prompt ?? '', 50);
|
|
84
|
+
return `Delegate(${role}${label}, "${task}")`;
|
|
85
|
+
}
|
|
72
86
|
case 'spawn_agents': {
|
|
73
87
|
const agents = Array.isArray(safeArgs.agents) ? safeArgs.agents : [];
|
|
74
88
|
const roles = agents.map((a) => String(a?.role ?? 'agent')).join(', ');
|
|
@@ -89,23 +103,35 @@ export function formatToolCall(name, args) {
|
|
|
89
103
|
// No args, or no string args — just show the name.
|
|
90
104
|
return `${pretty}()`;
|
|
91
105
|
}
|
|
106
|
+
// Server ids registered at boot. MCP server names may contain underscores
|
|
107
|
+
// (`my_server`), so a naive `^mcp_[^_]+_(.+)$` regex would mis-strip
|
|
108
|
+
// `mcp_my_server_memory_search` to `server_memory_search`. Callers that
|
|
109
|
+
// know the live server inventory register it here so `stripMcpPrefix` can
|
|
110
|
+
// match the longest known id first.
|
|
111
|
+
let knownMcpServerIds = [];
|
|
112
|
+
export function setKnownMcpServerIds(ids) {
|
|
113
|
+
knownMcpServerIds = [...ids].sort((a, b) => b.length - a.length);
|
|
114
|
+
}
|
|
92
115
|
/**
|
|
93
|
-
* Strip the `
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `
|
|
98
|
-
*
|
|
99
|
-
* `mcp_brainrouter_memory_search` → `memory_search`
|
|
116
|
+
* Strip the `mcp_<server>_` namespace prefix from MCP tool names. As of
|
|
117
|
+
* 0.3.8-R5 the pool normalises to single-underscore at the boundary, so
|
|
118
|
+
* downstream call-sites only ever see this shape.
|
|
119
|
+
* `mcp_brainrouter_memory_search` → `memory_search`
|
|
120
|
+
* `mcp_my_server_memory_search` → `memory_search` (when `my_server`
|
|
121
|
+
* is registered via `setKnownMcpServerIds`)
|
|
100
122
|
*/
|
|
101
123
|
export function stripMcpPrefix(name) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
if (!name.startsWith('mcp_'))
|
|
125
|
+
return name;
|
|
126
|
+
const rest = name.slice('mcp_'.length);
|
|
127
|
+
for (const id of knownMcpServerIds) {
|
|
128
|
+
if (rest.startsWith(`${id}_`))
|
|
129
|
+
return rest.slice(id.length + 1);
|
|
130
|
+
}
|
|
131
|
+
// Fallback when no server ids are registered (test fixtures, early boot):
|
|
132
|
+
// assume the server id has no underscores.
|
|
133
|
+
const idx = rest.indexOf('_');
|
|
134
|
+
return idx >= 0 ? rest.slice(idx + 1) : name;
|
|
109
135
|
}
|
|
110
136
|
/**
|
|
111
137
|
* Convert snake_case to PascalCase for readable display names.
|
package/dist/cli/repl.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { ReplContext } from './commands/_context.js';
|
|
|
10
10
|
* The Ink chat REPL (cli/ink/runChat.tsx) consumes this same list for its
|
|
11
11
|
* inline slash palette so both surfaces stay in lockstep as new commands land.
|
|
12
12
|
*/
|
|
13
|
-
export declare const SLASH_COMMANDS: readonly ["/help", "/status", "/workspace", "/where", "/tools", "/skills", "/plan", "/transcript", "/doctor", "/config", "/diff", "/commit", "/clear", "/compact", "/exit", "/quit", "/roles", "/agents", "/agent", "/spawn", "/wait", "/spec", "/feature-dev", "/grill-me", "/review", "/implement-plan", "/skill", "/workflow", "/workflows", "/approve", "/memory", "/recall", "/briefing", "/scenes", "/working", "/forget", "/init", "/login", "/sessions", "/resume", "/model", "/mcp", "/goal", "/copy", "/fork", "/rename", "/permissions", "/hooks", "/hookify", "/loop", "/continue", "/auto-review", "/vim", "/statusline", "/quiet", "/handover", "/explain", "/trace", "/failed", "/verify", "/audit", "/export", "/import", "/persona", "/skill-hints", "/diagnostics", "/tokens", "/watch", "/yolo", "/mode", "/review-policy", "/sandbox", "/kill", "/theme", "/title", "/personality", "/effort", "/new", "/side", "/btw", "/raw", "/feedback", "/rollout", "/ps", "/stop", "/logout", "/apps", "/plugins", "/experimental", "/memories", "/debug-config", "/mention", "/keymap", "/ide"];
|
|
13
|
+
export declare const SLASH_COMMANDS: readonly ["/help", "/status", "/workspace", "/where", "/tools", "/skills", "/plan", "/transcript", "/doctor", "/config", "/diff", "/commit", "/clear", "/compact", "/exit", "/quit", "/roles", "/agents", "/agent", "/spawn", "/wait", "/spec", "/feature-dev", "/grill-me", "/review", "/implement-plan", "/skill", "/workflow", "/workflows", "/approve", "/memory", "/recall", "/briefing", "/scenes", "/working", "/forget", "/init", "/login", "/sessions", "/resume", "/model", "/mcp", "/goal", "/copy", "/fork", "/rename", "/permissions", "/hooks", "/hookify", "/loop", "/schedule", "/continue", "/auto-review", "/vim", "/statusline", "/quiet", "/release-notes", "/handover", "/explain", "/trace", "/failed", "/verify", "/audit", "/export", "/import", "/persona", "/skill-hints", "/diagnostics", "/tokens", "/watch", "/yolo", "/mode", "/review-policy", "/sandbox", "/kill", "/theme", "/title", "/personality", "/effort", "/new", "/side", "/btw", "/raw", "/feedback", "/rollout", "/ps", "/stop", "/logout", "/apps", "/plugins", "/experimental", "/memories", "/debug-config", "/mention", "/keymap", "/ide"];
|
|
14
14
|
export declare function renderHelp(category?: string): void;
|
|
15
15
|
/**
|
|
16
16
|
* Look up a one-line description for a slash command by walking the
|
package/dist/cli/repl.js
CHANGED
|
@@ -15,6 +15,8 @@ import { tryHandleMcpCommand } from './commands/mcp.js';
|
|
|
15
15
|
import { tryHandleInitCommand } from './commands/init.js';
|
|
16
16
|
import { tryHandleConfigCommand } from './commands/config.js';
|
|
17
17
|
import { tryHandleLoginCommand } from './commands/login.js';
|
|
18
|
+
import { tryHandleScheduleCommand } from './commands/schedule.js';
|
|
19
|
+
import { tryHandleReleaseNotesCommand } from './commands/releaseNotes.js';
|
|
18
20
|
/**
|
|
19
21
|
* All slash commands the REPL recognizes. Used for tab autocomplete and for
|
|
20
22
|
* the readline completer. Keep alphabetically grouped roughly by surface area.
|
|
@@ -29,8 +31,8 @@ export const SLASH_COMMANDS = [
|
|
|
29
31
|
'/spec', '/feature-dev', '/grill-me', '/review', '/implement-plan', '/skill', '/workflow', '/workflows', '/approve',
|
|
30
32
|
'/memory', '/recall', '/briefing', '/scenes', '/working', '/forget',
|
|
31
33
|
'/init', '/login', '/sessions', '/resume', '/model', '/mcp',
|
|
32
|
-
'/goal', '/copy', '/fork', '/rename', '/permissions', '/hooks', '/hookify', '/loop',
|
|
33
|
-
'/continue', '/auto-review', '/vim', '/statusline', '/quiet',
|
|
34
|
+
'/goal', '/copy', '/fork', '/rename', '/permissions', '/hooks', '/hookify', '/loop', '/schedule',
|
|
35
|
+
'/continue', '/auto-review', '/vim', '/statusline', '/quiet', '/release-notes',
|
|
34
36
|
'/handover', '/explain', '/trace', '/failed', '/verify', '/audit',
|
|
35
37
|
'/export', '/import', '/persona', '/skill-hints', '/diagnostics',
|
|
36
38
|
'/tokens', '/watch', '/yolo', '/mode', '/review-policy', '/sandbox', '/kill',
|
|
@@ -173,6 +175,7 @@ const HELP_CATEGORIES = [
|
|
|
173
175
|
{ cmd: '/apps /plugins', desc: 'List workspace skills and plugin folders' },
|
|
174
176
|
{ cmd: '/feedback [message]', desc: 'Append feedback entry' },
|
|
175
177
|
{ cmd: '/experimental [on|off]', desc: 'Toggle experimental features' },
|
|
178
|
+
{ cmd: '/release-notes [version|list]', desc: 'Show changelog for current (or specified) CLI version' },
|
|
176
179
|
],
|
|
177
180
|
},
|
|
178
181
|
];
|
|
@@ -263,6 +266,10 @@ export async function handleSlashCommand(command, args, agent, mcpClient, config
|
|
|
263
266
|
return;
|
|
264
267
|
if (await tryHandleWorkflowCommand(cmdCtx))
|
|
265
268
|
return;
|
|
269
|
+
if (await tryHandleScheduleCommand(cmdCtx))
|
|
270
|
+
return;
|
|
271
|
+
if (await tryHandleReleaseNotesCommand(cmdCtx))
|
|
272
|
+
return;
|
|
266
273
|
if (await tryHandleObsCommand(cmdCtx))
|
|
267
274
|
return;
|
|
268
275
|
if (await tryHandleOrchestrationCommand(cmdCtx))
|
package/dist/config/config.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -84,6 +84,7 @@ import chalk from 'chalk';
|
|
|
84
84
|
import { loadConfig, loadOrInitConfig, saveConfig, getConfigPath } from './config/config.js';
|
|
85
85
|
import { McpClientWrapper } from './runtime/mcpClient.js';
|
|
86
86
|
import { McpClientPool, selectMcpServerIds } from './runtime/mcpPool.js';
|
|
87
|
+
import { setKnownMcpServerIds } from './cli/ink/toolFormat.js';
|
|
87
88
|
import { Agent } from './agent/agent.js';
|
|
88
89
|
import { runChat } from './cli/ink/runChat.js';
|
|
89
90
|
import { applyWorkspaceRoot, findWorkspaceRoot } from './config/workspace.js';
|
|
@@ -100,7 +101,7 @@ const program = new Command();
|
|
|
100
101
|
program
|
|
101
102
|
.name('brainrouter')
|
|
102
103
|
.description('BrainRouter CLI — Premium interactive terminal-based agent client.')
|
|
103
|
-
.version('0.3.
|
|
104
|
+
.version('0.3.8');
|
|
104
105
|
// Chat Command (default)
|
|
105
106
|
program
|
|
106
107
|
.command('chat', { isDefault: true })
|
|
@@ -197,6 +198,10 @@ program
|
|
|
197
198
|
// comment); the banner's per-server row is the success signal.
|
|
198
199
|
const mcpClient = new McpClientPool();
|
|
199
200
|
const statuses = await mcpClient.connectAll(targetServers, llm, { timeoutMs: 5_000 });
|
|
201
|
+
// Register live server ids for Ink tool-name display so multi-word
|
|
202
|
+
// server names (e.g. `my_server`) don't get mis-stripped by the
|
|
203
|
+
// single-underscore prefix regex.
|
|
204
|
+
setKnownMcpServerIds(mcpClient.getServerIds());
|
|
200
205
|
const failures = statuses.filter((s) => s.status === 'failed');
|
|
201
206
|
if (failures.length === statuses.length) {
|
|
202
207
|
// Every server failed — equivalent to the pre-0.3.7 "MCP
|
|
@@ -302,6 +307,10 @@ program
|
|
|
302
307
|
llm.model = options.model;
|
|
303
308
|
const mcpClient = new McpClientPool();
|
|
304
309
|
const statuses = await mcpClient.connectAll(targetServers, llm, { timeoutMs: 5_000 });
|
|
310
|
+
// Register live server ids for Ink tool-name display so multi-word
|
|
311
|
+
// server names (e.g. `my_server`) don't get mis-stripped by the
|
|
312
|
+
// single-underscore prefix regex.
|
|
313
|
+
setKnownMcpServerIds(mcpClient.getServerIds());
|
|
305
314
|
const allFailed = statuses.length > 0 && statuses.every((s) => s.status === 'failed');
|
|
306
315
|
if (allFailed) {
|
|
307
316
|
const summary = statuses.map((s) => `${s.serverId}: ${s.error ?? 'unknown'}`).join('; ');
|
package/dist/memory/briefing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redactText } from '../state/sessionStore.js';
|
|
2
|
-
import { callMcpTool } from '../runtime/mcpUtils.js';
|
|
2
|
+
import { callMcpTool, hasMcpTool } from '../runtime/mcpUtils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Run pre-turn memory queries in parallel and assemble a compact briefing block.
|
|
5
5
|
* This is the System-1 entry point: every turn pays a small fixed cost to ask
|
|
@@ -11,13 +11,13 @@ export async function buildMemoryBriefing(inputs) {
|
|
|
11
11
|
const maxChars = inputs.maxCharsPerSource ?? 4000;
|
|
12
12
|
const toolNames = new Set(mcpTools.map((t) => t.name));
|
|
13
13
|
const tasks = [];
|
|
14
|
-
if (toolNames
|
|
14
|
+
if (hasMcpTool(toolNames, 'memory_recall')) {
|
|
15
15
|
tasks.push(callSafe('memory_recall', { sessionKey, query, activeSkill }, mcpClient, maxChars, extractRecords));
|
|
16
16
|
}
|
|
17
|
-
if (toolNames
|
|
17
|
+
if (hasMcpTool(toolNames, 'memory_working_context')) {
|
|
18
18
|
tasks.push(callSafe('memory_working_context', { sessionKey, workspacePath: workspaceRoot }, mcpClient, maxChars));
|
|
19
19
|
}
|
|
20
|
-
if (toolNames
|
|
20
|
+
if (hasMcpTool(toolNames, 'memory_task_state') && !inputs.hasActiveGoal) {
|
|
21
21
|
tasks.push(callSafe('memory_task_state', { query }, mcpClient, maxChars));
|
|
22
22
|
}
|
|
23
23
|
const results = await Promise.all(tasks);
|
|
@@ -31,13 +31,26 @@ export interface OrchestrationContext {
|
|
|
31
31
|
launchCwd: string;
|
|
32
32
|
/** Called when a child output got offloaded — chars beyond preview that didn't land in parent context. */
|
|
33
33
|
recordOffload?: (charsAvoided: number) => void;
|
|
34
|
-
/**
|
|
35
|
-
|
|
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: {
|
|
36
47
|
childId: string;
|
|
37
48
|
role: string;
|
|
38
49
|
tool: string;
|
|
39
50
|
ok: boolean;
|
|
40
51
|
summary: string;
|
|
52
|
+
preview?: string;
|
|
53
|
+
durationMs: number;
|
|
41
54
|
}) => void;
|
|
42
55
|
/**
|
|
43
56
|
* Called when a child agent's runTurn ends — success, fail, or empty answer.
|
|
@@ -117,6 +130,86 @@ export declare function createSpawnAgentTool(): {
|
|
|
117
130
|
required: string[];
|
|
118
131
|
};
|
|
119
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
|
+
};
|
|
120
213
|
export declare function createListAgentsTool(): {
|
|
121
214
|
name: string;
|
|
122
215
|
description: string;
|
|
@@ -55,7 +55,16 @@ export function extractChildPreview(output, maxChars) {
|
|
|
55
55
|
const tail = maxChars - head - 6; // 6 chars for the `\n...\n` divider
|
|
56
56
|
return output.slice(0, head) + '\n…\n' + output.slice(-tail);
|
|
57
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;
|
|
58
65
|
const ORCHESTRATION_TOOL_NAMES = new Set([
|
|
66
|
+
'task_agent',
|
|
67
|
+
'delegate_agent',
|
|
59
68
|
'spawn_agent',
|
|
60
69
|
'spawn_agents',
|
|
61
70
|
'list_agents',
|
|
@@ -122,6 +131,54 @@ export function createSpawnAgentTool() {
|
|
|
122
131
|
},
|
|
123
132
|
};
|
|
124
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'],
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
125
182
|
export function createListAgentsTool() {
|
|
126
183
|
return {
|
|
127
184
|
name: 'list_agents',
|
|
@@ -225,6 +282,10 @@ export function createRouteAgentTool() {
|
|
|
225
282
|
}
|
|
226
283
|
export async function executeOrchestrationTool(name, args, ctx) {
|
|
227
284
|
switch (name) {
|
|
285
|
+
case 'task_agent':
|
|
286
|
+
return await handleTaskAgent(args, ctx);
|
|
287
|
+
case 'delegate_agent':
|
|
288
|
+
return await handleDelegateAgent(args, ctx);
|
|
228
289
|
case 'spawn_agent':
|
|
229
290
|
return await handleSpawn(args, ctx);
|
|
230
291
|
case 'spawn_agents':
|
|
@@ -245,6 +306,31 @@ export async function executeOrchestrationTool(name, args, ctx) {
|
|
|
245
306
|
throw new Error(`Unknown orchestration tool: ${name}`);
|
|
246
307
|
}
|
|
247
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
|
+
}
|
|
248
334
|
async function handleSpawnBatch(args, ctx) {
|
|
249
335
|
const list = Array.isArray(args?.agents) ? args.agents : [];
|
|
250
336
|
if (list.length === 0)
|
|
@@ -396,16 +482,32 @@ async function handleSpawn(args, ctx) {
|
|
|
396
482
|
updateSession(ctx.workspaceRoot, record.id, { status: 'running' });
|
|
397
483
|
const promise = (async () => {
|
|
398
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();
|
|
399
488
|
const output = await childAgent.runTurn(prompt, {
|
|
400
489
|
onStatusUpdate: () => { },
|
|
401
|
-
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
|
+
},
|
|
402
499
|
onToolEnd: (tool, result) => {
|
|
403
|
-
|
|
500
|
+
const startedAt = childToolStarts.get(tool);
|
|
501
|
+
childToolStarts.delete(tool);
|
|
502
|
+
const durationMs = startedAt ? Date.now() - startedAt : 0;
|
|
503
|
+
ctx.onChildToolEnd?.({
|
|
404
504
|
childId: record.id,
|
|
405
505
|
role: role.name,
|
|
406
506
|
tool,
|
|
407
507
|
ok: result.success,
|
|
408
508
|
summary: result.summary,
|
|
509
|
+
preview: result.preview,
|
|
510
|
+
durationMs,
|
|
409
511
|
});
|
|
410
512
|
},
|
|
411
513
|
});
|
|
@@ -506,12 +608,25 @@ async function handleWait(args, ctx) {
|
|
|
506
608
|
const promise = runningPromises.get(id);
|
|
507
609
|
if (promise) {
|
|
508
610
|
let timedOut = false;
|
|
611
|
+
let timeout;
|
|
509
612
|
await Promise.race([
|
|
510
613
|
promise,
|
|
511
|
-
new Promise((resolve) =>
|
|
614
|
+
new Promise((resolve) => {
|
|
615
|
+
timeout = setTimeout(() => { timedOut = true; resolve(); }, timeoutMs);
|
|
616
|
+
}),
|
|
512
617
|
]);
|
|
618
|
+
if (timeout)
|
|
619
|
+
clearTimeout(timeout);
|
|
513
620
|
if (timedOut) {
|
|
514
|
-
|
|
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);
|
|
515
630
|
}
|
|
516
631
|
}
|
|
517
632
|
const record = getSession(ctx.workspaceRoot, id);
|
|
@@ -77,9 +77,9 @@ function clarifyOverlay(activeSkill) {
|
|
|
77
77
|
function isBrainOnline(connectedTools) {
|
|
78
78
|
if (!connectedTools)
|
|
79
79
|
return true;
|
|
80
|
-
// Match bare `memory_recall
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// Match bare `memory_recall` and the canonical single-underscore prefixed
|
|
81
|
+
// form `mcp_<server>_memory_recall` (pool normalises any legacy
|
|
82
|
+
// double-underscore emissions at the boundary — 0.3.8-R5).
|
|
83
83
|
return connectedTools.some((tool) => tool === 'memory_recall' ||
|
|
84
84
|
(tool.startsWith('mcp_') && tool.endsWith('memory_recall')));
|
|
85
85
|
}
|
|
@@ -127,7 +127,8 @@ export function buildSystemPrompt(context) {
|
|
|
127
127
|
brainOnline ? memoryFirstSection() : brainOfflineNotice(),
|
|
128
128
|
'',
|
|
129
129
|
'## Multi-agent orchestration',
|
|
130
|
-
'-
|
|
130
|
+
'- Delegation order: direct answer → direct tool → `task_agent` for needed child results → `delegate_agent` when you can keep working. `spawn_agent` / `spawn_agents` are low-level compatibility/batch tools.',
|
|
131
|
+
'- Roles: explorer, architect, reviewer, worker, verifier. Omit `role` in `spawn_agents` to auto-route from the leading verb; use `route_agent` for a dry run.',
|
|
131
132
|
'- Fan-out triggers: phrasings like "everything", "all", "in 1 go", "in parallel", "thoroughly", "comprehensive", "across the codebase" → ALWAYS `spawn_agents` with ≥3 children. One tool call + "what next?" is NOT acceptable for those prompts.',
|
|
132
133
|
'- Use `wait_agent` / `wait_agents` to drain before yielding. Synthesize child outputs in your own words — never claim work is done just because a child returned.',
|
|
133
134
|
'',
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { LLMConfig } from '../config/config.js';
|
|
2
|
+
import type { EffortLevel } from '../state/preferencesStore.js';
|
|
3
|
+
/**
|
|
4
|
+
* Route to the native adapter when the profile is Anthropic AND the
|
|
5
|
+
* endpoint hostname is `api.anthropic.com`, OR the explicit
|
|
6
|
+
* `BRAINROUTER_ANTHROPIC_NATIVE=1` override is set (for vended /
|
|
7
|
+
* reverse-proxied endpoints that still speak the native shape).
|
|
8
|
+
*
|
|
9
|
+
* Anything else — including `provider:'anthropic'` pointed at an
|
|
10
|
+
* OpenAI-compat gateway — stays on the existing OpenAI path so we
|
|
11
|
+
* don't break the OpenRouter / Anthropic-compat-shim flows.
|
|
12
|
+
*/
|
|
13
|
+
export declare function shouldUseAnthropicNative(config: LLMConfig, env?: NodeJS.ProcessEnv): boolean;
|
|
14
|
+
export interface AnthropicBuildOptions {
|
|
15
|
+
effort?: EffortLevel;
|
|
16
|
+
cacheEnabled?: boolean;
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
thinkingBudgetTokens?: number;
|
|
19
|
+
}
|
|
20
|
+
interface AnthropicMessage {
|
|
21
|
+
role: 'user' | 'assistant';
|
|
22
|
+
content: any[];
|
|
23
|
+
}
|
|
24
|
+
interface AnthropicTool {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
input_schema: any;
|
|
28
|
+
cache_control?: {
|
|
29
|
+
type: 'ephemeral';
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface AnthropicRequestPayload {
|
|
33
|
+
model: string;
|
|
34
|
+
max_tokens: number;
|
|
35
|
+
system?: any;
|
|
36
|
+
messages: AnthropicMessage[];
|
|
37
|
+
tools?: AnthropicTool[];
|
|
38
|
+
thinking?: {
|
|
39
|
+
type: 'enabled';
|
|
40
|
+
budget_tokens: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Pure transform: BrainRouter chat history (OpenAI shape) →
|
|
45
|
+
* Anthropic `/v1/messages` request body.
|
|
46
|
+
*
|
|
47
|
+
* Invariants enforced here so callers don't need to know the Anthropic
|
|
48
|
+
* rules:
|
|
49
|
+
* - The system message (first message with role:'system') is hoisted
|
|
50
|
+
* to the top-level `system` field and dropped from `messages`.
|
|
51
|
+
* - Consecutive `tool` role entries are merged into one synthetic
|
|
52
|
+
* `user` message whose content is an array of `tool_result` blocks.
|
|
53
|
+
* - Assistant messages with `tool_calls` emit a content array that
|
|
54
|
+
* interleaves text (when present) and `tool_use` blocks. The
|
|
55
|
+
* OpenAI tool_call.id is reused as the Anthropic tool_use.id —
|
|
56
|
+
* callers must echo it back on the matching tool_result.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildAnthropicRequest(config: LLMConfig, messages: any[], tools: any[], options?: AnthropicBuildOptions): AnthropicRequestPayload;
|
|
59
|
+
export interface AnthropicParsedResponse {
|
|
60
|
+
content: string;
|
|
61
|
+
toolCalls?: Array<{
|
|
62
|
+
id: string;
|
|
63
|
+
type: 'function';
|
|
64
|
+
function: {
|
|
65
|
+
name: string;
|
|
66
|
+
arguments: string;
|
|
67
|
+
};
|
|
68
|
+
}>;
|
|
69
|
+
usage?: {
|
|
70
|
+
prompt_tokens?: number;
|
|
71
|
+
completion_tokens?: number;
|
|
72
|
+
};
|
|
73
|
+
thinking?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Pure transform: Anthropic response body → BrainRouter's internal
|
|
77
|
+
* `ChatResponse` shape. tool_use blocks become OpenAI-style toolCalls
|
|
78
|
+
* with the Anthropic id preserved verbatim, and `input` is re-serialized
|
|
79
|
+
* to the `function.arguments` JSON string the agent loop expects.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseAnthropicResponse(data: any): AnthropicParsedResponse;
|
|
82
|
+
export interface CallAnthropicOptions extends AnthropicBuildOptions {
|
|
83
|
+
onThinking?: (text: string) => void;
|
|
84
|
+
}
|
|
85
|
+
export declare function callAnthropic(config: LLMConfig, messages: any[], tools: any[], options?: CallAnthropicOptions): Promise<{
|
|
86
|
+
content: string;
|
|
87
|
+
toolCalls: {
|
|
88
|
+
id: string;
|
|
89
|
+
type: "function";
|
|
90
|
+
function: {
|
|
91
|
+
name: string;
|
|
92
|
+
arguments: string;
|
|
93
|
+
};
|
|
94
|
+
}[] | undefined;
|
|
95
|
+
usage: {
|
|
96
|
+
prompt_tokens?: number;
|
|
97
|
+
completion_tokens?: number;
|
|
98
|
+
} | undefined;
|
|
99
|
+
}>;
|
|
100
|
+
export {};
|