agent-sh 0.13.7 → 0.14.0
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/README.md +1 -1
- package/dist/agent/agent-loop.d.ts +13 -17
- package/dist/agent/agent-loop.js +118 -224
- package/dist/agent/conversation-state.d.ts +1 -1
- package/dist/agent/events.d.ts +218 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/host-types.d.ts +20 -0
- package/dist/agent/index.d.ts +5 -9
- package/dist/agent/index.js +269 -167
- package/dist/agent/llm-facade.d.ts +13 -0
- package/dist/{utils → agent}/llm-facade.js +1 -1
- package/dist/agent/nuclear-form.d.ts +1 -1
- package/dist/agent/providers/deepseek.js +2 -5
- package/dist/agent/providers/openai-compatible.js +2 -2
- package/dist/agent/providers/openai.js +2 -5
- package/dist/agent/providers/openrouter.js +5 -5
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-registry.d.ts +1 -1
- package/dist/cli/auth/cli.js +11 -6
- package/dist/cli/auth/discover.d.ts +5 -0
- package/dist/cli/auth/discover.js +25 -0
- package/dist/cli/auth/keys.d.ts +5 -2
- package/dist/cli/auth/keys.js +22 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +12 -2
- package/dist/core/event-bus.d.ts +28 -371
- package/dist/core/extension-loader.js +6 -6
- package/dist/core/index.d.ts +10 -29
- package/dist/core/index.js +32 -84
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.js +1 -1
- package/dist/extensions/slash-commands/events.d.ts +18 -0
- package/dist/extensions/slash-commands/events.js +1 -0
- package/dist/extensions/slash-commands/index.d.ts +15 -0
- package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
- package/dist/shell/events.d.ts +85 -0
- package/dist/shell/events.js +1 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
- package/examples/extensions/ashi/package.json +1 -1
- package/examples/extensions/ollama.ts +47 -42
- package/examples/extensions/opencode-bridge/README.md +4 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -1
- package/examples/extensions/pi-bridge/index.ts +3 -4
- package/examples/extensions/zai-coding-plan.ts +2 -6
- package/package.json +1 -1
- package/dist/extensions/slash-commands.d.ts +0 -2
- package/dist/utils/llm-facade.d.ts +0 -11
- /package/dist/{utils → agent}/llm-client.d.ts +0 -0
- /package/dist/{utils → agent}/llm-client.js +0 -0
package/dist/agent/index.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode resolution is deferred to `core:extensions-loaded` so a persisted
|
|
3
|
+
* `defaultProvider: "openrouter"` doesn't lose to a cold-start race.
|
|
4
|
+
*/
|
|
5
|
+
import "./events.js";
|
|
1
6
|
import { AgentLoop } from "./agent-loop.js";
|
|
2
|
-
import { LlmClient } from "
|
|
3
|
-
import { createLlmFacade } from "
|
|
7
|
+
import { LlmClient } from "./llm-client.js";
|
|
8
|
+
import { createLlmFacade } from "./llm-facade.js";
|
|
9
|
+
import { registerReadOnlyTool, unregisterReadOnlyTool } from "./nuclear-form.js";
|
|
4
10
|
import { resolveProvider, getProviderNames, getSettings } from "../core/settings.js";
|
|
5
11
|
import { discoverSkills } from "./skills.js";
|
|
6
|
-
import { resolveApiKey } from "../cli/auth/keys.js";
|
|
7
12
|
import activateOpenrouter from "./providers/openrouter.js";
|
|
8
13
|
import activateOpenai from "./providers/openai.js";
|
|
9
14
|
import activateOpenaiCompatible from "./providers/openai-compatible.js";
|
|
10
15
|
import activateDeepseek from "./providers/deepseek.js";
|
|
16
|
+
import { findBash } from "../utils/executor.js";
|
|
17
|
+
import { createBashTool } from "./tools/bash.js";
|
|
18
|
+
import { createPwshTool } from "./tools/pwsh.js";
|
|
19
|
+
import { createReadFileTool } from "./tools/read-file.js";
|
|
20
|
+
import { createWriteFileTool } from "./tools/write-file.js";
|
|
21
|
+
import { createEditFileTool } from "./tools/edit-file.js";
|
|
22
|
+
import { createGrepTool } from "./tools/grep.js";
|
|
23
|
+
import { createGlobTool } from "./tools/glob.js";
|
|
24
|
+
import { createLsTool } from "./tools/ls.js";
|
|
25
|
+
import { createListSkillsTool } from "./tools/list-skills.js";
|
|
11
26
|
function persistedModelFor(providerName) {
|
|
12
27
|
if (!providerName)
|
|
13
28
|
return undefined;
|
|
@@ -36,24 +51,142 @@ function mergeCaps(settingsCaps, payloadCaps, modelIds) {
|
|
|
36
51
|
}
|
|
37
52
|
return out.size > 0 ? out : undefined;
|
|
38
53
|
}
|
|
54
|
+
function splitRegistration(p) {
|
|
55
|
+
const raw = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
56
|
+
const ids = [];
|
|
57
|
+
const caps = new Map();
|
|
58
|
+
for (const m of raw) {
|
|
59
|
+
if (typeof m === "string") {
|
|
60
|
+
ids.push(m);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
ids.push(m.id);
|
|
64
|
+
caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { ids, caps };
|
|
68
|
+
}
|
|
39
69
|
export default function agentBackend(ctx) {
|
|
40
70
|
const { bus } = ctx;
|
|
41
71
|
const config = ctx.call("config:get-app-config") ?? {};
|
|
72
|
+
const toolContribs = new Map();
|
|
73
|
+
const instructionContribs = new Map();
|
|
74
|
+
const skillContribs = new Map();
|
|
75
|
+
const providerContribs = new Map();
|
|
76
|
+
// Settings overlay — fields here win over contributing extensions' payloads.
|
|
77
|
+
const settingsProviders = new Map();
|
|
78
|
+
for (const name of getProviderNames()) {
|
|
79
|
+
const p = resolveProvider(name);
|
|
80
|
+
if (p)
|
|
81
|
+
settingsProviders.set(name, p);
|
|
82
|
+
}
|
|
83
|
+
const providerHooks = new Map();
|
|
84
|
+
// Bakes model id so AgentMode.buildReasoningParams keeps its (level) signature.
|
|
85
|
+
const bindReasoning = (shapeId, model) => {
|
|
86
|
+
const hook = providerHooks.get(shapeId)?.reasoningParams;
|
|
87
|
+
return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
|
|
88
|
+
};
|
|
42
89
|
const agentSurface = {
|
|
43
90
|
llm: createLlmFacade({ list: ctx.list, call: ctx.call }),
|
|
44
91
|
providers: {
|
|
92
|
+
register: (reg) => {
|
|
93
|
+
const existing = providerContribs.get(reg.id);
|
|
94
|
+
if (existing)
|
|
95
|
+
bus.offPipe("agent:providers", existing);
|
|
96
|
+
const contrib = (acc) => {
|
|
97
|
+
acc.providers.push(reg);
|
|
98
|
+
return acc;
|
|
99
|
+
};
|
|
100
|
+
providerContribs.set(reg.id, contrib);
|
|
101
|
+
bus.onPipe("agent:providers", contrib);
|
|
102
|
+
bus.emit("agent:providers:changed", {});
|
|
103
|
+
return () => agentSurface.providers.unregister(reg.id);
|
|
104
|
+
},
|
|
105
|
+
unregister: (id) => {
|
|
106
|
+
const contrib = providerContribs.get(id);
|
|
107
|
+
if (!contrib)
|
|
108
|
+
return;
|
|
109
|
+
bus.offPipe("agent:providers", contrib);
|
|
110
|
+
providerContribs.delete(id);
|
|
111
|
+
bus.emit("agent:providers:changed", {});
|
|
112
|
+
},
|
|
45
113
|
configure: (id, configureOpts) => bus.emit("provider:configure", { id, ...configureOpts }),
|
|
46
114
|
},
|
|
47
|
-
registerTool: (tool) =>
|
|
48
|
-
|
|
115
|
+
registerTool: (tool) => {
|
|
116
|
+
if (toolContribs.has(tool.name)) {
|
|
117
|
+
throw new Error(`Tool "${tool.name}" already registered. Use ctx.agent.adviseTool() to wrap it.`);
|
|
118
|
+
}
|
|
119
|
+
ctx.define(`tool:${tool.name}`, tool.execute.bind(tool));
|
|
120
|
+
ctx.define(`tool:${tool.name}:schema`, () => ({
|
|
121
|
+
description: tool.description,
|
|
122
|
+
parameters: tool.input_schema,
|
|
123
|
+
}));
|
|
124
|
+
if (tool.readOnly)
|
|
125
|
+
registerReadOnlyTool(tool.name);
|
|
126
|
+
else
|
|
127
|
+
unregisterReadOnlyTool(tool.name);
|
|
128
|
+
const contrib = (acc) => {
|
|
129
|
+
// Pull through schema so adviseToolSchema reflects.
|
|
130
|
+
const view = ctx.call(`tool:${tool.name}:schema`);
|
|
131
|
+
acc.tools.push({ ...tool, description: view.description, input_schema: view.parameters });
|
|
132
|
+
return acc;
|
|
133
|
+
};
|
|
134
|
+
toolContribs.set(tool.name, contrib);
|
|
135
|
+
bus.onPipe("agent:tools", contrib);
|
|
136
|
+
},
|
|
137
|
+
unregisterTool: (name) => {
|
|
138
|
+
const contrib = toolContribs.get(name);
|
|
139
|
+
if (!contrib)
|
|
140
|
+
return;
|
|
141
|
+
bus.offPipe("agent:tools", contrib);
|
|
142
|
+
toolContribs.delete(name);
|
|
143
|
+
unregisterReadOnlyTool(name);
|
|
144
|
+
// Handlers retained so external advisors survive a reload.
|
|
145
|
+
},
|
|
49
146
|
adviseTool: (name, advisor) => ctx.advise(`tool:${name}`, advisor),
|
|
50
147
|
adviseToolSchema: (name, advisor) => ctx.advise(`tool:${name}:schema`, advisor),
|
|
51
|
-
getTools: () => bus.emitPipe("agent:
|
|
52
|
-
registerInstruction: (name, text) =>
|
|
53
|
-
|
|
148
|
+
getTools: () => bus.emitPipe("agent:tools", { tools: [] }).tools,
|
|
149
|
+
registerInstruction: (name, text) => {
|
|
150
|
+
const existing = instructionContribs.get(name);
|
|
151
|
+
if (existing)
|
|
152
|
+
bus.offPipe("agent:instructions", existing);
|
|
153
|
+
ctx.define(`instruction:${name}`, () => text);
|
|
154
|
+
const contrib = (acc) => {
|
|
155
|
+
const current = ctx.call(`instruction:${name}`);
|
|
156
|
+
acc.instructions.push({ name, text: current });
|
|
157
|
+
return acc;
|
|
158
|
+
};
|
|
159
|
+
instructionContribs.set(name, contrib);
|
|
160
|
+
bus.onPipe("agent:instructions", contrib);
|
|
161
|
+
},
|
|
162
|
+
removeInstruction: (name) => {
|
|
163
|
+
const contrib = instructionContribs.get(name);
|
|
164
|
+
if (!contrib)
|
|
165
|
+
return;
|
|
166
|
+
bus.offPipe("agent:instructions", contrib);
|
|
167
|
+
instructionContribs.delete(name);
|
|
168
|
+
},
|
|
54
169
|
adviseInstruction: (name, advisor) => ctx.advise(`instruction:${name}`, advisor),
|
|
55
|
-
registerSkill: (name, description, filePath) =>
|
|
56
|
-
|
|
170
|
+
registerSkill: (name, description, filePath) => {
|
|
171
|
+
const existing = skillContribs.get(name);
|
|
172
|
+
if (existing)
|
|
173
|
+
bus.offPipe("agent:skills", existing);
|
|
174
|
+
ctx.define(`skill:${name}:view`, () => ({ description, filePath }));
|
|
175
|
+
const contrib = (acc) => {
|
|
176
|
+
const view = ctx.call(`skill:${name}:view`);
|
|
177
|
+
acc.skills.push({ name, description: view.description, filePath: view.filePath });
|
|
178
|
+
return acc;
|
|
179
|
+
};
|
|
180
|
+
skillContribs.set(name, contrib);
|
|
181
|
+
bus.onPipe("agent:skills", contrib);
|
|
182
|
+
},
|
|
183
|
+
removeSkill: (name) => {
|
|
184
|
+
const contrib = skillContribs.get(name);
|
|
185
|
+
if (!contrib)
|
|
186
|
+
return;
|
|
187
|
+
bus.offPipe("agent:skills", contrib);
|
|
188
|
+
skillContribs.delete(name);
|
|
189
|
+
},
|
|
57
190
|
adviseSkill: (name, advisor) => ctx.advise(`skill:${name}:view`, advisor),
|
|
58
191
|
registerContextProducer: (_name, producer, producerOpts) => {
|
|
59
192
|
const handlerName = producerOpts?.mode === "per-query"
|
|
@@ -72,32 +205,80 @@ export default function agentBackend(ctx) {
|
|
|
72
205
|
},
|
|
73
206
|
};
|
|
74
207
|
ctx.agent = agentSurface;
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
208
|
+
// Core tools register at activate — before extensions load — so
|
|
209
|
+
// extensions that look them up at activate time (e.g. scheme.ts) find them.
|
|
210
|
+
// conversation_recall stays in AgentLoop (needs session state).
|
|
211
|
+
const fileReadCache = new Map();
|
|
212
|
+
ctx.define("agent:file-read-cache", () => fileReadCache);
|
|
213
|
+
const getCwd = () => ctx.call("cwd");
|
|
214
|
+
const getEnv = () => {
|
|
215
|
+
const env = {};
|
|
216
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
217
|
+
if (v !== undefined)
|
|
218
|
+
env[k] = v;
|
|
83
219
|
}
|
|
220
|
+
return env;
|
|
221
|
+
};
|
|
222
|
+
if (findBash() !== null) {
|
|
223
|
+
agentSurface.registerTool(createBashTool({ getCwd, getEnv, bus }));
|
|
84
224
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
225
|
+
if (process.platform === "win32") {
|
|
226
|
+
agentSurface.registerTool(createPwshTool({ getCwd, getEnv, bus }));
|
|
227
|
+
}
|
|
228
|
+
agentSurface.registerTool(createReadFileTool(getCwd, fileReadCache));
|
|
229
|
+
agentSurface.registerTool(createWriteFileTool(getCwd));
|
|
230
|
+
agentSurface.registerTool(createEditFileTool(getCwd));
|
|
231
|
+
agentSurface.registerTool(createGrepTool(getCwd));
|
|
232
|
+
agentSurface.registerTool(createGlobTool(getCwd));
|
|
233
|
+
agentSurface.registerTool(createLsTool(getCwd));
|
|
234
|
+
agentSurface.registerTool(createListSkillsTool(getCwd));
|
|
235
|
+
let resolvedProviders = new Map();
|
|
236
|
+
const resolveWithSettings = (id, p) => {
|
|
237
|
+
const s = settingsProviders.get(id);
|
|
238
|
+
const { ids: payloadIds, caps: payloadCaps } = p ? splitRegistration(p) : { ids: [], caps: new Map() };
|
|
239
|
+
const fallbackIds = s?.models ?? (s?.defaultModel ? [s.defaultModel] : []);
|
|
240
|
+
const modelIds = s?.modelsExplicit && s.models.length > 0
|
|
241
|
+
? s.models
|
|
242
|
+
: payloadIds.length > 0 ? payloadIds : fallbackIds;
|
|
243
|
+
return {
|
|
244
|
+
id,
|
|
245
|
+
apiKey: s?.apiKey ?? p?.apiKey,
|
|
246
|
+
baseURL: s?.baseURL ?? p?.baseURL,
|
|
247
|
+
defaultModel: s?.defaultModel ?? p?.defaultModel ?? modelIds[0],
|
|
248
|
+
models: modelIds,
|
|
249
|
+
modelsExplicit: s?.modelsExplicit ?? false,
|
|
250
|
+
contextWindow: s?.contextWindow,
|
|
251
|
+
supportsReasoningEffort: s?.supportsReasoningEffort ?? p?.supportsReasoningEffort,
|
|
252
|
+
modelCapabilities: mergeCaps(s?.modelCapabilities, payloadCaps, modelIds),
|
|
253
|
+
reasoningShape: s?.reasoningShape,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const computeResolvedProviders = () => {
|
|
257
|
+
const out = new Map();
|
|
258
|
+
// Last contribution per id wins (openrouter's catalog-refresh replaces
|
|
259
|
+
// its curated default).
|
|
260
|
+
const { providers } = bus.emitPipe("agent:providers", { providers: [] });
|
|
261
|
+
const byId = new Map();
|
|
262
|
+
for (const p of providers)
|
|
263
|
+
byId.set(p.id, p);
|
|
264
|
+
for (const [id, p] of byId)
|
|
265
|
+
out.set(id, resolveWithSettings(id, p));
|
|
266
|
+
for (const [id] of settingsProviders) {
|
|
267
|
+
if (out.has(id))
|
|
268
|
+
continue;
|
|
269
|
+
out.set(id, resolveWithSettings(id, null));
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
91
272
|
};
|
|
92
273
|
const buildModes = () => {
|
|
93
|
-
const
|
|
94
|
-
for (const [id, p] of
|
|
274
|
+
const out = [];
|
|
275
|
+
for (const [id, p] of resolvedProviders) {
|
|
95
276
|
if (!p.apiKey)
|
|
96
277
|
continue;
|
|
97
278
|
const shapeId = p.reasoningShape ?? id;
|
|
98
279
|
for (const model of p.models) {
|
|
99
280
|
const mc = p.modelCapabilities?.get(model);
|
|
100
|
-
|
|
281
|
+
out.push({
|
|
101
282
|
model,
|
|
102
283
|
provider: id,
|
|
103
284
|
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
@@ -110,11 +291,10 @@ export default function agentBackend(ctx) {
|
|
|
110
291
|
});
|
|
111
292
|
}
|
|
112
293
|
}
|
|
113
|
-
return
|
|
294
|
+
return out;
|
|
114
295
|
};
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
// wire the loop until we've resolved, so users never hit that path.
|
|
296
|
+
ctx.define("agent:get-modes", () => buildModes());
|
|
297
|
+
// Reconfigured at core:extensions-loaded; start() gates on `resolved`.
|
|
118
298
|
const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
|
|
119
299
|
ctx.define("llm:get-client", () => llmClient);
|
|
120
300
|
ctx.define("llm:invoke", (messages, opts) => {
|
|
@@ -127,71 +307,59 @@ export default function agentBackend(ctx) {
|
|
|
127
307
|
...(clampedEffort && clampedEffort !== "off" ? { reasoning_effort: clampedEffort } : {}),
|
|
128
308
|
});
|
|
129
309
|
});
|
|
130
|
-
let modes = [];
|
|
131
|
-
let initialModeIndex = 0;
|
|
132
310
|
let resolved = false;
|
|
133
|
-
// Gates late-
|
|
311
|
+
// Gates late-reconcile so config:switch-model doesn't misroute under a non-ash backend.
|
|
134
312
|
let ashActive = false;
|
|
135
|
-
|
|
136
|
-
// AgentLoop must be constructed *before* user extensions activate,
|
|
137
|
-
// because its ctor defines handlers (history:append, etc.) that
|
|
138
|
-
// extensions like superash call synchronously during their own
|
|
139
|
-
// activate. Advise-before-define works for advisers, but plain calls
|
|
140
|
-
// would hit a no-op stub.
|
|
141
|
-
const agentLoop = new AgentLoop({
|
|
142
|
-
bus,
|
|
143
|
-
llmClient,
|
|
144
|
-
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
145
|
-
modes,
|
|
146
|
-
initialModeIndex,
|
|
147
|
-
compositor: ctx.shell?.compositor,
|
|
148
|
-
instanceId: ctx.instanceId,
|
|
149
|
-
history: config.history,
|
|
150
|
-
});
|
|
313
|
+
let agentLoop = null;
|
|
151
314
|
let loadedExtensionNames = [];
|
|
315
|
+
bus.on("agent:providers:changed", () => {
|
|
316
|
+
resolvedProviders = computeResolvedProviders();
|
|
317
|
+
if (!resolved)
|
|
318
|
+
return;
|
|
319
|
+
bus.emit("agent:modes-changed", {});
|
|
320
|
+
if (!ashActive)
|
|
321
|
+
return;
|
|
322
|
+
const pendingProvider = getSettings().defaultProvider;
|
|
323
|
+
if (!pendingProvider)
|
|
324
|
+
return;
|
|
325
|
+
const p = resolvedProviders.get(pendingProvider);
|
|
326
|
+
if (!p)
|
|
327
|
+
return;
|
|
328
|
+
const pendingModel = persistedModelFor(pendingProvider);
|
|
329
|
+
if (pendingModel && p.models.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
330
|
+
bus.emit("config:switch-model", { model: pendingModel });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
bus.on("provider:configure", ({ id, reasoningParams }) => {
|
|
334
|
+
const prev = providerHooks.get(id) ?? {};
|
|
335
|
+
if (reasoningParams !== undefined)
|
|
336
|
+
prev.reasoningParams = reasoningParams;
|
|
337
|
+
providerHooks.set(id, prev);
|
|
338
|
+
});
|
|
152
339
|
bus.on("core:extensions-loaded", ({ names }) => {
|
|
153
340
|
loadedExtensionNames = names;
|
|
341
|
+
resolvedProviders = computeResolvedProviders();
|
|
154
342
|
const settings = getSettings();
|
|
155
|
-
// If the user didn't pick a default, fall back to the first registered
|
|
156
|
-
// provider (built-in load order biases to openrouter → openai).
|
|
157
343
|
const providerName = config.provider ?? settings.defaultProvider
|
|
158
|
-
?? (
|
|
159
|
-
const activeProvider = providerName ?
|
|
160
|
-
//
|
|
161
|
-
// default. Dynamic providers (openrouter) re-register with their
|
|
162
|
-
// hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
|
|
163
|
-
// clobber the user's /model selection.
|
|
344
|
+
?? (resolvedProviders.size > 0 ? resolvedProviders.keys().next().value : undefined);
|
|
345
|
+
const activeProvider = providerName ? resolvedProviders.get(providerName) ?? null : null;
|
|
346
|
+
// Persisted defaultModel wins over openrouter's hardcoded DEFAULT_MODELS[0].
|
|
164
347
|
const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
|
|
165
348
|
const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
|
|
166
349
|
const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
|
|
167
|
-
// No provider → don't register ash
|
|
168
|
-
// claude-code-bridge) can own activation. index.ts hard-fails only
|
|
169
|
-
// when no backend ended up registered.
|
|
350
|
+
// No provider → don't register ash; let another backend own activation.
|
|
170
351
|
if (!effectiveApiKey || !effectiveModel)
|
|
171
352
|
return;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
modes = [
|
|
182
|
-
{
|
|
183
|
-
model: effectiveModel,
|
|
184
|
-
provider: activeProvider.id,
|
|
185
|
-
providerConfig: { apiKey: effectiveApiKey, baseURL: effectiveBaseURL },
|
|
186
|
-
supportsReasoningEffort: activeProvider.supportsReasoningEffort,
|
|
187
|
-
},
|
|
188
|
-
...modes,
|
|
189
|
-
];
|
|
190
|
-
foundIdx = 0;
|
|
191
|
-
}
|
|
192
|
-
initialModeIndex = Math.max(0, foundIdx);
|
|
353
|
+
const foundInModes = buildModes().find((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
|
|
354
|
+
// Stub when openrouter's async catalog hasn't returned yet; reconciled
|
|
355
|
+
// later via agent:providers:changed → config:switch-model.
|
|
356
|
+
const initialMode = foundInModes ?? (activeProvider ? {
|
|
357
|
+
model: effectiveModel,
|
|
358
|
+
provider: activeProvider.id,
|
|
359
|
+
providerConfig: { apiKey: effectiveApiKey, baseURL: effectiveBaseURL },
|
|
360
|
+
supportsReasoningEffort: activeProvider.supportsReasoningEffort,
|
|
361
|
+
} : { model: effectiveModel });
|
|
193
362
|
llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
|
|
194
|
-
bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
|
|
195
363
|
resolved = true;
|
|
196
364
|
bus.emit("agent:register-backend", {
|
|
197
365
|
name: "ash",
|
|
@@ -199,9 +367,19 @@ export default function agentBackend(ctx) {
|
|
|
199
367
|
ashActive = false;
|
|
200
368
|
bus.emit("command:unregister", { name: "/compact" });
|
|
201
369
|
bus.emit("command:unregister", { name: "/context" });
|
|
202
|
-
agentLoop
|
|
370
|
+
agentLoop?.kill();
|
|
371
|
+
agentLoop = null;
|
|
203
372
|
},
|
|
204
373
|
start: async () => {
|
|
374
|
+
agentLoop = new AgentLoop({
|
|
375
|
+
bus,
|
|
376
|
+
llmClient,
|
|
377
|
+
handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
|
|
378
|
+
initialMode,
|
|
379
|
+
compositor: ctx.shell?.compositor,
|
|
380
|
+
instanceId: ctx.instanceId,
|
|
381
|
+
history: config.history,
|
|
382
|
+
});
|
|
205
383
|
agentLoop.wire();
|
|
206
384
|
ashActive = true;
|
|
207
385
|
bus.emit("command:register", {
|
|
@@ -229,71 +407,8 @@ export default function agentBackend(ctx) {
|
|
|
229
407
|
},
|
|
230
408
|
});
|
|
231
409
|
});
|
|
232
|
-
bus.on("provider:configure", ({ id, reasoningParams }) => {
|
|
233
|
-
const prev = providerHooks.get(id) ?? {};
|
|
234
|
-
if (reasoningParams !== undefined)
|
|
235
|
-
prev.reasoningParams = reasoningParams;
|
|
236
|
-
providerHooks.set(id, prev);
|
|
237
|
-
});
|
|
238
|
-
bus.on("provider:register", (p) => {
|
|
239
|
-
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
240
|
-
const payloadModelIds = [];
|
|
241
|
-
const payloadCaps = new Map();
|
|
242
|
-
for (const m of rawModels) {
|
|
243
|
-
if (typeof m === "string") {
|
|
244
|
-
payloadModelIds.push(m);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
payloadModelIds.push(m.id);
|
|
248
|
-
payloadCaps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const settings = settingsProviders.get(p.id);
|
|
252
|
-
const modelIds = settings?.modelsExplicit && settings.models.length > 0 ? settings.models : payloadModelIds;
|
|
253
|
-
const mergedCaps = mergeCaps(settings?.modelCapabilities, payloadCaps, modelIds);
|
|
254
|
-
const merged = {
|
|
255
|
-
id: p.id,
|
|
256
|
-
apiKey: settings?.apiKey ?? p.apiKey,
|
|
257
|
-
baseURL: settings?.baseURL ?? p.baseURL,
|
|
258
|
-
defaultModel: settings?.defaultModel ?? p.defaultModel,
|
|
259
|
-
models: modelIds,
|
|
260
|
-
modelsExplicit: settings?.modelsExplicit ?? false,
|
|
261
|
-
contextWindow: settings?.contextWindow,
|
|
262
|
-
supportsReasoningEffort: settings?.supportsReasoningEffort ?? p.supportsReasoningEffort,
|
|
263
|
-
modelCapabilities: mergedCaps,
|
|
264
|
-
reasoningShape: settings?.reasoningShape,
|
|
265
|
-
};
|
|
266
|
-
providerRegistry.set(p.id, merged);
|
|
267
|
-
const addModes = modelIds.map((m) => {
|
|
268
|
-
const mc = mergedCaps?.get(m);
|
|
269
|
-
return {
|
|
270
|
-
model: m,
|
|
271
|
-
provider: p.id,
|
|
272
|
-
providerConfig: { apiKey: merged.apiKey ?? "", baseURL: merged.baseURL },
|
|
273
|
-
contextWindow: mc?.contextWindow,
|
|
274
|
-
maxTokens: mc?.maxTokens,
|
|
275
|
-
reasoning: mc?.reasoning,
|
|
276
|
-
supportsReasoningEffort: merged.supportsReasoningEffort,
|
|
277
|
-
echoReasoning: mc?.echoReasoning,
|
|
278
|
-
buildReasoningParams: bindReasoning(p.id, m),
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
bus.emit("config:add-modes", { modes: addModes });
|
|
282
|
-
// Late-registration reconcile: if this completes the user's persisted
|
|
283
|
-
// default (openrouter's async fetch delivers the full catalog after
|
|
284
|
-
// we've already fallen back to mode 0), quietly switch to it.
|
|
285
|
-
if (!resolved || !ashActive)
|
|
286
|
-
return;
|
|
287
|
-
const pendingProvider = getSettings().defaultProvider;
|
|
288
|
-
if (pendingProvider !== p.id)
|
|
289
|
-
return;
|
|
290
|
-
const pendingModel = persistedModelFor(pendingProvider);
|
|
291
|
-
if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
|
|
292
|
-
bus.emit("config:switch-model", { model: pendingModel });
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
410
|
bus.on("config:switch-provider", ({ provider: name }) => {
|
|
296
|
-
const p =
|
|
411
|
+
const p = resolvedProviders.get(name);
|
|
297
412
|
if (!p) {
|
|
298
413
|
bus.emit("ui:error", { message: `Unknown provider: ${name}` });
|
|
299
414
|
return;
|
|
@@ -308,20 +423,8 @@ export default function agentBackend(ctx) {
|
|
|
308
423
|
return;
|
|
309
424
|
}
|
|
310
425
|
llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
model: m,
|
|
315
|
-
provider: name,
|
|
316
|
-
providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
|
|
317
|
-
contextWindow: mc?.contextWindow ?? p.contextWindow,
|
|
318
|
-
maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
|
|
319
|
-
reasoning: mc?.reasoning,
|
|
320
|
-
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
321
|
-
echoReasoning: mc?.echoReasoning,
|
|
322
|
-
};
|
|
323
|
-
});
|
|
324
|
-
bus.emit("config:set-modes", { modes: newModes });
|
|
426
|
+
bus.emit("agent:modes-changed", {});
|
|
427
|
+
bus.emit("config:switch-model", { model: switchModel });
|
|
325
428
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
326
429
|
});
|
|
327
430
|
bus.onPipe("banner:collect", (e) => {
|
|
@@ -340,14 +443,13 @@ export default function agentBackend(ctx) {
|
|
|
340
443
|
export { AgentLoop } from "./agent-loop.js";
|
|
341
444
|
export { ToolRegistry } from "./tool-registry.js";
|
|
342
445
|
export { runSubagent } from "./subagent.js";
|
|
343
|
-
/**
|
|
446
|
+
/** Built-in providers register unconditionally so `auth list` can
|
|
447
|
+
* enumerate them; buildModes() skips entries without an apiKey. */
|
|
344
448
|
export function activateAgent(ctx) {
|
|
345
449
|
agentBackend(ctx);
|
|
346
450
|
const agentCtx = ctx;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (resolveApiKey("openai").key && !process.env.OPENAI_BASE_URL)
|
|
350
|
-
activateOpenai(agentCtx);
|
|
451
|
+
activateOpenrouter(agentCtx);
|
|
452
|
+
activateOpenai(agentCtx);
|
|
351
453
|
if (process.env.OPENAI_BASE_URL)
|
|
352
454
|
activateOpenaiCompatible(agentCtx);
|
|
353
455
|
activateDeepseek(agentCtx);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx.agent.llm facade — delegates to an `llm:invoke` handler defined
|
|
3
|
+
* by the ash backend. Other backends (claude-code, pi, opencode) bring
|
|
4
|
+
* their own LLM and do not define this handler; `available` is false
|
|
5
|
+
* under those backends and calls reject.
|
|
6
|
+
*/
|
|
7
|
+
import type { LlmInterface } from "./host-types.js";
|
|
8
|
+
interface HandlerGate {
|
|
9
|
+
list: () => string[];
|
|
10
|
+
call: (name: string, ...args: unknown[]) => unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare function createLlmFacade(handlers: HandlerGate): LlmInterface;
|
|
13
|
+
export {};
|
|
@@ -2,7 +2,7 @@ export function createLlmFacade(handlers) {
|
|
|
2
2
|
const invoke = (messages, maxTokens, model, reasoningEffort) => {
|
|
3
3
|
const result = handlers.call("llm:invoke", messages, { maxTokens, model, reasoningEffort });
|
|
4
4
|
if (result === undefined)
|
|
5
|
-
return Promise.reject(new Error("ctx.llm: no LLM backend available"));
|
|
5
|
+
return Promise.reject(new Error("ctx.agent.llm: no LLM backend available"));
|
|
6
6
|
return result;
|
|
7
7
|
};
|
|
8
8
|
return {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Nuclear entries are the currency of Tier 2 and Tier 3.
|
|
9
9
|
*/
|
|
10
|
-
import type { ChatCompletionMessageParam } from "
|
|
10
|
+
import type { ChatCompletionMessageParam } from "./llm-client.js";
|
|
11
11
|
export interface NuclearEntry {
|
|
12
12
|
/** Global sequence number. */
|
|
13
13
|
seq: number;
|
|
@@ -11,12 +11,9 @@ function buildReasoningParams(level, _model) {
|
|
|
11
11
|
}
|
|
12
12
|
export default function activate(ctx) {
|
|
13
13
|
ctx.agent.providers.configure("deepseek", { reasoningParams: buildReasoningParams });
|
|
14
|
-
|
|
15
|
-
if (!apiKey)
|
|
16
|
-
return;
|
|
17
|
-
ctx.bus.emit("provider:register", {
|
|
14
|
+
ctx.agent.providers.register({
|
|
18
15
|
id: "deepseek",
|
|
19
|
-
apiKey,
|
|
16
|
+
apiKey: resolveApiKey("deepseek").key ?? undefined,
|
|
20
17
|
baseURL: BASE_URL,
|
|
21
18
|
defaultModel: DEFAULT_MODELS[0].id,
|
|
22
19
|
models: DEFAULT_MODELS,
|
|
@@ -5,11 +5,11 @@ export default function activate(ctx) {
|
|
|
5
5
|
// Local servers often need no key; SDK still wants a non-empty string.
|
|
6
6
|
const apiKey = process.env.OPENAI_API_KEY || "no-key";
|
|
7
7
|
const id = "openai-compatible";
|
|
8
|
-
ctx.
|
|
8
|
+
ctx.agent.providers.register({ id, apiKey, baseURL, models: [] });
|
|
9
9
|
fetchModels(baseURL, apiKey).then((models) => {
|
|
10
10
|
if (models.length === 0)
|
|
11
11
|
return;
|
|
12
|
-
ctx.
|
|
12
|
+
ctx.agent.providers.register({
|
|
13
13
|
id,
|
|
14
14
|
apiKey,
|
|
15
15
|
baseURL,
|
|
@@ -32,15 +32,12 @@ function buildReasoningParams(level, model) {
|
|
|
32
32
|
return off ? { reasoning_effort: off } : {};
|
|
33
33
|
}
|
|
34
34
|
export default function activate(ctx) {
|
|
35
|
-
const apiKey = resolveApiKey("openai").key;
|
|
36
|
-
if (!apiKey)
|
|
37
|
-
return;
|
|
38
35
|
if (process.env.OPENAI_BASE_URL)
|
|
39
36
|
return; // openai-compatible handles this
|
|
40
37
|
ctx.agent.providers.configure("openai", { reasoningParams: buildReasoningParams });
|
|
41
|
-
ctx.
|
|
38
|
+
ctx.agent.providers.register({
|
|
42
39
|
id: "openai",
|
|
43
|
-
apiKey,
|
|
40
|
+
apiKey: resolveApiKey("openai").key ?? undefined,
|
|
44
41
|
defaultModel: CLOUD_MODELS[0].id,
|
|
45
42
|
models: CLOUD_MODELS,
|
|
46
43
|
});
|
|
@@ -16,22 +16,22 @@ function buildReasoningParams(level, _model) {
|
|
|
16
16
|
}
|
|
17
17
|
export default function activate(ctx) {
|
|
18
18
|
const apiKey = resolveApiKey("openrouter").key;
|
|
19
|
-
if (!apiKey)
|
|
20
|
-
return;
|
|
21
19
|
ctx.agent.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
|
|
22
|
-
ctx.
|
|
20
|
+
ctx.agent.providers.register({
|
|
23
21
|
id: "openrouter",
|
|
24
|
-
apiKey,
|
|
22
|
+
apiKey: apiKey ?? undefined,
|
|
25
23
|
baseURL: BASE_URL,
|
|
26
24
|
defaultModel: DEFAULT_MODELS[0],
|
|
27
25
|
models: DEFAULT_MODELS,
|
|
28
26
|
});
|
|
27
|
+
if (!apiKey)
|
|
28
|
+
return;
|
|
29
29
|
fetchModels(apiKey).then((models) => {
|
|
30
30
|
if (models.length === 0)
|
|
31
31
|
return;
|
|
32
32
|
const userOverrides = readUserOverrides();
|
|
33
33
|
const patterns = readEchoPatterns();
|
|
34
|
-
ctx.
|
|
34
|
+
ctx.agent.providers.register({
|
|
35
35
|
id: "openrouter",
|
|
36
36
|
apiKey,
|
|
37
37
|
baseURL: BASE_URL,
|