@bahulam/code 0.1.15 → 0.1.17
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/agents/loader.mjs +37 -10
- package/src/agents/registry.mjs +240 -0
- package/src/commands/install.mjs +8 -8
- package/src/commands/plugin-manage.mjs +16 -15
- package/src/commands/plugin.mjs +15 -12
- package/src/core/background-tasks.mjs +29 -3
- package/src/core/headless.mjs +46 -2
- package/src/core/stream-client.mjs +1 -0
- package/src/core/tool-executor.mjs +80 -159
- package/src/local-service/agent-relay.mjs +56 -1
- package/src/local-service/server.mjs +2 -2
- package/src/orchestration/dispatch.mjs +1 -1
- package/src/plugins/executor.mjs +2 -2
- package/src/plugins/loader.mjs +5 -5
- package/src/plugins/manifest.mjs +147 -24
- package/src/plugins/npm-install.mjs +13 -2
- package/src/plugins/pi-compat/scaffold.mjs +45 -29
- package/src/plugins/preflight.mjs +16 -7
- package/src/plugins/registry.mjs +6 -6
- package/src/plugins/state.mjs +141 -2
- package/src/terminal/repl.mjs +62 -56
- package/src/tools/bash.mjs +17 -1
package/src/core/headless.mjs
CHANGED
|
@@ -76,7 +76,28 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
76
76
|
const { dispatch } = await import('../orchestration/dispatch.mjs');
|
|
77
77
|
const { listLocalWorkflows } = await import('../agents/workflow_scaffold.mjs');
|
|
78
78
|
const pluginRegistry = new PluginRegistry().scan();
|
|
79
|
-
|
|
79
|
+
let toolExecutor = null;
|
|
80
|
+
const runDelegateFromTool = async ({ agent: targetAgent, slug, instruction: task, options = {} }) => {
|
|
81
|
+
const outcome = await dispatch({
|
|
82
|
+
type: 'invoke',
|
|
83
|
+
source: 'tool:delegate',
|
|
84
|
+
target: { kind: 'agent', slug: slug || targetAgent?.slug, agent: targetAgent },
|
|
85
|
+
params: { instruction: task || '' },
|
|
86
|
+
channel: 'local',
|
|
87
|
+
substrate: 'direct',
|
|
88
|
+
signal: options.signal,
|
|
89
|
+
}, {
|
|
90
|
+
toolExecutor,
|
|
91
|
+
listRunnables: () => toolExecutor.listRunnables(),
|
|
92
|
+
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
93
|
+
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
94
|
+
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
95
|
+
defaultModel: model || null,
|
|
96
|
+
cwd: process.cwd(),
|
|
97
|
+
});
|
|
98
|
+
return outcome;
|
|
99
|
+
};
|
|
100
|
+
toolExecutor = createToolExecutor({ pluginRegistry, delegateRunner: runDelegateFromTool });
|
|
80
101
|
const timer = setTimeout(() => {
|
|
81
102
|
emit({ type: 'timeout', duration_s: timeout });
|
|
82
103
|
process.exit(2);
|
|
@@ -121,7 +142,30 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
121
142
|
const pluginRegistry = new PluginRegistry().scan();
|
|
122
143
|
|
|
123
144
|
// Projects are registered and indexed only when the agent requests an overview.
|
|
124
|
-
|
|
145
|
+
let toolExecutor = null;
|
|
146
|
+
const runDelegateFromTool = async ({ agent: targetAgent, slug, instruction: task, options = {} }) => {
|
|
147
|
+
const { dispatch } = await import('../orchestration/dispatch.mjs');
|
|
148
|
+
const { listLocalWorkflows } = await import('../agents/workflow_scaffold.mjs');
|
|
149
|
+
const outcome = await dispatch({
|
|
150
|
+
type: 'invoke',
|
|
151
|
+
source: 'tool:delegate',
|
|
152
|
+
target: { kind: 'agent', slug: slug || targetAgent?.slug, agent: targetAgent },
|
|
153
|
+
params: { instruction: task || '' },
|
|
154
|
+
channel: 'local',
|
|
155
|
+
substrate: 'direct',
|
|
156
|
+
signal: options.signal,
|
|
157
|
+
}, {
|
|
158
|
+
toolExecutor,
|
|
159
|
+
listRunnables: () => toolExecutor.listRunnables(),
|
|
160
|
+
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
161
|
+
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
162
|
+
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
163
|
+
defaultModel: model || null,
|
|
164
|
+
cwd: process.cwd(),
|
|
165
|
+
});
|
|
166
|
+
return outcome;
|
|
167
|
+
};
|
|
168
|
+
toolExecutor = createToolExecutor({ pluginRegistry, delegateRunner: runDelegateFromTool });
|
|
125
169
|
|
|
126
170
|
// Auto-approve everything — no prompts
|
|
127
171
|
const approval = new ApprovalManager({ autoApprove: true });
|
|
@@ -226,6 +226,7 @@ export class BahulamStreamClient {
|
|
|
226
226
|
};
|
|
227
227
|
|
|
228
228
|
for (const agent of clientAgents || []) addAgent(agent);
|
|
229
|
+
for (const agent of context?.agent_context?.available_agents || []) addAgent(agent);
|
|
229
230
|
for (const agent of context?.agent_ctx?.available_agents || []) addAgent(agent);
|
|
230
231
|
for (const agent of context?.available_agents || []) addAgent(agent);
|
|
231
232
|
if (context?.sub_agent) addAgent(context.sub_agent);
|
|
@@ -16,15 +16,14 @@ import { analyzeCode } from '../context/ast-parser.mjs';
|
|
|
16
16
|
import { ProjectRegistry } from '../tools/project-overview.mjs';
|
|
17
17
|
import { SkillInstaller } from '../skills/installer.mjs';
|
|
18
18
|
import { SkillsLoader } from '../skills/loader.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { createAgentFile, listLocalAgents, syncAgentsToBackend } from '../agents/scaffold.mjs';
|
|
20
|
+
import { compactAgentMetadata, createAgentRegistry } from '../agents/registry.mjs';
|
|
20
21
|
import { createWorkflowFile, listLocalWorkflows, WORKFLOW_SYNC_ENDPOINT, slugifyWorkflowName } from '../agents/workflow_scaffold.mjs';
|
|
21
22
|
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
22
23
|
import { detectImageFile } from './attachments.mjs';
|
|
23
24
|
import { streamResponse } from './streaming.mjs';
|
|
24
25
|
import { sendApprovalDecision, sendCallback } from './callback-client.mjs';
|
|
25
26
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
26
|
-
import { loadBahulamSettings } from '../config/settings-loader.mjs';
|
|
27
|
-
import { BUILTIN_AGENTS } from '../terminal/agents.mjs';
|
|
28
27
|
import { buildFileDiff } from './file-diff.mjs';
|
|
29
28
|
import { buildWorkScope } from './work-scope.mjs';
|
|
30
29
|
import { loadDiskMemory, ensureBahulamDir, globalMemoryPath, projectMemoryPath } from './memory-disk.mjs';
|
|
@@ -60,6 +59,7 @@ export function createToolExecutor({
|
|
|
60
59
|
// REPL/headless callers leave this null — state still works, just
|
|
61
60
|
// no reactive pulse.
|
|
62
61
|
stateEmit = null,
|
|
62
|
+
delegateRunner = null,
|
|
63
63
|
// Execution channel. 'main' (REPL/headless/CLI): plugin agents are
|
|
64
64
|
// workspace-scoped and excluded from listings and the agent-context
|
|
65
65
|
// envelope unless allowlisted in settings plugins.agent_allowlist.
|
|
@@ -81,6 +81,12 @@ export function createToolExecutor({
|
|
|
81
81
|
// guards it, but doing it here means the first read is a plain fs stat
|
|
82
82
|
// rather than a mkdir round-trip.
|
|
83
83
|
try { ensureBahulamDir('global'); } catch { /* ignore */ }
|
|
84
|
+
let activeDelegateRunner = delegateRunner;
|
|
85
|
+
const agentRegistry = createAgentRegistry({
|
|
86
|
+
cwd: () => process.cwd(),
|
|
87
|
+
pluginRegistry,
|
|
88
|
+
channel,
|
|
89
|
+
});
|
|
84
90
|
let _memoryCache = null; // { key: string, facts: Fact[], digest: string }
|
|
85
91
|
function _readMemorySnapshot() {
|
|
86
92
|
const gPath = globalMemoryPath();
|
|
@@ -232,169 +238,16 @@ export function createToolExecutor({
|
|
|
232
238
|
return ['read_file', 'search_code', 'list_files'];
|
|
233
239
|
}
|
|
234
240
|
|
|
235
|
-
function agentMatches(agent, query) {
|
|
236
|
-
const needle = String(query || '').trim().toLowerCase();
|
|
237
|
-
if (!needle) return true;
|
|
238
|
-
return [
|
|
239
|
-
agent.slug,
|
|
240
|
-
agent.name,
|
|
241
|
-
agent.description,
|
|
242
|
-
agent.role,
|
|
243
|
-
agent.model,
|
|
244
|
-
...(Array.isArray(agent.tools) ? agent.tools : []),
|
|
245
|
-
...(Array.isArray(agent.capabilities) ? agent.capabilities : []),
|
|
246
|
-
...(Array.isArray(agent.domains) ? agent.domains : []),
|
|
247
|
-
].some(value => String(value || '').toLowerCase().includes(needle));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function compactAgentMetadata(agent) {
|
|
251
|
-
return {
|
|
252
|
-
slug: agent.slug,
|
|
253
|
-
name: agent.name,
|
|
254
|
-
description: agent.description || '',
|
|
255
|
-
role: agent.role || 'specialist',
|
|
256
|
-
model: agent.model || null,
|
|
257
|
-
models: agent.models && Object.keys(agent.models).length ? agent.models : undefined,
|
|
258
|
-
tools: Array.isArray(agent.tools) ? agent.tools : [],
|
|
259
|
-
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
260
|
-
domains: Array.isArray(agent.domains) ? agent.domains : [],
|
|
261
|
-
source_scope: agent.source_scope || 'unknown',
|
|
262
|
-
source: agent.source || '',
|
|
263
|
-
content_hash: agent.content_hash || '',
|
|
264
|
-
runnable: agent.runnable !== false,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function pluginAgentToLocalShape(agentDef) {
|
|
269
|
-
const pluginName = agentDef._plugin_name
|
|
270
|
-
|| String(agentDef.source || '').replace(/^plugin:/, '')
|
|
271
|
-
|| 'unknown';
|
|
272
|
-
const source = `plugin:${pluginName}`;
|
|
273
|
-
const base = {
|
|
274
|
-
...agentDef,
|
|
275
|
-
slug: agentDef.slug || agentDef.name || '',
|
|
276
|
-
name: agentDef.name || agentDef.slug || '',
|
|
277
|
-
description: agentDef.description || '',
|
|
278
|
-
role: agentDef.role || 'specialist',
|
|
279
|
-
model: agentDef.model || null,
|
|
280
|
-
models: agentDef.models || undefined,
|
|
281
|
-
tools: Array.isArray(agentDef.tools)
|
|
282
|
-
? agentDef.tools
|
|
283
|
-
: (Array.isArray(agentDef.agent_tools) ? agentDef.agent_tools : []),
|
|
284
|
-
capabilities: Array.isArray(agentDef.capabilities) ? agentDef.capabilities : [],
|
|
285
|
-
domains: Array.isArray(agentDef.domains) ? agentDef.domains : [],
|
|
286
|
-
system_prompt: agentDef.system_prompt || agentDef.prompt || agentDef.instructions || '',
|
|
287
|
-
prompt: agentDef.prompt || agentDef.system_prompt || agentDef.instructions || '',
|
|
288
|
-
source_scope: 'plugin',
|
|
289
|
-
source,
|
|
290
|
-
};
|
|
291
|
-
const spec = {
|
|
292
|
-
...agentToSpec(base),
|
|
293
|
-
source,
|
|
294
|
-
source_scope: 'plugin',
|
|
295
|
-
plugin_name: pluginName,
|
|
296
|
-
};
|
|
297
|
-
if (spec.config?.metadata && typeof spec.config.metadata === 'object') {
|
|
298
|
-
spec.config.metadata.source = source;
|
|
299
|
-
spec.config.metadata.source_scope = 'plugin';
|
|
300
|
-
}
|
|
301
|
-
const content = JSON.stringify(spec);
|
|
302
|
-
return {
|
|
303
|
-
...base,
|
|
304
|
-
slug: spec.slug,
|
|
305
|
-
spec,
|
|
306
|
-
source,
|
|
307
|
-
source_scope: 'plugin',
|
|
308
|
-
content_hash: crypto.createHash('sha256').update(content).digest('hex'),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function listPluginAgents() {
|
|
313
|
-
if (!pluginRegistry) return [];
|
|
314
|
-
return pluginRegistry.listAgents()
|
|
315
|
-
.map(pluginAgentToLocalShape)
|
|
316
|
-
.filter(agent => agent.slug);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Plugin agents are workspace-scoped entities. They enter the
|
|
320
|
-
// main-loop registry only via an explicit settings allowlist.
|
|
321
|
-
function pluginAgentAllowlist() {
|
|
322
|
-
try {
|
|
323
|
-
const { settings } = loadBahulamSettings({ cwd: process.cwd() });
|
|
324
|
-
const list = settings?.plugins?.agent_allowlist;
|
|
325
|
-
return Array.isArray(list) ? list.map(item => String(item)) : [];
|
|
326
|
-
} catch {
|
|
327
|
-
return [];
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const BUILTIN_RUNNABLES = BUILTIN_AGENTS.map(def => ({
|
|
332
|
-
slug: def.command,
|
|
333
|
-
name: def.name,
|
|
334
|
-
description: def.description || '',
|
|
335
|
-
role: 'builtin',
|
|
336
|
-
model: null,
|
|
337
|
-
models: undefined,
|
|
338
|
-
tools: [],
|
|
339
|
-
capabilities: [],
|
|
340
|
-
domains: [],
|
|
341
|
-
source_scope: 'builtin',
|
|
342
|
-
source: 'builtin',
|
|
343
|
-
content_hash: '',
|
|
344
|
-
read_only: Boolean(def.readOnly),
|
|
345
|
-
runnable: true,
|
|
346
|
-
}));
|
|
347
|
-
|
|
348
|
-
// The deterministic sub-agent registry. Resolution precedence:
|
|
349
|
-
// project agent → global agent → builtin → allowlisted plugin agent.
|
|
350
|
-
// In workspace-channel executors the session plugin's agents are
|
|
351
|
-
// runnable without an allowlist entry.
|
|
352
241
|
function listRunnables() {
|
|
353
|
-
|
|
354
|
-
for (const agent of listLocalAgents(process.cwd())) {
|
|
355
|
-
if (agent.slug && !bySlug.has(agent.slug)) {
|
|
356
|
-
bySlug.set(agent.slug, { ...agent, runnable: true });
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
for (const builtin of BUILTIN_RUNNABLES) {
|
|
360
|
-
if (!bySlug.has(builtin.slug)) bySlug.set(builtin.slug, builtin);
|
|
361
|
-
}
|
|
362
|
-
const allowlist = new Set(pluginAgentAllowlist());
|
|
363
|
-
for (const agent of listPluginAgents()) {
|
|
364
|
-
if (!agent.slug || bySlug.has(agent.slug)) continue;
|
|
365
|
-
if (channel === 'workspace' || allowlist.has(agent.slug)) {
|
|
366
|
-
bySlug.set(agent.slug, { ...agent, runnable: true });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return [...bySlug.values()];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Installed plugin agents NOT admitted to the main-loop registry —
|
|
373
|
-
// still discoverable (scope:'plugin') but flagged not runnable.
|
|
374
|
-
function listWorkspaceScopedPluginAgents() {
|
|
375
|
-
const runnableSlugs = new Set(listRunnables().map(agent => agent.slug));
|
|
376
|
-
return listPluginAgents()
|
|
377
|
-
.filter(agent => agent.slug && !runnableSlugs.has(agent.slug))
|
|
378
|
-
.map(agent => ({ ...agent, runnable: false }));
|
|
242
|
+
return agentRegistry.listRunnables();
|
|
379
243
|
}
|
|
380
244
|
|
|
381
|
-
// Agent-context envelope population: the runnable registry minus
|
|
382
|
-
// builtins (the backend has its own delegation vocabulary for those;
|
|
383
|
-
// adding them to available_agents would change wire behavior).
|
|
384
245
|
function listAvailableAgents() {
|
|
385
|
-
return
|
|
246
|
+
return agentRegistry.listAvailableAgents();
|
|
386
247
|
}
|
|
387
248
|
|
|
388
249
|
function filterLocalAgents(args = {}) {
|
|
389
|
-
|
|
390
|
-
if (scope && !['project', 'global', 'plugin', 'builtin'].includes(scope)) {
|
|
391
|
-
throw new Error('scope must be "project", "global", "plugin", or "builtin"');
|
|
392
|
-
}
|
|
393
|
-
const pool = scope === 'plugin'
|
|
394
|
-
? [...listRunnables(), ...listWorkspaceScopedPluginAgents()]
|
|
395
|
-
: listRunnables();
|
|
396
|
-
const combined = pool.filter(agent => !scope || agent.source_scope === scope);
|
|
397
|
-
return combined.filter(agent => agentMatches(agent, args.query || args.name || ''));
|
|
250
|
+
return agentRegistry.filterAgents(args);
|
|
398
251
|
}
|
|
399
252
|
|
|
400
253
|
function selectAgentsForSync(args = {}) {
|
|
@@ -1067,6 +920,57 @@ export function createToolExecutor({
|
|
|
1067
920
|
};
|
|
1068
921
|
},
|
|
1069
922
|
|
|
923
|
+
// Reserved meta-tool adapter. Cloud backends may implement Delegate
|
|
924
|
+
// natively; local callbacks use this to route through the exact same
|
|
925
|
+
// registry + dispatch funnel as /run and workflows.
|
|
926
|
+
delegate: async (args = {}, options = {}) => {
|
|
927
|
+
throwIfAborted(options.signal);
|
|
928
|
+
const target = String(args.agent || args.name || args.slug || args.sub_agent || '').trim();
|
|
929
|
+
const instruction = String(args.instruction || args.task || args.prompt || args.request || '').trim();
|
|
930
|
+
if (!target) {
|
|
931
|
+
return { success: false, output: 'delegate requires an agent slug or name.', _tool: 'delegate' };
|
|
932
|
+
}
|
|
933
|
+
if (!instruction) {
|
|
934
|
+
return { success: false, output: 'delegate requires an instruction.', _tool: 'delegate' };
|
|
935
|
+
}
|
|
936
|
+
const agent = agentRegistry.findAgent(target);
|
|
937
|
+
if (!agent) {
|
|
938
|
+
return {
|
|
939
|
+
success: false,
|
|
940
|
+
output: `Unknown delegate target '${target}'. Available agents: ${listRunnables().map(item => item.slug).join(', ') || '(none)'}`,
|
|
941
|
+
_tool: 'delegate',
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
if (typeof activeDelegateRunner !== 'function') {
|
|
945
|
+
return {
|
|
946
|
+
success: false,
|
|
947
|
+
output: 'Local delegate execution is not wired for this surface. Use /run <agent> "<task>" or delegate from a cloud execute session.',
|
|
948
|
+
_tool: 'delegate',
|
|
949
|
+
agent: compactAgentMetadata(agent),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const delegated = await activeDelegateRunner({
|
|
953
|
+
agent,
|
|
954
|
+
slug: agent.slug,
|
|
955
|
+
instruction,
|
|
956
|
+
context: args.context && typeof args.context === 'object' ? args.context : {},
|
|
957
|
+
options,
|
|
958
|
+
});
|
|
959
|
+
const payload = delegated?.result || delegated || {};
|
|
960
|
+
const output = payload.output
|
|
961
|
+
|| payload.final_response
|
|
962
|
+
|| payload.result
|
|
963
|
+
|| (typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2));
|
|
964
|
+
return {
|
|
965
|
+
success: delegated?.dispatched === false ? false : payload.success !== false,
|
|
966
|
+
output: String(output || ''),
|
|
967
|
+
agent: compactAgentMetadata(agent),
|
|
968
|
+
run_id: payload.run_id || payload.graph_run_id || null,
|
|
969
|
+
node_results: payload.node_results || undefined,
|
|
970
|
+
_tool: 'delegate',
|
|
971
|
+
};
|
|
972
|
+
},
|
|
973
|
+
|
|
1070
974
|
analyze_image: async (args, options = {}) => {
|
|
1071
975
|
throwIfAborted(options.signal);
|
|
1072
976
|
const tool = occRegistry.get('analyze_image');
|
|
@@ -2807,6 +2711,22 @@ export function createToolExecutor({
|
|
|
2807
2711
|
|
|
2808
2712
|
listRunnables,
|
|
2809
2713
|
|
|
2714
|
+
findAgent(target) {
|
|
2715
|
+
return agentRegistry.findAgent(target);
|
|
2716
|
+
},
|
|
2717
|
+
|
|
2718
|
+
filterAgents(args = {}) {
|
|
2719
|
+
return agentRegistry.filterAgents(args);
|
|
2720
|
+
},
|
|
2721
|
+
|
|
2722
|
+
getSubAgentObservability() {
|
|
2723
|
+
return agentRegistry.observability();
|
|
2724
|
+
},
|
|
2725
|
+
|
|
2726
|
+
setDelegateRunner(fn) {
|
|
2727
|
+
activeDelegateRunner = typeof fn === 'function' ? fn : null;
|
|
2728
|
+
},
|
|
2729
|
+
|
|
2810
2730
|
// Plugin tool schemas (name/description/input_schema) for callers
|
|
2811
2731
|
// that compose model-facing tool lists — e.g. the graph engine's
|
|
2812
2732
|
// direct substrate giving a plugin agent its declared tools.
|
|
@@ -2849,6 +2769,7 @@ export function createToolExecutor({
|
|
|
2849
2769
|
source: agent.source,
|
|
2850
2770
|
spec: agent.spec,
|
|
2851
2771
|
})),
|
|
2772
|
+
sub_agent_observability: agentRegistry.observability(),
|
|
2852
2773
|
// Background jobs the model should know about. Stable fields
|
|
2853
2774
|
// only (no durations) so the entry — and the prompt cache —
|
|
2854
2775
|
// changes on status transitions, not every turn.
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import { BahulamStreamClient } from '../core/stream-client.mjs';
|
|
20
20
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
21
21
|
import { buildWorkScope } from '../core/work-scope.mjs';
|
|
22
|
+
import { listLocalWorkflows } from '../agents/workflow_scaffold.mjs';
|
|
23
|
+
import { dispatch } from '../orchestration/dispatch.mjs';
|
|
22
24
|
import { BrowserApprovalManager } from './approval-bridge.mjs';
|
|
23
25
|
|
|
24
26
|
const __require = createRequire(import.meta.url);
|
|
@@ -617,7 +619,31 @@ export class LocalAgentRelay {
|
|
|
617
619
|
try { this.emit('plugin_state_changed', evt); }
|
|
618
620
|
catch { /* never let SSE failure break a tool call */ }
|
|
619
621
|
};
|
|
620
|
-
|
|
622
|
+
let toolExecutor = null;
|
|
623
|
+
const runDelegateFromTool = async ({ agent, slug, instruction, options = {} }) => {
|
|
624
|
+
const turnId = `delegate_${Date.now().toString(36)}`;
|
|
625
|
+
return await dispatch({
|
|
626
|
+
type: 'invoke',
|
|
627
|
+
source: 'tool:delegate',
|
|
628
|
+
target: { kind: 'agent', slug: slug || agent?.slug, agent },
|
|
629
|
+
params: { instruction: instruction || '' },
|
|
630
|
+
channel: 'local',
|
|
631
|
+
signal: options.signal,
|
|
632
|
+
}, {
|
|
633
|
+
toolExecutor,
|
|
634
|
+
listRunnables: () => toolExecutor?.listRunnables?.() || [],
|
|
635
|
+
listLocalWorkflows: () => listLocalWorkflows(this.session.root_path),
|
|
636
|
+
renderEvent: event => this._emitAgentEvent(event, { turnId, contentText: event?.data?.text || event?.data?.content || null }),
|
|
637
|
+
sessionSubstrate: this._makeWorkspaceSessionSubstrate(pluginRegistry),
|
|
638
|
+
auth: { token: creds.token || null },
|
|
639
|
+
credentials: {
|
|
640
|
+
apiKey: process.env.ANTHROPIC_API_KEY || creds.anthropicKey || null,
|
|
641
|
+
openRouterKey: process.env.OPENROUTER_API_KEY || creds.openRouterKey || null,
|
|
642
|
+
},
|
|
643
|
+
cwd: this.session.root_path,
|
|
644
|
+
});
|
|
645
|
+
};
|
|
646
|
+
toolExecutor = createToolExecutor({ pluginRegistry, stateEmit, channel: 'workspace', delegateRunner: runDelegateFromTool });
|
|
621
647
|
await toolExecutor.waitForAutoRegister?.();
|
|
622
648
|
await toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
|
|
623
649
|
|
|
@@ -726,6 +752,35 @@ export class LocalAgentRelay {
|
|
|
726
752
|
return execContext;
|
|
727
753
|
}
|
|
728
754
|
|
|
755
|
+
_makeWorkspaceSessionSubstrate(pluginRegistry) {
|
|
756
|
+
return (agent, node, instruction, { scopedExecutor } = {}) => (async function* (relay) {
|
|
757
|
+
const execContext = await relay._buildExecContext(instruction);
|
|
758
|
+
const slug = agent.slug || agent.command || agent.name || node?.agent_slug || node?.id || 'agent';
|
|
759
|
+
execContext.sub_agent = {
|
|
760
|
+
slug,
|
|
761
|
+
name: agent.name || slug,
|
|
762
|
+
role: agent.role || 'specialist',
|
|
763
|
+
description: agent.description || '',
|
|
764
|
+
tools: Array.isArray(agent.tools) ? agent.tools : [],
|
|
765
|
+
source: agent.source || '',
|
|
766
|
+
};
|
|
767
|
+
const systemPrompt = agent.systemPrompt || agent.system_prompt || agent.prompt || `You are ${agent.name || slug}, a Bahulam Code sub-agent.`;
|
|
768
|
+
const fullInstruction = `${systemPrompt}\n\n---\n\nUser request: ${instruction || 'Run your assigned task now.'}`;
|
|
769
|
+
const client = new BahulamStreamClient({
|
|
770
|
+
baseUrl: relay.creds.backendUrl,
|
|
771
|
+
token: relay.creds.token,
|
|
772
|
+
toolExecutor: scopedExecutor || relay.toolExecutor,
|
|
773
|
+
approvalManager: relay.approvalManager,
|
|
774
|
+
mode: 'remote',
|
|
775
|
+
pluginRegistry,
|
|
776
|
+
});
|
|
777
|
+
if (relay.resumeSessionId) client.sessionId = relay.resumeSessionId;
|
|
778
|
+
for await (const event of client.execute(fullInstruction, execContext)) {
|
|
779
|
+
yield event;
|
|
780
|
+
}
|
|
781
|
+
})(this);
|
|
782
|
+
}
|
|
783
|
+
|
|
729
784
|
_buildInstruction(prompt, currentPath, attachments = []) {
|
|
730
785
|
const lines = [
|
|
731
786
|
'You are running inside Bahulam Local IDE.',
|
|
@@ -538,7 +538,7 @@ async function routeRequest({ req, res, sessionId, token, events, sseClients, em
|
|
|
538
538
|
}
|
|
539
539
|
|
|
540
540
|
// Plugin workspace views (PRD-101 §4.6): panels declared under
|
|
541
|
-
//
|
|
541
|
+
// config.views in plugin.yaml, rendered as sandboxed iframes.
|
|
542
542
|
// Generic infra — ANY installed plugin (project-local or ~/.bahulam)
|
|
543
543
|
// contributes tabs; its whole directory is served statically so views
|
|
544
544
|
// can ship css/js/assets with relative paths.
|
|
@@ -650,7 +650,7 @@ function scanPlugins(session) {
|
|
|
650
650
|
if (!pluginName || !manifest._dir) continue;
|
|
651
651
|
if (scope !== '__all__' && pluginName.toLowerCase() !== scope) continue;
|
|
652
652
|
dirs.set(pluginName, manifest._dir);
|
|
653
|
-
(manifest.
|
|
653
|
+
(manifest.config?.views || []).forEach((view) => {
|
|
654
654
|
const source = String(view?.source || '').trim().replace(/^\.\//, '');
|
|
655
655
|
if (!source) return;
|
|
656
656
|
views.push({
|
|
@@ -70,7 +70,7 @@ function resolveAgentBySlug(slug, ctx) {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Bare-name resolution order (uniform across surfaces, preserves today's
|
|
73
|
-
* /run behavior): project agent → global agent →
|
|
73
|
+
* /run behavior): project agent → global agent → platform → allowlisted
|
|
74
74
|
* plugin agent → local workflow → synced server workflow fallback.
|
|
75
75
|
* listRunnables() already returns agents deduped in that precedence.
|
|
76
76
|
*/
|
package/src/plugins/executor.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export async function loadPluginTool(pluginDir, handlerPath) {
|
|
|
68
68
|
*/
|
|
69
69
|
export async function createPluginToolExecutor(manifest, opts = {}) {
|
|
70
70
|
const pluginDir = manifest._dir || '';
|
|
71
|
-
const tools = manifest.
|
|
71
|
+
const tools = manifest.config?.tools || [];
|
|
72
72
|
const handlers = new Map(); // name → { handler, toolDef }
|
|
73
73
|
const pluginName = manifest.metadata?.name || '';
|
|
74
74
|
|
|
@@ -118,4 +118,4 @@ export async function createPluginToolExecutor(manifest, opts = {}) {
|
|
|
118
118
|
/** Get (or open) the plugin's state handle — used by the view API. */
|
|
119
119
|
get state() { return getState(); },
|
|
120
120
|
};
|
|
121
|
-
}
|
|
121
|
+
}
|
package/src/plugins/loader.mjs
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import os from 'os';
|
|
11
10
|
import { execSync } from 'child_process';
|
|
12
11
|
import { PluginRegistry } from './registry.mjs';
|
|
12
|
+
import { parsePluginManifestFile } from './manifest.mjs';
|
|
13
|
+
import { bahulamHome } from '../core/paths.mjs';
|
|
13
14
|
|
|
14
15
|
export class PluginLoader {
|
|
15
16
|
/**
|
|
@@ -21,11 +22,11 @@ export class PluginLoader {
|
|
|
21
22
|
constructor(options = {}) {
|
|
22
23
|
const { pluginDir, pluginDirs, disabled } = options;
|
|
23
24
|
this.registry = new PluginRegistry({
|
|
24
|
-
pluginDir: pluginDir || path.join(
|
|
25
|
+
pluginDir: pluginDir || path.join(bahulamHome(), 'plugins'),
|
|
25
26
|
pluginDirs,
|
|
26
27
|
disabled,
|
|
27
28
|
});
|
|
28
|
-
this.pluginDir = pluginDir || path.join(
|
|
29
|
+
this.pluginDir = pluginDir || path.join(bahulamHome(), 'plugins');
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -73,7 +74,6 @@ export class PluginLoader {
|
|
|
73
74
|
const exists = fs.existsSync(manifestPath) ? manifestPath : (fs.existsSync(altPath) ? altPath : null);
|
|
74
75
|
|
|
75
76
|
if (exists) {
|
|
76
|
-
const { parsePluginManifestFile } = await import('./manifest.mjs');
|
|
77
77
|
const manifest = parsePluginManifestFile(exists);
|
|
78
78
|
if (manifest) {
|
|
79
79
|
this.registry.register(manifest);
|
|
@@ -135,4 +135,4 @@ export class PluginLoader {
|
|
|
135
135
|
count() {
|
|
136
136
|
return this.registry.count();
|
|
137
137
|
}
|
|
138
|
-
}
|
|
138
|
+
}
|