@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
|
@@ -23,9 +23,12 @@ import { detectImageFile } from './attachments.mjs';
|
|
|
23
23
|
import { streamResponse } from './streaming.mjs';
|
|
24
24
|
import { sendApprovalDecision, sendCallback } from './callback-client.mjs';
|
|
25
25
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
26
|
+
import { loadBahulamSettings } from '../config/settings-loader.mjs';
|
|
27
|
+
import { BUILTIN_AGENTS } from '../terminal/agents.mjs';
|
|
26
28
|
import { buildFileDiff } from './file-diff.mjs';
|
|
27
29
|
import { buildWorkScope } from './work-scope.mjs';
|
|
28
30
|
import { loadDiskMemory, ensureBahulamDir, globalMemoryPath, projectMemoryPath } from './memory-disk.mjs';
|
|
31
|
+
import { backgroundTasks } from './background-tasks.mjs';
|
|
29
32
|
import { resolveLintCommand } from './lint-resolver.mjs';
|
|
30
33
|
import { PluginRegistry } from '../plugins/registry.mjs';
|
|
31
34
|
import { loadPluginTool } from '../plugins/executor.mjs';
|
|
@@ -57,6 +60,12 @@ export function createToolExecutor({
|
|
|
57
60
|
// REPL/headless callers leave this null — state still works, just
|
|
58
61
|
// no reactive pulse.
|
|
59
62
|
stateEmit = null,
|
|
63
|
+
// Execution channel. 'main' (REPL/headless/CLI): plugin agents are
|
|
64
|
+
// workspace-scoped and excluded from listings and the agent-context
|
|
65
|
+
// envelope unless allowlisted in settings plugins.agent_allowlist.
|
|
66
|
+
// 'workspace' (plugin workspace sessions via agent-relay): the
|
|
67
|
+
// session plugin's agents are fully available.
|
|
68
|
+
channel = 'main',
|
|
60
69
|
} = {}) {
|
|
61
70
|
// Cross-session memory cache. Ships in getAgentContext() on every turn,
|
|
62
71
|
// so we need it to be byte-identical when the underlying disk file hasn't
|
|
@@ -87,7 +96,7 @@ export function createToolExecutor({
|
|
|
87
96
|
_memoryCache = { key, facts, digest };
|
|
88
97
|
return _memoryCache;
|
|
89
98
|
}
|
|
90
|
-
const occRegistry = createToolRegistry();
|
|
99
|
+
const occRegistry = createToolRegistry({ pluginRegistry, stateEmit });
|
|
91
100
|
const skillTool = occRegistry.get('Skill');
|
|
92
101
|
if (skillTool) skillTool._skillsLoader = skillsLoader;
|
|
93
102
|
const installer = skillInstaller || new SkillInstaller({
|
|
@@ -252,6 +261,7 @@ export function createToolExecutor({
|
|
|
252
261
|
source_scope: agent.source_scope || 'unknown',
|
|
253
262
|
source: agent.source || '',
|
|
254
263
|
content_hash: agent.content_hash || '',
|
|
264
|
+
runnable: agent.runnable !== false,
|
|
255
265
|
};
|
|
256
266
|
}
|
|
257
267
|
|
|
@@ -306,24 +316,84 @@ export function createToolExecutor({
|
|
|
306
316
|
.filter(agent => agent.slug);
|
|
307
317
|
}
|
|
308
318
|
|
|
309
|
-
|
|
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
|
+
function listRunnables() {
|
|
310
353
|
const bySlug = new Map();
|
|
311
354
|
for (const agent of listLocalAgents(process.cwd())) {
|
|
312
|
-
if (agent.slug && !bySlug.has(agent.slug))
|
|
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);
|
|
313
361
|
}
|
|
362
|
+
const allowlist = new Set(pluginAgentAllowlist());
|
|
314
363
|
for (const agent of listPluginAgents()) {
|
|
315
|
-
if (agent.slug
|
|
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
|
+
}
|
|
316
368
|
}
|
|
317
369
|
return [...bySlug.values()];
|
|
318
370
|
}
|
|
319
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 }));
|
|
379
|
+
}
|
|
380
|
+
|
|
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
|
+
function listAvailableAgents() {
|
|
385
|
+
return listRunnables().filter(agent => agent.source_scope !== 'builtin');
|
|
386
|
+
}
|
|
387
|
+
|
|
320
388
|
function filterLocalAgents(args = {}) {
|
|
321
389
|
const scope = String(args.scope || '').trim();
|
|
322
|
-
if (scope && !['project', 'global', 'plugin'].includes(scope)) {
|
|
323
|
-
throw new Error('scope must be "project", "global", or "
|
|
390
|
+
if (scope && !['project', 'global', 'plugin', 'builtin'].includes(scope)) {
|
|
391
|
+
throw new Error('scope must be "project", "global", "plugin", or "builtin"');
|
|
324
392
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
393
|
+
const pool = scope === 'plugin'
|
|
394
|
+
? [...listRunnables(), ...listWorkspaceScopedPluginAgents()]
|
|
395
|
+
: listRunnables();
|
|
396
|
+
const combined = pool.filter(agent => !scope || agent.source_scope === scope);
|
|
327
397
|
return combined.filter(agent => agentMatches(agent, args.query || args.name || ''));
|
|
328
398
|
}
|
|
329
399
|
|
|
@@ -694,13 +764,14 @@ export function createToolExecutor({
|
|
|
694
764
|
// They are dispatched with lower priority (built-in tools win on name collision).
|
|
695
765
|
const pluginToolMap = new Map(); // name → async handler function
|
|
696
766
|
|
|
697
|
-
function registerPluginTool(name, handler) {
|
|
767
|
+
function registerPluginTool(name, handler, metadata = {}) {
|
|
698
768
|
if (pluginToolMap.has(name)) {
|
|
699
769
|
if (process.env.DEBUG) {
|
|
700
770
|
console.warn(`Plugin tool "${name}" already registered from another plugin — skipping.`);
|
|
701
771
|
}
|
|
702
772
|
return false;
|
|
703
773
|
}
|
|
774
|
+
handler._pluginTool = metadata;
|
|
704
775
|
pluginToolMap.set(name, handler);
|
|
705
776
|
return true;
|
|
706
777
|
}
|
|
@@ -735,7 +806,7 @@ export function createToolExecutor({
|
|
|
735
806
|
}
|
|
736
807
|
return false;
|
|
737
808
|
}
|
|
738
|
-
|
|
809
|
+
const mcpHandler = async (args, options = {}) => {
|
|
739
810
|
// The lazy-state getter matches JS plugin tools so an MCP
|
|
740
811
|
// "wrapper" tool can trivially write its result to the same
|
|
741
812
|
// Shared Blackboard (rare, but useful for cache-and-return).
|
|
@@ -765,7 +836,9 @@ export function createToolExecutor({
|
|
|
765
836
|
_mcp_server: serverName,
|
|
766
837
|
};
|
|
767
838
|
}
|
|
768
|
-
}
|
|
839
|
+
};
|
|
840
|
+
mcpHandler._pluginTool = { pluginName, source: 'mcp', serverName, toolName };
|
|
841
|
+
pluginToolMap.set(qualified, mcpHandler);
|
|
769
842
|
// Track schema for tool-listing surfaces (also help /tools discovery).
|
|
770
843
|
pluginToolMap.get(qualified)._mcp = { pluginName, serverName, toolName, schema: toolSchema };
|
|
771
844
|
return true;
|
|
@@ -777,12 +850,59 @@ export function createToolExecutor({
|
|
|
777
850
|
const name = String(toolDef.name || '').trim();
|
|
778
851
|
if (!name || toolMap[name]) continue;
|
|
779
852
|
const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
|
|
853
|
+
if (toolDef._composed?.kind === 'pi') {
|
|
854
|
+
// Composed pi tools resolve at invocation time: look up the
|
|
855
|
+
// installed pi package's directory, load the specific handler
|
|
856
|
+
// via the shim-backed probe, invoke, return the result. Handler
|
|
857
|
+
// cache is per-session (per tool name) to amortize the ~50ms
|
|
858
|
+
// child-process overhead on repeat calls.
|
|
859
|
+
let _piInvokeP = null;
|
|
860
|
+
registerPluginTool(name, async (args, options = {}) => {
|
|
861
|
+
try {
|
|
862
|
+
if (!_piInvokeP) {
|
|
863
|
+
const { loadPiToolHandler } = await import('../plugins/pi-compat/probe.mjs');
|
|
864
|
+
const packageName = toolDef._composed.package_name;
|
|
865
|
+
const originalName = toolDef._composed.original_name;
|
|
866
|
+
const { bahulamHome } = await import('./paths.mjs');
|
|
867
|
+
const piBaseDir = path.join(bahulamHome(), 'plugins-pi');
|
|
868
|
+
const piDir = path.join(piBaseDir, packageName.replace(/[/@]/g, '_'));
|
|
869
|
+
if (!fs.existsSync(piDir)) {
|
|
870
|
+
return {
|
|
871
|
+
success: false,
|
|
872
|
+
output: `Composed pi tool '${name}' unavailable: pi package ${packageName} is not installed. Run \`bahulam plugin install pi:${packageName}\`.`,
|
|
873
|
+
_tool: name,
|
|
874
|
+
_plugin: pluginName,
|
|
875
|
+
_composed: toolDef._composed,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
_piInvokeP = loadPiToolHandler(piDir, originalName, { pluginName: packageName });
|
|
879
|
+
}
|
|
880
|
+
const invoke = await _piInvokeP;
|
|
881
|
+
const result = await invoke(args || {});
|
|
882
|
+
return {
|
|
883
|
+
...(result && typeof result === 'object' ? result : { success: true, output: String(result) }),
|
|
884
|
+
_tool: name,
|
|
885
|
+
_plugin: pluginName,
|
|
886
|
+
_composed: toolDef._composed,
|
|
887
|
+
};
|
|
888
|
+
} catch (err) {
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
output: `Composed pi tool '${name}' failed: ${err.message}`,
|
|
892
|
+
_tool: name,
|
|
893
|
+
_plugin: pluginName,
|
|
894
|
+
_composed: toolDef._composed,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}, { pluginName, source: 'pi', composed: toolDef._composed });
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
780
900
|
registerPluginTool(name, async (args, options = {}) => {
|
|
781
|
-
const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.
|
|
901
|
+
const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.tool);
|
|
782
902
|
if (!handler) {
|
|
783
903
|
return {
|
|
784
904
|
success: false,
|
|
785
|
-
output: `Plugin tool
|
|
905
|
+
output: `Plugin tool module could not be loaded: ${name}`,
|
|
786
906
|
_tool: name,
|
|
787
907
|
_plugin: pluginName,
|
|
788
908
|
};
|
|
@@ -826,15 +946,57 @@ export function createToolExecutor({
|
|
|
826
946
|
_plugin: pluginName,
|
|
827
947
|
};
|
|
828
948
|
}
|
|
829
|
-
});
|
|
949
|
+
}, { pluginName, source: 'plugin' });
|
|
830
950
|
}
|
|
831
951
|
}
|
|
832
952
|
|
|
953
|
+
function pluginAgentForTool(toolName, pluginName) {
|
|
954
|
+
if (!pluginRegistry) return null;
|
|
955
|
+
return (pluginRegistry.listAgents?.() || []).find(agent => {
|
|
956
|
+
const agentPlugin = agent._plugin_name
|
|
957
|
+
|| String(agent.source || '').replace(/^plugin:/, '')
|
|
958
|
+
|| null;
|
|
959
|
+
if (pluginName && agentPlugin && agentPlugin !== pluginName) return false;
|
|
960
|
+
return Array.isArray(agent.tools) && agent.tools.includes(toolName);
|
|
961
|
+
}) || null;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function primaryModelPluginToolBlock(name, handler, options = {}) {
|
|
965
|
+
if (!handler?._pluginTool) return null;
|
|
966
|
+
if (options.toolCallSource !== 'model') return null;
|
|
967
|
+
if (options.internal || options.subAgent || options.allowPrimaryPluginToolCall) return null;
|
|
968
|
+
|
|
969
|
+
const pluginName = handler._pluginTool.pluginName || 'plugin';
|
|
970
|
+
// Tools-only plugins have no delegation owner: with no agent to
|
|
971
|
+
// route through, the primary agent uses the tools directly (the
|
|
972
|
+
// user consented by enabling the plugin). The delegate-only rule
|
|
973
|
+
// applies only when the plugin ships an owning agent.
|
|
974
|
+
const pluginShipsAgents = (pluginRegistry?.listAgents?.() || [])
|
|
975
|
+
.some(agent => (agent._plugin_name || '') === pluginName);
|
|
976
|
+
if (!pluginShipsAgents) return null;
|
|
977
|
+
|
|
978
|
+
const agent = pluginAgentForTool(name, pluginName);
|
|
979
|
+
const delegateHint = agent?.slug
|
|
980
|
+
? `Delegate to the '${agent.slug}' sub-agent instead, or run it explicitly with /run ${agent.slug} "...".`
|
|
981
|
+
: `Delegate to the plugin's sub-agent instead, or run the plugin agent explicitly.`;
|
|
982
|
+
return {
|
|
983
|
+
success: false,
|
|
984
|
+
output: `Plugin tool '${name}' is scoped to plugin '${pluginName}' and should not be called directly by the primary agent. ${delegateHint}`,
|
|
985
|
+
_tool: name,
|
|
986
|
+
_plugin: pluginName,
|
|
987
|
+
_blocked: true,
|
|
988
|
+
_requires_agent_delegation: true,
|
|
989
|
+
_agent: agent?.slug || null,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
|
|
833
993
|
async function executeToolWithHooks(name, args, options = {}) {
|
|
834
994
|
const handler = toolMap[name] || pluginToolMap.get(name);
|
|
835
995
|
if (!handler) {
|
|
836
996
|
return { success: false, output: `Unknown tool: ${name}`, _tool: name };
|
|
837
997
|
}
|
|
998
|
+
const pluginToolBlock = primaryModelPluginToolBlock(name, handler, options);
|
|
999
|
+
if (pluginToolBlock) return pluginToolBlock;
|
|
838
1000
|
const hooks = hookRunner || new HookRunner({ cwd: process.cwd() });
|
|
839
1001
|
try {
|
|
840
1002
|
throwIfAborted(options.signal);
|
|
@@ -1188,6 +1350,30 @@ export function createToolExecutor({
|
|
|
1188
1350
|
args._classification = classification.classification; // 'safe' or 'contained'
|
|
1189
1351
|
const cwd = await commandCwd(args);
|
|
1190
1352
|
|
|
1353
|
+
// Background execution: start via the BackgroundTasks registry
|
|
1354
|
+
// and return immediately. Safety checks above still apply;
|
|
1355
|
+
// results are retrieved with job_output / killed with job_kill.
|
|
1356
|
+
if (args.run_in_background) {
|
|
1357
|
+
const job = backgroundTasks.start({
|
|
1358
|
+
command: args.command,
|
|
1359
|
+
cwd,
|
|
1360
|
+
timeoutMs: args.timeout ? Math.min(Number(args.timeout), 3_600_000) : undefined,
|
|
1361
|
+
// Deterministic wake-on-finish: completion dispatches the
|
|
1362
|
+
// named agent through the trigger funnel (chain-guarded).
|
|
1363
|
+
on_complete: args.on_complete_agent ? {
|
|
1364
|
+
target: `agent:${String(args.on_complete_agent).trim()}`,
|
|
1365
|
+
instruction: args.on_complete_instruction || null,
|
|
1366
|
+
} : null,
|
|
1367
|
+
});
|
|
1368
|
+
return {
|
|
1369
|
+
success: true,
|
|
1370
|
+
output: `Background job started: ${job.id} (pid ${job.pid}). `
|
|
1371
|
+
+ `Check progress with job_output {"job_id": "${job.id}"}; stop with job_kill.`,
|
|
1372
|
+
job_id: job.id,
|
|
1373
|
+
_tool: 'shell',
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1191
1377
|
// Pre-check: if command is rm/unlink, verify targets exist first
|
|
1192
1378
|
const rmMatch = (args.command || '').match(/^rm\s+(?:-\w+\s+)*(.+)$/);
|
|
1193
1379
|
if (rmMatch) {
|
|
@@ -2118,9 +2304,47 @@ export function createToolExecutor({
|
|
|
2118
2304
|
},
|
|
2119
2305
|
|
|
2120
2306
|
// User-defined agents — metadata first, project YAML + backend sync on demand.
|
|
2307
|
+
job_output: async (args = {}) => {
|
|
2308
|
+
const jobId = String(args.job_id || '').trim();
|
|
2309
|
+
if (!jobId) {
|
|
2310
|
+
const jobs = backgroundTasks.list();
|
|
2311
|
+
return {
|
|
2312
|
+
success: true,
|
|
2313
|
+
output: jobs.length
|
|
2314
|
+
? JSON.stringify({ jobs }, null, 2)
|
|
2315
|
+
: 'No background jobs in this session.',
|
|
2316
|
+
jobs,
|
|
2317
|
+
_tool: 'job_output',
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
const job = args.block
|
|
2321
|
+
? await backgroundTasks.wait(jobId)
|
|
2322
|
+
: backgroundTasks.describe(jobId);
|
|
2323
|
+
if (!job) return { success: false, output: `Unknown job: ${jobId}`, _tool: 'job_output' };
|
|
2324
|
+
const tailLines = Number(args.tail_lines) || 80;
|
|
2325
|
+
const tail = String(job.tail || '').split('\n').slice(-tailLines).join('\n');
|
|
2326
|
+
return {
|
|
2327
|
+
success: true,
|
|
2328
|
+
output: `${job.id} · ${job.status}`
|
|
2329
|
+
+ (job.exit_code != null ? ` (exit ${job.exit_code})` : '')
|
|
2330
|
+
+ ` · ${job.duration_s}s\n${tail}`,
|
|
2331
|
+
job: { ...job, tail: undefined },
|
|
2332
|
+
_tool: 'job_output',
|
|
2333
|
+
};
|
|
2334
|
+
},
|
|
2335
|
+
|
|
2336
|
+
job_kill: async (args = {}) => {
|
|
2337
|
+
const job = backgroundTasks.kill(String(args.job_id || '').trim());
|
|
2338
|
+
if (!job) return { success: false, output: `Unknown job: ${args.job_id}`, _tool: 'job_kill' };
|
|
2339
|
+
return { success: true, output: `${job.id} → ${job.status}`, job, _tool: 'job_kill' };
|
|
2340
|
+
},
|
|
2341
|
+
|
|
2121
2342
|
agents_list: async (args = {}) => {
|
|
2122
2343
|
const agents = filterLocalAgents(args).map(compactAgentMetadata);
|
|
2123
2344
|
const payload = { agents, count: agents.length };
|
|
2345
|
+
if (agents.some(agent => agent.runnable === false)) {
|
|
2346
|
+
payload.note = 'Agents with runnable:false are workspace-scoped plugin agents; add their slug to settings plugins.agent_allowlist to invoke them from the main loop.';
|
|
2347
|
+
}
|
|
2124
2348
|
return {
|
|
2125
2349
|
success: true,
|
|
2126
2350
|
output: JSON.stringify(payload, null, 2),
|
|
@@ -2151,7 +2375,8 @@ export function createToolExecutor({
|
|
|
2151
2375
|
: null,
|
|
2152
2376
|
next_actions: [
|
|
2153
2377
|
`Edit ${result.filePath}`,
|
|
2154
|
-
`Run /
|
|
2378
|
+
`Run /run ${result.slug} "<task>" or delegate to it from chat immediately`,
|
|
2379
|
+
`Optional: /agents sync ${result.slug} to publish it to the backend for account/cloud reuse`,
|
|
2155
2380
|
],
|
|
2156
2381
|
};
|
|
2157
2382
|
return {
|
|
@@ -2580,6 +2805,21 @@ export function createToolExecutor({
|
|
|
2580
2805
|
return results;
|
|
2581
2806
|
},
|
|
2582
2807
|
|
|
2808
|
+
listRunnables,
|
|
2809
|
+
|
|
2810
|
+
// Plugin tool schemas (name/description/input_schema) for callers
|
|
2811
|
+
// that compose model-facing tool lists — e.g. the graph engine's
|
|
2812
|
+
// direct substrate giving a plugin agent its declared tools.
|
|
2813
|
+
listPluginToolSchemas() {
|
|
2814
|
+
if (!pluginRegistry) return [];
|
|
2815
|
+
return (pluginRegistry.listTools?.() || []).map(tool => ({
|
|
2816
|
+
name: tool.name,
|
|
2817
|
+
description: tool.description || '',
|
|
2818
|
+
input_schema: tool.input_schema || { type: 'object', properties: {} },
|
|
2819
|
+
plugin_name: tool._plugin_name || tool.plugin_name || null,
|
|
2820
|
+
})).filter(tool => tool.name);
|
|
2821
|
+
},
|
|
2822
|
+
|
|
2583
2823
|
getAgentContext() {
|
|
2584
2824
|
const global = projectRegistry.getGlobalContext();
|
|
2585
2825
|
const mem = _readMemorySnapshot();
|
|
@@ -2609,6 +2849,17 @@ export function createToolExecutor({
|
|
|
2609
2849
|
source: agent.source,
|
|
2610
2850
|
spec: agent.spec,
|
|
2611
2851
|
})),
|
|
2852
|
+
// Background jobs the model should know about. Stable fields
|
|
2853
|
+
// only (no durations) so the entry — and the prompt cache —
|
|
2854
|
+
// changes on status transitions, not every turn.
|
|
2855
|
+
...(backgroundTasks.list().length ? {
|
|
2856
|
+
background_jobs: backgroundTasks.list().map(job => ({
|
|
2857
|
+
id: job.id,
|
|
2858
|
+
name: job.name,
|
|
2859
|
+
status: job.status,
|
|
2860
|
+
exit_code: job.exit_code,
|
|
2861
|
+
})),
|
|
2862
|
+
} : {}),
|
|
2612
2863
|
available_workflows: listLocalWorkflows(process.cwd()).map(workflow => ({
|
|
2613
2864
|
slug: workflow.slug,
|
|
2614
2865
|
name: workflow.name,
|
|
@@ -617,7 +617,7 @@ export class LocalAgentRelay {
|
|
|
617
617
|
try { this.emit('plugin_state_changed', evt); }
|
|
618
618
|
catch { /* never let SSE failure break a tool call */ }
|
|
619
619
|
};
|
|
620
|
-
const toolExecutor = createToolExecutor({ pluginRegistry, stateEmit });
|
|
620
|
+
const toolExecutor = createToolExecutor({ pluginRegistry, stateEmit, channel: 'workspace' });
|
|
621
621
|
await toolExecutor.waitForAutoRegister?.();
|
|
622
622
|
await toolExecutor.registerProjectRoots?.([this.session.root_path], { forceRefresh: false });
|
|
623
623
|
|