@bahulam/code 0.1.23 → 0.1.25
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/commands/install.mjs +88 -6
- package/src/commands/plugin-manage.mjs +146 -5
- package/src/config/cli-args.mjs +23 -3
- package/src/config/model-catalog.mjs +9 -0
- package/src/config/model-defaults.mjs +16 -0
- package/src/core/approval.mjs +8 -1
- package/src/core/attachments.mjs +4 -0
- package/src/core/backend-url.mjs +16 -0
- package/src/core/context-reduction.mjs +207 -0
- package/src/core/headless.mjs +85 -13
- package/src/core/local-agent.mjs +352 -44
- package/src/core/mode-selector.mjs +5 -4
- package/src/core/model-selection.mjs +72 -0
- package/src/core/request-retry.mjs +106 -0
- package/src/core/tool-error.mjs +155 -0
- package/src/core/tool-executor.mjs +8 -3
- package/src/core/usage-normalization.mjs +32 -0
- package/src/local-service/agent-relay.mjs +286 -7
- package/src/local-service/server.mjs +37 -0
- package/src/orchestration/node-runner.mjs +7 -3
- package/src/permissions/command-classifier.mjs +35 -0
- package/src/plugins/executor.mjs +4 -4
- package/src/plugins/lifecycle.mjs +337 -0
- package/src/plugins/manifest.mjs +37 -0
- package/src/plugins/pi-compat/requirements.mjs +30 -17
- package/src/plugins/preflight.mjs +37 -0
- package/src/terminal/main.mjs +10 -1
- package/src/terminal/paste-input.mjs +52 -0
- package/src/terminal/repl.mjs +169 -36
- package/src/tools/bash.mjs +15 -2
- package/src/tools/registry.mjs +1 -16
- package/src/core/agent-loop.mjs +0 -503
- package/src/core/context-manager.mjs +0 -198
- package/src/tools/agent.mjs +0 -142
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared retry policy for one-shot model HTTP requests.
|
|
3
|
+
*
|
|
4
|
+
* Streaming /api/execute has its own event-id resume protocol in
|
|
5
|
+
* stream-client.mjs. This helper is for npm-owned local/direct model calls
|
|
6
|
+
* where the request must finish before the agent loop can continue.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504, 529]);
|
|
10
|
+
const NETWORK_CODES = new Set([
|
|
11
|
+
'ECONNRESET', 'ECONNREFUSED', 'ECONNABORTED', 'ETIMEDOUT',
|
|
12
|
+
'EAI_AGAIN', 'ENETUNREACH', 'ENETDOWN', 'EHOSTUNREACH', 'UND_ERR_CONNECT_TIMEOUT',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function numberEnv(name, fallback) {
|
|
16
|
+
const value = Number.parseInt(process.env[name] || '', 10);
|
|
17
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isNetworkError(error) {
|
|
21
|
+
const code = error?.cause?.code || error?.code;
|
|
22
|
+
return error?.name === 'AbortError'
|
|
23
|
+
|| NETWORK_CODES.has(String(code || '').toUpperCase())
|
|
24
|
+
|| /fetch failed|network|socket|timed out|timeout|connection reset|connection refused/i.test(String(error?.message || ''));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isRetryableStatus(status) {
|
|
28
|
+
return RETRYABLE_STATUS.has(Number(status));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function retryDelay(attempt, base, max) {
|
|
32
|
+
const exponential = Math.min(max, base * (2 ** Math.max(0, attempt - 1)));
|
|
33
|
+
return Math.min(max, Math.round(exponential * (0.8 + Math.random() * 0.4)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function wait(ms) {
|
|
37
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class RequestError extends Error {
|
|
41
|
+
constructor(message, { status = null, code = 'request_error', retryable = false, attempts = 1, cause = null } = {}) {
|
|
42
|
+
super(message, { cause: cause || undefined });
|
|
43
|
+
this.name = 'RequestError';
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.retryable = retryable;
|
|
47
|
+
this.attempts = attempts;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch with bounded retry for transient transport/server failures.
|
|
53
|
+
* 401/403 and all other non-retryable 4xx responses return immediately.
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchWithRetry(url, options = {}, {
|
|
56
|
+
fetchImpl = globalThis.fetch,
|
|
57
|
+
maxRetries = numberEnv('BAHULAM_REQUEST_MAX_RETRIES', 2),
|
|
58
|
+
baseDelayMs = numberEnv('BAHULAM_REQUEST_RETRY_BASE_MS', 500),
|
|
59
|
+
maxDelayMs = numberEnv('BAHULAM_REQUEST_RETRY_MAX_MS', 8000),
|
|
60
|
+
onRetry = null,
|
|
61
|
+
} = {}) {
|
|
62
|
+
const retries = Math.max(0, Number(maxRetries) || 0);
|
|
63
|
+
let attempt = 0;
|
|
64
|
+
|
|
65
|
+
while (true) {
|
|
66
|
+
attempt++;
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetchImpl(url, options);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (!isNetworkError(error) || attempt > retries + 1) {
|
|
72
|
+
throw new RequestError(
|
|
73
|
+
`Network request failed after ${attempt} attempt${attempt === 1 ? '' : 's'}: ${error?.message || error}`,
|
|
74
|
+
{ code: 'network_error', retryable: true, attempts: attempt, cause: error },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const delayMs = retryDelay(attempt, baseDelayMs, maxDelayMs);
|
|
78
|
+
onRetry?.({ attempt, delayMs, reason: 'network', error });
|
|
79
|
+
await wait(delayMs);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isRetryableStatus(response.status) || attempt > retries + 1) {
|
|
84
|
+
return response;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Release the failed response before retrying. The final response remains
|
|
88
|
+
// available to the caller for its normal provider-specific error body.
|
|
89
|
+
try { response.body?.cancel?.(); } catch {}
|
|
90
|
+
const delayMs = retryDelay(attempt, baseDelayMs, maxDelayMs);
|
|
91
|
+
onRetry?.({ attempt, delayMs, reason: `http_${response.status}`, status: response.status });
|
|
92
|
+
await wait(delayMs);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function requestErrorData(error, { phase = 'model', provider = null } = {}) {
|
|
97
|
+
return {
|
|
98
|
+
message: error?.message || String(error),
|
|
99
|
+
code: error?.code || (error?.status ? `http_${error.status}` : 'request_error'),
|
|
100
|
+
phase,
|
|
101
|
+
provider,
|
|
102
|
+
status: error?.status ?? null,
|
|
103
|
+
retryable: error?.retryable === true || isRetryableStatus(error?.status),
|
|
104
|
+
attempts: error?.attempts || 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured tool errors.
|
|
3
|
+
*
|
|
4
|
+
* Plugin tool handlers used to return { success: false, output: "<msg>" }
|
|
5
|
+
* and thrown errors got wrapped the same way — the message became a
|
|
6
|
+
* blob the agent had to re-read to figure out what went wrong.
|
|
7
|
+
*
|
|
8
|
+
* This module standardizes the envelope so the trace UI can filter and
|
|
9
|
+
* the agent can self-correct on the next turn without re-parsing prose.
|
|
10
|
+
*
|
|
11
|
+
* Handler patterns
|
|
12
|
+
* ----------------
|
|
13
|
+
*
|
|
14
|
+
* Return a structured error explicitly:
|
|
15
|
+
*
|
|
16
|
+
* import { toolError } from '../../src/core/tool-error.mjs';
|
|
17
|
+
* if (!scene) return toolError('MISSING_SCENE', `Scene "${slug}" not found.`,
|
|
18
|
+
* 'Call create_scene(name="<slug>") first.');
|
|
19
|
+
*
|
|
20
|
+
* OR throw a ToolError:
|
|
21
|
+
*
|
|
22
|
+
* throw new ToolError('INVALID_ARGS', 'position must be [x,y,z]', 'Pass a length-3 array.');
|
|
23
|
+
*
|
|
24
|
+
* OR throw a plain Error — normalizeToolResult wraps it as { code: 'UNKNOWN' }
|
|
25
|
+
* so nothing crashes the executor.
|
|
26
|
+
*
|
|
27
|
+
* Error codes (extend freely; keep short, SCREAMING_SNAKE_CASE)
|
|
28
|
+
* ---
|
|
29
|
+
* INVALID_ARGS — schema violation or missing required field
|
|
30
|
+
* MISSING_RESOURCE — referenced id (node, ref, section, ...) not found
|
|
31
|
+
* IO — filesystem or network operation failed
|
|
32
|
+
* PROVIDER_UNAVAILABLE — external provider missing key or offline
|
|
33
|
+
* PROVIDER_ERROR — external provider returned an error response
|
|
34
|
+
* STATE_ERROR — plugin state DB read/write failure
|
|
35
|
+
* RATE_LIMITED — provider or platform rate limit hit
|
|
36
|
+
* TIMEOUT — operation exceeded its deadline
|
|
37
|
+
* UNKNOWN — thrown Error that wasn't a ToolError (fallback)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export class ToolError extends Error {
|
|
41
|
+
constructor(code, message, hint) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'ToolError';
|
|
44
|
+
this.code = String(code || 'UNKNOWN');
|
|
45
|
+
this.hint = hint ? String(hint) : null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a structured error envelope suitable for `return`-ing from a
|
|
51
|
+
* plugin tool handler. Never throws.
|
|
52
|
+
*/
|
|
53
|
+
export function toolError(code, message, hint) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
output: String(message || ''),
|
|
57
|
+
error: {
|
|
58
|
+
code: String(code || 'UNKNOWN'),
|
|
59
|
+
message: String(message || ''),
|
|
60
|
+
...(hint ? { hint: String(hint) } : {}),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Normalize an arbitrary handler return value or thrown error into the
|
|
67
|
+
* canonical shape:
|
|
68
|
+
*
|
|
69
|
+
* Success: { success: true, output, _tool, _plugin }
|
|
70
|
+
* Failure: { success: false, output, error: { code, message, hint?, stack? },
|
|
71
|
+
* _tool, _plugin }
|
|
72
|
+
*
|
|
73
|
+
* `stack` is only attached in DEBUG mode so the trace export can show
|
|
74
|
+
* it without leaking noise into normal responses.
|
|
75
|
+
*
|
|
76
|
+
* `traceId` is optional; when provided by the caller it's forwarded so
|
|
77
|
+
* export / logs can join back to the trace row.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeToolResult({ tool, plugin, traceId }, result, thrown) {
|
|
80
|
+
const meta = {
|
|
81
|
+
_tool: tool,
|
|
82
|
+
...(plugin ? { _plugin: plugin } : {}),
|
|
83
|
+
...(traceId ? { _trace_id: traceId } : {}),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (thrown) {
|
|
87
|
+
const err = thrown instanceof ToolError
|
|
88
|
+
? { code: thrown.code, message: thrown.message, ...(thrown.hint ? { hint: thrown.hint } : {}) }
|
|
89
|
+
: { code: 'UNKNOWN', message: String(thrown?.message || thrown) };
|
|
90
|
+
if (process.env.DEBUG && thrown?.stack) err.stack = String(thrown.stack).split('\n').slice(0, 6).join('\n');
|
|
91
|
+
const outputText = [`[${err.code}] ${err.message}`, err.hint ? `(hint: ${err.hint})` : ''].filter(Boolean).join(' ');
|
|
92
|
+
return { success: false, output: outputText, error: err, ...meta };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!result || typeof result !== 'object') {
|
|
96
|
+
return { success: true, output: result, ...meta };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.success !== false) {
|
|
100
|
+
return { success: true, output: result.output ?? result, ...meta };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// success === false — extract structured error if present, else synthesize one
|
|
104
|
+
const err = result.error && typeof result.error === 'object'
|
|
105
|
+
? {
|
|
106
|
+
code: String(result.error.code || 'UNKNOWN'),
|
|
107
|
+
message: String(result.error.message || result.output || 'Tool call failed.'),
|
|
108
|
+
...(result.error.hint ? { hint: String(result.error.hint) } : {}),
|
|
109
|
+
}
|
|
110
|
+
: {
|
|
111
|
+
code: 'UNKNOWN',
|
|
112
|
+
message: typeof result.output === 'string' ? result.output : JSON.stringify(result.output ?? 'Tool call failed.'),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// The agent reads `output` on the next turn — make it self-contained so
|
|
116
|
+
// the LLM doesn't need to re-parse. Format: [CODE] message (hint: ...)
|
|
117
|
+
const rawOutput = typeof result.output === 'string' && result.output.trim()
|
|
118
|
+
? result.output
|
|
119
|
+
: err.message;
|
|
120
|
+
const alreadyHasCode = rawOutput.startsWith(`[${err.code}]`);
|
|
121
|
+
const alreadyHasHint = err.hint && rawOutput.toLowerCase().includes(String(err.hint).toLowerCase());
|
|
122
|
+
const outputText = [
|
|
123
|
+
alreadyHasCode ? rawOutput : `[${err.code}] ${rawOutput}`,
|
|
124
|
+
err.hint && !alreadyHasHint ? `(hint: ${err.hint})` : '',
|
|
125
|
+
].filter(Boolean).join(' ');
|
|
126
|
+
return { success: false, output: outputText, error: err, ...meta };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format an error envelope as a short one-line summary for logs and
|
|
131
|
+
* compact trace rows.
|
|
132
|
+
*/
|
|
133
|
+
export function formatErrorSummary(err) {
|
|
134
|
+
if (!err) return '';
|
|
135
|
+
const code = err.code || 'UNKNOWN';
|
|
136
|
+
const msg = String(err.message || '').replace(/\s+/g, ' ').slice(0, 240);
|
|
137
|
+
return `[${code}] ${msg}${err.hint ? ` — hint: ${String(err.hint).slice(0, 140)}` : ''}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build a compact hint the next agent turn can consume so it can
|
|
142
|
+
* self-correct without re-reading a raw stack trace.
|
|
143
|
+
*
|
|
144
|
+
* Example:
|
|
145
|
+
* "Previous tool call `create_node` failed. [MISSING_RESOURCE] Node
|
|
146
|
+
* 'chair_99' not found. Hint: call get_scene(slug='cafe') to see
|
|
147
|
+
* current node ids."
|
|
148
|
+
*/
|
|
149
|
+
export function buildNextTurnHint({ tool, error }) {
|
|
150
|
+
if (!error) return '';
|
|
151
|
+
const parts = [`Previous tool call \`${tool || 'unknown'}\` failed.`];
|
|
152
|
+
parts.push(`[${error.code || 'UNKNOWN'}] ${error.message || ''}`.trim());
|
|
153
|
+
if (error.hint) parts.push(`Hint: ${error.hint}`);
|
|
154
|
+
return parts.join(' ');
|
|
155
|
+
}
|
|
@@ -152,8 +152,13 @@ export function createToolExecutor({
|
|
|
152
152
|
return project.resource.root;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
async function commandCwd(args = {}) {
|
|
156
|
-
|
|
155
|
+
async function commandCwd(args = {}, { readOnly = false } = {}) {
|
|
156
|
+
try {
|
|
157
|
+
return await resolvePath(args.cwd || null, args);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (readOnly) return args.cwd ? path.resolve(args.cwd) : process.cwd();
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
function shellTargetPath(cwd, target) {
|
|
@@ -1390,7 +1395,7 @@ export function createToolExecutor({
|
|
|
1390
1395
|
args._riskReason = classification.reason || shellCheck.reason;
|
|
1391
1396
|
}
|
|
1392
1397
|
args._classification = classification.classification; // 'safe' or 'contained'
|
|
1393
|
-
const cwd = await commandCwd(args);
|
|
1398
|
+
const cwd = await commandCwd(args, { readOnly: classification.classification === 'safe' });
|
|
1394
1399
|
|
|
1395
1400
|
// Background execution: start via the BackgroundTasks registry
|
|
1396
1401
|
// and return immediately. Safety checks above still apply;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize provider/gateway usage into the npm agent's canonical shape.
|
|
3
|
+
*
|
|
4
|
+
* Providers use prompt_tokens/completion_tokens, Anthropic-style callers use
|
|
5
|
+
* input_tokens/output_tokens, and remote complete events use total_* fields.
|
|
6
|
+
* Keeping this conversion here prevents each runtime mode from drifting.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeUsage(usage) {
|
|
9
|
+
if (!usage) return null;
|
|
10
|
+
|
|
11
|
+
const input = usage.input_tokens
|
|
12
|
+
?? usage.total_input_tokens
|
|
13
|
+
?? usage.prompt_tokens
|
|
14
|
+
?? 0;
|
|
15
|
+
const output = usage.output_tokens
|
|
16
|
+
?? usage.total_output_tokens
|
|
17
|
+
?? usage.completion_tokens
|
|
18
|
+
?? 0;
|
|
19
|
+
const promptDetails = usage.prompt_tokens_details || {};
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
input_tokens: input,
|
|
23
|
+
output_tokens: output,
|
|
24
|
+
cache_read_input_tokens: usage.cache_read_input_tokens
|
|
25
|
+
?? usage.cache_read_tokens
|
|
26
|
+
?? promptDetails.cached_tokens
|
|
27
|
+
?? 0,
|
|
28
|
+
cache_creation_input_tokens: usage.cache_creation_input_tokens
|
|
29
|
+
?? usage.cache_creation_tokens
|
|
30
|
+
?? 0,
|
|
31
|
+
};
|
|
32
|
+
}
|