agent-sh 0.15.0 → 0.15.2

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.
Files changed (124) hide show
  1. package/dist/agent/agent-loop.js +11 -8
  2. package/dist/agent/events.d.ts +4 -0
  3. package/docs/README.md +14 -0
  4. package/docs/agent.md +398 -0
  5. package/docs/architecture.md +196 -0
  6. package/docs/context-management.md +200 -0
  7. package/docs/extensions.md +951 -0
  8. package/docs/library.md +84 -0
  9. package/docs/troubleshooting.md +65 -0
  10. package/docs/tui-composition.md +294 -0
  11. package/docs/usage.md +306 -0
  12. package/examples/extensions/ash-scheme/package.json +1 -1
  13. package/examples/extensions/ashi/EXTENDING.md +2 -2
  14. package/examples/extensions/ashi/README.md +2 -2
  15. package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
  16. package/examples/extensions/ashi/package.json +5 -3
  17. package/examples/extensions/ashi/src/chat/tool-group.ts +3 -2
  18. package/examples/extensions/ashi/src/cli.ts +9 -8
  19. package/examples/extensions/ashi/src/dialogs.ts +16 -1
  20. package/examples/extensions/ashi/src/events.ts +1 -0
  21. package/examples/extensions/ashi/src/frontend.ts +26 -6
  22. package/examples/extensions/ashi/src/renderer.ts +24 -4
  23. package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +4 -3
  24. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
  25. package/examples/extensions/ashi/src/ui.ts +11 -0
  26. package/examples/extensions/ashi-ink/package.json +2 -2
  27. package/examples/extensions/claude-code-bridge/package.json +1 -1
  28. package/examples/extensions/opencode-bridge/package.json +1 -1
  29. package/package.json +3 -1
  30. package/src/agent/agent-loop.ts +1566 -0
  31. package/src/agent/entry-format.ts +19 -0
  32. package/src/agent/events.ts +153 -0
  33. package/src/agent/extensions/rolling-history/constants.ts +1 -0
  34. package/src/agent/extensions/rolling-history/index.ts +202 -0
  35. package/src/agent/extensions/rolling-history/recall.ts +131 -0
  36. package/src/agent/extensions/rolling-history/strategy.ts +404 -0
  37. package/src/agent/host-types.ts +192 -0
  38. package/src/agent/index.ts +591 -0
  39. package/src/agent/live-view.ts +279 -0
  40. package/src/agent/llm-client.ts +111 -0
  41. package/src/agent/llm-facade.ts +43 -0
  42. package/src/agent/normalize-args.ts +61 -0
  43. package/src/agent/nuclear-form.ts +382 -0
  44. package/src/agent/providers/deepseek.ts +39 -0
  45. package/src/agent/providers/ollama.ts +92 -0
  46. package/src/agent/providers/openai-compatible.ts +36 -0
  47. package/src/agent/providers/openai.ts +52 -0
  48. package/src/agent/providers/opencode.ts +142 -0
  49. package/src/agent/providers/openrouter.ts +105 -0
  50. package/src/agent/providers/zai-coding-plan.ts +33 -0
  51. package/src/agent/session-store.ts +336 -0
  52. package/src/agent/skills.ts +228 -0
  53. package/src/agent/store.ts +310 -0
  54. package/src/agent/subagent.ts +305 -0
  55. package/src/agent/system-prompt.ts +151 -0
  56. package/src/agent/token-budget.ts +12 -0
  57. package/src/agent/tool-protocol.ts +722 -0
  58. package/src/agent/tool-registry.ts +66 -0
  59. package/src/agent/tools/bash.ts +95 -0
  60. package/src/agent/tools/edit-file.ts +154 -0
  61. package/src/agent/tools/expand-home.ts +7 -0
  62. package/src/agent/tools/glob.ts +108 -0
  63. package/src/agent/tools/grep.ts +228 -0
  64. package/src/agent/tools/list-skills.ts +37 -0
  65. package/src/agent/tools/ls.ts +81 -0
  66. package/src/agent/tools/pwsh.ts +140 -0
  67. package/src/agent/tools/read-file.ts +164 -0
  68. package/src/agent/tools/write-file.ts +72 -0
  69. package/src/agent/types.ts +149 -0
  70. package/src/cli/args.ts +91 -0
  71. package/src/cli/auth/cli.ts +244 -0
  72. package/src/cli/auth/discover.ts +52 -0
  73. package/src/cli/auth/keys.ts +143 -0
  74. package/src/cli/index.ts +295 -0
  75. package/src/cli/init.ts +74 -0
  76. package/src/cli/install.ts +439 -0
  77. package/src/cli/shell-env.ts +68 -0
  78. package/src/cli/subcommands.ts +24 -0
  79. package/src/core/event-bus.ts +252 -0
  80. package/src/core/extension-loader.ts +347 -0
  81. package/src/core/index.ts +152 -0
  82. package/src/core/settings.ts +398 -0
  83. package/src/core/types.ts +61 -0
  84. package/src/extensions/file-autocomplete.ts +71 -0
  85. package/src/extensions/index.ts +38 -0
  86. package/src/extensions/slash-commands/events.ts +14 -0
  87. package/src/extensions/slash-commands/index.ts +269 -0
  88. package/src/shell/events.ts +73 -0
  89. package/src/shell/host-types.ts +150 -0
  90. package/src/shell/index.ts +159 -0
  91. package/src/shell/input-handler.ts +505 -0
  92. package/src/shell/output-parser.ts +156 -0
  93. package/src/shell/shell-context.ts +193 -0
  94. package/src/shell/shell.ts +414 -0
  95. package/src/shell/strategies/bash.ts +83 -0
  96. package/src/shell/strategies/fish.ts +77 -0
  97. package/src/shell/strategies/index.ts +24 -0
  98. package/src/shell/strategies/types.ts +64 -0
  99. package/src/shell/strategies/zsh.ts +92 -0
  100. package/src/shell/terminal.ts +124 -0
  101. package/src/shell/tui-input-view.ts +222 -0
  102. package/src/shell/tui-renderer.ts +1126 -0
  103. package/src/utils/ansi.ts +140 -0
  104. package/src/utils/box-frame.ts +138 -0
  105. package/src/utils/compositor.ts +157 -0
  106. package/src/utils/diff-renderer.ts +829 -0
  107. package/src/utils/diff.ts +244 -0
  108. package/src/utils/executor.ts +305 -0
  109. package/src/utils/file-watcher.ts +110 -0
  110. package/src/utils/floating-panel.ts +1160 -0
  111. package/src/utils/handler-registry.ts +110 -0
  112. package/src/utils/line-editor.ts +636 -0
  113. package/src/utils/markdown.ts +437 -0
  114. package/src/utils/message-utils.ts +113 -0
  115. package/src/utils/package-version.ts +12 -0
  116. package/src/utils/palette.ts +64 -0
  117. package/src/utils/ref-counter.ts +9 -0
  118. package/src/utils/ripgrep-path.ts +17 -0
  119. package/src/utils/shell-output-spill.ts +76 -0
  120. package/src/utils/stream-transform.ts +292 -0
  121. package/src/utils/terminal-buffer.ts +213 -0
  122. package/src/utils/tool-display.ts +315 -0
  123. package/src/utils/tool-interactive.ts +71 -0
  124. package/src/utils/tty.ts +14 -0
