agent-sh 0.12.9 → 0.12.11
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/dist/agent/agent-loop.d.ts +4 -3
- package/dist/agent/agent-loop.js +31 -16
- package/dist/agent/history-file.d.ts +30 -1
- package/dist/agent/history-file.js +40 -17
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/agent/nuclear-form.js +19 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +4 -0
- package/dist/event-bus.d.ts +8 -1
- package/dist/extensions/agent-backend.js +16 -0
- package/dist/extensions/openai.d.ts +5 -3
- package/dist/extensions/openai.js +23 -20
- package/dist/extensions/openrouter.js +6 -0
- package/dist/extensions/slash-commands.js +15 -9
- package/dist/init.js +1 -3
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +1 -0
- package/dist/types.d.ts +9 -0
- package/examples/extensions/ollama.ts +96 -0
- package/examples/extensions/peer-mesh.ts +260 -248
- package/examples/extensions/rtk-proxy.ts +143 -0
- package/package.json +1 -1
- package/examples/extensions/openrouter.ts +0 -87
|
@@ -17,6 +17,7 @@ import type { ContextManager } from "../context-manager.js";
|
|
|
17
17
|
import type { LlmClient } from "../utils/llm-client.js";
|
|
18
18
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
19
19
|
import type { AgentBackend, ToolDefinition } from "./types.js";
|
|
20
|
+
import { type HistoryAdapter } from "./history-file.js";
|
|
20
21
|
import type { Compositor } from "../utils/compositor.js";
|
|
21
22
|
export interface AgentLoopConfig {
|
|
22
23
|
bus: EventBus;
|
|
@@ -28,11 +29,12 @@ export interface AgentLoopConfig {
|
|
|
28
29
|
compositor?: Compositor;
|
|
29
30
|
/** Instance ID from core — ensures history entries match the ID in prompts. */
|
|
30
31
|
instanceId?: string;
|
|
32
|
+
history?: HistoryAdapter;
|
|
31
33
|
}
|
|
32
34
|
export declare class AgentLoop implements AgentBackend {
|
|
33
35
|
private abortController;
|
|
34
36
|
private toolRegistry;
|
|
35
|
-
private
|
|
37
|
+
private history;
|
|
36
38
|
private conversation;
|
|
37
39
|
private fileReadCache;
|
|
38
40
|
private modes;
|
|
@@ -100,8 +102,7 @@ export declare class AgentLoop implements AgentBackend {
|
|
|
100
102
|
buildExtensionSections(): string[];
|
|
101
103
|
kill(): void;
|
|
102
104
|
private cancel;
|
|
103
|
-
|
|
104
|
-
private shouldSendReasoningEffort;
|
|
105
|
+
private reasoningParams;
|
|
105
106
|
private get currentMode();
|
|
106
107
|
private get currentModel();
|
|
107
108
|
/**
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -50,7 +50,7 @@ function summarizeDescription(desc) {
|
|
|
50
50
|
export class AgentLoop {
|
|
51
51
|
abortController = null;
|
|
52
52
|
toolRegistry = new ToolRegistry();
|
|
53
|
-
|
|
53
|
+
history;
|
|
54
54
|
conversation;
|
|
55
55
|
fileReadCache = new Map();
|
|
56
56
|
modes;
|
|
@@ -110,7 +110,7 @@ export class AgentLoop {
|
|
|
110
110
|
// `history:append` handler registered below; extensions swap the
|
|
111
111
|
// backend without touching this wiring.
|
|
112
112
|
const filePath = process.env.AGENT_SH_HISTORY_FILE || getSettings().historyFilePath;
|
|
113
|
-
this.
|
|
113
|
+
this.history = config.history ?? new HistoryFile({ instanceId: this.instanceId, filePath });
|
|
114
114
|
this.conversation = new ConversationState(this.handlers, this.instanceId);
|
|
115
115
|
// Fall back to a single-mode placeholder if the caller passed an
|
|
116
116
|
// empty array (agent-backend does this pre-resolution).
|
|
@@ -180,6 +180,16 @@ export class AgentLoop {
|
|
|
180
180
|
message: `${prev.provider}:${prev.model} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
|
+
const active = this.modes[this.currentModeIndex];
|
|
184
|
+
if (active && active.contextWindow !== prev?.contextWindow) {
|
|
185
|
+
this.bus.emit("agent:info", {
|
|
186
|
+
name: "ash",
|
|
187
|
+
version: PACKAGE_VERSION,
|
|
188
|
+
model: active.model,
|
|
189
|
+
provider: active.provider,
|
|
190
|
+
contextWindow: active.contextWindow,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
183
193
|
this.bus.emit("config:changed", {});
|
|
184
194
|
});
|
|
185
195
|
// Fires before wire() too — agent-backend emits this from
|
|
@@ -216,7 +226,10 @@ export class AgentLoop {
|
|
|
216
226
|
this.abortController?.abort(e.silent ? "silent" : undefined);
|
|
217
227
|
});
|
|
218
228
|
on("config:switch-model", ({ model: target }) => {
|
|
219
|
-
const
|
|
229
|
+
const atIdx = target.lastIndexOf("@");
|
|
230
|
+
const modelId = atIdx > 0 ? target.slice(0, atIdx) : target;
|
|
231
|
+
const providerHint = atIdx > 0 ? target.slice(atIdx + 1) : undefined;
|
|
232
|
+
const idx = this.modes.findIndex((m) => m.model === modelId && (!providerHint || m.provider === providerHint));
|
|
220
233
|
if (idx === -1) {
|
|
221
234
|
this.bus.emit("ui:error", { message: `Unknown model: ${target}` });
|
|
222
235
|
return;
|
|
@@ -249,7 +262,8 @@ export class AgentLoop {
|
|
|
249
262
|
});
|
|
250
263
|
this.bus.onPipe("config:get-models", (payload) => {
|
|
251
264
|
const models = this.modes.map((m) => ({ model: m.model, provider: m.provider ?? "" }));
|
|
252
|
-
const
|
|
265
|
+
const cur = this.modes[this.currentModeIndex];
|
|
266
|
+
const active = cur ? { model: cur.model, provider: cur.provider ?? "" } : null;
|
|
253
267
|
return { models, active };
|
|
254
268
|
});
|
|
255
269
|
on("config:set-thinking", ({ level }) => {
|
|
@@ -461,16 +475,17 @@ export class AgentLoop {
|
|
|
461
475
|
cancel() {
|
|
462
476
|
this.abortController?.abort();
|
|
463
477
|
}
|
|
464
|
-
|
|
465
|
-
shouldSendReasoningEffort() {
|
|
466
|
-
if (this.thinkingLevel === "off")
|
|
467
|
-
return false;
|
|
478
|
+
reasoningParams() {
|
|
468
479
|
const mode = this.currentMode;
|
|
469
480
|
if (mode.reasoning === false)
|
|
470
|
-
return
|
|
481
|
+
return {};
|
|
471
482
|
if (mode.supportsReasoningEffort === false)
|
|
472
|
-
return
|
|
473
|
-
|
|
483
|
+
return {};
|
|
484
|
+
if (mode.buildReasoningParams)
|
|
485
|
+
return mode.buildReasoningParams(this.thinkingLevel);
|
|
486
|
+
if (this.thinkingLevel === "off")
|
|
487
|
+
return {};
|
|
488
|
+
return { reasoning_effort: this.thinkingLevel };
|
|
474
489
|
}
|
|
475
490
|
get currentMode() {
|
|
476
491
|
return this.modes[this.currentModeIndex];
|
|
@@ -822,11 +837,11 @@ export class AgentLoop {
|
|
|
822
837
|
return;
|
|
823
838
|
const writable = entries.filter((e) => !isReadOnly(e));
|
|
824
839
|
if (writable.length > 0)
|
|
825
|
-
this.
|
|
840
|
+
this.history.append(writable).catch(() => { });
|
|
826
841
|
});
|
|
827
|
-
h.define("history:search", async (query) => this.
|
|
828
|
-
h.define("history:find-by-seq", async (seq) => this.
|
|
829
|
-
h.define("history:read-recent", async (max) => this.
|
|
842
|
+
h.define("history:search", async (query) => this.history.search(query));
|
|
843
|
+
h.define("history:find-by-seq", async (seq) => this.history.findBySeq(seq));
|
|
844
|
+
h.define("history:read-recent", async (max) => this.history.readRecent(max));
|
|
830
845
|
// Prior-session preamble renderer. Default: flat chronological list.
|
|
831
846
|
h.define("conversation:format-prior-history", (entries) => {
|
|
832
847
|
if (!entries || entries.length === 0)
|
|
@@ -1525,7 +1540,7 @@ export class AgentLoop {
|
|
|
1525
1540
|
messages,
|
|
1526
1541
|
tools: apiTools,
|
|
1527
1542
|
model: this.currentModel,
|
|
1528
|
-
|
|
1543
|
+
...this.reasoningParams(),
|
|
1529
1544
|
};
|
|
1530
1545
|
this.bus.emit("llm:request", requestParams);
|
|
1531
1546
|
const stream = await this.llmClient.stream({ ...requestParams, signal });
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { type NuclearEntry } from "./nuclear-form.js";
|
|
2
|
-
export
|
|
2
|
+
export interface HistoryAdapter {
|
|
3
|
+
append(entries: NuclearEntry[]): Promise<void>;
|
|
4
|
+
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
5
|
+
search(query: string): Promise<{
|
|
6
|
+
entry: NuclearEntry;
|
|
7
|
+
line: string;
|
|
8
|
+
}[]>;
|
|
9
|
+
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
10
|
+
}
|
|
11
|
+
export declare class InMemoryHistory implements HistoryAdapter {
|
|
12
|
+
private entries;
|
|
13
|
+
constructor(initial?: NuclearEntry[]);
|
|
14
|
+
append(entries: NuclearEntry[]): Promise<void>;
|
|
15
|
+
readRecent(maxEntries?: number): Promise<NuclearEntry[]>;
|
|
16
|
+
search(query: string): Promise<{
|
|
17
|
+
entry: NuclearEntry;
|
|
18
|
+
line: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
findBySeq(seq: number): Promise<NuclearEntry | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare class NoopHistory implements HistoryAdapter {
|
|
23
|
+
append(): Promise<void>;
|
|
24
|
+
readRecent(): Promise<NuclearEntry[]>;
|
|
25
|
+
search(): Promise<{
|
|
26
|
+
entry: NuclearEntry;
|
|
27
|
+
line: string;
|
|
28
|
+
}[]>;
|
|
29
|
+
findBySeq(): Promise<NuclearEntry | null>;
|
|
30
|
+
}
|
|
31
|
+
export declare class HistoryFile implements HistoryAdapter {
|
|
3
32
|
readonly instanceId: string;
|
|
4
33
|
private filePath;
|
|
5
34
|
private lockPath;
|
|
@@ -10,9 +10,43 @@ import * as fss from "node:fs";
|
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import * as crypto from "node:crypto";
|
|
12
12
|
import { CONFIG_DIR, getSettings } from "../settings.js";
|
|
13
|
-
import { serializeEntry, deserializeEntry,
|
|
13
|
+
import { serializeEntry, deserializeEntry, isReadOnly, compileSearchRegex, matchEntry, } from "./nuclear-form.js";
|
|
14
14
|
const HISTORY_PATH = path.join(CONFIG_DIR, "history");
|
|
15
15
|
const LOCK_STALE_MS = 10_000; // consider lock stale after 10s
|
|
16
|
+
export class InMemoryHistory {
|
|
17
|
+
entries;
|
|
18
|
+
constructor(initial = []) {
|
|
19
|
+
this.entries = [...initial];
|
|
20
|
+
}
|
|
21
|
+
async append(entries) {
|
|
22
|
+
this.entries.push(...entries);
|
|
23
|
+
}
|
|
24
|
+
async readRecent(maxEntries) {
|
|
25
|
+
const filtered = this.entries.filter((e) => !isReadOnly(e));
|
|
26
|
+
return maxEntries ? filtered.slice(-maxEntries) : filtered;
|
|
27
|
+
}
|
|
28
|
+
async search(query) {
|
|
29
|
+
if (!query.trim())
|
|
30
|
+
return [];
|
|
31
|
+
const re = compileSearchRegex(query);
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
34
|
+
const m = matchEntry(this.entries[i], re);
|
|
35
|
+
if (m)
|
|
36
|
+
out.push(m);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
async findBySeq(seq) {
|
|
41
|
+
return this.entries.find((e) => e.seq === seq) ?? null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class NoopHistory {
|
|
45
|
+
async append() { }
|
|
46
|
+
async readRecent() { return []; }
|
|
47
|
+
async search() { return []; }
|
|
48
|
+
async findBySeq() { return null; }
|
|
49
|
+
}
|
|
16
50
|
export class HistoryFile {
|
|
17
51
|
instanceId;
|
|
18
52
|
filePath;
|
|
@@ -65,16 +99,7 @@ export class HistoryFile {
|
|
|
65
99
|
async search(query) {
|
|
66
100
|
if (!query.trim())
|
|
67
101
|
return [];
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
regex = new RegExp(query, "i");
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
74
|
-
const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
75
|
-
const lookaheads = escaped.map((w) => `(?=.*${w})`).join("");
|
|
76
|
-
regex = new RegExp(lookaheads, "i");
|
|
77
|
-
}
|
|
102
|
+
const regex = compileSearchRegex(query);
|
|
78
103
|
const budgetBytes = 20 * 1024 * 1024;
|
|
79
104
|
let scanned = 0;
|
|
80
105
|
const results = [];
|
|
@@ -83,13 +108,11 @@ export class HistoryFile {
|
|
|
83
108
|
if (scanned > budgetBytes)
|
|
84
109
|
break;
|
|
85
110
|
const entry = deserializeEntry(line);
|
|
86
|
-
if (!entry
|
|
111
|
+
if (!entry)
|
|
87
112
|
continue;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
results.push({ entry, line: formatNuclearLine(entry) });
|
|
92
|
-
}
|
|
113
|
+
const m = matchEntry(entry, regex);
|
|
114
|
+
if (m)
|
|
115
|
+
results.push(m);
|
|
93
116
|
}
|
|
94
117
|
return results;
|
|
95
118
|
}
|
|
@@ -66,3 +66,10 @@ export declare function serializeEntry(entry: NuclearEntry): string;
|
|
|
66
66
|
export declare function deserializeEntry(line: string): NuclearEntry | null;
|
|
67
67
|
/** Check if a nuclear entry represents a read-only action (should be dropped). */
|
|
68
68
|
export declare function isReadOnly(entry: NuclearEntry): boolean;
|
|
69
|
+
/** Compile a search query, falling back to whitespace-split AND-of-words on invalid regex. */
|
|
70
|
+
export declare function compileSearchRegex(query: string): RegExp;
|
|
71
|
+
/** Match a writable entry against a search regex; null if filtered or no match. */
|
|
72
|
+
export declare function matchEntry(entry: NuclearEntry, re: RegExp): {
|
|
73
|
+
entry: NuclearEntry;
|
|
74
|
+
line: string;
|
|
75
|
+
} | null;
|
|
@@ -200,6 +200,25 @@ export function isReadOnly(entry) {
|
|
|
200
200
|
return false;
|
|
201
201
|
return READ_ONLY_TOOLS.has(entry.tool) || extraReadOnlyTools.has(entry.tool);
|
|
202
202
|
}
|
|
203
|
+
/** Compile a search query, falling back to whitespace-split AND-of-words on invalid regex. */
|
|
204
|
+
export function compileSearchRegex(query) {
|
|
205
|
+
try {
|
|
206
|
+
return new RegExp(query, "i");
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
210
|
+
const escaped = words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
211
|
+
const lookaheads = escaped.map((w) => `(?=.*${w})`).join("");
|
|
212
|
+
return new RegExp(lookaheads, "i");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** Match a writable entry against a search regex; null if filtered or no match. */
|
|
216
|
+
export function matchEntry(entry, re) {
|
|
217
|
+
if (isReadOnly(entry))
|
|
218
|
+
return null;
|
|
219
|
+
const text = [entry.sum, entry.body].filter(Boolean).join("\n");
|
|
220
|
+
return re.test(text) ? { entry, line: formatNuclearLine(entry) } : null;
|
|
221
|
+
}
|
|
203
222
|
// ── Internal helpers ──────────────────────────────────────────────
|
|
204
223
|
function truncate(text, maxLen) {
|
|
205
224
|
const oneLine = text.replace(/\n/g, " ").trim();
|
package/dist/core.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type { ColorPalette } from "./utils/palette.js";
|
|
|
28
28
|
export type { AgentBackend, ToolDefinition } from "./agent/types.js";
|
|
29
29
|
export { runSubagent, type SubagentOptions } from "./agent/subagent.js";
|
|
30
30
|
export { LlmClient } from "./utils/llm-client.js";
|
|
31
|
+
export { HistoryFile, InMemoryHistory, NoopHistory, type HistoryAdapter } from "./agent/history-file.js";
|
|
32
|
+
export type { NuclearEntry } from "./agent/nuclear-form.js";
|
|
31
33
|
export interface AgentShellCore {
|
|
32
34
|
bus: EventBus;
|
|
33
35
|
contextManager: ContextManager;
|
package/dist/core.js
CHANGED
|
@@ -34,6 +34,7 @@ export { EventBus } from "./event-bus.js";
|
|
|
34
34
|
export { palette, setPalette, resetPalette } from "./utils/palette.js";
|
|
35
35
|
export { runSubagent } from "./agent/subagent.js";
|
|
36
36
|
export { LlmClient } from "./utils/llm-client.js";
|
|
37
|
+
export { HistoryFile, InMemoryHistory, NoopHistory } from "./agent/history-file.js";
|
|
37
38
|
export function createCore(config) {
|
|
38
39
|
const bus = new EventBus();
|
|
39
40
|
const handlers = new HandlerRegistry();
|
|
@@ -164,6 +165,9 @@ export function createCore(config) {
|
|
|
164
165
|
contextManager,
|
|
165
166
|
instanceId,
|
|
166
167
|
llm: createLlmFacade(handlers),
|
|
168
|
+
providers: {
|
|
169
|
+
configure: (id, opts) => bus.emit("provider:configure", { id, ...opts }),
|
|
170
|
+
},
|
|
167
171
|
quit: opts.quit,
|
|
168
172
|
setPalette,
|
|
169
173
|
createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -279,7 +279,10 @@ export interface ShellEvents {
|
|
|
279
279
|
model: string;
|
|
280
280
|
provider: string;
|
|
281
281
|
}[];
|
|
282
|
-
active:
|
|
282
|
+
active: {
|
|
283
|
+
model: string;
|
|
284
|
+
provider: string;
|
|
285
|
+
} | null;
|
|
283
286
|
};
|
|
284
287
|
"config:set-thinking": {
|
|
285
288
|
level: string;
|
|
@@ -320,6 +323,10 @@ export interface ShellEvents {
|
|
|
320
323
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
321
324
|
supportsReasoningEffort?: boolean;
|
|
322
325
|
};
|
|
326
|
+
"provider:configure": {
|
|
327
|
+
id: string;
|
|
328
|
+
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
329
|
+
};
|
|
323
330
|
"agent:register-tool": {
|
|
324
331
|
tool: import("./agent/types.js").ToolDefinition;
|
|
325
332
|
extensionName?: string;
|
|
@@ -8,6 +8,9 @@ function persistedModelFor(providerName) {
|
|
|
8
8
|
return undefined;
|
|
9
9
|
return getSettings().providers?.[providerName]?.defaultModel;
|
|
10
10
|
}
|
|
11
|
+
function defaultReasoningBuilder(level) {
|
|
12
|
+
return level === "off" ? {} : { reasoning_effort: level };
|
|
13
|
+
}
|
|
11
14
|
export default function agentBackend(ctx) {
|
|
12
15
|
const { bus } = ctx;
|
|
13
16
|
const config = ctx.call("config:get-shell-config") ?? {};
|
|
@@ -18,11 +21,14 @@ export default function agentBackend(ctx) {
|
|
|
18
21
|
if (p)
|
|
19
22
|
providerRegistry.set(name, p);
|
|
20
23
|
}
|
|
24
|
+
const providerHooks = new Map();
|
|
21
25
|
const buildModes = () => {
|
|
22
26
|
const allModes = [];
|
|
23
27
|
for (const [id, p] of providerRegistry) {
|
|
24
28
|
if (!p.apiKey)
|
|
25
29
|
continue;
|
|
30
|
+
const shapeId = p.reasoningShape ?? id;
|
|
31
|
+
const buildReasoningParams = providerHooks.get(shapeId)?.reasoningParams ?? defaultReasoningBuilder;
|
|
26
32
|
for (const model of p.models) {
|
|
27
33
|
const mc = p.modelCapabilities?.get(model);
|
|
28
34
|
allModes.push({
|
|
@@ -33,6 +39,7 @@ export default function agentBackend(ctx) {
|
|
|
33
39
|
reasoning: mc?.reasoning,
|
|
34
40
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
35
41
|
echoReasoning: mc?.echoReasoning,
|
|
42
|
+
buildReasoningParams,
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
45
|
}
|
|
@@ -67,6 +74,7 @@ export default function agentBackend(ctx) {
|
|
|
67
74
|
initialModeIndex,
|
|
68
75
|
compositor: ctx.compositor,
|
|
69
76
|
instanceId: ctx.instanceId,
|
|
77
|
+
history: config.history,
|
|
70
78
|
});
|
|
71
79
|
bus.on("core:extensions-loaded", () => {
|
|
72
80
|
const settings = getSettings();
|
|
@@ -126,6 +134,12 @@ export default function agentBackend(ctx) {
|
|
|
126
134
|
},
|
|
127
135
|
});
|
|
128
136
|
});
|
|
137
|
+
bus.on("provider:configure", ({ id, reasoningParams }) => {
|
|
138
|
+
const prev = providerHooks.get(id) ?? {};
|
|
139
|
+
if (reasoningParams !== undefined)
|
|
140
|
+
prev.reasoningParams = reasoningParams;
|
|
141
|
+
providerHooks.set(id, prev);
|
|
142
|
+
});
|
|
129
143
|
bus.on("provider:register", (p) => {
|
|
130
144
|
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
131
145
|
const modelIds = [];
|
|
@@ -148,6 +162,7 @@ export default function agentBackend(ctx) {
|
|
|
148
162
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
149
163
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
150
164
|
});
|
|
165
|
+
const buildReasoningParams = providerHooks.get(p.id)?.reasoningParams ?? defaultReasoningBuilder;
|
|
151
166
|
const addModes = modelIds.map((m) => {
|
|
152
167
|
const mc = caps.get(m);
|
|
153
168
|
return {
|
|
@@ -158,6 +173,7 @@ export default function agentBackend(ctx) {
|
|
|
158
173
|
reasoning: mc?.reasoning,
|
|
159
174
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
160
175
|
echoReasoning: mc?.echoReasoning,
|
|
176
|
+
buildReasoningParams,
|
|
161
177
|
};
|
|
162
178
|
});
|
|
163
179
|
bus.emit("config:add-modes", { modes: addModes });
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in OpenAI-compatible provider
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Built-in OpenAI-compatible provider. Two activation paths:
|
|
3
|
+
* - OPENAI_API_KEY only → cloud OpenAI, ships a curated catalog.
|
|
4
|
+
* - OPENAI_BASE_URL (any key) → local/3rd-party server (Ollama, LM Studio,
|
|
5
|
+
* vLLM, llama.cpp); the catalog is fetched
|
|
6
|
+
* from the server's /models endpoint.
|
|
5
7
|
*/
|
|
6
8
|
import type { ExtensionContext } from "../types.js";
|
|
7
9
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
const
|
|
2
|
-
"gpt-5",
|
|
3
|
-
"gpt-4.1",
|
|
4
|
-
"gpt-4o",
|
|
5
|
-
"gpt-4o-mini",
|
|
6
|
-
"o3",
|
|
7
|
-
"o3-mini",
|
|
1
|
+
const OPENAI_CLOUD_MODELS = [
|
|
2
|
+
{ id: "gpt-5", reasoning: true },
|
|
3
|
+
{ id: "gpt-4.1", reasoning: false },
|
|
4
|
+
{ id: "gpt-4o", reasoning: false },
|
|
5
|
+
{ id: "gpt-4o-mini", reasoning: false },
|
|
6
|
+
{ id: "o3", reasoning: true },
|
|
7
|
+
{ id: "o3-mini", reasoning: true },
|
|
8
8
|
];
|
|
9
9
|
export default function activate(ctx) {
|
|
10
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
11
|
-
if (!apiKey)
|
|
12
|
-
return;
|
|
10
|
+
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
13
11
|
const baseURL = process.env.OPENAI_BASE_URL;
|
|
14
|
-
const id = baseURL ? "openai-compatible" : "openai";
|
|
15
12
|
if (!baseURL) {
|
|
13
|
+
if (!apiKey)
|
|
14
|
+
return;
|
|
16
15
|
ctx.bus.emit("provider:register", {
|
|
17
|
-
id,
|
|
16
|
+
id: "openai",
|
|
18
17
|
apiKey,
|
|
19
|
-
defaultModel:
|
|
20
|
-
models:
|
|
18
|
+
defaultModel: OPENAI_CLOUD_MODELS[0].id,
|
|
19
|
+
models: OPENAI_CLOUD_MODELS,
|
|
21
20
|
});
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const id = "openai-compatible";
|
|
24
|
+
// Local servers (Ollama, llama.cpp) often need no key; the SDK still
|
|
25
|
+
// requires a non-empty string for construction.
|
|
26
|
+
const sdkKey = apiKey || "no-key";
|
|
27
|
+
ctx.bus.emit("provider:register", { id, apiKey: sdkKey, baseURL, models: [] });
|
|
26
28
|
fetchModels(baseURL, apiKey).then((models) => {
|
|
27
29
|
if (models.length === 0)
|
|
28
30
|
return;
|
|
29
31
|
ctx.bus.emit("provider:register", {
|
|
30
32
|
id,
|
|
31
|
-
apiKey,
|
|
33
|
+
apiKey: sdkKey,
|
|
32
34
|
baseURL,
|
|
33
35
|
defaultModel: models[0],
|
|
34
36
|
models,
|
|
@@ -36,9 +38,10 @@ export default function activate(ctx) {
|
|
|
36
38
|
}).catch(() => { });
|
|
37
39
|
}
|
|
38
40
|
async function fetchModels(baseURL, apiKey) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const headers = {};
|
|
42
|
+
if (apiKey)
|
|
43
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
44
|
+
const res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, { headers });
|
|
42
45
|
if (!res.ok)
|
|
43
46
|
return [];
|
|
44
47
|
const data = await res.json();
|
|
@@ -6,10 +6,16 @@ const DEFAULT_MODELS = ["deepseek/deepseek-v4-flash"];
|
|
|
6
6
|
// providers.openrouter.echoReasoningPatterns = ["deepseek", "..."]
|
|
7
7
|
// providers.openrouter.models[*].echoReasoning = true | false
|
|
8
8
|
const BUILTIN_ECHO_REASONING_PATTERNS = [/deepseek/i];
|
|
9
|
+
function buildReasoningParams(level) {
|
|
10
|
+
return level === "off"
|
|
11
|
+
? { reasoning: { enabled: false } }
|
|
12
|
+
: { reasoning: { effort: level } };
|
|
13
|
+
}
|
|
9
14
|
export default function activate(ctx) {
|
|
10
15
|
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
11
16
|
if (!apiKey)
|
|
12
17
|
return;
|
|
18
|
+
ctx.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
|
|
13
19
|
ctx.bus.emit("provider:register", {
|
|
14
20
|
id: "openrouter",
|
|
15
21
|
apiKey,
|
|
@@ -37,11 +37,10 @@ export default function activate(ctx) {
|
|
|
37
37
|
handler: (args) => {
|
|
38
38
|
const name = args.trim();
|
|
39
39
|
if (!name) {
|
|
40
|
-
const {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: active ?? "none";
|
|
40
|
+
const { active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
41
|
+
const label = active
|
|
42
|
+
? `${active.model}${active.provider ? ` [${active.provider}]` : ""}`
|
|
43
|
+
: "none";
|
|
45
44
|
bus.emit("ui:info", { message: `Model: ${label}` });
|
|
46
45
|
}
|
|
47
46
|
else {
|
|
@@ -180,13 +179,20 @@ export default function activate(ctx) {
|
|
|
180
179
|
return payload;
|
|
181
180
|
const partial = (payload.commandArgs ?? "").toLowerCase();
|
|
182
181
|
const { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
|
|
182
|
+
const counts = new Map();
|
|
183
|
+
for (const m of models)
|
|
184
|
+
counts.set(m.model, (counts.get(m.model) ?? 0) + 1);
|
|
183
185
|
const items = models
|
|
184
186
|
.filter((m) => m.model.toLowerCase().includes(partial))
|
|
185
187
|
.slice(0, 15)
|
|
186
|
-
.map((m) =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
.map((m) => {
|
|
189
|
+
const ambiguous = (counts.get(m.model) ?? 0) > 1 && m.provider;
|
|
190
|
+
const qualified = ambiguous ? `${m.model}@${m.provider}` : m.model;
|
|
191
|
+
return {
|
|
192
|
+
name: `/model ${qualified}`,
|
|
193
|
+
description: `${m.provider ? `[${m.provider}]` : ""}${active && m.model === active.model && m.provider === active.provider ? " (active)" : ""}`,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
190
196
|
if (items.length === 0)
|
|
191
197
|
return payload;
|
|
192
198
|
return { ...payload, items: [...payload.items, ...items] };
|
package/dist/init.js
CHANGED
package/dist/settings.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export interface ProviderConfig {
|
|
|
27
27
|
/** Case-insensitive regex sources matched against model id; matches default
|
|
28
28
|
* to echoReasoning=true. Per-model echoReasoning still wins. */
|
|
29
29
|
echoReasoningPatterns?: string[];
|
|
30
|
+
/** Borrow another registered provider's reasoning request shape by id
|
|
31
|
+
* (e.g. "openrouter"). Defaults to OpenAI-compat. */
|
|
32
|
+
reasoningShape?: string;
|
|
30
33
|
}
|
|
31
34
|
export interface Settings {
|
|
32
35
|
/** Extensions to load (npm packages or file paths). */
|
|
@@ -145,6 +148,8 @@ export interface ResolvedProvider {
|
|
|
145
148
|
contextWindow?: number;
|
|
146
149
|
echoReasoning?: boolean;
|
|
147
150
|
}>;
|
|
151
|
+
/** Borrow another registered provider's reasoning request shape by id. */
|
|
152
|
+
reasoningShape?: string;
|
|
148
153
|
}
|
|
149
154
|
/**
|
|
150
155
|
* Resolve a provider config by name from settings.
|
package/dist/settings.js
CHANGED
|
@@ -161,6 +161,7 @@ export function resolveProvider(name) {
|
|
|
161
161
|
models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
|
|
162
162
|
contextWindow: provider.contextWindow,
|
|
163
163
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
164
|
+
reasoningShape: provider.reasoningShape,
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
/** Get all configured provider names. */
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils
|
|
|
5
5
|
import type { ToolDefinition } from "./agent/types.js";
|
|
6
6
|
import type { TerminalBuffer } from "./utils/terminal-buffer.js";
|
|
7
7
|
import type { Compositor } from "./utils/compositor.js";
|
|
8
|
+
import type { HistoryAdapter } from "./agent/history-file.js";
|
|
8
9
|
export type { ContentBlock } from "./event-bus.js";
|
|
9
10
|
export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
10
11
|
export type { RenderSurface } from "./utils/compositor.js";
|
|
@@ -51,6 +52,7 @@ export interface AgentMode {
|
|
|
51
52
|
/** Echo reasoning_content back on assistant turns. Required by DeepSeek;
|
|
52
53
|
* default off (leaky shims may forward it to the model as OOD input). */
|
|
53
54
|
echoReasoning?: boolean;
|
|
55
|
+
buildReasoningParams?: (level: string) => Record<string, unknown>;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Backend-agnostic LLM interface exposed via `ctx.llm`. Backends fulfill it
|
|
@@ -87,6 +89,8 @@ export interface AgentShellConfig {
|
|
|
87
89
|
baseURL?: string;
|
|
88
90
|
/** Named provider to use from settings.json. */
|
|
89
91
|
provider?: string;
|
|
92
|
+
/** Conversation history backend. Defaults to the on-disk HistoryFile. */
|
|
93
|
+
history?: HistoryAdapter;
|
|
90
94
|
}
|
|
91
95
|
/**
|
|
92
96
|
* Context passed to user/third-party extensions.
|
|
@@ -130,6 +134,11 @@ export interface ExtensionContext {
|
|
|
130
134
|
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
131
135
|
/** Remove a registered skill by name. */
|
|
132
136
|
removeSkill: (name: string) => void;
|
|
137
|
+
providers: {
|
|
138
|
+
configure: (id: string, opts: {
|
|
139
|
+
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
140
|
+
}) => void;
|
|
141
|
+
};
|
|
133
142
|
llm: LlmInterface;
|
|
134
143
|
/** Register a named handler. */
|
|
135
144
|
define: (name: string, fn: (...args: any[]) => any) => void;
|