@bahulam/code 0.1.11 → 0.1.12
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/config/cli-args.mjs +16 -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 +219 -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 +27 -27
- package/src/plugins/preflight.mjs +8 -8
- package/src/terminal/agents.mjs +8 -3
- package/src/terminal/main.mjs +8 -2
- package/src/terminal/repl-render.mjs +65 -10
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +572 -96
- package/src/tools/agent.mjs +6 -2
- package/src/tools/registry.mjs +88 -4
- package/src/ui/slash-commands.mjs +1 -1
- package/src/ui/sub-agent.mjs +14 -8
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
|
};
|
|
@@ -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;
|
|
@@ -778,11 +851,11 @@ export function createToolExecutor({
|
|
|
778
851
|
if (!name || toolMap[name]) continue;
|
|
779
852
|
const pluginName = toolDef._plugin_name || toolDef.plugin_name || null;
|
|
780
853
|
registerPluginTool(name, async (args, options = {}) => {
|
|
781
|
-
const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.
|
|
854
|
+
const handler = await loadPluginTool(toolDef._plugin_dir, toolDef.tool);
|
|
782
855
|
if (!handler) {
|
|
783
856
|
return {
|
|
784
857
|
success: false,
|
|
785
|
-
output: `Plugin tool
|
|
858
|
+
output: `Plugin tool module could not be loaded: ${name}`,
|
|
786
859
|
_tool: name,
|
|
787
860
|
_plugin: pluginName,
|
|
788
861
|
};
|
|
@@ -826,15 +899,57 @@ export function createToolExecutor({
|
|
|
826
899
|
_plugin: pluginName,
|
|
827
900
|
};
|
|
828
901
|
}
|
|
829
|
-
});
|
|
902
|
+
}, { pluginName, source: 'plugin' });
|
|
830
903
|
}
|
|
831
904
|
}
|
|
832
905
|
|
|
906
|
+
function pluginAgentForTool(toolName, pluginName) {
|
|
907
|
+
if (!pluginRegistry) return null;
|
|
908
|
+
return (pluginRegistry.listAgents?.() || []).find(agent => {
|
|
909
|
+
const agentPlugin = agent._plugin_name
|
|
910
|
+
|| String(agent.source || '').replace(/^plugin:/, '')
|
|
911
|
+
|| null;
|
|
912
|
+
if (pluginName && agentPlugin && agentPlugin !== pluginName) return false;
|
|
913
|
+
return Array.isArray(agent.tools) && agent.tools.includes(toolName);
|
|
914
|
+
}) || null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function primaryModelPluginToolBlock(name, handler, options = {}) {
|
|
918
|
+
if (!handler?._pluginTool) return null;
|
|
919
|
+
if (options.toolCallSource !== 'model') return null;
|
|
920
|
+
if (options.internal || options.subAgent || options.allowPrimaryPluginToolCall) return null;
|
|
921
|
+
|
|
922
|
+
const pluginName = handler._pluginTool.pluginName || 'plugin';
|
|
923
|
+
// Tools-only plugins have no delegation owner: with no agent to
|
|
924
|
+
// route through, the primary agent uses the tools directly (the
|
|
925
|
+
// user consented by enabling the plugin). The delegate-only rule
|
|
926
|
+
// applies only when the plugin ships an owning agent.
|
|
927
|
+
const pluginShipsAgents = (pluginRegistry?.listAgents?.() || [])
|
|
928
|
+
.some(agent => (agent._plugin_name || '') === pluginName);
|
|
929
|
+
if (!pluginShipsAgents) return null;
|
|
930
|
+
|
|
931
|
+
const agent = pluginAgentForTool(name, pluginName);
|
|
932
|
+
const delegateHint = agent?.slug
|
|
933
|
+
? `Delegate to the '${agent.slug}' sub-agent instead, or run it explicitly with /run ${agent.slug} "...".`
|
|
934
|
+
: `Delegate to the plugin's sub-agent instead, or run the plugin agent explicitly.`;
|
|
935
|
+
return {
|
|
936
|
+
success: false,
|
|
937
|
+
output: `Plugin tool '${name}' is scoped to plugin '${pluginName}' and should not be called directly by the primary agent. ${delegateHint}`,
|
|
938
|
+
_tool: name,
|
|
939
|
+
_plugin: pluginName,
|
|
940
|
+
_blocked: true,
|
|
941
|
+
_requires_agent_delegation: true,
|
|
942
|
+
_agent: agent?.slug || null,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
833
946
|
async function executeToolWithHooks(name, args, options = {}) {
|
|
834
947
|
const handler = toolMap[name] || pluginToolMap.get(name);
|
|
835
948
|
if (!handler) {
|
|
836
949
|
return { success: false, output: `Unknown tool: ${name}`, _tool: name };
|
|
837
950
|
}
|
|
951
|
+
const pluginToolBlock = primaryModelPluginToolBlock(name, handler, options);
|
|
952
|
+
if (pluginToolBlock) return pluginToolBlock;
|
|
838
953
|
const hooks = hookRunner || new HookRunner({ cwd: process.cwd() });
|
|
839
954
|
try {
|
|
840
955
|
throwIfAborted(options.signal);
|
|
@@ -1188,6 +1303,30 @@ export function createToolExecutor({
|
|
|
1188
1303
|
args._classification = classification.classification; // 'safe' or 'contained'
|
|
1189
1304
|
const cwd = await commandCwd(args);
|
|
1190
1305
|
|
|
1306
|
+
// Background execution: start via the BackgroundTasks registry
|
|
1307
|
+
// and return immediately. Safety checks above still apply;
|
|
1308
|
+
// results are retrieved with job_output / killed with job_kill.
|
|
1309
|
+
if (args.run_in_background) {
|
|
1310
|
+
const job = backgroundTasks.start({
|
|
1311
|
+
command: args.command,
|
|
1312
|
+
cwd,
|
|
1313
|
+
timeoutMs: args.timeout ? Math.min(Number(args.timeout), 3_600_000) : undefined,
|
|
1314
|
+
// Deterministic wake-on-finish: completion dispatches the
|
|
1315
|
+
// named agent through the trigger funnel (chain-guarded).
|
|
1316
|
+
on_complete: args.on_complete_agent ? {
|
|
1317
|
+
target: `agent:${String(args.on_complete_agent).trim()}`,
|
|
1318
|
+
instruction: args.on_complete_instruction || null,
|
|
1319
|
+
} : null,
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
success: true,
|
|
1323
|
+
output: `Background job started: ${job.id} (pid ${job.pid}). `
|
|
1324
|
+
+ `Check progress with job_output {"job_id": "${job.id}"}; stop with job_kill.`,
|
|
1325
|
+
job_id: job.id,
|
|
1326
|
+
_tool: 'shell',
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1191
1330
|
// Pre-check: if command is rm/unlink, verify targets exist first
|
|
1192
1331
|
const rmMatch = (args.command || '').match(/^rm\s+(?:-\w+\s+)*(.+)$/);
|
|
1193
1332
|
if (rmMatch) {
|
|
@@ -2118,9 +2257,47 @@ export function createToolExecutor({
|
|
|
2118
2257
|
},
|
|
2119
2258
|
|
|
2120
2259
|
// User-defined agents — metadata first, project YAML + backend sync on demand.
|
|
2260
|
+
job_output: async (args = {}) => {
|
|
2261
|
+
const jobId = String(args.job_id || '').trim();
|
|
2262
|
+
if (!jobId) {
|
|
2263
|
+
const jobs = backgroundTasks.list();
|
|
2264
|
+
return {
|
|
2265
|
+
success: true,
|
|
2266
|
+
output: jobs.length
|
|
2267
|
+
? JSON.stringify({ jobs }, null, 2)
|
|
2268
|
+
: 'No background jobs in this session.',
|
|
2269
|
+
jobs,
|
|
2270
|
+
_tool: 'job_output',
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
const job = args.block
|
|
2274
|
+
? await backgroundTasks.wait(jobId)
|
|
2275
|
+
: backgroundTasks.describe(jobId);
|
|
2276
|
+
if (!job) return { success: false, output: `Unknown job: ${jobId}`, _tool: 'job_output' };
|
|
2277
|
+
const tailLines = Number(args.tail_lines) || 80;
|
|
2278
|
+
const tail = String(job.tail || '').split('\n').slice(-tailLines).join('\n');
|
|
2279
|
+
return {
|
|
2280
|
+
success: true,
|
|
2281
|
+
output: `${job.id} · ${job.status}`
|
|
2282
|
+
+ (job.exit_code != null ? ` (exit ${job.exit_code})` : '')
|
|
2283
|
+
+ ` · ${job.duration_s}s\n${tail}`,
|
|
2284
|
+
job: { ...job, tail: undefined },
|
|
2285
|
+
_tool: 'job_output',
|
|
2286
|
+
};
|
|
2287
|
+
},
|
|
2288
|
+
|
|
2289
|
+
job_kill: async (args = {}) => {
|
|
2290
|
+
const job = backgroundTasks.kill(String(args.job_id || '').trim());
|
|
2291
|
+
if (!job) return { success: false, output: `Unknown job: ${args.job_id}`, _tool: 'job_kill' };
|
|
2292
|
+
return { success: true, output: `${job.id} → ${job.status}`, job, _tool: 'job_kill' };
|
|
2293
|
+
},
|
|
2294
|
+
|
|
2121
2295
|
agents_list: async (args = {}) => {
|
|
2122
2296
|
const agents = filterLocalAgents(args).map(compactAgentMetadata);
|
|
2123
2297
|
const payload = { agents, count: agents.length };
|
|
2298
|
+
if (agents.some(agent => agent.runnable === false)) {
|
|
2299
|
+
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.';
|
|
2300
|
+
}
|
|
2124
2301
|
return {
|
|
2125
2302
|
success: true,
|
|
2126
2303
|
output: JSON.stringify(payload, null, 2),
|
|
@@ -2151,7 +2328,8 @@ export function createToolExecutor({
|
|
|
2151
2328
|
: null,
|
|
2152
2329
|
next_actions: [
|
|
2153
2330
|
`Edit ${result.filePath}`,
|
|
2154
|
-
`Run /
|
|
2331
|
+
`Run /run ${result.slug} "<task>" or delegate to it from chat immediately`,
|
|
2332
|
+
`Optional: /agents sync ${result.slug} to publish it to the backend for account/cloud reuse`,
|
|
2155
2333
|
],
|
|
2156
2334
|
};
|
|
2157
2335
|
return {
|
|
@@ -2580,6 +2758,21 @@ export function createToolExecutor({
|
|
|
2580
2758
|
return results;
|
|
2581
2759
|
},
|
|
2582
2760
|
|
|
2761
|
+
listRunnables,
|
|
2762
|
+
|
|
2763
|
+
// Plugin tool schemas (name/description/input_schema) for callers
|
|
2764
|
+
// that compose model-facing tool lists — e.g. the graph engine's
|
|
2765
|
+
// direct substrate giving a plugin agent its declared tools.
|
|
2766
|
+
listPluginToolSchemas() {
|
|
2767
|
+
if (!pluginRegistry) return [];
|
|
2768
|
+
return (pluginRegistry.listTools?.() || []).map(tool => ({
|
|
2769
|
+
name: tool.name,
|
|
2770
|
+
description: tool.description || '',
|
|
2771
|
+
input_schema: tool.input_schema || { type: 'object', properties: {} },
|
|
2772
|
+
plugin_name: tool._plugin_name || tool.plugin_name || null,
|
|
2773
|
+
})).filter(tool => tool.name);
|
|
2774
|
+
},
|
|
2775
|
+
|
|
2583
2776
|
getAgentContext() {
|
|
2584
2777
|
const global = projectRegistry.getGlobalContext();
|
|
2585
2778
|
const mem = _readMemorySnapshot();
|
|
@@ -2609,6 +2802,17 @@ export function createToolExecutor({
|
|
|
2609
2802
|
source: agent.source,
|
|
2610
2803
|
spec: agent.spec,
|
|
2611
2804
|
})),
|
|
2805
|
+
// Background jobs the model should know about. Stable fields
|
|
2806
|
+
// only (no durations) so the entry — and the prompt cache —
|
|
2807
|
+
// changes on status transitions, not every turn.
|
|
2808
|
+
...(backgroundTasks.list().length ? {
|
|
2809
|
+
background_jobs: backgroundTasks.list().map(job => ({
|
|
2810
|
+
id: job.id,
|
|
2811
|
+
name: job.name,
|
|
2812
|
+
status: job.status,
|
|
2813
|
+
exit_code: job.exit_code,
|
|
2814
|
+
})),
|
|
2815
|
+
} : {}),
|
|
2612
2816
|
available_workflows: listLocalWorkflows(process.cwd()).map(workflow => ({
|
|
2613
2817
|
slug: workflow.slug,
|
|
2614
2818
|
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
|
|