@bahulam/code 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/auth/tarang-auth.mjs +313 -0
- package/src/commands/agent.mjs +7 -7
- package/src/commands/install.mjs +295 -0
- package/src/commands/plugin-manage.mjs +280 -88
- package/src/config/cli-args.mjs +16 -0
- package/src/config/settings-loader.mjs +15 -0
- package/src/core/background-tasks.mjs +186 -0
- package/src/core/headless.mjs +54 -3
- package/src/core/local-agent.mjs +10 -1
- package/src/core/risk-tier.mjs +1 -0
- package/src/core/stream-client.mjs +95 -15
- package/src/core/tool-executor.mjs +266 -15
- package/src/local-service/agent-relay.mjs +1 -1
- package/src/local-service/server.mjs +116 -14
- package/src/orchestration/approval.mjs +30 -0
- package/src/orchestration/completion-triggers.mjs +40 -0
- package/src/orchestration/dispatch.mjs +118 -0
- package/src/orchestration/events.mjs +19 -0
- package/src/orchestration/graph.mjs +126 -0
- package/src/orchestration/node-runner.mjs +193 -0
- package/src/orchestration/runner.mjs +200 -0
- package/src/plugins/executor.mjs +2 -2
- package/src/plugins/manifest.mjs +30 -27
- package/src/plugins/pi-compat/loader-hook.mjs +45 -0
- package/src/plugins/pi-compat/probe.mjs +294 -0
- package/src/plugins/pi-compat/scaffold.mjs +487 -0
- package/src/plugins/pi-compat/shim.mjs +134 -0
- package/src/plugins/pi-compose.mjs +147 -0
- package/src/plugins/preflight.mjs +35 -10
- package/src/plugins/registry.mjs +6 -0
- package/src/terminal/agents.mjs +8 -3
- package/src/terminal/main.mjs +39 -7
- package/src/terminal/paste-input.mjs +23 -0
- package/src/terminal/repl-render.mjs +65 -10
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +624 -103
- package/src/tools/agent.mjs +6 -2
- package/src/tools/registry.mjs +107 -4
- package/src/ui/input-dock.mjs +5 -2
- package/src/ui/slash-commands.mjs +1 -1
- package/src/ui/sub-agent.mjs +14 -8
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundTasks — the one registry for long-running processes the agent
|
|
3
|
+
* starts (docker build, npm run dev, test suites). Jobs get a run id,
|
|
4
|
+
* a per-job timeout with SIGTERM→SIGKILL escalation, output spooled to
|
|
5
|
+
* .bahulam/tmp/jobs/<id>.log (with a bounded in-memory tail), completion
|
|
6
|
+
* listeners for wake-on-finish delivery, and best-effort cleanup of the
|
|
7
|
+
* whole process group when the CLI exits.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
14
|
+
const KILL_ESCALATION_MS = 5000;
|
|
15
|
+
const MAX_TAIL_BYTES = 64 * 1024;
|
|
16
|
+
|
|
17
|
+
function stripAnsi(str) {
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
return String(str || '').replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class BackgroundTasks {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.jobs = new Map();
|
|
25
|
+
this._seq = 0;
|
|
26
|
+
this._listeners = new Set();
|
|
27
|
+
this._exitHookInstalled = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onExit(listener) {
|
|
31
|
+
this._listeners.add(listener);
|
|
32
|
+
return () => this._listeners.delete(listener);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start({ command, cwd = process.cwd(), timeoutMs = DEFAULT_TIMEOUT_MS, name = '', on_complete = null }) {
|
|
36
|
+
this._installExitHook();
|
|
37
|
+
const id = `job-${++this._seq}-${Date.now().toString(36)}`;
|
|
38
|
+
const logDir = path.join(cwd, '.bahulam', 'tmp', 'jobs');
|
|
39
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
40
|
+
const logPath = path.join(logDir, `${id}.log`);
|
|
41
|
+
const logStream = fs.createWriteStream(logPath);
|
|
42
|
+
|
|
43
|
+
const proc = spawn('bash', ['-c', command], {
|
|
44
|
+
cwd,
|
|
45
|
+
env: { ...process.env },
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
detached: process.platform !== 'win32',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const job = {
|
|
51
|
+
id,
|
|
52
|
+
name: name || command.slice(0, 60),
|
|
53
|
+
command,
|
|
54
|
+
cwd,
|
|
55
|
+
pid: proc.pid,
|
|
56
|
+
status: 'running',
|
|
57
|
+
exit_code: null,
|
|
58
|
+
started_at: Date.now(),
|
|
59
|
+
ended_at: null,
|
|
60
|
+
log_path: logPath,
|
|
61
|
+
tail: '',
|
|
62
|
+
timed_out: false,
|
|
63
|
+
on_complete,
|
|
64
|
+
_proc: proc,
|
|
65
|
+
_done: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const appendTail = (chunk) => {
|
|
69
|
+
const next = job.tail + chunk.toString();
|
|
70
|
+
job.tail = next.length > MAX_TAIL_BYTES ? next.slice(next.length - MAX_TAIL_BYTES) : next;
|
|
71
|
+
};
|
|
72
|
+
proc.stdout.on('data', (d) => { logStream.write(d); appendTail(d); });
|
|
73
|
+
proc.stderr.on('data', (d) => { logStream.write(d); appendTail(d); });
|
|
74
|
+
|
|
75
|
+
let killTimer = null;
|
|
76
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
77
|
+
job.timed_out = true;
|
|
78
|
+
this._kill(job, 'SIGTERM');
|
|
79
|
+
killTimer = setTimeout(() => this._kill(job, 'SIGKILL'), KILL_ESCALATION_MS);
|
|
80
|
+
}, timeoutMs) : null;
|
|
81
|
+
if (timer?.unref) timer.unref();
|
|
82
|
+
|
|
83
|
+
job._done = new Promise((resolve) => {
|
|
84
|
+
proc.on('close', (code) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
clearTimeout(killTimer);
|
|
87
|
+
job.exit_code = code;
|
|
88
|
+
job.ended_at = Date.now();
|
|
89
|
+
job.status = job.timed_out ? 'timeout'
|
|
90
|
+
: job.status === 'killed' ? 'killed'
|
|
91
|
+
: code === 0 ? 'completed' : 'failed';
|
|
92
|
+
job.tail = stripAnsi(job.tail);
|
|
93
|
+
logStream.end();
|
|
94
|
+
for (const listener of this._listeners) {
|
|
95
|
+
try { listener(this.describe(job.id)); } catch { /* listeners are best-effort */ }
|
|
96
|
+
}
|
|
97
|
+
resolve(this.describe(job.id));
|
|
98
|
+
});
|
|
99
|
+
proc.on('error', (err) => {
|
|
100
|
+
job.status = 'failed';
|
|
101
|
+
job.tail = `${job.tail}\n${err.message}`.trim();
|
|
102
|
+
job.ended_at = Date.now();
|
|
103
|
+
logStream.end();
|
|
104
|
+
resolve(this.describe(job.id));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
proc.unref();
|
|
109
|
+
this.jobs.set(id, job);
|
|
110
|
+
return this.describe(id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Await a job's completion; resolves with its final description. */
|
|
114
|
+
wait(id) {
|
|
115
|
+
const job = this.jobs.get(id);
|
|
116
|
+
if (!job) return Promise.resolve(null);
|
|
117
|
+
if (job.status !== 'running') return Promise.resolve(this.describe(id));
|
|
118
|
+
// Background jobs are unref'd so fire-and-forget tasks do not pin the CLI
|
|
119
|
+
// open. When a caller explicitly awaits wait(id), temporarily ref the
|
|
120
|
+
// process so fast commands still get their close event before Node decides
|
|
121
|
+
// the top-level await is unsettled.
|
|
122
|
+
try { job._proc?.ref?.(); } catch { /* best effort */ }
|
|
123
|
+
return job._done.finally(() => {
|
|
124
|
+
try { job._proc?.unref?.(); } catch { /* best effort */ }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe(id) {
|
|
129
|
+
const job = this.jobs.get(id);
|
|
130
|
+
if (!job) return null;
|
|
131
|
+
return {
|
|
132
|
+
id: job.id,
|
|
133
|
+
name: job.name,
|
|
134
|
+
command: job.command,
|
|
135
|
+
pid: job.pid,
|
|
136
|
+
status: job.status,
|
|
137
|
+
exit_code: job.exit_code,
|
|
138
|
+
duration_s: Math.round(((job.ended_at || Date.now()) - job.started_at) / 1000),
|
|
139
|
+
log_path: job.log_path,
|
|
140
|
+
tail: job.tail,
|
|
141
|
+
timed_out: job.timed_out,
|
|
142
|
+
on_complete: job.on_complete || null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
list() {
|
|
147
|
+
return [...this.jobs.keys()].map(id => {
|
|
148
|
+
const d = this.describe(id);
|
|
149
|
+
return { ...d, tail: undefined };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
kill(id) {
|
|
154
|
+
const job = this.jobs.get(id);
|
|
155
|
+
if (!job) return null;
|
|
156
|
+
if (job.status === 'running') {
|
|
157
|
+
job.status = 'killed';
|
|
158
|
+
this._kill(job, 'SIGTERM');
|
|
159
|
+
setTimeout(() => this._kill(job, 'SIGKILL'), KILL_ESCALATION_MS)?.unref?.();
|
|
160
|
+
}
|
|
161
|
+
return this.describe(id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_kill(job, signal) {
|
|
165
|
+
if (!job?._proc?.pid) return;
|
|
166
|
+
try {
|
|
167
|
+
if (process.platform !== 'win32') {
|
|
168
|
+
process.kill(-job._proc.pid, signal);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
} catch { /* fall through */ }
|
|
172
|
+
try { job._proc.kill(signal); } catch { /* already exited */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_installExitHook() {
|
|
176
|
+
if (this._exitHookInstalled) return;
|
|
177
|
+
this._exitHookInstalled = true;
|
|
178
|
+
process.on('exit', () => {
|
|
179
|
+
for (const job of this.jobs.values()) {
|
|
180
|
+
if (job.status === 'running') this._kill(job, 'SIGKILL');
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const backgroundTasks = new BackgroundTasks();
|
package/src/core/headless.mjs
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
* @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
|
|
48
48
|
* @param {boolean} [opts.verbose] - show progress on stderr
|
|
49
49
|
*/
|
|
50
|
-
export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, vision = [] }) {
|
|
50
|
+
export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, vision = [], agent = null, workflow = null }) {
|
|
51
51
|
const startTime = Date.now();
|
|
52
52
|
|
|
53
53
|
const log = (msg) => {
|
|
@@ -61,12 +61,63 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
61
61
|
// ── Auth ──
|
|
62
62
|
const auth = new BahulamAuth();
|
|
63
63
|
const creds = auth.loadCredentials();
|
|
64
|
-
|
|
64
|
+
const graphTarget = agent || workflow;
|
|
65
|
+
const anthKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
|
|
66
|
+
const orKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
|
|
67
|
+
// Graph runs execute locally and only need a model key; everything
|
|
68
|
+
// else still requires login (the backend runs the agent loop).
|
|
69
|
+
if (!creds.token && !(graphTarget && (anthKey || orKey))) {
|
|
65
70
|
emit({ type: 'error', error: 'Not logged in. Run: bahulam login' });
|
|
66
71
|
process.exit(1);
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
//
|
|
74
|
+
// ── Deterministic graph target: --agent <slug> / --workflow <name> ──
|
|
75
|
+
if (graphTarget) {
|
|
76
|
+
const { dispatch } = await import('../orchestration/dispatch.mjs');
|
|
77
|
+
const { listLocalWorkflows } = await import('../agents/workflow_scaffold.mjs');
|
|
78
|
+
const pluginRegistry = new PluginRegistry().scan();
|
|
79
|
+
const toolExecutor = createToolExecutor({ pluginRegistry });
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
emit({ type: 'timeout', duration_s: timeout });
|
|
82
|
+
process.exit(2);
|
|
83
|
+
}, timeout * 1000);
|
|
84
|
+
|
|
85
|
+
const outcome = await dispatch({
|
|
86
|
+
type: 'invoke',
|
|
87
|
+
source: 'cli:headless',
|
|
88
|
+
target: agent ? { kind: 'agent', slug: agent } : { kind: 'workflow', slug: workflow },
|
|
89
|
+
params: { instruction: instruction || '' },
|
|
90
|
+
channel: null,
|
|
91
|
+
substrate: 'direct',
|
|
92
|
+
}, {
|
|
93
|
+
toolExecutor,
|
|
94
|
+
listRunnables: () => toolExecutor.listRunnables(),
|
|
95
|
+
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
96
|
+
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
97
|
+
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
98
|
+
defaultModel: model || null,
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (!outcome.dispatched) {
|
|
104
|
+
emit({ type: 'error', error: outcome.reason });
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
const result = outcome.result || {};
|
|
108
|
+
emit({
|
|
109
|
+
type: 'result',
|
|
110
|
+
status: result.status || (result.success === false ? 'failed' : 'completed'),
|
|
111
|
+
channel: outcome.channel,
|
|
112
|
+
output: result.output || '',
|
|
113
|
+
node_results: result.node_results || undefined,
|
|
114
|
+
duration_s: Math.round((Date.now() - startTime) / 1000),
|
|
115
|
+
});
|
|
116
|
+
process.exit(result.status === 'failed' || result.success === false ? 1 : 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Scan plugins so client_agents and agent-scoped plugin tool schemas
|
|
120
|
+
// are sent to the backend.
|
|
70
121
|
const pluginRegistry = new PluginRegistry().scan();
|
|
71
122
|
|
|
72
123
|
// Projects are registered and indexed only when the agent requests an overview.
|
package/src/core/local-agent.mjs
CHANGED
|
@@ -206,6 +206,11 @@ export class LocalAgent {
|
|
|
206
206
|
maxTurns = null,
|
|
207
207
|
stagnationDetection = false,
|
|
208
208
|
stagnationThreshold = 3,
|
|
209
|
+
// Additional tool schemas beyond the built-in set — e.g. plugin
|
|
210
|
+
// tools a sub-agent node declares. Execution still routes through
|
|
211
|
+
// the (scoped) toolExecutor; this only makes the schemas visible
|
|
212
|
+
// to the model.
|
|
213
|
+
extraToolSchemas = [],
|
|
209
214
|
}) {
|
|
210
215
|
this.apiKey = apiKey;
|
|
211
216
|
this.openRouterKey = openRouterKey;
|
|
@@ -218,6 +223,7 @@ export class LocalAgent {
|
|
|
218
223
|
this.maxTurns = maxTurns || MAX_ITERATIONS;
|
|
219
224
|
this.stagnationDetection = stagnationDetection;
|
|
220
225
|
this.stagnationThreshold = stagnationThreshold;
|
|
226
|
+
this.extraToolSchemas = Array.isArray(extraToolSchemas) ? extraToolSchemas : [];
|
|
221
227
|
this._cancelled = false;
|
|
222
228
|
this.promptCache = new PromptCache();
|
|
223
229
|
}
|
|
@@ -498,7 +504,10 @@ export class LocalAgent {
|
|
|
498
504
|
}
|
|
499
505
|
|
|
500
506
|
_buildToolDefs() {
|
|
501
|
-
return TOOL_SCHEMAS;
|
|
507
|
+
if (!this.extraToolSchemas.length) return TOOL_SCHEMAS;
|
|
508
|
+
const names = new Set(TOOL_SCHEMAS.map(t => t.name));
|
|
509
|
+
const extras = this.extraToolSchemas.filter(t => t?.name && !names.has(t.name));
|
|
510
|
+
return extras.length ? [...TOOL_SCHEMAS, ...extras] : TOOL_SCHEMAS;
|
|
502
511
|
}
|
|
503
512
|
|
|
504
513
|
_buildSystemPrompt(context, retrievedContext = null) {
|
package/src/core/risk-tier.mjs
CHANGED
|
@@ -190,17 +190,69 @@ export class BahulamStreamClient {
|
|
|
190
190
|
this._bundledReady = false;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
_getPluginToolMap() {
|
|
194
|
+
const tools = new Map();
|
|
195
|
+
if (!this.pluginRegistry) return tools;
|
|
196
|
+
for (const tool of this.pluginRegistry.listTools?.() || []) {
|
|
197
|
+
const name = String(tool.name || '').trim();
|
|
198
|
+
if (!name || tools.has(name)) continue;
|
|
199
|
+
tools.set(name, tool);
|
|
200
|
+
}
|
|
201
|
+
return tools;
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
205
|
+
* Plugin tools are intentionally not advertised as primary client_tools.
|
|
206
|
+
* They are executable by the local callback handler, but the primary model
|
|
207
|
+
* should reach them by delegating to an agent that declares them.
|
|
196
208
|
*/
|
|
197
209
|
_getPluginToolSchemas() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_collectAgentScopedToolRefs(context = {}, clientAgents = []) {
|
|
214
|
+
const refs = new Map();
|
|
215
|
+
const pluginTools = this._getPluginToolMap();
|
|
216
|
+
const addAgent = (agent = {}) => {
|
|
217
|
+
const slug = String(agent.slug || agent.command || agent.name || '').trim();
|
|
218
|
+
const tools = Array.isArray(agent.tools) ? agent.tools : [];
|
|
219
|
+
for (const toolName of tools) {
|
|
220
|
+
const name = String(toolName || '').trim();
|
|
221
|
+
if (!name || !pluginTools.has(name)) continue;
|
|
222
|
+
if (!refs.has(name)) refs.set(name, new Set());
|
|
223
|
+
if (slug) refs.get(name).add(slug);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (const agent of clientAgents || []) addAgent(agent);
|
|
228
|
+
for (const agent of context?.agent_ctx?.available_agents || []) addAgent(agent);
|
|
229
|
+
for (const agent of context?.available_agents || []) addAgent(agent);
|
|
230
|
+
if (context?.sub_agent) addAgent(context.sub_agent);
|
|
231
|
+
return refs;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Plugin tool schemas scoped to sub-agents that declare those tools.
|
|
236
|
+
* This keeps plugin tools out of the primary model's direct tool surface
|
|
237
|
+
* while still giving delegated/custom/plugin agents the schemas they need.
|
|
238
|
+
*
|
|
239
|
+
* @returns {Array<{name: string, description: string, input_schema: object, source_scope: string, plugin_name: string|null, allowed_agents: string[]}>}
|
|
240
|
+
*/
|
|
241
|
+
_getClientAgentToolSchemas(context = {}, clientAgents = []) {
|
|
242
|
+
const pluginTools = this._getPluginToolMap();
|
|
243
|
+
if (!pluginTools.size) return [];
|
|
244
|
+
const refs = this._collectAgentScopedToolRefs(context, clientAgents);
|
|
245
|
+
return [...refs.entries()].map(([name, allowedAgents]) => {
|
|
246
|
+
const tool = pluginTools.get(name) || {};
|
|
247
|
+
return {
|
|
248
|
+
name,
|
|
249
|
+
description: tool.description || '',
|
|
250
|
+
input_schema: tool.input_schema || { type: 'object', properties: {} },
|
|
251
|
+
source_scope: 'plugin',
|
|
252
|
+
plugin_name: tool._plugin_name || tool.plugin_name || null,
|
|
253
|
+
allowed_agents: [...allowedAgents],
|
|
254
|
+
};
|
|
255
|
+
});
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
/**
|
|
@@ -209,13 +261,30 @@ export class BahulamStreamClient {
|
|
|
209
261
|
*/
|
|
210
262
|
_getPluginAgentSchemas() {
|
|
211
263
|
if (!this.pluginRegistry) return [];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
264
|
+
// Only plugin agents admitted to the main-loop registry (settings
|
|
265
|
+
// plugins.agent_allowlist, or the session plugin in workspace-channel
|
|
266
|
+
// executors) are advertised. Workspace-scoped plugin agents stay out
|
|
267
|
+
// of the main-turn payload; without an executor registry, fall back
|
|
268
|
+
// to advertising everything (legacy behavior).
|
|
269
|
+
const runnables = this.toolExecutor?.listRunnables?.();
|
|
270
|
+
const admitted = Array.isArray(runnables)
|
|
271
|
+
? new Set(runnables.filter(a => a.source_scope === 'plugin').map(a => a.slug))
|
|
272
|
+
: null;
|
|
273
|
+
return this.pluginRegistry.listAgents()
|
|
274
|
+
.filter(a => !admitted || admitted.has(a.slug || a.name || ''))
|
|
275
|
+
.map(a => ({
|
|
276
|
+
slug: a.slug || a.name || '',
|
|
277
|
+
name: a.name || a.slug || '',
|
|
278
|
+
role: a.role || 'specialist',
|
|
279
|
+
description: a.description || '',
|
|
280
|
+
tools: Array.isArray(a.tools) ? a.tools : [],
|
|
281
|
+
system_prompt: a.system_prompt || a.systemPrompt || a.prompt || '',
|
|
282
|
+
model: a.model || null,
|
|
283
|
+
models: a.models || null,
|
|
284
|
+
source: a.source || (a._plugin_name ? `plugin:${a._plugin_name}` : 'plugin'),
|
|
285
|
+
source_scope: 'plugin',
|
|
286
|
+
plugin_name: a._plugin_name || null,
|
|
287
|
+
}));
|
|
219
288
|
}
|
|
220
289
|
|
|
221
290
|
/**
|
|
@@ -297,6 +366,8 @@ export class BahulamStreamClient {
|
|
|
297
366
|
if (clientTools.length > 0) body.client_tools = clientTools;
|
|
298
367
|
const clientAgents = this._getPluginAgentSchemas();
|
|
299
368
|
if (clientAgents.length > 0) body.client_agents = clientAgents;
|
|
369
|
+
const clientAgentTools = this._getClientAgentToolSchemas(context, clientAgents);
|
|
370
|
+
if (clientAgentTools.length > 0) body.client_agent_tools = clientAgentTools;
|
|
300
371
|
const requestId = `cli-${_uuidLike()}`;
|
|
301
372
|
|
|
302
373
|
// daemon cache-guard hook. If BAHULAM_CAPTURE_REQUEST is set to a file
|
|
@@ -795,6 +866,7 @@ export class BahulamStreamClient {
|
|
|
795
866
|
const callId = call_id || request_id;
|
|
796
867
|
const toolName = tool;
|
|
797
868
|
const isInternal = Boolean(data?.internal || data?.sub_agent);
|
|
869
|
+
const subAgentRunId = data?.run_id || data?.sub_agent_run_id || null;
|
|
798
870
|
|
|
799
871
|
if (this.verbose) {
|
|
800
872
|
process.stderr.write(`\x1b[2m[tool] ${toolName}(${JSON.stringify(args).slice(0, 80)}...)\x1b[0m\n`);
|
|
@@ -812,6 +884,8 @@ export class BahulamStreamClient {
|
|
|
812
884
|
_cancelled: true,
|
|
813
885
|
internal: isInternal,
|
|
814
886
|
sub_agent: data?.sub_agent || null,
|
|
887
|
+
run_id: subAgentRunId,
|
|
888
|
+
sub_agent_run_id: subAgentRunId,
|
|
815
889
|
local_callback: false,
|
|
816
890
|
},
|
|
817
891
|
};
|
|
@@ -823,6 +897,10 @@ export class BahulamStreamClient {
|
|
|
823
897
|
try {
|
|
824
898
|
result = await this.toolExecutor.execute(toolName, args || {}, {
|
|
825
899
|
signal: this._toolAbort?.signal,
|
|
900
|
+
toolCallSource: 'model',
|
|
901
|
+
internal: isInternal,
|
|
902
|
+
subAgent: data?.sub_agent || null,
|
|
903
|
+
subAgentRunId,
|
|
826
904
|
});
|
|
827
905
|
} catch (err) {
|
|
828
906
|
if (err?.name === 'AbortError' || this._cancelled) {
|
|
@@ -862,6 +940,8 @@ export class BahulamStreamClient {
|
|
|
862
940
|
duration_ms: durationMs,
|
|
863
941
|
internal: isInternal,
|
|
864
942
|
sub_agent: data?.sub_agent || null,
|
|
943
|
+
run_id: subAgentRunId,
|
|
944
|
+
sub_agent_run_id: subAgentRunId,
|
|
865
945
|
local_callback: true,
|
|
866
946
|
},
|
|
867
947
|
};
|