@@ -0,0 +1,591 @@
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";
6
+ import type { ExtensionContext } from "../shell/host-types.js";
7
+ import type { AgentContext, Model, ModelEndpoint, AgentSurface, ProviderRegistration } from "../agent/host-types.js";
8
+ import type { AppConfig } from "../shell/host-types.js";
9
+ import { AgentLoop } from "./agent-loop.js";
10
+ import { LlmClient } from "./llm-client.js";
11
+ import { createLlmFacade } from "./llm-facade.js";
12
+ import type { ToolDefinition, ToolSchemaView } from "./types.js";
13
+ import { registerReadOnlyTool, unregisterReadOnlyTool } from "./nuclear-form.js";
14
+ import {
15
+ resolveProvider,
16
+ getProviderNames,
17
+ getSettings,
18
+ setSessionOverlay,
19
+ clearSessionOverlay,
20
+ getSettingSource,
21
+ type ResolvedProvider,
22
+ } from "../core/settings.js";
23
+ import { resolveApiKey } from "../cli/auth/keys.js";
24
+ import { discoverSkills } from "./skills.js";
25
+ import activateOpenrouter from "./providers/openrouter.js";
26
+ import activateOpenai from "./providers/openai.js";
27
+ import activateOpenaiCompatible from "./providers/openai-compatible.js";
28
+ import activateDeepseek from "./providers/deepseek.js";
29
+ import activateOllama from "./providers/ollama.js";
30
+ import activateZaiCodingPlan from "./providers/zai-coding-plan.js";
31
+ import activateOpencode from "./providers/opencode.js";
32
+ import { findBash } from "../utils/executor.js";
33
+ import { createBashTool } from "./tools/bash.js";
34
+ import { createPwshTool } from "./tools/pwsh.js";
35
+ import { createReadFileTool, type FileReadCache } from "./tools/read-file.js";
36
+ import { createWriteFileTool } from "./tools/write-file.js";
37
+ import { createEditFileTool } from "./tools/edit-file.js";
38
+ import { createGrepTool } from "./tools/grep.js";
39
+ import { createGlobTool } from "./tools/glob.js";
40
+ import { createLsTool } from "./tools/ls.js";
41
+ import { createListSkillsTool } from "./tools/list-skills.js";
42
+
43
+ function persistedModelFor(providerName: string | undefined): string | undefined {
44
+ if (!providerName) return undefined;
45
+ return getSettings().providers?.[providerName]?.defaultModel;
46
+ }
47
+
48
+ /** The OpenAI SDK silently defaults an empty baseURL to api.openai.com, so a
49
+ * provider with a key but no endpoint would misroute its key there. `openai`
50
+ * is exempt: that default is its endpoint. */
51
+ function usableProvider(p: ResolvedProvider | null | undefined): boolean {
52
+ return !!p?.apiKey && (!!p.baseURL || p.id === "openai");
53
+ }
54
+
55
+ type ModelCap = { reasoning?: boolean; contextWindow?: number; maxTokens?: number; echoReasoning?: boolean; modalities?: ("text" | "image")[] };
56
+
57
+ function defaultReasoningBuilder(level: string): Record<string, unknown> {
58
+ if (level === "off") return {};
59
+ return { reasoning_effort: level === "xhigh" ? "high" : level };
60
+ }
61
+
62
+ function defaultCacheTokens(usage: Record<string, unknown>): number | undefined {
63
+ const details = usage.prompt_tokens_details as { cached_tokens?: number } | undefined;
64
+ return typeof details?.cached_tokens === "number" ? details.cached_tokens : undefined;
65
+ }
66
+
67
+ function mergeCaps(
68
+ settingsCaps: Map<string, ModelCap> | undefined,
69
+ payloadCaps: Map<string, ModelCap>,
70
+ modelIds: string[],
71
+ ): Map<string, ModelCap> | undefined {
72
+ if (!settingsCaps) return payloadCaps.size > 0 ? payloadCaps : undefined;
73
+ const out = new Map<string, ModelCap>();
74
+ for (const id of modelIds) {
75
+ const s = settingsCaps.get(id);
76
+ const p = payloadCaps.get(id);
77
+ if (!s && !p) continue;
78
+ out.set(id, {
79
+ reasoning: s?.reasoning ?? p?.reasoning,
80
+ contextWindow: s?.contextWindow ?? p?.contextWindow,
81
+ maxTokens: s?.maxTokens ?? p?.maxTokens,
82
+ echoReasoning: s?.echoReasoning ?? p?.echoReasoning,
83
+ });
84
+ }
85
+ return out.size > 0 ? out : undefined;
86
+ }
87
+
88
+ function splitRegistration(p: ProviderRegistration): { ids: string[]; caps: Map<string, ModelCap> } {
89
+ const raw = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
90
+ const ids: string[] = [];
91
+ const caps = new Map<string, ModelCap>();
92
+ for (const m of raw) {
93
+ if (typeof m === "string") {
94
+ ids.push(m);
95
+ } else {
96
+ ids.push(m.id);
97
+ caps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning, modalities: m.modalities });
98
+ }
99
+ }
100
+ return { ids, caps };
101
+ }
102
+
103
+ export default function agentBackend(ctx: ExtensionContext): void {
104
+ const { bus } = ctx;
105
+ const config: AppConfig = ctx.call("config:get-app-config") ?? {};
106
+
107
+ type ToolContributor = (acc: { tools: ToolDefinition[] }) => { tools: ToolDefinition[] };
108
+ type InstructionContributor = (acc: { instructions: Array<{ name: string; text: string }> }) => { instructions: Array<{ name: string; text: string }> };
109
+ type SkillContributor = (acc: { skills: Array<{ name: string; description: string; filePath: string }> }) => { skills: Array<{ name: string; description: string; filePath: string }> };
110
+ type ProviderContributor = (acc: { providers: ProviderRegistration[] }) => { providers: ProviderRegistration[] };
111
+
112
+ const toolContribs = new Map<string, ToolContributor>();
113
+ const instructionContribs = new Map<string, InstructionContributor>();
114
+ const skillContribs = new Map<string, SkillContributor>();
115
+ const providerContribs = new Map<string, ProviderContributor>();
116
+
117
+ // Settings overlay — fields here win over contributing extensions' payloads.
118
+ const settingsProviders = new Map<string, ResolvedProvider>();
119
+ for (const name of getProviderNames()) {
120
+ const p = resolveProvider(name);
121
+ if (p) settingsProviders.set(name, p);
122
+ }
123
+
124
+ const providerHooks = new Map<string, {
125
+ reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
126
+ cacheTokens?: (usage: Record<string, unknown>) => number | undefined;
127
+ }>();
128
+
129
+ // Bakes model id so ModelEndpoint.buildReasoningParams keeps its (level) signature.
130
+ const bindReasoning = (shapeId: string, model: string) => {
131
+ const hook = providerHooks.get(shapeId)?.reasoningParams;
132
+ return hook ? (level: string) => hook(level, model) : defaultReasoningBuilder;
133
+ };
134
+
135
+ const bindCacheTokens = (shapeId: string) =>
136
+ providerHooks.get(shapeId)?.cacheTokens ?? defaultCacheTokens;
137
+
138
+ const agentSurface: AgentSurface = {
139
+ llm: createLlmFacade({ list: ctx.list, call: ctx.call }),
140
+ providers: {
141
+ register: (reg) => {
142
+ const existing = providerContribs.get(reg.id);
143
+ if (existing) bus.offPipe("agent:providers", existing);
144
+ const contrib: ProviderContributor = (acc) => {
145
+ acc.providers.push(reg);
146
+ return acc;
147
+ };
148
+ providerContribs.set(reg.id, contrib);
149
+ bus.onPipe("agent:providers", contrib);
150
+ bus.emit("agent:providers:changed", {});
151
+ return () => agentSurface.providers.unregister(reg.id);
152
+ },
153
+ unregister: (id) => {
154
+ const contrib = providerContribs.get(id);
155
+ if (!contrib) return;
156
+ bus.offPipe("agent:providers", contrib);
157
+ providerContribs.delete(id);
158
+ bus.emit("agent:providers:changed", {});
159
+ },
160
+ configure: (id, configureOpts) => bus.emit("provider:configure", { id, ...configureOpts }),
161
+ },
162
+ registerTool: (tool) => {
163
+ if (toolContribs.has(tool.name)) {
164
+ throw new Error(`Tool "${tool.name}" already registered. Use ctx.agent.adviseTool() to wrap it.`);
165
+ }
166
+ ctx.define(`tool:${tool.name}`, tool.execute.bind(tool));
167
+ ctx.define(`tool:${tool.name}:schema`, (): ToolSchemaView => ({
168
+ description: tool.description,
169
+ parameters: tool.input_schema,
170
+ }));
171
+ if (tool.readOnly) registerReadOnlyTool(tool.name);
172
+ else unregisterReadOnlyTool(tool.name);
173
+ const contrib: ToolContributor = (acc) => {
174
+ // Pull through schema so adviseToolSchema reflects.
175
+ const view = ctx.call(`tool:${tool.name}:schema`) as ToolSchemaView;
176
+ acc.tools.push({ ...tool, description: view.description, input_schema: view.parameters });
177
+ return acc;
178
+ };
179
+ toolContribs.set(tool.name, contrib);
180
+ bus.onPipe("agent:tools", contrib);
181
+ },
182
+ unregisterTool: (name) => {
183
+ const contrib = toolContribs.get(name);
184
+ if (!contrib) return;
185
+ bus.offPipe("agent:tools", contrib);
186
+ toolContribs.delete(name);
187
+ unregisterReadOnlyTool(name);
188
+ // Handlers retained so external advisors survive a reload.
189
+ },
190
+ adviseTool: (name, advisor) => ctx.advise(`tool:${name}`, advisor as Parameters<typeof ctx.advise>[1]),
191
+ adviseToolSchema: (name, advisor) => ctx.advise(`tool:${name}:schema`, advisor as Parameters<typeof ctx.advise>[1]),
192
+ getTools: () => bus.emitPipe("agent:tools", { tools: [] }).tools,
193
+ registerInstruction: (name, text) => {
194
+ const existing = instructionContribs.get(name);
195
+ if (existing) bus.offPipe("agent:instructions", existing);
196
+ ctx.define(`instruction:${name}`, () => text);
197
+ const contrib: InstructionContributor = (acc) => {
198
+ const current = ctx.call(`instruction:${name}`) as string;
199
+ acc.instructions.push({ name, text: current });
200
+ return acc;
201
+ };
202
+ instructionContribs.set(name, contrib);
203
+ bus.onPipe("agent:instructions", contrib);
204
+ },
205
+ removeInstruction: (name) => {
206
+ const contrib = instructionContribs.get(name);
207
+ if (!contrib) return;
208
+ bus.offPipe("agent:instructions", contrib);
209
+ instructionContribs.delete(name);
210
+ },
211
+ adviseInstruction: (name, advisor) => ctx.advise(`instruction:${name}`, advisor as Parameters<typeof ctx.advise>[1]),
212
+ registerSkill: (name, description, filePath) => {
213
+ const existing = skillContribs.get(name);
214
+ if (existing) bus.offPipe("agent:skills", existing);
215
+ ctx.define(`skill:${name}:view`, () => ({ description, filePath }));
216
+ const contrib: SkillContributor = (acc) => {
217
+ const view = ctx.call(`skill:${name}:view`) as { description: string; filePath: string };
218
+ acc.skills.push({ name, description: view.description, filePath: view.filePath });
219
+ return acc;
220
+ };
221
+ skillContribs.set(name, contrib);
222
+ bus.onPipe("agent:skills", contrib);
223
+ },
224
+ removeSkill: (name) => {
225
+ const contrib = skillContribs.get(name);
226
+ if (!contrib) return;
227
+ bus.offPipe("agent:skills", contrib);
228
+ skillContribs.delete(name);
229
+ },
230
+ adviseSkill: (name, advisor) => ctx.advise(`skill:${name}:view`, advisor as Parameters<typeof ctx.advise>[1]),
231
+ registerContextProducer: (_name, producer, producerOpts) => {
232
+ const handlerName = producerOpts?.mode === "per-query"
233
+ ? "query-context:build"
234
+ : "dynamic-context:build";
235
+ return ctx.advise(handlerName, (next) => {
236
+ const base = next() as string;
237
+ const part = producer();
238
+ if (!part) return base;
239
+ const trimmed = part.trim();
240
+ if (!trimmed) return base;
241
+ return base ? `${base}\n\n${trimmed}` : trimmed;
242
+ });
243
+ },
244
+ };
245
+ (ctx as { agent?: AgentSurface }).agent = agentSurface;
246
+
247
+ ctx.define("provider:resolve-api-key", (id: string) => resolveApiKey(id));
248
+
249
+ // Core tools register at activate — before extensions load — so
250
+ // extensions that look them up at activate time (e.g. scheme.ts) find them.
251
+ const fileReadCache: FileReadCache = new Map();
252
+ ctx.define("agent:file-read-cache", () => fileReadCache);
253
+ const getCwd = () => ctx.call("cwd") as string;
254
+ const getEnv = () => {
255
+ const env: Record<string, string> = {};
256
+ for (const [k, v] of Object.entries(process.env)) {
257
+ if (v !== undefined) env[k] = v;
258
+ }
259
+ return env;
260
+ };
261
+ if (findBash() !== null) {
262
+ agentSurface.registerTool(createBashTool({ getCwd, getEnv, bus }));
263
+ }
264
+ if (process.platform === "win32") {
265
+ agentSurface.registerTool(createPwshTool({ getCwd, getEnv, bus }));
266
+ }
267
+ agentSurface.registerTool(createReadFileTool(getCwd, fileReadCache));
268
+ agentSurface.registerTool(createWriteFileTool(getCwd));
269
+ agentSurface.registerTool(createEditFileTool(getCwd));
270
+ agentSurface.registerTool(createGrepTool(getCwd));
271
+ agentSurface.registerTool(createGlobTool(getCwd));
272
+ agentSurface.registerTool(createLsTool(getCwd));
273
+ agentSurface.registerTool(createListSkillsTool(getCwd));
274
+
275
+ let resolvedProviders = new Map<string, ResolvedProvider>();
276
+
277
+ const resolveWithSettings = (id: string, p: ProviderRegistration | null): ResolvedProvider => {
278
+ const s = settingsProviders.get(id);
279
+ const { ids: payloadIds, caps: payloadCaps } = p ? splitRegistration(p) : { ids: [], caps: new Map<string, ModelCap>() };
280
+ const fallbackIds = s?.models ?? (s?.defaultModel ? [s.defaultModel] : []);
281
+ const modelIds = s?.modelsExplicit && s.models.length > 0
282
+ ? s.models
283
+ : payloadIds.length > 0 ? payloadIds : fallbackIds;
284
+ return {
285
+ id,
286
+ apiKey: s?.apiKey ?? p?.apiKey,
287
+ baseURL: s?.baseURL ?? p?.baseURL,
288
+ defaultModel: s?.defaultModel ?? p?.defaultModel ?? modelIds[0],
289
+ models: modelIds,
290
+ modelsExplicit: s?.modelsExplicit ?? false,
291
+ contextWindow: s?.contextWindow,
292
+ supportsReasoningEffort: s?.supportsReasoningEffort ?? p?.supportsReasoningEffort,
293
+ modelCapabilities: mergeCaps(s?.modelCapabilities, payloadCaps, modelIds),
294
+ reasoningShape: s?.reasoningShape,
295
+ };
296
+ };
297
+
298
+ const computeResolvedProviders = (): Map<string, ResolvedProvider> => {
299
+ const out = new Map<string, ResolvedProvider>();
300
+ // Last contribution per id wins (openrouter's catalog-refresh replaces
301
+ // its curated default).
302
+ const { providers } = bus.emitPipe("agent:providers", { providers: [] as ProviderRegistration[] });
303
+ const byId = new Map<string, ProviderRegistration>();
304
+ for (const p of providers) byId.set(p.id, p);
305
+ for (const [id, p] of byId) out.set(id, resolveWithSettings(id, p));
306
+ for (const [id] of settingsProviders) {
307
+ if (out.has(id)) continue;
308
+ out.set(id, resolveWithSettings(id, null));
309
+ }
310
+ return out;
311
+ };
312
+
313
+ const buildModels = (): Model[] => {
314
+ const out: Model[] = [];
315
+ for (const [id, p] of resolvedProviders) {
316
+ if (!p.apiKey) continue;
317
+ if (!usableProvider(p)) continue;
318
+ for (const model of p.models) {
319
+ const mc = p.modelCapabilities?.get(model);
320
+ out.push({
321
+ id: model,
322
+ provider: id,
323
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
324
+ maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
325
+ reasoning: mc?.reasoning,
326
+ supportsReasoningEffort: p.supportsReasoningEffort,
327
+ echoReasoning: mc?.echoReasoning,
328
+ modalities: mc?.modalities,
329
+ });
330
+ }
331
+ }
332
+ return out;
333
+ };
334
+
335
+ const resolveEndpoint = (providerId: string, modelId: string): ModelEndpoint | undefined => {
336
+ const p = resolvedProviders.get(providerId);
337
+ if (!p?.apiKey) return undefined;
338
+ const shapeId = p.reasoningShape ?? providerId;
339
+ return {
340
+ apiKey: p.apiKey,
341
+ baseURL: p.baseURL,
342
+ buildReasoningParams: bindReasoning(shapeId, modelId),
343
+ extractCachedTokens: bindCacheTokens(shapeId),
344
+ };
345
+ };
346
+
347
+ ctx.define("agent:get-models", () => buildModels());
348
+ ctx.define("agent:resolve-endpoint", ({ provider, id }: { provider: string; id: string }) => resolveEndpoint(provider, id));
349
+
350
+ // Reconfigured at core:extensions-loaded; start() gates on `resolved`.
351
+ const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
352
+ ctx.define("llm:get-client", () => llmClient);
353
+ ctx.define("llm:invoke", (messages: { role: string; content: string }[], opts?: { maxTokens?: number; model?: string; reasoningEffort?: string }) => {
354
+ const effort = opts?.reasoningEffort;
355
+ const clampedEffort = effort === "xhigh" ? "high" : effort;
356
+ return llmClient.complete({
357
+ messages: messages as Parameters<typeof llmClient.complete>[0]["messages"],
358
+ max_tokens: opts?.maxTokens,
359
+ model: opts?.model,
360
+ ...(clampedEffort && clampedEffort !== "off" ? { reasoning_effort: clampedEffort } : {}),
361
+ });
362
+ });
363
+
364
+ let resolved = false;
365
+ // Gates late-reconcile so config:switch-model doesn't misroute under a non-ash backend.
366
+ let ashActive = false;
367
+ let agentLoop: AgentLoop | null = null;
368
+ let loadedExtensionNames: string[] = [];
369
+
370
+ bus.on("agent:providers:changed", () => {
371
+ resolvedProviders = computeResolvedProviders();
372
+ if (!resolved) return;
373
+ bus.emit("agent:models-changed", {});
374
+ if (!ashActive) return;
375
+ if (buildModels().some((m) => m.id === llmClient.model)) return;
376
+ const pendingProvider = getSettings().defaultProvider;
377
+ if (!pendingProvider) return;
378
+ const p = resolvedProviders.get(pendingProvider);
379
+ if (!p) return;
380
+ const pendingModel = persistedModelFor(pendingProvider);
381
+ if (pendingModel && p.models.includes(pendingModel) && llmClient.model !== pendingModel) {
382
+ bus.emit("config:switch-model", { id: pendingModel, provider: pendingProvider });
383
+ }
384
+ });
385
+
386
+ bus.on("provider:configure", ({ id, reasoningParams, cacheTokens }) => {
387
+ const prev = providerHooks.get(id) ?? {};
388
+ if (reasoningParams !== undefined) prev.reasoningParams = reasoningParams;
389
+ if (cacheTokens !== undefined) prev.cacheTokens = cacheTokens;
390
+ providerHooks.set(id, prev);
391
+ });
392
+
393
+ bus.on("core:extensions-loaded", ({ names }) => {
394
+ loadedExtensionNames = names;
395
+ resolvedProviders = computeResolvedProviders();
396
+
397
+ const settings = getSettings();
398
+
399
+ let providerName: string | undefined = config.provider ?? settings.defaultProvider;
400
+ let activeProvider = providerName ? resolvedProviders.get(providerName) ?? null : null;
401
+
402
+ // Inline CLI credentials carry their own endpoint, so they skip the
403
+ // usable-provider fallback that registry-driven selection needs.
404
+ if (!config.apiKey) {
405
+ if (!providerName) {
406
+ const first = [...resolvedProviders].find(([, p]) => usableProvider(p));
407
+ providerName = first?.[0];
408
+ activeProvider = first?.[1] ?? null;
409
+ } else if (!usableProvider(activeProvider)) {
410
+ const reason = !activeProvider ? "is not registered"
411
+ : !activeProvider.apiKey ? "has no API key configured"
412
+ : "has no endpoint configured";
413
+ const next = [...resolvedProviders].find(([, p]) => usableProvider(p));
414
+ if (next) {
415
+ bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}; falling back to "${next[0]}".` });
416
+ providerName = next[0];
417
+ activeProvider = next[1];
418
+ } else {
419
+ bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}, and no other configured provider has both an API key and an endpoint. Run \`agent-sh auth\` to configure one.` });
420
+ return;
421
+ }
422
+ }
423
+ }
424
+
425
+ // Persisted defaultModel wins over openrouter's hardcoded DEFAULT_MODELS[0].
426
+ const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
427
+ const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
428
+ const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
429
+
430
+ // No provider → don't register ash; let another backend own activation.
431
+ if (!effectiveApiKey || !effectiveModel) return;
432
+
433
+ const foundModel = buildModels().find(
434
+ (m) => m.id === effectiveModel && (!activeProvider || m.provider === activeProvider.id),
435
+ );
436
+ // Stub when openrouter's async catalog hasn't returned yet; reconciled
437
+ // later via agent:providers:changed → config:switch-model.
438
+ const initialModel: Model = foundModel ?? {
439
+ id: effectiveModel,
440
+ provider: activeProvider?.id ?? providerName ?? "custom",
441
+ supportsReasoningEffort: activeProvider?.supportsReasoningEffort,
442
+ };
443
+
444
+ llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
445
+ resolved = true;
446
+
447
+ bus.emit("agent:register-backend", {
448
+ name: "ash",
449
+ kill: () => {
450
+ ashActive = false;
451
+ bus.emit("command:unregister", { name: "/compact" });
452
+ bus.emit("command:unregister", { name: "/context" });
453
+ agentLoop?.kill();
454
+ agentLoop = null;
455
+ },
456
+ start: async () => {
457
+ agentLoop = new AgentLoop({
458
+ bus,
459
+ llmClient,
460
+ handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
461
+ initialModel,
462
+ compositor: ctx.shell?.compositor,
463
+ instanceId: ctx.instanceId,
464
+ });
465
+ agentLoop.wire();
466
+ ashActive = true;
467
+ bus.emit("command:register", {
468
+ name: "/compact",
469
+ description: "Compact now, or: off | on | threshold <0..1> | status",
470
+ handler: (args: string) => {
471
+ const trimmed = args.trim();
472
+ if (!trimmed) {
473
+ bus.emit("agent:compact-request", {});
474
+ return;
475
+ }
476
+ const [sub, ...rest] = trimmed.split(/\s+/);
477
+ if (sub === "off" || sub === "on") {
478
+ setSessionOverlay({ autoCompact: sub === "on" });
479
+ bus.emit("ui:info", { message: `auto-compact: ${sub} (session)` });
480
+ return;
481
+ }
482
+ if (sub === "threshold") {
483
+ const raw = rest[0];
484
+ const n = Number(raw);
485
+ if (!raw || !Number.isFinite(n) || n < 0 || n > 1) {
486
+ bus.emit("ui:error", { message: "usage: /compact threshold <0.0..1.0>" });
487
+ return;
488
+ }
489
+ setSessionOverlay({ autoCompactThreshold: n });
490
+ bus.emit("ui:info", { message: `auto-compact threshold: ${n} (session)` });
491
+ return;
492
+ }
493
+ if (sub === "reset") {
494
+ clearSessionOverlay("autoCompact", "autoCompactThreshold");
495
+ bus.emit("ui:info", { message: "auto-compact: session overrides cleared" });
496
+ return;
497
+ }
498
+ if (sub === "status") {
499
+ const s = getSettings();
500
+ const enabledSrc = getSettingSource("autoCompact");
501
+ const thSrc = getSettingSource("autoCompactThreshold");
502
+ bus.emit("ui:info", {
503
+ message:
504
+ `auto-compact: ${s.autoCompact ? "on" : "off"} (${enabledSrc}), ` +
505
+ `threshold: ${s.autoCompactThreshold} (${thSrc})`,
506
+ });
507
+ return;
508
+ }
509
+ bus.emit("ui:error", {
510
+ message: `unknown subcommand: ${sub}. usage: /compact [off|on|threshold <0..1>|reset|status]`,
511
+ });
512
+ },
513
+ });
514
+ bus.emit("command:register", {
515
+ name: "/context",
516
+ description: "Show context budget usage",
517
+ handler: () => {
518
+ const stats = bus.emitPipe("context:get-stats", {
519
+ activeTokens: 0,
520
+ totalTokens: 0,
521
+ budgetTokens: 0,
522
+ });
523
+ const pct = stats.budgetTokens > 0
524
+ ? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
525
+ : 0;
526
+ bus.emit("ui:info", {
527
+ message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
528
+ });
529
+ },
530
+ });
531
+ },
532
+ });
533
+ });
534
+
535
+ bus.on("config:switch-provider", ({ provider: name }) => {
536
+ const p = resolvedProviders.get(name);
537
+ if (!p) {
538
+ bus.emit("ui:error", { message: `Unknown provider: ${name}` });
539
+ return;
540
+ }
541
+ if (!p.apiKey) {
542
+ bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
543
+ return;
544
+ }
545
+ if (!p.baseURL && p.id !== "openai") {
546
+ bus.emit("ui:error", { message: `Provider "${name}" has no endpoint configured` });
547
+ return;
548
+ }
549
+ const switchModel = p.defaultModel ?? p.models[0];
550
+ if (!switchModel) {
551
+ bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
552
+ return;
553
+ }
554
+ llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
555
+ bus.emit("agent:models-changed", {});
556
+ bus.emit("config:switch-model", { id: switchModel, provider: name });
557
+ bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
558
+ });
559
+
560
+ bus.onPipe("banner:collect", (e) => {
561
+ if (e.activeBackend && e.activeBackend !== "ash") return e;
562
+ if (loadedExtensionNames.length > 0) {
563
+ e.sections.push({ label: "Extensions", items: [...loadedExtensionNames] });
564
+ }
565
+ const skills = discoverSkills(ctx.call("cwd") ?? process.cwd());
566
+ if (skills.length > 0) {
567
+ e.sections.push({ label: "Skills", items: skills.map((s) => s.name) });
568
+ }
569
+ return e;
570
+ });
571
+ }
572
+
573
+ export type { AgentBackend } from "./types.js";
574
+ export type { ToolDefinition, ToolResult, ToolDisplayInfo } from "./types.js";
575
+ export { AgentLoop } from "./agent-loop.js";
576
+ export { ToolRegistry } from "./tool-registry.js";
577
+ export { runSubagent, type SubagentOptions } from "./subagent.js";
578
+
579
+ /** Built-in providers register unconditionally so `auth list` can
580
+ * enumerate them; buildModels() skips entries without an apiKey. */
581
+ export function activateAgent(ctx: ExtensionContext): void {
582
+ agentBackend(ctx);
583
+ const agentCtx = ctx as AgentContext;
584
+ activateOpenrouter(agentCtx);
585
+ activateOpenai(agentCtx);
586
+ if (process.env.OPENAI_BASE_URL) activateOpenaiCompatible(agentCtx);
587
+ activateDeepseek(agentCtx);
588
+ activateOllama(agentCtx);
589
+ activateZaiCodingPlan(agentCtx);
590
+ activateOpencode(agentCtx);
591
+ }