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.
@@ -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 historyFile;
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
- /** Check if reasoning_effort should be sent for the current model/provider. */
104
- private shouldSendReasoningEffort;
105
+ private reasoningParams;
105
106
  private get currentMode();
106
107
  private get currentModel();
107
108
  /**
@@ -50,7 +50,7 @@ function summarizeDescription(desc) {
50
50
  export class AgentLoop {
51
51
  abortController = null;
52
52
  toolRegistry = new ToolRegistry();
53
- historyFile;
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.historyFile = new HistoryFile({ instanceId: this.instanceId, filePath });
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 idx = this.modes.findIndex((m) => m.model === target);
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 active = this.modes[this.currentModeIndex]?.model ?? null;
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
- /** Check if reasoning_effort should be sent for the current model/provider. */
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 false;
481
+ return {};
471
482
  if (mode.supportsReasoningEffort === false)
472
- return false;
473
- return true;
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.historyFile.append(writable).catch(() => { });
840
+ this.history.append(writable).catch(() => { });
826
841
  });
827
- h.define("history:search", async (query) => this.historyFile.search(query));
828
- h.define("history:find-by-seq", async (seq) => this.historyFile.findBySeq(seq));
829
- h.define("history:read-recent", async (max) => this.historyFile.readRecent(max));
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
- reasoning_effort: this.shouldSendReasoningEffort() ? this.thinkingLevel : undefined,
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 declare class HistoryFile {
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, formatNuclearLine, isReadOnly, } from "./nuclear-form.js";
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
- let regex;
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 || isReadOnly(entry))
111
+ if (!entry)
87
112
  continue;
88
- // Body can hold ~4000 chars the summary truncates — search both.
89
- const searchText = [entry.sum, entry.body].filter(Boolean).join("\n");
90
- if (regex.test(searchText)) {
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),
@@ -279,7 +279,10 @@ export interface ShellEvents {
279
279
  model: string;
280
280
  provider: string;
281
281
  }[];
282
- active: string | null;
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 auto-activates when OPENAI_API_KEY
3
- * is set. OPENAI_BASE_URL redirects to local servers (Ollama, LM Studio,
4
- * vLLM, llama.cpp) which then get their catalog via /models.
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 DEFAULT_MODELS = [
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: DEFAULT_MODELS[0],
20
- models: DEFAULT_MODELS,
18
+ defaultModel: OPENAI_CLOUD_MODELS[0].id,
19
+ models: OPENAI_CLOUD_MODELS,
21
20
  });
22
21
  return;
23
22
  }
24
- // Register empty immediately so the provider resolves; refill from /models.
25
- ctx.bus.emit("provider:register", { id, apiKey, baseURL, models: [] });
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 res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, {
40
- headers: { Authorization: `Bearer ${apiKey}` },
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 { models, active } = bus.emitPipe("config:get-models", { models: [], active: null });
41
- const current = models.find((m) => m.model === active);
42
- const label = current
43
- ? `${current.model}${current.provider ? ` [${current.provider}]` : ""}`
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
- name: `/model ${m.model}`,
188
- description: `${m.provider ? `[${m.provider}]` : ""}${m.model === active ? " (active)" : ""}`,
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
@@ -37,9 +37,7 @@ const EXAMPLE_SETTINGS = {
37
37
  defaultModel: "llama3.3",
38
38
  },
39
39
  },
40
- extensions: [
41
- "./examples/extensions/openrouter.ts",
42
- ],
40
+ extensions: [],
43
41
  disabledBuiltins: [],
44
42
  disabledExtensions: [],
45
43
  };
@@ -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;