agent-sh 0.12.9 → 0.12.10
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 +25 -14
- 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 +4 -0
- 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/settings.d.ts +5 -0
- package/dist/settings.js +1 -0
- package/dist/types.d.ts +9 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -461,16 +471,17 @@ export class AgentLoop {
|
|
|
461
471
|
cancel() {
|
|
462
472
|
this.abortController?.abort();
|
|
463
473
|
}
|
|
464
|
-
|
|
465
|
-
shouldSendReasoningEffort() {
|
|
466
|
-
if (this.thinkingLevel === "off")
|
|
467
|
-
return false;
|
|
474
|
+
reasoningParams() {
|
|
468
475
|
const mode = this.currentMode;
|
|
469
476
|
if (mode.reasoning === false)
|
|
470
|
-
return
|
|
477
|
+
return {};
|
|
471
478
|
if (mode.supportsReasoningEffort === false)
|
|
472
|
-
return
|
|
473
|
-
|
|
479
|
+
return {};
|
|
480
|
+
if (mode.buildReasoningParams)
|
|
481
|
+
return mode.buildReasoningParams(this.thinkingLevel);
|
|
482
|
+
if (this.thinkingLevel === "off")
|
|
483
|
+
return {};
|
|
484
|
+
return { reasoning_effort: this.thinkingLevel };
|
|
474
485
|
}
|
|
475
486
|
get currentMode() {
|
|
476
487
|
return this.modes[this.currentModeIndex];
|
|
@@ -822,11 +833,11 @@ export class AgentLoop {
|
|
|
822
833
|
return;
|
|
823
834
|
const writable = entries.filter((e) => !isReadOnly(e));
|
|
824
835
|
if (writable.length > 0)
|
|
825
|
-
this.
|
|
836
|
+
this.history.append(writable).catch(() => { });
|
|
826
837
|
});
|
|
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.
|
|
838
|
+
h.define("history:search", async (query) => this.history.search(query));
|
|
839
|
+
h.define("history:find-by-seq", async (seq) => this.history.findBySeq(seq));
|
|
840
|
+
h.define("history:read-recent", async (max) => this.history.readRecent(max));
|
|
830
841
|
// Prior-session preamble renderer. Default: flat chronological list.
|
|
831
842
|
h.define("conversation:format-prior-history", (entries) => {
|
|
832
843
|
if (!entries || entries.length === 0)
|
|
@@ -1525,7 +1536,7 @@ export class AgentLoop {
|
|
|
1525
1536
|
messages,
|
|
1526
1537
|
tools: apiTools,
|
|
1527
1538
|
model: this.currentModel,
|
|
1528
|
-
|
|
1539
|
+
...this.reasoningParams(),
|
|
1529
1540
|
};
|
|
1530
1541
|
this.bus.emit("llm:request", requestParams);
|
|
1531
1542
|
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
|
@@ -320,6 +320,10 @@ export interface ShellEvents {
|
|
|
320
320
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
321
321
|
supportsReasoningEffort?: boolean;
|
|
322
322
|
};
|
|
323
|
+
"provider:configure": {
|
|
324
|
+
id: string;
|
|
325
|
+
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
326
|
+
};
|
|
323
327
|
"agent:register-tool": {
|
|
324
328
|
tool: import("./agent/types.js").ToolDefinition;
|
|
325
329
|
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,
|
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;
|