agent-sh 0.12.26 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +13 -2
  2. package/dist/agent/agent-loop.d.ts +3 -5
  3. package/dist/agent/agent-loop.js +44 -100
  4. package/dist/agent/conversation-state.d.ts +9 -0
  5. package/dist/agent/conversation-state.js +38 -1
  6. package/dist/agent/history-file.d.ts +6 -0
  7. package/dist/agent/history-file.js +1 -1
  8. package/dist/agent/host-types.d.ts +125 -0
  9. package/dist/agent/index.d.ts +12 -4
  10. package/dist/agent/index.js +357 -6
  11. package/dist/agent/nuclear-form.d.ts +7 -0
  12. package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
  13. package/dist/{extensions → agent}/providers/deepseek.js +5 -4
  14. package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
  15. package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
  16. package/dist/{extensions → agent}/providers/openai.js +3 -2
  17. package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
  18. package/dist/{extensions → agent}/providers/openrouter.js +4 -3
  19. package/dist/agent/skills.js +51 -7
  20. package/dist/agent/subagent.d.ts +1 -1
  21. package/dist/agent/system-prompt.js +14 -17
  22. package/dist/agent/tool-protocol.d.ts +1 -1
  23. package/dist/agent/tool-protocol.js +5 -3
  24. package/dist/agent/tool-registry.d.ts +9 -4
  25. package/dist/agent/tool-registry.js +27 -4
  26. package/dist/agent/tools/bash.d.ts +1 -1
  27. package/dist/agent/tools/bash.js +3 -2
  28. package/dist/agent/tools/edit-file.js +0 -1
  29. package/dist/agent/tools/glob.js +1 -1
  30. package/dist/agent/tools/grep.js +1 -1
  31. package/dist/agent/tools/pwsh.d.ts +1 -1
  32. package/dist/agent/tools/pwsh.js +1 -2
  33. package/dist/agent/tools/read-file.js +7 -4
  34. package/dist/agent/tools/write-file.js +0 -1
  35. package/dist/agent/types.d.ts +17 -2
  36. package/dist/cli/auth/cli.d.ts +1 -0
  37. package/dist/cli/auth/cli.js +216 -0
  38. package/dist/cli/auth/keys.d.ts +31 -0
  39. package/dist/cli/auth/keys.js +102 -0
  40. package/dist/{index.js → cli/index.js} +29 -32
  41. package/dist/{init.js → cli/init.js} +1 -1
  42. package/dist/{install.js → cli/install.js} +114 -5
  43. package/dist/cli/subcommands.d.ts +1 -0
  44. package/dist/cli/subcommands.js +17 -0
  45. package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
  46. package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
  47. package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
  48. package/dist/{core.d.ts → core/index.d.ts} +18 -15
  49. package/dist/{core.js → core/index.js} +18 -92
  50. package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
  51. package/dist/{settings.js → core/settings.js} +1 -0
  52. package/dist/core/types.d.ts +49 -0
  53. package/dist/core/types.js +1 -0
  54. package/dist/extensions/file-autocomplete.d.ts +1 -1
  55. package/dist/extensions/index.d.ts +7 -14
  56. package/dist/extensions/index.js +2 -19
  57. package/dist/extensions/slash-commands.d.ts +1 -1
  58. package/dist/extensions/slash-commands.js +7 -2
  59. package/dist/shell/host-types.d.ts +114 -0
  60. package/dist/shell/host-types.js +1 -0
  61. package/dist/shell/index.d.ts +8 -7
  62. package/dist/shell/index.js +58 -9
  63. package/dist/shell/input-handler.d.ts +7 -1
  64. package/dist/shell/input-handler.js +5 -2
  65. package/dist/shell/output-parser.d.ts +1 -1
  66. package/dist/{extensions → shell}/shell-context.d.ts +1 -1
  67. package/dist/{extensions → shell}/shell-context.js +18 -12
  68. package/dist/shell/shell.d.ts +6 -4
  69. package/dist/shell/shell.js +33 -109
  70. package/dist/shell/strategies/bash.d.ts +2 -0
  71. package/dist/shell/strategies/bash.js +68 -0
  72. package/dist/shell/strategies/fish.d.ts +2 -0
  73. package/dist/shell/strategies/fish.js +65 -0
  74. package/dist/shell/strategies/index.d.ts +13 -0
  75. package/dist/shell/strategies/index.js +17 -0
  76. package/dist/shell/strategies/types.d.ts +50 -0
  77. package/dist/shell/strategies/types.js +9 -0
  78. package/dist/shell/strategies/zsh.d.ts +2 -0
  79. package/dist/shell/strategies/zsh.js +72 -0
  80. package/dist/shell/tui-input-view.js +14 -3
  81. package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
  82. package/dist/{extensions → shell}/tui-renderer.js +27 -55
  83. package/dist/utils/box-frame.d.ts +4 -0
  84. package/dist/utils/box-frame.js +17 -6
  85. package/dist/utils/compositor.d.ts +1 -1
  86. package/dist/utils/compositor.js +2 -1
  87. package/dist/{executor.js → utils/executor.js} +1 -1
  88. package/dist/utils/floating-panel.d.ts +17 -5
  89. package/dist/utils/floating-panel.js +218 -70
  90. package/dist/utils/llm-facade.d.ts +7 -3
  91. package/dist/utils/stream-transform.d.ts +1 -1
  92. package/dist/utils/terminal-buffer.d.ts +1 -1
  93. package/dist/utils/tool-display.js +4 -0
  94. package/dist/utils/tool-interactive.d.ts +1 -1
  95. package/dist/utils/tty.d.ts +7 -0
  96. package/dist/utils/tty.js +15 -0
  97. package/examples/extensions/ash-acp-bridge/README.md +4 -1
  98. package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
  99. package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
  100. package/examples/extensions/ashi/README.md +250 -0
  101. package/examples/extensions/ashi/package.json +60 -0
  102. package/examples/extensions/ashi/src/autocomplete.ts +91 -0
  103. package/examples/extensions/ashi/src/capture.ts +34 -0
  104. package/examples/extensions/ashi/src/cli.ts +126 -0
  105. package/examples/extensions/ashi/src/commands.ts +82 -0
  106. package/examples/extensions/ashi/src/compaction.ts +157 -0
  107. package/examples/extensions/ashi/src/components.ts +332 -0
  108. package/examples/extensions/ashi/src/default-renderers.ts +153 -0
  109. package/examples/extensions/ashi/src/display-config.ts +62 -0
  110. package/examples/extensions/ashi/src/frontend.ts +735 -0
  111. package/examples/extensions/ashi/src/hooks.ts +136 -0
  112. package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
  113. package/examples/extensions/ashi/src/session-commands.ts +76 -0
  114. package/examples/extensions/ashi/src/session-store.ts +264 -0
  115. package/examples/extensions/ashi/src/status-footer.ts +66 -0
  116. package/examples/extensions/ashi/src/theme.ts +151 -0
  117. package/examples/extensions/ashi/tsconfig.json +14 -0
  118. package/examples/extensions/emacs-buffer.ts +364 -0
  119. package/examples/extensions/interactive-prompts.ts +114 -69
  120. package/examples/extensions/latex-images.ts +3 -3
  121. package/examples/extensions/opencode-bridge/index.ts +1 -1
  122. package/examples/extensions/overlay-agent.ts +35 -10
  123. package/examples/extensions/peer-mesh.ts +1 -1
  124. package/examples/extensions/pi-bridge/index.ts +0 -1
  125. package/examples/extensions/questionnaire.ts +2 -1
  126. package/examples/extensions/rtk-proxy.ts +3 -3
  127. package/examples/extensions/solarized-theme.ts +3 -3
  128. package/examples/extensions/subagents.ts +6 -6
  129. package/examples/extensions/terminal-buffer.ts +174 -33
  130. package/examples/extensions/tmux-pane.ts +6 -4
  131. package/examples/extensions/tunnel-vision.ts +405 -0
  132. package/examples/extensions/user-shell.ts +1 -1
  133. package/examples/extensions/web-access.ts +8 -113
  134. package/package.json +26 -22
  135. package/dist/extensions/agent-backend.d.ts +0 -14
  136. package/dist/extensions/agent-backend.js +0 -307
  137. package/dist/types.d.ts +0 -227
  138. /package/dist/{types.js → agent/host-types.js} +0 -0
  139. /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
  140. /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
  141. /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
  142. /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
  143. /package/dist/{event-bus.js → core/event-bus.js} +0 -0
  144. /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
