@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,207 @@
|
|
|
1
|
+
import { findShippedModel } from '../config/model-catalog.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared client-side context reduction contract.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors the backend context contract so LocalAgent can reduce the
|
|
7
|
+
* same history shape at the same boundary. The transport-specific LLM call
|
|
8
|
+
* is supplied by the caller; this module owns policy and history rewriting.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const SUMMARY_MARKER = '[Context summary — earlier conversation condensed]';
|
|
12
|
+
export const DISTILLATION_MARKER = '[Context distillation — active ingredients preserved]';
|
|
13
|
+
|
|
14
|
+
const PRODUCT_POLICIES = Object.freeze({
|
|
15
|
+
// workingSetRatio is an optimization budget, not a provider hard limit.
|
|
16
|
+
// It scales with the selected model's usable context window so large
|
|
17
|
+
// models do not grow to their full capacity before we protect cache reuse
|
|
18
|
+
// and attention quality.
|
|
19
|
+
chat: Object.freeze({ triggerRatio: 0.72, targetRatio: 0.48, workingSetRatio: 0.18, preserve: 12, prune: 'aggressive' }),
|
|
20
|
+
ide: Object.freeze({ triggerRatio: 0.78, targetRatio: 0.52, workingSetRatio: 0.20, preserve: 10, prune: 'conservative' }),
|
|
21
|
+
workspace: Object.freeze({ triggerRatio: 0.70, targetRatio: 0.45, workingSetRatio: 0.16, preserve: 14, prune: 'protected' }),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function contextProduct(product = 'ide') {
|
|
25
|
+
const value = String(product || '').toLowerCase();
|
|
26
|
+
if (value.includes('chat')) return 'chat';
|
|
27
|
+
if (value.includes('workspace')) return 'workspace';
|
|
28
|
+
return 'ide';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function contextPolicy(product = 'ide') {
|
|
32
|
+
return PRODUCT_POLICIES[contextProduct(product)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toolPruningPolicy(product = 'ide') {
|
|
36
|
+
const profile = contextProduct(product);
|
|
37
|
+
return {
|
|
38
|
+
profile,
|
|
39
|
+
protected: [
|
|
40
|
+
'write_file', 'edit_file', 'delete_file', 'validate_build',
|
|
41
|
+
'lint_check', 'remember', 'todo_write', 'ask_user',
|
|
42
|
+
],
|
|
43
|
+
eligible: profile === 'chat'
|
|
44
|
+
? ['read_file', 'search_code', 'search_files', 'list_files', 'shell']
|
|
45
|
+
: profile === 'workspace'
|
|
46
|
+
? ['read_file', 'search_code', 'search_files', 'list_files']
|
|
47
|
+
: ['read_file', 'search_code', 'search_files', 'list_files', 'shell'],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveContextBudget({
|
|
52
|
+
product = 'ide', model = null, contextLength = null, maxOutput = null,
|
|
53
|
+
fixedPromptTokens = 0, explicitThreshold = null,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const policy = contextPolicy(product);
|
|
56
|
+
const catalog = findShippedModel(model);
|
|
57
|
+
const window = Number(contextLength || catalog?.context_length || 0);
|
|
58
|
+
const output = Number(maxOutput || catalog?.max_output || 0);
|
|
59
|
+
const explicit = Number(explicitThreshold || 0);
|
|
60
|
+
if (explicit > 0) {
|
|
61
|
+
return { ...policy, threshold: Math.max(20_000, Math.floor(explicit)), source: 'explicit' };
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isFinite(window) || window <= 0) {
|
|
64
|
+
return { ...policy, threshold: 160_000, source: 'fallback' };
|
|
65
|
+
}
|
|
66
|
+
const reservedOutput = output > 0 ? output : Math.floor(window * 0.10);
|
|
67
|
+
const safety = Math.max(1024, Math.floor(window * 0.03));
|
|
68
|
+
const usable = Math.max(20_000, window - reservedOutput - Math.max(0, fixedPromptTokens) - safety);
|
|
69
|
+
const workingSet = Math.max(20_000, Math.floor(usable * policy.workingSetRatio));
|
|
70
|
+
return {
|
|
71
|
+
...policy,
|
|
72
|
+
threshold: Math.max(20_000, Math.floor(workingSet * policy.triggerRatio)),
|
|
73
|
+
targetTokens: Math.max(10_000, Math.floor(workingSet * policy.targetRatio)),
|
|
74
|
+
contextLength: window,
|
|
75
|
+
reservedOutput,
|
|
76
|
+
usableTokens: usable,
|
|
77
|
+
workingSetTokens: workingSet,
|
|
78
|
+
fixedPromptTokens: Math.max(0, fixedPromptTokens),
|
|
79
|
+
source: 'model_catalog',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function contextReductionConfig(env = process.env, product = 'ide') {
|
|
84
|
+
const profileName = contextProduct(product).toUpperCase();
|
|
85
|
+
const strategyRaw = String(
|
|
86
|
+
env[`BAHULAM_${profileName}_CONTEXT_STRATEGY`]
|
|
87
|
+
|| env.BAHULAM_CONTEXT_STRATEGY
|
|
88
|
+
|| env.BAHULAM_CONTEXT_REDUCTION_STRATEGY
|
|
89
|
+
|| 'distillation',
|
|
90
|
+
).trim().toLowerCase();
|
|
91
|
+
const threshold = Number.parseInt(
|
|
92
|
+
env.BAHULAM_SUMMARIZE_THRESHOLD || env.BAHULAM_CHAT_SUMMARIZE_THRESHOLD || '',
|
|
93
|
+
10,
|
|
94
|
+
);
|
|
95
|
+
const preserve = Number.parseInt(
|
|
96
|
+
env.BAHULAM_SUMMARIZE_PRESERVE_TURNS || env.BAHULAM_CHAT_SUMMARIZE_PRESERVE_TURNS || '',
|
|
97
|
+
10,
|
|
98
|
+
);
|
|
99
|
+
const sigma = Number.parseFloat(
|
|
100
|
+
env.BAHULAM_CONTEXT_DISTILLATION_SIGMA || env.BAHULAM_COMPACTION_SIGMA || '1.5',
|
|
101
|
+
);
|
|
102
|
+
return {
|
|
103
|
+
enabled: !['0', 'false', 'no', 'off'].includes(String(env.BAHULAM_SUMMARIZE || 'true').toLowerCase()),
|
|
104
|
+
strategy: ['summary', 'summarize', 'summarization'].includes(strategyRaw)
|
|
105
|
+
? 'summarization'
|
|
106
|
+
: 'distillation',
|
|
107
|
+
threshold: Number.isFinite(threshold) && threshold > 0 ? Math.max(20_000, threshold) : null,
|
|
108
|
+
preserve: Number.isFinite(preserve) && preserve > 0 ? Math.max(2, preserve) : null,
|
|
109
|
+
sigma: Number.isFinite(sigma) ? Math.max(0.5, Math.min(4, sigma)) : 1.5,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function estimateMessagesTokens(messages = []) {
|
|
114
|
+
return (Array.isArray(messages) ? messages : []).reduce((total, message) => {
|
|
115
|
+
let content = message?.content;
|
|
116
|
+
if (typeof content !== 'string') {
|
|
117
|
+
try { content = JSON.stringify(content ?? ''); } catch { content = String(content ?? ''); }
|
|
118
|
+
}
|
|
119
|
+
return total + Math.floor(String(content).length / 4) + 8;
|
|
120
|
+
}, 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function messageText(message) {
|
|
124
|
+
const content = message?.content;
|
|
125
|
+
if (typeof content === 'string') return content;
|
|
126
|
+
if (Array.isArray(content)) {
|
|
127
|
+
return content.map(block => block?.text || block?.content || block?.name || '').join(' ');
|
|
128
|
+
}
|
|
129
|
+
try { return JSON.stringify(content ?? ''); } catch { return String(content ?? ''); }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toolNames(message) {
|
|
133
|
+
return (Array.isArray(message?.content) ? message.content : [])
|
|
134
|
+
.map(block => block?.name || block?.tool || block?.tool_name)
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map(String)
|
|
137
|
+
.map(name => name.toLowerCase());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scoreMessage(message, index, total, preserve) {
|
|
141
|
+
const pos = total <= 1 ? 1 : index / (total - 1);
|
|
142
|
+
const edgeScore = 2 * ((Math.abs(pos - 0.5) * 2) ** 2);
|
|
143
|
+
const text = messageText(message);
|
|
144
|
+
const tools = toolNames(message);
|
|
145
|
+
let score = edgeScore;
|
|
146
|
+
const reasons = ['position'];
|
|
147
|
+
if (index === 0) { score += 4; reasons.push('root_intent'); }
|
|
148
|
+
if (index >= Math.max(0, total - preserve)) { score += 4; reasons.push('recent_tail'); }
|
|
149
|
+
if (message?.role === 'user') { score += 1.5; reasons.push('user_instruction'); }
|
|
150
|
+
if (tools.some(t => ['write', 'write_file', 'edit', 'edit_file', 'delete_file'].includes(t))) {
|
|
151
|
+
score += 4; reasons.push('durable_write');
|
|
152
|
+
}
|
|
153
|
+
if (tools.some(t => ['read', 'read_file', 'search_code', 'search_files', 'list_files'].includes(t))) {
|
|
154
|
+
score += 1.2; reasons.push('reference');
|
|
155
|
+
}
|
|
156
|
+
if (/(exit[_ -]?code|traceback|error|failed|exception|fatal)/i.test(text)) {
|
|
157
|
+
score += 1.2; reasons.push('failure_signal');
|
|
158
|
+
}
|
|
159
|
+
return { score, reasons, tools, text };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function distillMessages(messages = [], { preserve = 10, sigma = 1.5, maxLines = 180 } = {}) {
|
|
163
|
+
const rows = (Array.isArray(messages) ? messages : []).filter(Boolean);
|
|
164
|
+
if (!rows.length) return null;
|
|
165
|
+
const scored = rows.map((message, index) => scoreMessage(message, index, rows.length, preserve));
|
|
166
|
+
const mean = scored.reduce((sum, row) => sum + row.score, 0) / scored.length;
|
|
167
|
+
const variance = scored.reduce((sum, row) => sum + ((row.score - mean) ** 2), 0) / scored.length;
|
|
168
|
+
const fullThreshold = mean + sigma * Math.sqrt(variance);
|
|
169
|
+
const groups = { full: [], structured: [], distilled: [] };
|
|
170
|
+
scored.forEach((row, index) => {
|
|
171
|
+
const forced = index === 0 || index >= Math.max(0, rows.length - preserve) || row.reasons.includes('durable_write');
|
|
172
|
+
const action = forced || row.score >= fullThreshold
|
|
173
|
+
? 'full'
|
|
174
|
+
: row.score >= mean ? 'structured' : 'distilled';
|
|
175
|
+
const compact = row.text.replace(/\s+/g, ' ').trim().slice(0, action === 'full' ? 400 : 260);
|
|
176
|
+
groups[action].push(`- ${action}: ${row.tools.length ? `tool=${row.tools.slice(0, 3).join(',')}; ` : ''}${rows[index]?.role || 'message'}: ${compact}`);
|
|
177
|
+
});
|
|
178
|
+
return [
|
|
179
|
+
DISTILLATION_MARKER,
|
|
180
|
+
`policy=context-distillation-v1 sigma=${sigma} counts=${JSON.stringify({ keep_full: groups.full.length, keep_structured: groups.structured.length, distill: groups.distilled.length })}`,
|
|
181
|
+
'', 'Active ingredients kept full:', ...(groups.full.slice(0, maxLines) || ['- none']),
|
|
182
|
+
'', 'Structured middle evidence:', ...(groups.structured.slice(0, maxLines) || ['- none']),
|
|
183
|
+
'', 'Boiled-away noisy trail:', ...(groups.distilled.slice(0, Math.max(12, Math.floor(maxLines / 3))) || ['- none']),
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function collapseMessages(messages, summary, preserve = 10) {
|
|
188
|
+
const rows = Array.isArray(messages) ? messages : [];
|
|
189
|
+
let start = Math.max(0, rows.length - Math.max(2, preserve));
|
|
190
|
+
// Never hand the next provider an orphaned tool_result. If the retention
|
|
191
|
+
// boundary lands on a result-only user message, include the preceding
|
|
192
|
+
// assistant tool-use message as well.
|
|
193
|
+
while (start > 0 && isToolResultOnly(rows[start])) start--;
|
|
194
|
+
const tail = rows.slice(start);
|
|
195
|
+
return [
|
|
196
|
+
{ role: 'user', content: summary },
|
|
197
|
+
{ role: 'assistant', content: 'Understood — continuing from the summarized context above.' },
|
|
198
|
+
...tail,
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isToolResultOnly(message) {
|
|
203
|
+
return message?.role === 'user'
|
|
204
|
+
&& Array.isArray(message.content)
|
|
205
|
+
&& message.content.length > 0
|
|
206
|
+
&& message.content.every(block => block?.type === 'tool_result');
|
|
207
|
+
}
|
package/src/core/headless.mjs
CHANGED
|
@@ -29,6 +29,9 @@ import { startHttpDashboard } from '../daemon/http-dashboard.mjs';
|
|
|
29
29
|
import { resolvePending } from '../daemon/approval-store.mjs';
|
|
30
30
|
import { startRelayBridge } from '../daemon/relay-client.mjs';
|
|
31
31
|
import { loadRemoteConfig } from '../commands/remote.mjs';
|
|
32
|
+
import { resolveGatewayUrl } from './backend-url.mjs';
|
|
33
|
+
import { DEFAULT_REASONING_MODEL } from '../config/model-defaults.mjs';
|
|
34
|
+
import { applyModelSelection, resolveModelSelection } from './model-selection.mjs';
|
|
32
35
|
import { writeSessionMeta } from './event-log.mjs';
|
|
33
36
|
import { daemonSessionDir } from './paths.mjs';
|
|
34
37
|
import { publishSessionDirectory, markSessionClosed } from '../daemon/session-publisher.mjs';
|
|
@@ -49,8 +52,10 @@ import {
|
|
|
49
52
|
* @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
|
|
50
53
|
* @param {boolean} [opts.verbose] - show progress on stderr
|
|
51
54
|
*/
|
|
52
|
-
export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, vision = [], agent = null, workflow = null }) {
|
|
55
|
+
export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, mode = null, vision = [], agent = null, workflow = null }) {
|
|
53
56
|
const startTime = Date.now();
|
|
57
|
+
const runtimeMode = mode || 'local';
|
|
58
|
+
const cliLocal = runtimeMode === 'local' || runtimeMode === 'direct';
|
|
54
59
|
|
|
55
60
|
const log = (msg) => {
|
|
56
61
|
if (verbose) process.stderr.write(`[headless] ${msg}\n`);
|
|
@@ -66,9 +71,11 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
66
71
|
const graphTarget = agent || workflow;
|
|
67
72
|
const anthKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
|
|
68
73
|
const orKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
|
|
74
|
+
const gatewayUrl = resolveGatewayUrl();
|
|
75
|
+
const localSessionId = `local_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
|
69
76
|
// Graph runs execute locally and only need a model key; everything
|
|
70
77
|
// else still requires login (the backend runs the agent loop).
|
|
71
|
-
if (!creds.token && !(graphTarget && (anthKey || orKey))) {
|
|
78
|
+
if (!creds.token && runtimeMode !== 'direct' && !(graphTarget && (anthKey || orKey))) {
|
|
72
79
|
emit({ type: 'error', error: 'Not logged in. Run: bahulam login' });
|
|
73
80
|
process.exit(1);
|
|
74
81
|
}
|
|
@@ -94,6 +101,10 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
94
101
|
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
95
102
|
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
96
103
|
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
104
|
+
modelTransport: runtimeMode === 'local' ? 'gateway' : 'direct',
|
|
105
|
+
gatewayUrl: runtimeMode === 'local' ? gatewayUrl : null,
|
|
106
|
+
gatewayToken: runtimeMode === 'local' ? creds.token : null,
|
|
107
|
+
sessionId: localSessionId,
|
|
97
108
|
defaultModel: model || null,
|
|
98
109
|
cwd: process.cwd(),
|
|
99
110
|
});
|
|
@@ -121,6 +132,10 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
121
132
|
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
122
133
|
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
123
134
|
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
135
|
+
modelTransport: runtimeMode === 'local' ? 'gateway' : 'direct',
|
|
136
|
+
gatewayUrl: runtimeMode === 'local' ? gatewayUrl : null,
|
|
137
|
+
gatewayToken: runtimeMode === 'local' ? creds.token : null,
|
|
138
|
+
sessionId: localSessionId,
|
|
124
139
|
defaultModel: model || null,
|
|
125
140
|
cwd: process.cwd(),
|
|
126
141
|
});
|
|
@@ -165,6 +180,10 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
165
180
|
listLocalWorkflows: () => listLocalWorkflows(process.cwd()),
|
|
166
181
|
renderEvent: (event) => emit({ type: event.type, ...event.data }),
|
|
167
182
|
credentials: { apiKey: anthKey, openRouterKey: orKey },
|
|
183
|
+
modelTransport: runtimeMode === 'local' ? 'gateway' : 'direct',
|
|
184
|
+
gatewayUrl: runtimeMode === 'local' ? gatewayUrl : null,
|
|
185
|
+
gatewayToken: runtimeMode === 'local' ? creds.token : null,
|
|
186
|
+
sessionId: localSessionId,
|
|
168
187
|
defaultModel: model || null,
|
|
169
188
|
cwd: process.cwd(),
|
|
170
189
|
});
|
|
@@ -185,27 +204,70 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
185
204
|
// just added to _callClaude / _callOpenRouter. Model comes from the
|
|
186
205
|
// --model flag (which overrides settings dynamically for benchmarking).
|
|
187
206
|
let client;
|
|
188
|
-
if (local) {
|
|
207
|
+
if (runtimeMode === 'local') {
|
|
189
208
|
const { LocalAgent } = await import('./local-agent.mjs');
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
const localSelection = resolveModelSelection({
|
|
210
|
+
explicitModel: model,
|
|
211
|
+
modelOverrides: creds.modelConfig,
|
|
212
|
+
modelMode: creds.modelMode,
|
|
213
|
+
modelRoute: creds.routePreference,
|
|
214
|
+
profileModels: creds.models,
|
|
215
|
+
modeModels: { fast: creds.models?.fast },
|
|
216
|
+
fallbackModel: DEFAULT_REASONING_MODEL,
|
|
217
|
+
});
|
|
218
|
+
const localModel = localSelection.model;
|
|
219
|
+
const gatewayToken = creds.token;
|
|
220
|
+
if (!gatewayToken) {
|
|
221
|
+
emit({ type: 'error', error: '--local requires an authenticated Bahulam session' });
|
|
195
222
|
process.exit(1);
|
|
196
223
|
}
|
|
224
|
+
const pluginSchemas = toolExecutor.listPluginToolSchemas?.() || [];
|
|
197
225
|
client = {
|
|
198
226
|
execute: (instr, ctx) => new LocalAgent({
|
|
199
|
-
apiKey: anthKey,
|
|
200
|
-
openRouterKey: orKey,
|
|
201
227
|
model: localModel,
|
|
202
228
|
toolExecutor,
|
|
203
229
|
verbose,
|
|
204
230
|
cwd: process.cwd(),
|
|
205
231
|
maxTurns: 50,
|
|
232
|
+
gatewayUrl,
|
|
233
|
+
gatewayToken,
|
|
234
|
+
sessionId: localSessionId,
|
|
235
|
+
extraToolSchemas: pluginSchemas,
|
|
236
|
+
}).execute(instr, ctx),
|
|
237
|
+
};
|
|
238
|
+
log(`Local mode via Bahulam Gateway: ${localModel}`);
|
|
239
|
+
} else if (runtimeMode === 'direct') {
|
|
240
|
+
const { LocalAgent } = await import('./local-agent.mjs');
|
|
241
|
+
const directSelection = resolveModelSelection({
|
|
242
|
+
explicitModel: model,
|
|
243
|
+
modelOverrides: creds.modelConfig,
|
|
244
|
+
modelMode: creds.modelMode,
|
|
245
|
+
modelRoute: creds.routePreference,
|
|
246
|
+
profileModels: creds.models,
|
|
247
|
+
modeModels: { fast: creds.models?.fast },
|
|
248
|
+
fallbackModel: DEFAULT_REASONING_MODEL,
|
|
249
|
+
});
|
|
250
|
+
const directModel = directSelection.model;
|
|
251
|
+
const directOpenRouterKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
|
|
252
|
+
const directAnthropicKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
|
|
253
|
+
if (!directOpenRouterKey && !directAnthropicKey) {
|
|
254
|
+
emit({ type: 'error', error: '--direct requires OPENROUTER_API_KEY or ANTHROPIC_API_KEY' });
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
const pluginSchemas = toolExecutor.listPluginToolSchemas?.() || [];
|
|
258
|
+
client = {
|
|
259
|
+
execute: (instr, ctx) => new LocalAgent({
|
|
260
|
+
apiKey: directAnthropicKey,
|
|
261
|
+
openRouterKey: directOpenRouterKey,
|
|
262
|
+
model: directModel,
|
|
263
|
+
toolExecutor,
|
|
264
|
+
verbose,
|
|
265
|
+
cwd: process.cwd(),
|
|
266
|
+
maxTurns: 50,
|
|
267
|
+
extraToolSchemas: pluginSchemas,
|
|
206
268
|
}).execute(instr, ctx),
|
|
207
269
|
};
|
|
208
|
-
log(`
|
|
270
|
+
log(`Direct mode: ${directModel}`);
|
|
209
271
|
} else {
|
|
210
272
|
client = new BahulamStreamClient({
|
|
211
273
|
baseUrl: creds.backendUrl,
|
|
@@ -213,6 +275,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
213
275
|
toolExecutor,
|
|
214
276
|
approvalManager: approval,
|
|
215
277
|
pluginRegistry,
|
|
278
|
+
mode: runtimeMode === 'bundled' ? 'bundled' : 'remote',
|
|
216
279
|
});
|
|
217
280
|
}
|
|
218
281
|
|
|
@@ -225,7 +288,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
225
288
|
}, timeoutMs);
|
|
226
289
|
|
|
227
290
|
// ── Vision analysis preflight ──
|
|
228
|
-
if (!
|
|
291
|
+
if (!cliLocal) {
|
|
229
292
|
try {
|
|
230
293
|
const prepared = prepareImageAttachments(instruction, {
|
|
231
294
|
cwd: process.cwd(),
|
|
@@ -280,7 +343,16 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
|
|
|
280
343
|
}),
|
|
281
344
|
agent_context: toolExecutor.getAgentContext(),
|
|
282
345
|
};
|
|
283
|
-
|
|
346
|
+
const modelSelection = resolveModelSelection({
|
|
347
|
+
explicitModel: model,
|
|
348
|
+
modelOverrides: creds.modelConfig,
|
|
349
|
+
modelMode: creds.modelMode,
|
|
350
|
+
modelRoute: creds.routePreference,
|
|
351
|
+
profileModels: creds.models,
|
|
352
|
+
modeModels: { fast: creds.models?.fast },
|
|
353
|
+
fallbackModel: DEFAULT_REASONING_MODEL,
|
|
354
|
+
});
|
|
355
|
+
Object.assign(execContext, applyModelSelection({}, modelSelection));
|
|
284
356
|
|
|
285
357
|
let primaryToolCount = 0;
|
|
286
358
|
let subAgentForwardedToolCount = 0;
|