@bahulam/code 0.1.10 → 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 +5 -2
- package/src/agents/loader.mjs +26 -4
- package/src/auth/bahulam-auth.mjs +2 -13
- package/src/auth/tarang-auth.mjs +313 -0
- package/src/commands/agent.mjs +7 -7
- package/src/commands/plugin-manage.mjs +449 -0
- package/src/commands/plugin.mjs +247 -0
- package/src/config/cli-args.mjs +16 -0
- package/src/config/env.mjs +2 -0
- package/src/config/hook-runner.mjs +8 -8
- package/src/config/memory-loader.mjs +7 -3
- package/src/config/settings-loader.mjs +5 -3
- package/src/core/attachments.mjs +2 -2
- package/src/core/background-tasks.mjs +186 -0
- package/src/core/headless.mjs +59 -3
- package/src/core/local-agent.mjs +10 -1
- package/src/core/local-store.mjs +10 -10
- package/src/core/paths.mjs +10 -96
- package/src/core/policy-resolver.mjs +1 -1
- package/src/core/project-context-loader.mjs +2 -2
- package/src/core/risk-tier.mjs +1 -0
- package/src/core/stream-client.mjs +148 -1
- package/src/core/system-prompt.mjs +31 -12
- package/src/core/tool-executor.mjs +457 -12
- package/src/local-service/agent-relay.mjs +139 -12
- package/src/local-service/server.mjs +345 -20
- 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 +121 -0
- package/src/plugins/loader.mjs +123 -123
- package/src/plugins/manifest.mjs +291 -0
- package/src/plugins/preflight.mjs +227 -0
- package/src/plugins/registry.mjs +233 -0
- package/src/plugins/state.mjs +290 -0
- package/src/terminal/agents.mjs +26 -4
- package/src/terminal/init.mjs +2 -2
- package/src/terminal/main.mjs +83 -4
- package/src/terminal/repl-explore.mjs +1 -1
- package/src/terminal/repl-render.mjs +67 -12
- package/src/terminal/repl-state.mjs +4 -2
- package/src/terminal/repl.mjs +621 -99
- package/src/tools/agent.mjs +6 -2
- package/src/tools/analyze-image.mjs +1 -1
- package/src/tools/project-overview.mjs +7 -7
- package/src/tools/registry.mjs +88 -4
- package/src/ui/slash-commands.mjs +19 -1
- package/src/ui/sub-agent.mjs +14 -8
package/src/core/headless.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
|
|
|
17
17
|
import { persistProjectArtifacts } from './project-artifacts.mjs';
|
|
18
18
|
import { BahulamAuth } from '../auth/bahulam-auth.mjs';
|
|
19
19
|
import { ApprovalManager } from './approval.mjs';
|
|
20
|
+
import { PluginRegistry } from '../plugins/registry.mjs';
|
|
20
21
|
// daemon wiring — headless (and `bahulam daemonize`) also starts the socket
|
|
21
22
|
// server + relay bridge when eventlog is enabled. Without this the daemon
|
|
22
23
|
// is invisible to attach clients and to paired mobile devices.
|
|
@@ -46,7 +47,7 @@ import {
|
|
|
46
47
|
* @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
|
|
47
48
|
* @param {boolean} [opts.verbose] - show progress on stderr
|
|
48
49
|
*/
|
|
49
|
-
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 }) {
|
|
50
51
|
const startTime = Date.now();
|
|
51
52
|
|
|
52
53
|
const log = (msg) => {
|
|
@@ -60,13 +61,67 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
60
61
|
// ── Auth ──
|
|
61
62
|
const auth = new BahulamAuth();
|
|
62
63
|
const creds = auth.loadCredentials();
|
|
63
|
-
|
|
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))) {
|
|
64
70
|
emit({ type: 'error', error: 'Not logged in. Run: bahulam login' });
|
|
65
71
|
process.exit(1);
|
|
66
72
|
}
|
|
67
73
|
|
|
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.
|
|
121
|
+
const pluginRegistry = new PluginRegistry().scan();
|
|
122
|
+
|
|
68
123
|
// Projects are registered and indexed only when the agent requests an overview.
|
|
69
|
-
const toolExecutor = createToolExecutor();
|
|
124
|
+
const toolExecutor = createToolExecutor({ pluginRegistry });
|
|
70
125
|
|
|
71
126
|
// Auto-approve everything — no prompts
|
|
72
127
|
const approval = new ApprovalManager({ autoApprove: true });
|
|
@@ -104,6 +159,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
104
159
|
token: creds.token,
|
|
105
160
|
toolExecutor,
|
|
106
161
|
approvalManager: approval,
|
|
162
|
+
pluginRegistry,
|
|
107
163
|
});
|
|
108
164
|
}
|
|
109
165
|
|
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/local-store.mjs
CHANGED
|
@@ -10,9 +10,9 @@ import * as path from 'node:path';
|
|
|
10
10
|
import * as readline from 'node:readline';
|
|
11
11
|
import { bahulamHome } from './paths.mjs';
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const PROJECTS_DIR = path.join(
|
|
15
|
-
const
|
|
13
|
+
const BAHULAM_DIR = bahulamHome();
|
|
14
|
+
const PROJECTS_DIR = path.join(BAHULAM_DIR, 'projects');
|
|
15
|
+
const REPLAY_EVENT_RECORD_TYPE = 'bahulam_event';
|
|
16
16
|
|
|
17
17
|
function finiteNumber(value) {
|
|
18
18
|
const n = Number(value);
|
|
@@ -28,7 +28,7 @@ function firstFiniteNumber(...values) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function replayEventFromRecord(record) {
|
|
31
|
-
if (!record ||
|
|
31
|
+
if (!record || record.type !== REPLAY_EVENT_RECORD_TYPE || !record.event) return null;
|
|
32
32
|
const event = record.event;
|
|
33
33
|
if (!event || typeof event !== 'object' || !event.type) return null;
|
|
34
34
|
return {
|
|
@@ -334,8 +334,7 @@ async function parseSessionMeta(filePath) {
|
|
|
334
334
|
if (!meta.endTime || ts > meta.endTime) meta.endTime = ts;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
// Bahulam replay events may carry cost / error markers.
|
|
338
|
-
// transcripts used the same payload under the legacy kepler_event type.
|
|
337
|
+
// Bahulam replay events may carry cost / error markers.
|
|
339
338
|
const ev = replayEventFromRecord(obj);
|
|
340
339
|
if (ev) {
|
|
341
340
|
const data = ev.data || {};
|
|
@@ -518,7 +517,8 @@ export async function getSessionDetail(sessionId, options = {}) {
|
|
|
518
517
|
continue;
|
|
519
518
|
}
|
|
520
519
|
|
|
521
|
-
|
|
520
|
+
if (!obj.message || typeof obj.message !== 'object') continue;
|
|
521
|
+
const message = obj.message;
|
|
522
522
|
entries.push({
|
|
523
523
|
order: entryOrder,
|
|
524
524
|
type: obj.type || null,
|
|
@@ -965,7 +965,7 @@ export async function getModelBreakdown(days = 30) {
|
|
|
965
965
|
* @param {number} n — max entries to return (most recent first)
|
|
966
966
|
*/
|
|
967
967
|
export function getHistory(n = 50) {
|
|
968
|
-
const historyPath = path.join(
|
|
968
|
+
const historyPath = path.join(BAHULAM_DIR, 'history.jsonl');
|
|
969
969
|
try {
|
|
970
970
|
const content = fs.readFileSync(historyPath, 'utf-8');
|
|
971
971
|
const lines = content.trim().split('\n').filter(Boolean);
|
|
@@ -981,8 +981,8 @@ export function getHistory(n = 50) {
|
|
|
981
981
|
|
|
982
982
|
export function getStorePaths() {
|
|
983
983
|
return {
|
|
984
|
-
bahulamDir:
|
|
984
|
+
bahulamDir: BAHULAM_DIR,
|
|
985
985
|
projectsDir: PROJECTS_DIR,
|
|
986
|
-
historyPath: path.join(
|
|
986
|
+
historyPath: path.join(BAHULAM_DIR, 'history.jsonl'),
|
|
987
987
|
};
|
|
988
988
|
}
|
package/src/core/paths.mjs
CHANGED
|
@@ -16,15 +16,8 @@
|
|
|
16
16
|
* hooks.json — project-specific hooks
|
|
17
17
|
* projects.json — slug → project path mapping
|
|
18
18
|
*
|
|
19
|
-
* ── Legacy fallback ─────────────────────────────────────────────────────
|
|
20
|
-
* Pre-rename installs stored everything under ~/.kepler/. The resolver below
|
|
21
|
-
* prefers the new path but falls back to the legacy directory when it
|
|
22
|
-
* exists and the new one doesn't, so existing users keep their config,
|
|
23
|
-
* agents, workflows, and history until they explicitly migrate.
|
|
24
|
-
*
|
|
25
19
|
* Env vars:
|
|
26
|
-
* BAHULAM_HOME
|
|
27
|
-
* KEPLER_HOME legacy; still honored for backward compat
|
|
20
|
+
* BAHULAM_HOME explicit override for ~/.bahulam
|
|
28
21
|
*/
|
|
29
22
|
|
|
30
23
|
import * as fs from 'node:fs';
|
|
@@ -32,58 +25,14 @@ import * as path from 'node:path';
|
|
|
32
25
|
import * as os from 'node:os';
|
|
33
26
|
import * as crypto from 'node:crypto';
|
|
34
27
|
|
|
35
|
-
const NEW_HOME_NAME = '.bahulam';
|
|
36
|
-
const LEGACY_HOME_NAME = '.kepler';
|
|
37
|
-
|
|
38
|
-
let _legacyNoticeShown = false;
|
|
39
|
-
|
|
40
28
|
/**
|
|
41
29
|
* Resolve the CLI home directory. Priority:
|
|
42
|
-
* 1. $BAHULAM_HOME (explicit
|
|
43
|
-
* 2.
|
|
44
|
-
* 3. ~/.bahulam (if it exists)
|
|
45
|
-
* 4. ~/.kepler (if it exists — prints a one-time migration hint)
|
|
46
|
-
* 5. ~/.bahulam (fresh install, will be created on first write)
|
|
30
|
+
* 1. $BAHULAM_HOME (explicit override)
|
|
31
|
+
* 2. ~/.bahulam (standard location, created on first write)
|
|
47
32
|
*/
|
|
48
33
|
function resolveHome() {
|
|
49
34
|
if (process.env.BAHULAM_HOME) return process.env.BAHULAM_HOME;
|
|
50
|
-
|
|
51
|
-
maybeNoticeLegacyEnv();
|
|
52
|
-
return process.env.KEPLER_HOME;
|
|
53
|
-
}
|
|
54
|
-
const home = os.homedir();
|
|
55
|
-
const newPath = path.join(home, NEW_HOME_NAME);
|
|
56
|
-
const legacyPath = path.join(home, LEGACY_HOME_NAME);
|
|
57
|
-
try {
|
|
58
|
-
if (fs.existsSync(newPath)) return newPath;
|
|
59
|
-
} catch {}
|
|
60
|
-
try {
|
|
61
|
-
if (fs.existsSync(legacyPath)) {
|
|
62
|
-
maybeNoticeLegacyDir(legacyPath, newPath);
|
|
63
|
-
return legacyPath;
|
|
64
|
-
}
|
|
65
|
-
} catch {}
|
|
66
|
-
return newPath;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function maybeNoticeLegacyEnv() {
|
|
70
|
-
if (_legacyNoticeShown || process.env.B0_QUIET_MIGRATION === '1') return;
|
|
71
|
-
_legacyNoticeShown = true;
|
|
72
|
-
try {
|
|
73
|
-
process.stderr.write(
|
|
74
|
-
' \x1b[2mnote: KEPLER_HOME is deprecated; set BAHULAM_HOME instead.\x1b[0m\n'
|
|
75
|
-
);
|
|
76
|
-
} catch {}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function maybeNoticeLegacyDir(legacyPath, newPath) {
|
|
80
|
-
if (_legacyNoticeShown || process.env.B0_QUIET_MIGRATION === '1') return;
|
|
81
|
-
_legacyNoticeShown = true;
|
|
82
|
-
try {
|
|
83
|
-
process.stderr.write(
|
|
84
|
-
` \x1b[2mnote: reading legacy ${legacyPath}. Move to ${newPath} when convenient (silence with B0_QUIET_MIGRATION=1).\x1b[0m\n`
|
|
85
|
-
);
|
|
86
|
-
} catch {}
|
|
35
|
+
return path.join(os.homedir(), '.bahulam');
|
|
87
36
|
}
|
|
88
37
|
|
|
89
38
|
/**
|
|
@@ -91,7 +40,6 @@ function maybeNoticeLegacyDir(legacyPath, newPath) {
|
|
|
91
40
|
* Uses first 16 chars of SHA-256 (same as Claude Code).
|
|
92
41
|
*/
|
|
93
42
|
export function projectHash(projectDir) {
|
|
94
|
-
// Resolve symlinks (macOS: /tmp → /private/tmp) so the hash is stable
|
|
95
43
|
let resolved = projectDir;
|
|
96
44
|
try {
|
|
97
45
|
resolved = fs.realpathSync(projectDir);
|
|
@@ -104,14 +52,11 @@ export function projectHash(projectDir) {
|
|
|
104
52
|
.slice(0, 16);
|
|
105
53
|
}
|
|
106
54
|
|
|
107
|
-
/** Root ~/.bahulam/ directory
|
|
55
|
+
/** Root ~/.bahulam/ directory. */
|
|
108
56
|
export function bahulamHome() {
|
|
109
57
|
return resolveHome();
|
|
110
58
|
}
|
|
111
59
|
|
|
112
|
-
/** Backward-compat alias. Prefer `bahulamHome()` in new code. */
|
|
113
|
-
export const keplerHome = bahulamHome;
|
|
114
|
-
|
|
115
60
|
/** ~/.bahulam/projects/{hash}/ for a given project path. */
|
|
116
61
|
export function projectDir(projectPath) {
|
|
117
62
|
return path.join(bahulamHome(), 'projects', projectHash(projectPath));
|
|
@@ -163,20 +108,6 @@ export function historyPath() {
|
|
|
163
108
|
}
|
|
164
109
|
|
|
165
110
|
// ── daemon session paths ─────────────────────────────────────
|
|
166
|
-
//
|
|
167
|
-
// Daemon-owned sessions (bahulamd, detach/attach) live at:
|
|
168
|
-
// ~/.bahulam/sessions/<sess_id>/ per-session dir
|
|
169
|
-
// meta.json cwd, model, opened_at, ...
|
|
170
|
-
// events.jsonl (+ events-1.jsonl, ...) append-only event log
|
|
171
|
-
// snapshot-<seq>.json periodic compacted snapshot
|
|
172
|
-
// approvals/ pending + decided approvals
|
|
173
|
-
// input-lock.json who holds input right now
|
|
174
|
-
// daemon.pid pid of the owning daemon
|
|
175
|
-
// ~/.bahulam/sockets/<sess_id>.sock Unix socket (0600)
|
|
176
|
-
//
|
|
177
|
-
// These are DIFFERENT from the projects/<hash>/sessions/ archive above.
|
|
178
|
-
// The archive is a historical index keyed on project path; daemon sessions
|
|
179
|
-
// are keyed on session id and are the live source of truth while running.
|
|
180
111
|
|
|
181
112
|
/** ~/.bahulam/sessions/ — root for daemon-owned sessions. */
|
|
182
113
|
export function daemonSessionsRoot() {
|
|
@@ -188,7 +119,7 @@ export function daemonSessionDir(sessionId) {
|
|
|
188
119
|
return path.join(daemonSessionsRoot(), sessionId);
|
|
189
120
|
}
|
|
190
121
|
|
|
191
|
-
/** ~/.bahulam/sockets/ — root for daemon Unix sockets
|
|
122
|
+
/** ~/.bahulam/sockets/ — root for daemon Unix sockets. */
|
|
192
123
|
export function daemonSocketsDir() {
|
|
193
124
|
return path.join(bahulamHome(), 'sockets');
|
|
194
125
|
}
|
|
@@ -198,29 +129,12 @@ export function daemonSocketPath(sessionId) {
|
|
|
198
129
|
return path.join(daemonSocketsDir(), `${sessionId}.sock`);
|
|
199
130
|
}
|
|
200
131
|
|
|
201
|
-
// ── Project-local config directory (.bahulam/
|
|
202
|
-
//
|
|
203
|
-
// Project-scoped stuff (agents/*.yaml, memory/*.md, hooks/, settings.json,
|
|
204
|
-
// tasks/) used to live in .kepler/ inside the project. Same resolver logic
|
|
205
|
-
// applies — prefer .bahulam/, fall back to .kepler/ when only the legacy
|
|
206
|
-
// dir exists.
|
|
207
|
-
|
|
208
|
-
const PROJECT_NEW_NAME = '.bahulam';
|
|
209
|
-
const PROJECT_LEGACY_NAME = '.kepler';
|
|
132
|
+
// ── Project-local config directory (.bahulam/ inside the project) ────
|
|
210
133
|
|
|
211
134
|
/**
|
|
212
|
-
* Resolve the project-local config directory for `cwd`.
|
|
213
|
-
*
|
|
214
|
-
* exist yet (callers that write should mkdir -p first).
|
|
135
|
+
* Resolve the project-local config directory for `cwd`.
|
|
136
|
+
* Returns an absolute path; the directory may not exist yet.
|
|
215
137
|
*/
|
|
216
138
|
export function projectConfigDir(cwd = process.cwd()) {
|
|
217
|
-
|
|
218
|
-
const legacyPath = path.join(cwd, PROJECT_LEGACY_NAME);
|
|
219
|
-
try {
|
|
220
|
-
if (fs.existsSync(newPath)) return newPath;
|
|
221
|
-
} catch {}
|
|
222
|
-
try {
|
|
223
|
-
if (fs.existsSync(legacyPath)) return legacyPath;
|
|
224
|
-
} catch {}
|
|
225
|
-
return newPath;
|
|
139
|
+
return path.join(cwd, '.bahulam');
|
|
226
140
|
}
|
|
@@ -5,7 +5,7 @@ import * as path from 'node:path';
|
|
|
5
5
|
export const DEFAULT_POLICY = Object.freeze({
|
|
6
6
|
version: 1,
|
|
7
7
|
context: {
|
|
8
|
-
loadEveryTurn: ['
|
|
8
|
+
loadEveryTurn: ['BAHULAM.md', 'project.md', 'style.md', 'goal.md', 'plan.md', 'tasks/*.md'],
|
|
9
9
|
showReloadNotice: true,
|
|
10
10
|
injectCommandOptions: true,
|
|
11
11
|
injectActionableTips: true,
|
|
@@ -69,8 +69,8 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
|
|
|
69
69
|
const bahulamDir = projectConfigDir(cwd);
|
|
70
70
|
const files = [];
|
|
71
71
|
for (const file of loadBahulamMemory({ cwd })) {
|
|
72
|
-
const label = file.path.endsWith(path.join('.bahulam', '
|
|
73
|
-
? '
|
|
72
|
+
const label = file.path.endsWith(path.join('.bahulam', 'BAHULAM.md'))
|
|
73
|
+
? 'BAHULAM.md'
|
|
74
74
|
: path.basename(file.path);
|
|
75
75
|
files.push({
|
|
76
76
|
label,
|
package/src/core/risk-tier.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { llmToolResultContent, sendCallback, sendSkippedCallback, sendApprovalDecision } from './callback-client.mjs';
|
|
13
13
|
import { ApprovalManager } from './approval.mjs';
|
|
14
|
+
import { upsertFacts } from './memory-disk.mjs';
|
|
14
15
|
import { normalizeBillingBrandCopy, quotaErrorDetail, rateLimitErrorMessage } from './rate-limit-display.mjs';
|
|
15
16
|
import * as telemetry from '../telemetry/index.mjs';
|
|
16
17
|
|
|
@@ -93,6 +94,17 @@ function transportDebug(message, data = {}) {
|
|
|
93
94
|
} catch {}
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function memoryFactsFromComplete(data) {
|
|
98
|
+
const facts = data?.memory_facts_to_persist;
|
|
99
|
+
if (!Array.isArray(facts) || facts.length === 0) return [];
|
|
100
|
+
return facts.filter(fact => (
|
|
101
|
+
fact
|
|
102
|
+
&& typeof fact === 'object'
|
|
103
|
+
&& fact.fact_id
|
|
104
|
+
&& String(fact.content || '').trim()
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
// Full jitter around the scheduled delay: pick a value in [delay*0.5, delay*1.5].
|
|
97
109
|
// Spreads out reconnect storms so N clients dropping simultaneously don't
|
|
98
110
|
// synchronize their retries. Clamped to the same 30s ceiling as the base delay.
|
|
@@ -136,6 +148,7 @@ export class BahulamStreamClient {
|
|
|
136
148
|
approvalManager = null,
|
|
137
149
|
reconnectMaxElapsedMs = null,
|
|
138
150
|
mode = null,
|
|
151
|
+
pluginRegistry = null,
|
|
139
152
|
}) {
|
|
140
153
|
this.baseUrl = (baseUrl || '').replace(/\/$/, '');
|
|
141
154
|
this.token = token;
|
|
@@ -148,7 +161,7 @@ export class BahulamStreamClient {
|
|
|
148
161
|
this.retryDelayMs = null;
|
|
149
162
|
this.pendingToolCallbacks = new Map();
|
|
150
163
|
this.reconnectMaxElapsedMs = reconnectMaxElapsedMs
|
|
151
|
-
?? Number(process.env.
|
|
164
|
+
?? Number(process.env.BAHULAM_RECONNECT_MAX_ELAPSED_MS || 300_000);
|
|
152
165
|
// Set by backend on first turn, reused on subsequent turns. Headless mode
|
|
153
166
|
// (which starts fresh per invocation) can pre-seed via TARANG_SESSION_ID
|
|
154
167
|
// so multi-turn benchmarks share one backend session across `node` runs.
|
|
@@ -173,9 +186,107 @@ export class BahulamStreamClient {
|
|
|
173
186
|
|| (process.env.TARANG_ENV === 'remote' ? 'remote' : null)
|
|
174
187
|
|| (process.env.TARANG_ENV === 'bundled' ? 'bundled' : null)
|
|
175
188
|
|| 'remote';
|
|
189
|
+
this.pluginRegistry = pluginRegistry || null;
|
|
176
190
|
this._bundledReady = false;
|
|
177
191
|
}
|
|
178
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
|
+
|
|
204
|
+
/**
|
|
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.
|
|
208
|
+
*/
|
|
209
|
+
_getPluginToolSchemas() {
|
|
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
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get plugin agent schemas for client_agents injection.
|
|
260
|
+
* @returns {Array<{slug: string, name: string, role: string, description: string, tools: string[]}>}
|
|
261
|
+
*/
|
|
262
|
+
_getPluginAgentSchemas() {
|
|
263
|
+
if (!this.pluginRegistry) return [];
|
|
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
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
179
290
|
/**
|
|
180
291
|
* Ensure the bundled runtime is spawned and this.baseUrl points at it.
|
|
181
292
|
* No-op in remote mode. Callers that hit the backend should invoke this
|
|
@@ -251,6 +362,12 @@ export class BahulamStreamClient {
|
|
|
251
362
|
const body = { instruction, context };
|
|
252
363
|
if (messages && messages.length > 0) body.messages = messages;
|
|
253
364
|
if (this.sessionId) body.session_id = this.sessionId;
|
|
365
|
+
const clientTools = this._getPluginToolSchemas();
|
|
366
|
+
if (clientTools.length > 0) body.client_tools = clientTools;
|
|
367
|
+
const clientAgents = this._getPluginAgentSchemas();
|
|
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;
|
|
254
371
|
const requestId = `cli-${_uuidLike()}`;
|
|
255
372
|
|
|
256
373
|
// daemon cache-guard hook. If BAHULAM_CAPTURE_REQUEST is set to a file
|
|
@@ -461,6 +578,10 @@ export class BahulamStreamClient {
|
|
|
461
578
|
return;
|
|
462
579
|
}
|
|
463
580
|
|
|
581
|
+
if (event === EVENT_TYPES.COMPLETE) {
|
|
582
|
+
this._persistMemoryFactsFromComplete(data);
|
|
583
|
+
}
|
|
584
|
+
|
|
464
585
|
// Tool requests — show to user, then execute locally and POST callback.
|
|
465
586
|
if (event === EVENT_TYPES.TOOL_REQUEST || event === EVENT_TYPES.TOOL_CALL) {
|
|
466
587
|
yield rendered;
|
|
@@ -486,6 +607,23 @@ export class BahulamStreamClient {
|
|
|
486
607
|
}
|
|
487
608
|
}
|
|
488
609
|
|
|
610
|
+
_persistMemoryFactsFromComplete(data) {
|
|
611
|
+
const facts = memoryFactsFromComplete(data);
|
|
612
|
+
if (facts.length === 0) return;
|
|
613
|
+
try {
|
|
614
|
+
upsertFacts(facts, process.cwd());
|
|
615
|
+
telemetry.track('memory.disk.upserted', { facts: facts.length });
|
|
616
|
+
} catch (err) {
|
|
617
|
+
telemetry.track('memory.disk.upsert_failed', {
|
|
618
|
+
facts: facts.length,
|
|
619
|
+
message: err?.message || String(err),
|
|
620
|
+
});
|
|
621
|
+
if (data && typeof data === 'object') {
|
|
622
|
+
data.memory_persist_error = err?.message || String(err);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
489
627
|
async *_reconnectAfterDrop(err) {
|
|
490
628
|
const taskId = this.currentTaskId;
|
|
491
629
|
if (!taskId || this.lastEventId == null) {
|
|
@@ -728,6 +866,7 @@ export class BahulamStreamClient {
|
|
|
728
866
|
const callId = call_id || request_id;
|
|
729
867
|
const toolName = tool;
|
|
730
868
|
const isInternal = Boolean(data?.internal || data?.sub_agent);
|
|
869
|
+
const subAgentRunId = data?.run_id || data?.sub_agent_run_id || null;
|
|
731
870
|
|
|
732
871
|
if (this.verbose) {
|
|
733
872
|
process.stderr.write(`\x1b[2m[tool] ${toolName}(${JSON.stringify(args).slice(0, 80)}...)\x1b[0m\n`);
|
|
@@ -745,6 +884,8 @@ export class BahulamStreamClient {
|
|
|
745
884
|
_cancelled: true,
|
|
746
885
|
internal: isInternal,
|
|
747
886
|
sub_agent: data?.sub_agent || null,
|
|
887
|
+
run_id: subAgentRunId,
|
|
888
|
+
sub_agent_run_id: subAgentRunId,
|
|
748
889
|
local_callback: false,
|
|
749
890
|
},
|
|
750
891
|
};
|
|
@@ -756,6 +897,10 @@ export class BahulamStreamClient {
|
|
|
756
897
|
try {
|
|
757
898
|
result = await this.toolExecutor.execute(toolName, args || {}, {
|
|
758
899
|
signal: this._toolAbort?.signal,
|
|
900
|
+
toolCallSource: 'model',
|
|
901
|
+
internal: isInternal,
|
|
902
|
+
subAgent: data?.sub_agent || null,
|
|
903
|
+
subAgentRunId,
|
|
759
904
|
});
|
|
760
905
|
} catch (err) {
|
|
761
906
|
if (err?.name === 'AbortError' || this._cancelled) {
|
|
@@ -795,6 +940,8 @@ export class BahulamStreamClient {
|
|
|
795
940
|
duration_ms: durationMs,
|
|
796
941
|
internal: isInternal,
|
|
797
942
|
sub_agent: data?.sub_agent || null,
|
|
943
|
+
run_id: subAgentRunId,
|
|
944
|
+
sub_agent_run_id: subAgentRunId,
|
|
798
945
|
local_callback: true,
|
|
799
946
|
},
|
|
800
947
|
};
|