@@ -1,9 +1,360 @@
1
- /**
2
- * Agent backend exports.
3
- *
4
- * The default backend is AgentLoop (in-process, OpenAI-compatible API).
5
- * Extensions can register alternative backends via agent:register-backend.
6
- */
1
+ import { AgentLoop } from "./agent-loop.js";
2
+ import { LlmClient } from "../utils/llm-client.js";
3
+ import { createLlmFacade } from "../utils/llm-facade.js";
4
+ import { resolveProvider, getProviderNames, getSettings } from "../core/settings.js";
5
+ import { PACKAGE_VERSION } from "../utils/package-version.js";
6
+ import { discoverSkills } from "./skills.js";
7
+ import { resolveApiKey } from "../cli/auth/keys.js";
8
+ import activateOpenrouter from "./providers/openrouter.js";
9
+ import activateOpenai from "./providers/openai.js";
10
+ import activateOpenaiCompatible from "./providers/openai-compatible.js";
11
+ import activateDeepseek from "./providers/deepseek.js";
12
+ function persistedModelFor(providerName) {
13
+ if (!providerName)
14
+ return undefined;
15
+ return getSettings().providers?.[providerName]?.defaultModel;
16
+ }
17
+ function defaultReasoningBuilder(level) {
18
+ return level === "off" ? {} : { reasoning_effort: level };
19
+ }
20
+ function mergeCaps(settingsCaps, payloadCaps, modelIds) {
21
+ if (!settingsCaps)
22
+ return payloadCaps.size > 0 ? payloadCaps : undefined;
23
+ const out = new Map();
24
+ for (const id of modelIds) {
25
+ const s = settingsCaps.get(id);
26
+ const p = payloadCaps.get(id);
27
+ if (!s && !p)
28
+ continue;
29
+ out.set(id, {
30
+ reasoning: s?.reasoning ?? p?.reasoning,
31
+ contextWindow: s?.contextWindow ?? p?.contextWindow,
32
+ maxTokens: s?.maxTokens ?? p?.maxTokens,
33
+ echoReasoning: s?.echoReasoning ?? p?.echoReasoning,
34
+ });
35
+ }
36
+ return out.size > 0 ? out : undefined;
37
+ }
38
+ export default function agentBackend(ctx) {
39
+ const { bus } = ctx;
40
+ const config = ctx.call("config:get-app-config") ?? {};
41
+ const agentSurface = {
42
+ llm: createLlmFacade({ list: ctx.list, call: ctx.call }),
43
+ providers: {
44
+ configure: (id, configureOpts) => bus.emit("provider:configure", { id, ...configureOpts }),
45
+ },
46
+ registerTool: (tool) => bus.emit("agent:register-tool", { tool, extensionName: "" }),
47
+ unregisterTool: (name) => bus.emit("agent:unregister-tool", { name }),
48
+ adviseTool: (name, advisor) => ctx.advise(`tool:${name}`, advisor),
49
+ adviseToolSchema: (name, advisor) => ctx.advise(`tool:${name}:schema`, advisor),
50
+ getTools: () => bus.emitPipe("agent:get-tools", { tools: [] }).tools,
51
+ registerInstruction: (name, text) => bus.emit("agent:register-instruction", { name, text, extensionName: "" }),
52
+ removeInstruction: (name) => bus.emit("agent:remove-instruction", { name }),
53
+ adviseInstruction: (name, advisor) => ctx.advise(`instruction:${name}`, advisor),
54
+ registerSkill: (name, description, filePath) => bus.emit("agent:register-skill", { name, description, filePath, extensionName: "" }),
55
+ removeSkill: (name) => bus.emit("agent:remove-skill", { name }),
56
+ adviseSkill: (name, advisor) => ctx.advise(`skill:${name}:view`, advisor),
57
+ registerContextProducer: (_name, producer, producerOpts) => {
58
+ const handlerName = producerOpts?.mode === "per-query"
59
+ ? "query-context:build"
60
+ : "dynamic-context:build";
61
+ return ctx.advise(handlerName, (next) => {
62
+ const base = next();
63
+ const part = producer();
64
+ if (!part)
65
+ return base;
66
+ const trimmed = part.trim();
67
+ if (!trimmed)
68
+ return base;
69
+ return base ? `${base}\n\n${trimmed}` : trimmed;
70
+ });
71
+ },
72
+ };
73
+ ctx.agent = agentSurface;
74
+ // Immutable settings snapshot; provider:register payloads merge against it.
75
+ const providerRegistry = new Map();
76
+ const settingsProviders = new Map();
77
+ for (const name of getProviderNames()) {
78
+ const p = resolveProvider(name);
79
+ if (p) {
80
+ providerRegistry.set(name, p);
81
+ settingsProviders.set(name, p);
82
+ }
83
+ }
84
+ const providerHooks = new Map();
85
+ // Bakes model id into the hook so AgentMode.buildReasoningParams keeps
86
+ // its (level) signature while the hook can branch on model.
87
+ const bindReasoning = (shapeId, model) => {
88
+ const hook = providerHooks.get(shapeId)?.reasoningParams;
89
+ return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
90
+ };
91
+ const buildModes = () => {
92
+ const allModes = [];
93
+ for (const [id, p] of providerRegistry) {
94
+ if (!p.apiKey)
95
+ continue;
96
+ const shapeId = p.reasoningShape ?? id;
97
+ for (const model of p.models) {
98
+ const mc = p.modelCapabilities?.get(model);
99
+ allModes.push({
100
+ model,
101
+ provider: id,
102
+ providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
103
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
104
+ maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
105
+ reasoning: mc?.reasoning,
106
+ supportsReasoningEffort: p.supportsReasoningEffort,
107
+ echoReasoning: mc?.echoReasoning,
108
+ buildReasoningParams: bindReasoning(shapeId, model),
109
+ });
110
+ }
111
+ }
112
+ return allModes;
113
+ };
114
+ // Placeholder client — reconfigured at core:extensions-loaded. Any
115
+ // stream() call before then fails from the OpenAI SDK; start() won't
116
+ // wire the loop until we've resolved, so users never hit that path.
117
+ const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
118
+ ctx.define("llm:get-client", () => llmClient);
119
+ ctx.define("llm:invoke", (messages, opts) => {
120
+ return llmClient.complete({
121
+ messages: messages,
122
+ max_tokens: opts?.maxTokens,
123
+ model: opts?.model,
124
+ reasoning_effort: opts?.reasoningEffort,
125
+ });
126
+ });
127
+ let modes = [];
128
+ let initialModeIndex = 0;
129
+ let resolved = false;
130
+ // Gates late-registration reconcile so its config:switch-model emit doesn't misroute under a non-ash backend.
131
+ let ashActive = false;
132
+ bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
133
+ // AgentLoop must be constructed *before* user extensions activate,
134
+ // because its ctor defines handlers (history:append, etc.) that
135
+ // extensions like superash call synchronously during their own
136
+ // activate. Advise-before-define works for advisers, but plain calls
137
+ // would hit a no-op stub.
138
+ const agentLoop = new AgentLoop({
139
+ bus,
140
+ llmClient,
141
+ handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
142
+ modes,
143
+ initialModeIndex,
144
+ compositor: ctx.shell?.compositor,
145
+ instanceId: ctx.instanceId,
146
+ history: config.history,
147
+ });
148
+ let loadedExtensionNames = [];
149
+ bus.on("core:extensions-loaded", ({ names }) => {
150
+ loadedExtensionNames = names;
151
+ const settings = getSettings();
152
+ // If the user didn't pick a default, fall back to the first registered
153
+ // provider (built-in load order biases to openrouter → openai).
154
+ const providerName = config.provider ?? settings.defaultProvider
155
+ ?? (providerRegistry.size > 0 ? providerRegistry.keys().next().value : undefined);
156
+ const activeProvider = providerName ? providerRegistry.get(providerName) ?? null : null;
157
+ // User's persisted defaultModel wins over the provider's declared
158
+ // default. Dynamic providers (openrouter) re-register with their
159
+ // hardcoded DEFAULT_MODELS[0] each startup, which would otherwise
160
+ // clobber the user's /model selection.
161
+ const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
162
+ const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
163
+ const effectiveModel = config.model ?? persistedModelFor(providerName) ?? activeProvider?.defaultModel;
164
+ // No provider → don't register ash at all, so another backend (e.g.
165
+ // claude-code-bridge) can own activation. index.ts hard-fails only
166
+ // when no backend ended up registered.
167
+ if (!effectiveApiKey || !effectiveModel)
168
+ return;
169
+ modes = buildModes();
170
+ if (modes.length === 0)
171
+ modes = [{ model: effectiveModel }];
172
+ let foundIdx = modes.findIndex((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
173
+ // Persisted default may not be in the provider's curated list yet (e.g.
174
+ // openrouter's async catalog fetch hasn't returned). Prepend a stub so
175
+ // the initial config:set-modes activeIndex points at the real model —
176
+ // otherwise AgentLoop reconfigures llmClient back to modes[0].
177
+ if (foundIdx === -1 && activeProvider) {
178
+ modes = [
179
+ {
180
+ model: effectiveModel,
181
+ provider: activeProvider.id,
182
+ providerConfig: { apiKey: effectiveApiKey, baseURL: effectiveBaseURL },
183
+ supportsReasoningEffort: activeProvider.supportsReasoningEffort,
184
+ },
185
+ ...modes,
186
+ ];
187
+ foundIdx = 0;
188
+ }
189
+ initialModeIndex = Math.max(0, foundIdx);
190
+ llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
191
+ bus.emit("config:set-modes", { modes, activeIndex: initialModeIndex });
192
+ resolved = true;
193
+ bus.emit("agent:register-backend", {
194
+ name: "ash",
195
+ kill: () => {
196
+ ashActive = false;
197
+ bus.emit("command:unregister", { name: "/compact" });
198
+ bus.emit("command:unregister", { name: "/context" });
199
+ agentLoop.kill();
200
+ },
201
+ start: async () => {
202
+ agentLoop.wire();
203
+ ashActive = true;
204
+ bus.emit("command:register", {
205
+ name: "/compact",
206
+ description: "Compact conversation via the active compaction strategy",
207
+ handler: () => bus.emit("agent:compact-request", {}),
208
+ });
209
+ bus.emit("command:register", {
210
+ name: "/context",
211
+ description: "Show context budget usage",
212
+ handler: () => {
213
+ const stats = bus.emitPipe("context:get-stats", {
214
+ activeTokens: 0,
215
+ totalTokens: 0,
216
+ budgetTokens: 0,
217
+ });
218
+ const pct = stats.budgetTokens > 0
219
+ ? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
220
+ : 0;
221
+ bus.emit("ui:info", {
222
+ message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
223
+ });
224
+ },
225
+ });
226
+ bus.emit("agent:info", {
227
+ name: "ash",
228
+ version: PACKAGE_VERSION,
229
+ model: llmClient.model,
230
+ provider: modes[initialModeIndex]?.provider,
231
+ contextWindow: modes[initialModeIndex]?.contextWindow,
232
+ });
233
+ },
234
+ });
235
+ });
236
+ bus.on("provider:configure", ({ id, reasoningParams }) => {
237
+ const prev = providerHooks.get(id) ?? {};
238
+ if (reasoningParams !== undefined)
239
+ prev.reasoningParams = reasoningParams;
240
+ providerHooks.set(id, prev);
241
+ });
242
+ bus.on("provider:register", (p) => {
243
+ const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
244
+ const payloadModelIds = [];
245
+ const payloadCaps = new Map();
246
+ for (const m of rawModels) {
247
+ if (typeof m === "string") {
248
+ payloadModelIds.push(m);
249
+ }
250
+ else {
251
+ payloadModelIds.push(m.id);
252
+ payloadCaps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
253
+ }
254
+ }
255
+ const settings = settingsProviders.get(p.id);
256
+ const modelIds = settings?.modelsExplicit && settings.models.length > 0 ? settings.models : payloadModelIds;
257
+ const mergedCaps = mergeCaps(settings?.modelCapabilities, payloadCaps, modelIds);
258
+ const merged = {
259
+ id: p.id,
260
+ apiKey: settings?.apiKey ?? p.apiKey,
261
+ baseURL: settings?.baseURL ?? p.baseURL,
262
+ defaultModel: settings?.defaultModel ?? p.defaultModel,
263
+ models: modelIds,
264
+ modelsExplicit: settings?.modelsExplicit ?? false,
265
+ contextWindow: settings?.contextWindow,
266
+ supportsReasoningEffort: settings?.supportsReasoningEffort ?? p.supportsReasoningEffort,
267
+ modelCapabilities: mergedCaps,
268
+ reasoningShape: settings?.reasoningShape,
269
+ };
270
+ providerRegistry.set(p.id, merged);
271
+ const addModes = modelIds.map((m) => {
272
+ const mc = mergedCaps?.get(m);
273
+ return {
274
+ model: m,
275
+ provider: p.id,
276
+ providerConfig: { apiKey: merged.apiKey ?? "", baseURL: merged.baseURL },
277
+ contextWindow: mc?.contextWindow,
278
+ maxTokens: mc?.maxTokens,
279
+ reasoning: mc?.reasoning,
280
+ supportsReasoningEffort: merged.supportsReasoningEffort,
281
+ echoReasoning: mc?.echoReasoning,
282
+ buildReasoningParams: bindReasoning(p.id, m),
283
+ };
284
+ });
285
+ bus.emit("config:add-modes", { modes: addModes });
286
+ // Late-registration reconcile: if this completes the user's persisted
287
+ // default (openrouter's async fetch delivers the full catalog after
288
+ // we've already fallen back to mode 0), quietly switch to it.
289
+ if (!resolved || !ashActive)
290
+ return;
291
+ const pendingProvider = getSettings().defaultProvider;
292
+ if (pendingProvider !== p.id)
293
+ return;
294
+ const pendingModel = persistedModelFor(pendingProvider);
295
+ if (pendingModel && modelIds.includes(pendingModel) && llmClient.model !== pendingModel) {
296
+ bus.emit("config:switch-model", { model: pendingModel });
297
+ }
298
+ });
299
+ bus.on("config:switch-provider", ({ provider: name }) => {
300
+ const p = providerRegistry.get(name);
301
+ if (!p) {
302
+ bus.emit("ui:error", { message: `Unknown provider: ${name}` });
303
+ return;
304
+ }
305
+ if (!p.apiKey) {
306
+ bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
307
+ return;
308
+ }
309
+ const switchModel = p.defaultModel ?? p.models[0];
310
+ if (!switchModel) {
311
+ bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
312
+ return;
313
+ }
314
+ llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
315
+ const newModes = p.models.map((m) => {
316
+ const mc = p.modelCapabilities?.get(m);
317
+ return {
318
+ model: m,
319
+ provider: name,
320
+ providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
321
+ contextWindow: mc?.contextWindow ?? p.contextWindow,
322
+ maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
323
+ reasoning: mc?.reasoning,
324
+ supportsReasoningEffort: p.supportsReasoningEffort,
325
+ echoReasoning: mc?.echoReasoning,
326
+ };
327
+ });
328
+ bus.emit("config:set-modes", { modes: newModes });
329
+ bus.emit("agent:info", { name: "ash", version: PACKAGE_VERSION, model: switchModel, provider: name, contextWindow: p.contextWindow });
330
+ bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
331
+ bus.emit("config:changed", {});
332
+ });
333
+ bus.onPipe("banner:collect", (e) => {
334
+ if (e.activeBackend && e.activeBackend !== "ash")
335
+ return e;
336
+ if (loadedExtensionNames.length > 0) {
337
+ e.sections.push({ label: "Extensions", items: [...loadedExtensionNames] });
338
+ }
339
+ const skills = discoverSkills(ctx.call("cwd") ?? process.cwd());
340
+ if (skills.length > 0) {
341
+ e.sections.push({ label: "Skills", items: skills.map((s) => s.name) });
342
+ }
343
+ return e;
344
+ });
345
+ }
7
346
  export { AgentLoop } from "./agent-loop.js";
8
347
  export { ToolRegistry } from "./tool-registry.js";
9
348
  export { runSubagent } from "./subagent.js";
349
+ /** Activate the ash backend and any provider whose key is configured. */
350
+ export function activateAgent(ctx) {
351
+ agentBackend(ctx);
352
+ const agentCtx = ctx;
353
+ if (resolveApiKey("openrouter").key)
354
+ activateOpenrouter(agentCtx);
355
+ if (resolveApiKey("openai").key && !process.env.OPENAI_BASE_URL)
356
+ activateOpenai(agentCtx);
357
+ if (process.env.OPENAI_BASE_URL)
358
+ activateOpenaiCompatible(agentCtx);
359
+ activateDeepseek(agentCtx);
360
+ }
@@ -32,6 +32,13 @@ export interface NuclearEntry {
32
32
  * survives into summaries. Displayed as `{why}` in formatNuclearLine.
33
33
  */
34
34
  why?: string;
35
+ /**
36
+ * Optional parent pointer for tree-shaped history. The default
37
+ * HistoryFile adapter ignores this and treats the file as linear;
38
+ * tree-aware HistoryAdapter implementations use it to fork and to
39
+ * walk a single path on resume.
40
+ */
41
+ parentSeq?: number;
35
42
  }
36
43
  /**
37
44
  * Create a session-start marker entry. Markers use seq=0 by default —
@@ -4,5 +4,5 @@
4
4
  * to enabled. The hook always attaches; provider registration via env
5
5
  * is opt-in alongside any settings.json entry.
6
6
  */
7
- import type { ExtensionContext } from "../../types.js";
8
- export default function activate(ctx: ExtensionContext): void;
7
+ import type { AgentContext } from "../host-types.js";
8
+ export default function activate(ctx: AgentContext): void;
@@ -1,7 +1,8 @@
1
+ import { resolveApiKey } from "../../cli/auth/keys.js";
1
2
  const BASE_URL = "https://api.deepseek.com";
2
3
  const DEFAULT_MODELS = [
3
- { id: "deepseek-v4-flash", reasoning: true, echoReasoning: true },
4
- { id: "deepseek-v4-pro", reasoning: true, echoReasoning: true },
4
+ { id: "deepseek-v4-flash", reasoning: true, echoReasoning: true, contextWindow: 1_000_000 },
5
+ { id: "deepseek-v4-pro", reasoning: true, echoReasoning: true, contextWindow: 1_000_000 },
5
6
  ];
6
7
  function buildReasoningParams(level, _model) {
7
8
  return level === "off"
@@ -9,8 +10,8 @@ function buildReasoningParams(level, _model) {
9
10
  : { thinking: { type: "enabled" }, reasoning_effort: level };
10
11
  }
11
12
  export default function activate(ctx) {
12
- ctx.providers.configure("deepseek", { reasoningParams: buildReasoningParams });
13
- const apiKey = process.env.DEEPSEEK_API_KEY;
13
+ ctx.agent.providers.configure("deepseek", { reasoningParams: buildReasoningParams });
14
+ const apiKey = resolveApiKey("deepseek").key;
14
15
  if (!apiKey)
15
16
  return;
16
17
  ctx.bus.emit("provider:register", {
@@ -3,5 +3,5 @@
3
3
  * Studio, vLLM, llama.cpp, …). No reasoning hook — the right shape depends
4
4
  * on which model the server is serving; user extensions can add one.
5
5
  */
6
- import type { ExtensionContext } from "../../types.js";
7
- export default function activate(ctx: ExtensionContext): void;
6
+ import type { AgentContext } from "../host-types.js";
7
+ export default function activate(ctx: AgentContext): void;
@@ -3,5 +3,5 @@
3
3
  * family: o-series has no off; gpt-5-codex floors at "low"; plain gpt-5
4
4
  * floors at "minimal"; gpt-5.1+ accepts "none" as documented full off.
5
5
  */
6
- import type { ExtensionContext } from "../../types.js";
7
- export default function activate(ctx: ExtensionContext): void;
6
+ import type { AgentContext } from "../host-types.js";
7
+ export default function activate(ctx: AgentContext): void;
@@ -1,3 +1,4 @@
1
+ import { resolveApiKey } from "../../cli/auth/keys.js";
1
2
  const CLOUD_MODELS = [
2
3
  { id: "gpt-5", reasoning: true },
3
4
  { id: "gpt-4.1", reasoning: false },
@@ -24,12 +25,12 @@ function buildReasoningParams(level, model) {
24
25
  return off ? { reasoning_effort: off } : {};
25
26
  }
26
27
  export default function activate(ctx) {
27
- const apiKey = process.env.OPENAI_API_KEY;
28
+ const apiKey = resolveApiKey("openai").key;
28
29
  if (!apiKey)
29
30
  return;
30
31
  if (process.env.OPENAI_BASE_URL)
31
32
  return; // openai-compatible handles this
32
- ctx.providers.configure("openai", { reasoningParams: buildReasoningParams });
33
+ ctx.agent.providers.configure("openai", { reasoningParams: buildReasoningParams });
33
34
  ctx.bus.emit("provider:register", {
34
35
  id: "openai",
35
36
  apiKey,
@@ -3,5 +3,5 @@
3
3
  * Registers curated defaults synchronously so the first query works, then
4
4
  * fetches the full catalog to populate /model autocomplete.
5
5
  */
6
- import type { ExtensionContext } from "../../types.js";
7
- export default function activate(ctx: ExtensionContext): void;
6
+ import type { AgentContext } from "../host-types.js";
7
+ export default function activate(ctx: AgentContext): void;
@@ -1,4 +1,5 @@
1
- import { getSettings } from "../../settings.js";
1
+ import { getSettings } from "../../core/settings.js";
2
+ import { resolveApiKey } from "../../cli/auth/keys.js";
2
3
  const BASE_URL = "https://openrouter.ai/api/v1";
3
4
  const DEFAULT_MODELS = ["deepseek/deepseek-v4-flash"];
4
5
  // Built-in defaults for models requiring reasoning_content echoed back
@@ -14,10 +15,10 @@ function buildReasoningParams(level, _model) {
14
15
  : { reasoning: { effort: level } };
15
16
  }
16
17
  export default function activate(ctx) {
17
- const apiKey = process.env.OPENROUTER_API_KEY;
18
+ const apiKey = resolveApiKey("openrouter").key;
18
19
  if (!apiKey)
19
20
  return;
20
- ctx.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
21
+ ctx.agent.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
21
22
  ctx.bus.emit("provider:register", {
22
23
  id: "openrouter",
23
24
  apiKey,
@@ -13,20 +13,64 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
  import * as os from "node:os";
16
- import { CONFIG_DIR, getSettings } from "../settings.js";
17
- /** Parse YAML frontmatter from a SKILL.md file. */
16
+ import { CONFIG_DIR, getSettings } from "../core/settings.js";
17
+ /** Parse YAML frontmatter from a SKILL.md file. Supports inline scalars
18
+ * and block scalars (`>`, `>-`, `|`, `|-`) for multi-line descriptions. */
18
19
  function parseFrontmatter(content) {
19
20
  const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
20
21
  if (!match)
21
22
  return null;
22
23
  const meta = {};
23
- for (const line of match[1].split("\n")) {
24
+ const lines = match[1].split("\n");
25
+ let i = 0;
26
+ while (i < lines.length) {
27
+ const line = lines[i];
28
+ const indent = line.length - line.trimStart().length;
24
29
  const colon = line.indexOf(":");
25
- if (colon > 0) {
26
- const key = line.slice(0, colon).trim();
27
- const value = line.slice(colon + 1).trim();
28
- meta[key] = value;
30
+ if (colon <= 0 || indent > 0) {
31
+ i++;
32
+ continue;
33
+ }
34
+ const key = line.slice(0, colon).trim();
35
+ const rawValue = line.slice(colon + 1).trim();
36
+ const blockStyle = rawValue.match(/^([>|])([+-]?)\s*$/);
37
+ if (!blockStyle) {
38
+ meta[key] = rawValue;
39
+ i++;
40
+ continue;
41
+ }
42
+ const folded = blockStyle[1] === ">";
43
+ const chomp = blockStyle[2];
44
+ const body = [];
45
+ let blockIndent = -1;
46
+ let j = i + 1;
47
+ while (j < lines.length) {
48
+ const next = lines[j];
49
+ if (next.trim() === "") {
50
+ body.push("");
51
+ j++;
52
+ continue;
53
+ }
54
+ const ind = next.length - next.trimStart().length;
55
+ if (blockIndent === -1)
56
+ blockIndent = ind;
57
+ if (ind < blockIndent)
58
+ break;
59
+ body.push(next.slice(blockIndent));
60
+ j++;
61
+ }
62
+ let end = body.length;
63
+ if (chomp !== "+") {
64
+ while (end > 0 && body[end - 1] === "")
65
+ end--;
66
+ if (chomp !== "-" && end < body.length)
67
+ end++;
29
68
  }
69
+ const kept = body.slice(0, end);
70
+ meta[key] = folded
71
+ ? kept.join(" ").replace(/\s+/g, " ").trim()
72
+ : kept.join("\n");
73
+ i = j;
30
74
  }
31
75
  return { meta, body: match[2] };
32
76
  }
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Used by the subagent extension to delegate tasks from the main agent.
11
11
  */
12
- import type { EventBus } from "../event-bus.js";
12
+ import type { EventBus } from "../core/event-bus.js";
13
13
  import type { LlmClient } from "../utils/llm-client.js";
14
14
  import type { ToolDefinition } from "./types.js";
15
15
  export interface SubagentOptions {
@@ -11,10 +11,10 @@ export function formatSkillsBlock(skills) {
11
11
  if (skills.length === 0)
12
12
  return "";
13
13
  return "# Available Skills\n\n"
14
- + "Load a skill's full content with read_file on its file path when needed.\n\n"
14
+ + "Load a skill's full content from its file path with your file-reading tool when needed.\n\n"
15
15
  + skills.map(s => `- **${s.name}**: ${s.description}\n Path: ${s.filePath}`).join("\n\n");
16
16
  }
17
- import { CONFIG_DIR } from "../settings.js";
17
+ import { CONFIG_DIR } from "../core/settings.js";
18
18
  const GLOBAL_AGENTS_MD = path.join(CONFIG_DIR, "AGENTS.md");
19
19
  // ── File caches ─────────────────────────────────────────────────────
20
20
  // Convention files (CLAUDE.md/AGENT.md) are walked synchronously from
@@ -88,24 +88,21 @@ function loadConventionFiles(dir) {
88
88
  * Static system prompt — identical across all queries, cacheable.
89
89
  * Contains only identity and behavioral instructions.
90
90
  */
91
- export const STATIC_SYSTEM_PROMPT = `You are ash, an AI coding assistant running inside agent-sh, a terminal shell.
92
- You have access to the user's shell environment and can read, write, and execute code.
93
- You share the user's working directory, environment variables, and shell history.
94
- agent-sh documentation is at ${path.join(CODE_DIR, "docs")} — start with README.md for an index. Read the docs when you need to understand how the runtime works.
91
+ export const STATIC_SYSTEM_PROMPT = `You are ash, an AI coding assistant running inside agent-sh a composable agent runtime with a small core and everything else, including the shell integration, layered on as extensions.
95
92
 
96
- # Tool Decision Guide
97
- bash, read_file, grep, glob, ls, edit_file, write_file::
98
- Use these to investigate, search, read, and modify files. Output is returned
99
- to you for reasoning — the user doesn't see it directly.
93
+ You may be paired with a terminal shell that shares the user's CWD, environment, and history — in that mode you can read shell events and act on the user's session. Otherwise you may be embedded as a library, exposed over a bridge protocol, or running headless, with no shell available; in those modes you operate purely through your registered tools.
100
94
 
101
- Extensions may register additional tools follow their instructions.
95
+ agent-sh source and documentation live at ${CODE_DIR}. Read them when you need to understand how the runtime works, or when the user asks how to modify or extend it:
96
+ - ${path.join(CODE_DIR, "docs")} — start with README.md; architecture.md and extensions.md cover the kernel boundary and extension API
97
+ - ${path.join(CODE_DIR, "src")} — kernel in src/core, default backend in src/agent, shell host in src/shell, built-in extensions in src/extensions
98
+ - ${path.join(CODE_DIR, "examples/extensions")} — reference extensions to study or copy when adding functionality
102
99
 
103
- # Tool Usage Guidelines
104
- - Use read_file before editing a file you haven't seen
105
- - Prefer edit_file over write_file for modifying existing files
106
- - Use grep/glob to find files before reading them
107
- - Keep bash commands focused; avoid long-running blocking commands
108
- - Always check command exit codes for errors
100
+ # Tools
101
+
102
+ Use your registered tools to investigate, search, read, and modify files.
103
+ Each tool's description tells you when and how to use it; follow that
104
+ guidance rather than assuming a particular tool exists. Tool output is
105
+ returned to you for reasoning the user doesn't see it directly.
109
106
 
110
107
  # Context Envelopes
111
108
  - \`<query_context>\` (contains \`<cwd>\` always, and \`<shell_events>\` when there were user shell commands since the last turn): the user's situation when they sent this turn — \`<cwd>\` anchors where they are right now, \`<shell_events>\` grounds "fix this" / "what just happened" requests. Trust the most recent \`<cwd>\` over any cwd referenced in earlier history.
@@ -102,4 +102,4 @@ export declare class DeferredLookupProtocol implements ToolProtocol {
102
102
  createStreamFilter(): null;
103
103
  getProtocolTools(): ToolDefinition[];
104
104
  }
105
- export declare function createToolProtocol(mode: "api" | "inline" | "deferred" | "deferred-lookup"): ToolProtocol;
105
+ export declare function createToolProtocol(mode: "api" | "inline" | "deferred" | "deferred-lookup", extraCore?: string[]): ToolProtocol;
@@ -545,13 +545,15 @@ const CORE_TOOLS = [
545
545
  "bash", "read_file", "write_file", "edit_file",
546
546
  "grep", "glob", "ls",
547
547
  "list_skills",
548
+ "conversation_recall",
548
549
  ];
549
- export function createToolProtocol(mode) {
550
+ export function createToolProtocol(mode, extraCore = []) {
551
+ const core = extraCore.length === 0 ? CORE_TOOLS : [...CORE_TOOLS, ...extraCore];
550
552
  if (mode === "inline")
551
553
  return new InlineToolProtocol();
552
554
  if (mode === "deferred")
553
- return new DeferredToolProtocol(CORE_TOOLS);
555
+ return new DeferredToolProtocol(core);
554
556
  if (mode === "deferred-lookup")
555
- return new DeferredLookupProtocol(CORE_TOOLS);
557
+ return new DeferredLookupProtocol(core);
556
558
  return new ApiToolProtocol();
557
559
  }