agent-sh 0.12.20 → 0.12.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/agent/agent-loop.d.ts +1 -0
- package/dist/agent/agent-loop.js +30 -5
- package/dist/agent/conversation-state.d.ts +3 -2
- package/dist/agent/conversation-state.js +27 -14
- package/dist/agent/normalize-args.d.ts +29 -0
- package/dist/agent/normalize-args.js +56 -0
- package/dist/agent/subagent.js +2 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +16 -22
- package/dist/event-bus.d.ts +9 -2
- package/dist/event-bus.js +9 -0
- package/dist/extensions/agent-backend.js +104 -24
- package/dist/extensions/index.js +8 -3
- package/dist/extensions/providers/deepseek.d.ts +8 -0
- package/dist/extensions/providers/deepseek.js +23 -0
- package/dist/extensions/providers/openai-compatible.d.ts +7 -0
- package/dist/extensions/providers/openai-compatible.js +30 -0
- package/dist/extensions/providers/openai.d.ts +7 -0
- package/dist/extensions/providers/openai.js +39 -0
- package/dist/extensions/{openrouter.d.ts → providers/openrouter.d.ts} +1 -1
- package/dist/extensions/{openrouter.js → providers/openrouter.js} +5 -3
- package/dist/extensions/slash-commands.js +0 -24
- package/dist/extensions/tui-renderer.js +28 -15
- package/dist/index.js +8 -33
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +1 -0
- package/dist/types.d.ts +14 -1
- package/dist/utils/box-frame.js +14 -8
- package/dist/utils/llm-client.d.ts +5 -1
- package/dist/utils/llm-client.js +6 -1
- package/dist/utils/llm-facade.js +5 -5
- package/examples/extensions/pi-bridge/README.md +12 -19
- package/examples/extensions/pi-bridge/index.ts +307 -35
- package/package.json +1 -1
- package/dist/extensions/openai.d.ts +0 -9
- package/dist/extensions/openai.js +0 -49
|
@@ -2,6 +2,7 @@ import { AgentLoop } from "../agent/agent-loop.js";
|
|
|
2
2
|
import { LlmClient } from "../utils/llm-client.js";
|
|
3
3
|
import { resolveProvider, getProviderNames, getSettings } from "../settings.js";
|
|
4
4
|
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
5
|
+
import { discoverSkills } from "../agent/skills.js";
|
|
5
6
|
/** Read the user's persisted defaultModel for a provider, if any. */
|
|
6
7
|
function persistedModelFor(providerName) {
|
|
7
8
|
if (!providerName)
|
|
@@ -11,24 +12,50 @@ function persistedModelFor(providerName) {
|
|
|
11
12
|
function defaultReasoningBuilder(level) {
|
|
12
13
|
return level === "off" ? {} : { reasoning_effort: level };
|
|
13
14
|
}
|
|
15
|
+
function mergeCaps(settingsCaps, payloadCaps, modelIds) {
|
|
16
|
+
if (!settingsCaps)
|
|
17
|
+
return payloadCaps.size > 0 ? payloadCaps : undefined;
|
|
18
|
+
const out = new Map();
|
|
19
|
+
for (const id of modelIds) {
|
|
20
|
+
const s = settingsCaps.get(id);
|
|
21
|
+
const p = payloadCaps.get(id);
|
|
22
|
+
if (!s && !p)
|
|
23
|
+
continue;
|
|
24
|
+
out.set(id, {
|
|
25
|
+
reasoning: s?.reasoning ?? p?.reasoning,
|
|
26
|
+
contextWindow: s?.contextWindow ?? p?.contextWindow,
|
|
27
|
+
maxTokens: s?.maxTokens ?? p?.maxTokens,
|
|
28
|
+
echoReasoning: s?.echoReasoning ?? p?.echoReasoning,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return out.size > 0 ? out : undefined;
|
|
32
|
+
}
|
|
14
33
|
export default function agentBackend(ctx) {
|
|
15
34
|
const { bus } = ctx;
|
|
16
35
|
const config = ctx.call("config:get-shell-config") ?? {};
|
|
17
|
-
//
|
|
36
|
+
// Immutable settings snapshot; provider:register payloads merge against it.
|
|
18
37
|
const providerRegistry = new Map();
|
|
38
|
+
const settingsProviders = new Map();
|
|
19
39
|
for (const name of getProviderNames()) {
|
|
20
40
|
const p = resolveProvider(name);
|
|
21
|
-
if (p)
|
|
41
|
+
if (p) {
|
|
22
42
|
providerRegistry.set(name, p);
|
|
43
|
+
settingsProviders.set(name, p);
|
|
44
|
+
}
|
|
23
45
|
}
|
|
24
46
|
const providerHooks = new Map();
|
|
47
|
+
// Bakes model id into the hook so AgentMode.buildReasoningParams keeps
|
|
48
|
+
// its (level) signature while the hook can branch on model.
|
|
49
|
+
const bindReasoning = (shapeId, model) => {
|
|
50
|
+
const hook = providerHooks.get(shapeId)?.reasoningParams;
|
|
51
|
+
return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
|
|
52
|
+
};
|
|
25
53
|
const buildModes = () => {
|
|
26
54
|
const allModes = [];
|
|
27
55
|
for (const [id, p] of providerRegistry) {
|
|
28
56
|
if (!p.apiKey)
|
|
29
57
|
continue;
|
|
30
58
|
const shapeId = p.reasoningShape ?? id;
|
|
31
|
-
const buildReasoningParams = providerHooks.get(shapeId)?.reasoningParams ?? defaultReasoningBuilder;
|
|
32
59
|
for (const model of p.models) {
|
|
33
60
|
const mc = p.modelCapabilities?.get(model);
|
|
34
61
|
allModes.push({
|
|
@@ -40,7 +67,7 @@ export default function agentBackend(ctx) {
|
|
|
40
67
|
reasoning: mc?.reasoning,
|
|
41
68
|
supportsReasoningEffort: p.supportsReasoningEffort,
|
|
42
69
|
echoReasoning: mc?.echoReasoning,
|
|
43
|
-
buildReasoningParams,
|
|
70
|
+
buildReasoningParams: bindReasoning(shapeId, model),
|
|
44
71
|
});
|
|
45
72
|
}
|
|
46
73
|
}
|
|
@@ -55,11 +82,15 @@ export default function agentBackend(ctx) {
|
|
|
55
82
|
return llmClient.complete({
|
|
56
83
|
messages: messages,
|
|
57
84
|
max_tokens: opts?.maxTokens,
|
|
85
|
+
model: opts?.model,
|
|
86
|
+
reasoning_effort: opts?.reasoningEffort,
|
|
58
87
|
});
|
|
59
88
|
});
|
|
60
89
|
let modes = [];
|
|
61
90
|
let initialModeIndex = 0;
|
|
62
91
|
let resolved = false;
|
|
92
|
+
// Gates late-registration reconcile so its config:switch-model emit doesn't misroute under a non-ash backend.
|
|
93
|
+
let ashActive = false;
|
|
63
94
|
bus.onPipe("config:get-initial-modes", () => ({ modes, initialModeIndex }));
|
|
64
95
|
// AgentLoop must be constructed *before* user extensions activate,
|
|
65
96
|
// because its ctor defines handlers (history:append, etc.) that
|
|
@@ -76,7 +107,9 @@ export default function agentBackend(ctx) {
|
|
|
76
107
|
instanceId: ctx.instanceId,
|
|
77
108
|
history: config.history,
|
|
78
109
|
});
|
|
79
|
-
|
|
110
|
+
let loadedExtensionNames = [];
|
|
111
|
+
bus.on("core:extensions-loaded", ({ names }) => {
|
|
112
|
+
loadedExtensionNames = names;
|
|
80
113
|
const settings = getSettings();
|
|
81
114
|
// If the user didn't pick a default, fall back to the first registered
|
|
82
115
|
// provider (built-in load order biases to openrouter → openai).
|
|
@@ -121,9 +154,37 @@ export default function agentBackend(ctx) {
|
|
|
121
154
|
resolved = true;
|
|
122
155
|
bus.emit("agent:register-backend", {
|
|
123
156
|
name: "ash",
|
|
124
|
-
kill: () =>
|
|
157
|
+
kill: () => {
|
|
158
|
+
ashActive = false;
|
|
159
|
+
bus.emit("command:unregister", { name: "/compact" });
|
|
160
|
+
bus.emit("command:unregister", { name: "/context" });
|
|
161
|
+
agentLoop.kill();
|
|
162
|
+
},
|
|
125
163
|
start: async () => {
|
|
126
164
|
agentLoop.wire();
|
|
165
|
+
ashActive = true;
|
|
166
|
+
bus.emit("command:register", {
|
|
167
|
+
name: "/compact",
|
|
168
|
+
description: "Compact conversation via the active compaction strategy",
|
|
169
|
+
handler: () => bus.emit("agent:compact-request", {}),
|
|
170
|
+
});
|
|
171
|
+
bus.emit("command:register", {
|
|
172
|
+
name: "/context",
|
|
173
|
+
description: "Show context budget usage",
|
|
174
|
+
handler: () => {
|
|
175
|
+
const stats = bus.emitPipe("context:get-stats", {
|
|
176
|
+
activeTokens: 0,
|
|
177
|
+
totalTokens: 0,
|
|
178
|
+
budgetTokens: 0,
|
|
179
|
+
});
|
|
180
|
+
const pct = stats.budgetTokens > 0
|
|
181
|
+
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
182
|
+
: 0;
|
|
183
|
+
bus.emit("ui:info", {
|
|
184
|
+
message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
});
|
|
127
188
|
bus.emit("agent:info", {
|
|
128
189
|
name: "ash",
|
|
129
190
|
version: PACKAGE_VERSION,
|
|
@@ -142,46 +203,52 @@ export default function agentBackend(ctx) {
|
|
|
142
203
|
});
|
|
143
204
|
bus.on("provider:register", (p) => {
|
|
144
205
|
const rawModels = p.models ?? (p.defaultModel ? [p.defaultModel] : []);
|
|
145
|
-
const
|
|
146
|
-
const
|
|
206
|
+
const payloadModelIds = [];
|
|
207
|
+
const payloadCaps = new Map();
|
|
147
208
|
for (const m of rawModels) {
|
|
148
209
|
if (typeof m === "string") {
|
|
149
|
-
|
|
210
|
+
payloadModelIds.push(m);
|
|
150
211
|
}
|
|
151
212
|
else {
|
|
152
|
-
|
|
153
|
-
|
|
213
|
+
payloadModelIds.push(m.id);
|
|
214
|
+
payloadCaps.set(m.id, { reasoning: m.reasoning, contextWindow: m.contextWindow, maxTokens: m.maxTokens, echoReasoning: m.echoReasoning });
|
|
154
215
|
}
|
|
155
216
|
}
|
|
156
|
-
|
|
217
|
+
const settings = settingsProviders.get(p.id);
|
|
218
|
+
const modelIds = settings?.modelsExplicit && settings.models.length > 0 ? settings.models : payloadModelIds;
|
|
219
|
+
const mergedCaps = mergeCaps(settings?.modelCapabilities, payloadCaps, modelIds);
|
|
220
|
+
const merged = {
|
|
157
221
|
id: p.id,
|
|
158
|
-
apiKey: p.apiKey,
|
|
159
|
-
baseURL: p.baseURL,
|
|
160
|
-
defaultModel: p.defaultModel,
|
|
222
|
+
apiKey: settings?.apiKey ?? p.apiKey,
|
|
223
|
+
baseURL: settings?.baseURL ?? p.baseURL,
|
|
224
|
+
defaultModel: settings?.defaultModel ?? p.defaultModel,
|
|
161
225
|
models: modelIds,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
226
|
+
modelsExplicit: settings?.modelsExplicit ?? false,
|
|
227
|
+
contextWindow: settings?.contextWindow,
|
|
228
|
+
supportsReasoningEffort: settings?.supportsReasoningEffort ?? p.supportsReasoningEffort,
|
|
229
|
+
modelCapabilities: mergedCaps,
|
|
230
|
+
reasoningShape: settings?.reasoningShape,
|
|
231
|
+
};
|
|
232
|
+
providerRegistry.set(p.id, merged);
|
|
166
233
|
const addModes = modelIds.map((m) => {
|
|
167
|
-
const mc =
|
|
234
|
+
const mc = mergedCaps?.get(m);
|
|
168
235
|
return {
|
|
169
236
|
model: m,
|
|
170
237
|
provider: p.id,
|
|
171
|
-
providerConfig: { apiKey:
|
|
238
|
+
providerConfig: { apiKey: merged.apiKey ?? "", baseURL: merged.baseURL },
|
|
172
239
|
contextWindow: mc?.contextWindow,
|
|
173
240
|
maxTokens: mc?.maxTokens,
|
|
174
241
|
reasoning: mc?.reasoning,
|
|
175
|
-
supportsReasoningEffort:
|
|
242
|
+
supportsReasoningEffort: merged.supportsReasoningEffort,
|
|
176
243
|
echoReasoning: mc?.echoReasoning,
|
|
177
|
-
buildReasoningParams,
|
|
244
|
+
buildReasoningParams: bindReasoning(p.id, m),
|
|
178
245
|
};
|
|
179
246
|
});
|
|
180
247
|
bus.emit("config:add-modes", { modes: addModes });
|
|
181
248
|
// Late-registration reconcile: if this completes the user's persisted
|
|
182
249
|
// default (openrouter's async fetch delivers the full catalog after
|
|
183
250
|
// we've already fallen back to mode 0), quietly switch to it.
|
|
184
|
-
if (!resolved)
|
|
251
|
+
if (!resolved || !ashActive)
|
|
185
252
|
return;
|
|
186
253
|
const pendingProvider = getSettings().defaultProvider;
|
|
187
254
|
if (pendingProvider !== p.id)
|
|
@@ -225,4 +292,17 @@ export default function agentBackend(ctx) {
|
|
|
225
292
|
bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
|
|
226
293
|
bus.emit("config:changed", {});
|
|
227
294
|
});
|
|
295
|
+
bus.onPipe("banner:collect", (e) => {
|
|
296
|
+
const settings = getSettings();
|
|
297
|
+
if (settings.defaultBackend && settings.defaultBackend !== "ash")
|
|
298
|
+
return e;
|
|
299
|
+
if (loadedExtensionNames.length > 0) {
|
|
300
|
+
e.sections.push({ label: "Extensions", items: [...loadedExtensionNames] });
|
|
301
|
+
}
|
|
302
|
+
const skills = discoverSkills(ctx.call("cwd") ?? process.cwd());
|
|
303
|
+
if (skills.length > 0) {
|
|
304
|
+
e.sections.push({ label: "Skills", items: skills.map((s) => s.name) });
|
|
305
|
+
}
|
|
306
|
+
return e;
|
|
307
|
+
});
|
|
228
308
|
}
|
package/dist/extensions/index.js
CHANGED
|
@@ -3,10 +3,15 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
3
3
|
{ name: "agent-backend", load: () => import("./agent-backend.js").then(m => m.default) },
|
|
4
4
|
{ name: "openrouter",
|
|
5
5
|
when: () => !!process.env.OPENROUTER_API_KEY,
|
|
6
|
-
load: () => import("./openrouter.js").then(m => m.default) },
|
|
6
|
+
load: () => import("./providers/openrouter.js").then(m => m.default) },
|
|
7
7
|
{ name: "openai",
|
|
8
|
-
when: () => !!process.env.OPENAI_API_KEY,
|
|
9
|
-
load: () => import("./openai.js").then(m => m.default) },
|
|
8
|
+
when: () => !!process.env.OPENAI_API_KEY && !process.env.OPENAI_BASE_URL,
|
|
9
|
+
load: () => import("./providers/openai.js").then(m => m.default) },
|
|
10
|
+
{ name: "openai-compatible",
|
|
11
|
+
when: () => !!process.env.OPENAI_BASE_URL,
|
|
12
|
+
load: () => import("./providers/openai-compatible.js").then(m => m.default) },
|
|
13
|
+
{ name: "deepseek",
|
|
14
|
+
load: () => import("./providers/deepseek.js").then(m => m.default) },
|
|
10
15
|
{ name: "tui-renderer", load: () => import("./tui-renderer.js").then(m => m.default) },
|
|
11
16
|
{ name: "slash-commands", load: () => import("./slash-commands.js").then(m => m.default) },
|
|
12
17
|
{ name: "file-autocomplete", load: () => import("./file-autocomplete.js").then(m => m.default) },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native DeepSeek (api.deepseek.com). V4 ignores reasoning_effort for
|
|
3
|
+
* on/off — disable lives in a separate `thinking` field that defaults
|
|
4
|
+
* to enabled. The hook always attaches; provider registration via env
|
|
5
|
+
* is opt-in alongside any settings.json entry.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionContext } from "../../types.js";
|
|
8
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const BASE_URL = "https://api.deepseek.com";
|
|
2
|
+
const DEFAULT_MODELS = [
|
|
3
|
+
{ id: "deepseek-v4-flash", reasoning: true, echoReasoning: true },
|
|
4
|
+
{ id: "deepseek-v4-pro", reasoning: true, echoReasoning: true },
|
|
5
|
+
];
|
|
6
|
+
function buildReasoningParams(level, _model) {
|
|
7
|
+
return level === "off"
|
|
8
|
+
? { thinking: { type: "disabled" } }
|
|
9
|
+
: { thinking: { type: "enabled" }, reasoning_effort: level };
|
|
10
|
+
}
|
|
11
|
+
export default function activate(ctx) {
|
|
12
|
+
ctx.providers.configure("deepseek", { reasoningParams: buildReasoningParams });
|
|
13
|
+
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
14
|
+
if (!apiKey)
|
|
15
|
+
return;
|
|
16
|
+
ctx.bus.emit("provider:register", {
|
|
17
|
+
id: "deepseek",
|
|
18
|
+
apiKey,
|
|
19
|
+
baseURL: BASE_URL,
|
|
20
|
+
defaultModel: DEFAULT_MODELS[0].id,
|
|
21
|
+
models: DEFAULT_MODELS,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions-compatible local/3rd-party server (Ollama, LM
|
|
3
|
+
* Studio, vLLM, llama.cpp, …). No reasoning hook — the right shape depends
|
|
4
|
+
* on which model the server is serving; user extensions can add one.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionContext } from "../../types.js";
|
|
7
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export default function activate(ctx) {
|
|
2
|
+
const baseURL = process.env.OPENAI_BASE_URL;
|
|
3
|
+
if (!baseURL)
|
|
4
|
+
return;
|
|
5
|
+
// Local servers often need no key; SDK still wants a non-empty string.
|
|
6
|
+
const apiKey = process.env.OPENAI_API_KEY || "no-key";
|
|
7
|
+
const id = "openai-compatible";
|
|
8
|
+
ctx.bus.emit("provider:register", { id, apiKey, baseURL, models: [] });
|
|
9
|
+
fetchModels(baseURL, apiKey).then((models) => {
|
|
10
|
+
if (models.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
ctx.bus.emit("provider:register", {
|
|
13
|
+
id,
|
|
14
|
+
apiKey,
|
|
15
|
+
baseURL,
|
|
16
|
+
defaultModel: models[0],
|
|
17
|
+
models,
|
|
18
|
+
});
|
|
19
|
+
}).catch(() => { });
|
|
20
|
+
}
|
|
21
|
+
async function fetchModels(baseURL, apiKey) {
|
|
22
|
+
const headers = {};
|
|
23
|
+
if (apiKey && apiKey !== "no-key")
|
|
24
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
25
|
+
const res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, { headers });
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
return [];
|
|
28
|
+
const data = await res.json();
|
|
29
|
+
return (data.data ?? []).map((m) => m.id);
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud OpenAI (api.openai.com). reasoning_effort vocabulary diverges per
|
|
3
|
+
* family: o-series has no off; gpt-5-codex floors at "low"; plain gpt-5
|
|
4
|
+
* floors at "minimal"; gpt-5.1+ accepts "none" as documented full off.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionContext } from "../../types.js";
|
|
7
|
+
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const 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
|
+
];
|
|
9
|
+
function offEffortFor(model) {
|
|
10
|
+
if (/^o\d/.test(model))
|
|
11
|
+
return null;
|
|
12
|
+
if (model.startsWith("gpt-5-codex"))
|
|
13
|
+
return "low";
|
|
14
|
+
if (/^gpt-5\.[1-9]/.test(model))
|
|
15
|
+
return "none";
|
|
16
|
+
if (/^gpt-5(?!\.)/.test(model))
|
|
17
|
+
return "minimal";
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function buildReasoningParams(level, model) {
|
|
21
|
+
if (level !== "off")
|
|
22
|
+
return { reasoning_effort: level };
|
|
23
|
+
const off = model ? offEffortFor(model) : null;
|
|
24
|
+
return off ? { reasoning_effort: off } : {};
|
|
25
|
+
}
|
|
26
|
+
export default function activate(ctx) {
|
|
27
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
28
|
+
if (!apiKey)
|
|
29
|
+
return;
|
|
30
|
+
if (process.env.OPENAI_BASE_URL)
|
|
31
|
+
return; // openai-compatible handles this
|
|
32
|
+
ctx.providers.configure("openai", { reasoningParams: buildReasoningParams });
|
|
33
|
+
ctx.bus.emit("provider:register", {
|
|
34
|
+
id: "openai",
|
|
35
|
+
apiKey,
|
|
36
|
+
defaultModel: CLOUD_MODELS[0].id,
|
|
37
|
+
models: CLOUD_MODELS,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -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 "
|
|
6
|
+
import type { ExtensionContext } from "../../types.js";
|
|
7
7
|
export default function activate(ctx: ExtensionContext): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getSettings } from "
|
|
1
|
+
import { getSettings } from "../../settings.js";
|
|
2
2
|
const BASE_URL = "https://openrouter.ai/api/v1";
|
|
3
3
|
const DEFAULT_MODELS = ["deepseek/deepseek-v4-flash"];
|
|
4
4
|
// Built-in defaults for models requiring reasoning_content echoed back
|
|
@@ -6,9 +6,11 @@ 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
|
-
|
|
9
|
+
// `effort: "none"` is the documented disable; honored by OpenAI/Grok, ignored
|
|
10
|
+
// by Anthropic/Gemini/DeepSeek-via-OpenRouter (use native deepseek for a hard off).
|
|
11
|
+
function buildReasoningParams(level, _model) {
|
|
10
12
|
return level === "off"
|
|
11
|
-
? { reasoning: {
|
|
13
|
+
? { reasoning: { effort: "none" } }
|
|
12
14
|
: { reasoning: { effort: level } };
|
|
13
15
|
}
|
|
14
16
|
export default function activate(ctx) {
|
|
@@ -76,30 +76,6 @@ export default function activate(ctx) {
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
});
|
|
79
|
-
register({
|
|
80
|
-
name: "/compact",
|
|
81
|
-
description: "Compact conversation via the active compaction strategy",
|
|
82
|
-
handler: () => {
|
|
83
|
-
bus.emit("agent:compact-request", {});
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
register({
|
|
87
|
-
name: "/context",
|
|
88
|
-
description: "Show context budget usage",
|
|
89
|
-
handler: () => {
|
|
90
|
-
const stats = bus.emitPipe("context:get-stats", {
|
|
91
|
-
activeTokens: 0,
|
|
92
|
-
totalTokens: 0,
|
|
93
|
-
budgetTokens: 0,
|
|
94
|
-
});
|
|
95
|
-
const pct = stats.budgetTokens > 0
|
|
96
|
-
? Math.round((stats.activeTokens / stats.budgetTokens) * 100)
|
|
97
|
-
: 0;
|
|
98
|
-
bus.emit("ui:info", {
|
|
99
|
-
message: `Active context: ~${stats.activeTokens.toLocaleString()} tokens / ${stats.budgetTokens.toLocaleString()} budget (${pct}%)`,
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
79
|
register({
|
|
104
80
|
name: "/reload",
|
|
105
81
|
description: "Reload user extensions from ~/.agent-sh/extensions/",
|
|
@@ -67,6 +67,8 @@ function createRenderState() {
|
|
|
67
67
|
isThinking: false,
|
|
68
68
|
showThinkingText: false,
|
|
69
69
|
thinkingPending: false,
|
|
70
|
+
previewedDiffPending: false,
|
|
71
|
+
previewedDiffToolIds: new Set(),
|
|
70
72
|
};
|
|
71
73
|
}
|
|
72
74
|
export default function activate(ctx) {
|
|
@@ -175,21 +177,20 @@ export default function activate(ctx) {
|
|
|
175
177
|
s.thinkingPending = true;
|
|
176
178
|
if (!s.isThinking) {
|
|
177
179
|
s.isThinking = true;
|
|
178
|
-
if (s.showThinkingText)
|
|
179
|
-
stopCurrentSpinner();
|
|
180
|
-
if (!s.renderer)
|
|
181
|
-
startAgentResponse();
|
|
182
|
-
}
|
|
183
|
-
else {
|
|
180
|
+
if (!s.showThinkingText)
|
|
184
181
|
startThinkingSpinner();
|
|
185
|
-
}
|
|
186
182
|
}
|
|
187
|
-
if (s.showThinkingText
|
|
188
|
-
|
|
183
|
+
if (s.showThinkingText) {
|
|
184
|
+
stopCurrentSpinner();
|
|
189
185
|
if (!s.renderer)
|
|
190
186
|
startAgentResponse();
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
if (e.text) {
|
|
188
|
+
s.thinkingPending = false;
|
|
189
|
+
// Wrap each sub-line so dim survives \n boundaries in the renderer.
|
|
190
|
+
const wrapped = `${p.dim}${e.text.replace(/\n/g, `${p.reset}\n${p.dim}`)}${p.reset}`;
|
|
191
|
+
s.renderer.push(wrapped);
|
|
192
|
+
drain();
|
|
193
|
+
}
|
|
193
194
|
}
|
|
194
195
|
});
|
|
195
196
|
bus.on("agent:response-chunk", (e) => {
|
|
@@ -272,6 +273,10 @@ export default function activate(ctx) {
|
|
|
272
273
|
s.currentToolKind = e.kind;
|
|
273
274
|
s.toolStartTime = Date.now();
|
|
274
275
|
s.orphanContHeaderKind = undefined;
|
|
276
|
+
if (s.previewedDiffPending && e.toolCallId) {
|
|
277
|
+
s.previewedDiffToolIds.add(e.toolCallId);
|
|
278
|
+
}
|
|
279
|
+
s.previewedDiffPending = false;
|
|
275
280
|
if (e.title === "user_shell") {
|
|
276
281
|
finalizeToolGroup();
|
|
277
282
|
closeToolLine();
|
|
@@ -335,11 +340,18 @@ export default function activate(ctx) {
|
|
|
335
340
|
s.toolExitCode = e.exitCode;
|
|
336
341
|
if (e.exitCode !== 0)
|
|
337
342
|
s.toolGroupAllOk = false;
|
|
343
|
+
let resultDisplay = e.resultDisplay;
|
|
344
|
+
if (e.toolCallId && s.previewedDiffToolIds.has(e.toolCallId)) {
|
|
345
|
+
s.previewedDiffToolIds.delete(e.toolCallId);
|
|
346
|
+
if (resultDisplay?.body?.kind === "diff") {
|
|
347
|
+
resultDisplay = { ...resultDisplay, body: undefined };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
338
350
|
if (s.toolGroupKind) {
|
|
339
351
|
// Grouped tool — track success/failure and summaries, show aggregate on ⎿ line.
|
|
340
352
|
// Don't restart spinner between grouped tools — it's already running from group start.
|
|
341
|
-
if (
|
|
342
|
-
s.toolGroupSummaries.push(
|
|
353
|
+
if (resultDisplay?.summary)
|
|
354
|
+
s.toolGroupSummaries.push(resultDisplay.summary);
|
|
343
355
|
if (e.toolCallId)
|
|
344
356
|
s.pendingToolCompletes.delete(e.toolCallId);
|
|
345
357
|
s.toolGroupCompletedCount++;
|
|
@@ -358,10 +370,10 @@ export default function activate(ctx) {
|
|
|
358
370
|
if (pending)
|
|
359
371
|
s.pendingToolCompletes.delete(e.toolCallId);
|
|
360
372
|
if (pending?.orphaned) {
|
|
361
|
-
showOrphanedComplete(e.exitCode,
|
|
373
|
+
showOrphanedComplete(e.exitCode, resultDisplay, pending.title, pending.kind, pending.displayDetail);
|
|
362
374
|
}
|
|
363
375
|
else {
|
|
364
|
-
showToolComplete(e.exitCode,
|
|
376
|
+
showToolComplete(e.exitCode, resultDisplay, pending?.displayDetail ?? pending?.title);
|
|
365
377
|
}
|
|
366
378
|
s.currentToolKind = undefined;
|
|
367
379
|
s.spinnerStartTime = 0;
|
|
@@ -432,6 +444,7 @@ export default function activate(ctx) {
|
|
|
432
444
|
// Mark lastContentKind as "tool" so the tool call line that follows
|
|
433
445
|
// doesn't inject an extra gap between the diff box and the checkmark.
|
|
434
446
|
s.lastContentKind = "tool";
|
|
447
|
+
s.previewedDiffPending = true;
|
|
435
448
|
}
|
|
436
449
|
// Don't endAgentResponse() here — permission requests that aren't
|
|
437
450
|
// file-write diffs are handled inline (auto-approved or by extensions).
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import { palette as p } from "./utils/palette.js";
|
|
|
7
7
|
import { loadBuiltinExtensions } from "./extensions/index.js";
|
|
8
8
|
import { loadExtensions } from "./extension-loader.js";
|
|
9
9
|
import { getSettings } from "./settings.js";
|
|
10
|
-
import { discoverSkills } from "./agent/skills.js";
|
|
11
10
|
import { runInit } from "./init.js";
|
|
12
11
|
import { PACKAGE_VERSION } from "./utils/package-version.js";
|
|
13
12
|
/**
|
|
@@ -270,11 +269,8 @@ async function main() {
|
|
|
270
269
|
if (process.env.DEBUG) {
|
|
271
270
|
console.error('[agent-sh] Extensions loaded');
|
|
272
271
|
}
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
core.bus.emit("core:extensions-loaded", {});
|
|
276
|
-
// ── Discover skills ───────────────────────────────────────────
|
|
277
|
-
const skills = discoverSkills(process.cwd());
|
|
272
|
+
// Names ride along so backend extensions can build banner sections.
|
|
273
|
+
core.bus.emit("core:extensions-loaded", { names: loadedExtensions });
|
|
278
274
|
// ── Activate agent backend ────────────────────────────────────
|
|
279
275
|
// Extensions had their chance to register via agent:register-backend.
|
|
280
276
|
// If none did, the built-in AgentLoop gets wired to bus events.
|
|
@@ -288,6 +284,7 @@ async function main() {
|
|
|
288
284
|
" Alternatively, install a bridge extension (claude-code-bridge, pi-bridge).\n");
|
|
289
285
|
process.exit(1);
|
|
290
286
|
}
|
|
287
|
+
// No await: banner must out-race the shell's PS1 arriving via PTY.
|
|
291
288
|
core.activateBackend();
|
|
292
289
|
// ── Startup banner ───────────────────────────────────────────
|
|
293
290
|
const settings = getSettings();
|
|
@@ -295,31 +292,11 @@ async function main() {
|
|
|
295
292
|
const termW = process.stdout.columns || 80;
|
|
296
293
|
const bannerW = Math.min(termW, 60);
|
|
297
294
|
const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const model = info?.model;
|
|
302
|
-
const provider = info?.provider;
|
|
303
|
-
const modelValue = model
|
|
304
|
-
? provider ? `${model} [${provider}]` : model
|
|
305
|
-
: null;
|
|
295
|
+
const backendName = settings.defaultBackend && backendNames.includes(settings.defaultBackend)
|
|
296
|
+
? settings.defaultBackend
|
|
297
|
+
: backendNames[0];
|
|
306
298
|
let sections = "";
|
|
307
|
-
sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${
|
|
308
|
-
if (modelValue) {
|
|
309
|
-
sections += `\n ${p.muted}Model:${p.reset} ${p.dim}${modelValue}${p.reset}`;
|
|
310
|
-
}
|
|
311
|
-
if (loadedExtensions.length > 0) {
|
|
312
|
-
sections += `\n\n ${p.muted}Extensions:${p.reset}`;
|
|
313
|
-
for (const name of loadedExtensions) {
|
|
314
|
-
sections += `\n ${p.dim}${name}${p.reset}`;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (skills.length > 0) {
|
|
318
|
-
sections += `\n\n ${p.muted}Skills:${p.reset}`;
|
|
319
|
-
for (const s of skills) {
|
|
320
|
-
sections += `\n ${p.dim}${s.name}${p.reset}`;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
299
|
+
sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${p.reset}`;
|
|
323
300
|
const extSections = bus.emitPipe("banner:collect", { sections: [] }).sections;
|
|
324
301
|
for (const sec of extSections) {
|
|
325
302
|
sections += `\n\n ${p.muted}${sec.label}:${p.reset}`;
|
|
@@ -327,9 +304,7 @@ async function main() {
|
|
|
327
304
|
sections += `\n ${p.dim}${item}${p.reset}`;
|
|
328
305
|
}
|
|
329
306
|
}
|
|
330
|
-
const hint =
|
|
331
|
-
? `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`
|
|
332
|
-
: `${p.muted}Set ${p.warning}OPENROUTER_API_KEY${p.muted} or ${p.warning}OPENAI_API_KEY${p.muted} and restart to enable AI${p.reset}`;
|
|
307
|
+
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
|
|
333
308
|
const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
|
|
334
309
|
process.stdout.write("\n" + borderLine + "\n" +
|
|
335
310
|
" " + productName +
|
package/dist/settings.d.ts
CHANGED
|
@@ -143,6 +143,8 @@ export interface ResolvedProvider {
|
|
|
143
143
|
baseURL?: string;
|
|
144
144
|
defaultModel?: string;
|
|
145
145
|
models: string[];
|
|
146
|
+
/** User explicitly listed `models` (locks the catalog to that list). */
|
|
147
|
+
modelsExplicit: boolean;
|
|
146
148
|
contextWindow?: number;
|
|
147
149
|
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
148
150
|
supportsReasoningEffort?: boolean;
|
package/dist/settings.js
CHANGED
|
@@ -160,6 +160,7 @@ export function resolveProvider(name) {
|
|
|
160
160
|
baseURL: provider.baseURL,
|
|
161
161
|
defaultModel,
|
|
162
162
|
models: modelIds.length ? modelIds : (defaultModel ? [defaultModel] : []),
|
|
163
|
+
modelsExplicit: Array.isArray(provider.models),
|
|
163
164
|
contextWindow: provider.contextWindow,
|
|
164
165
|
modelCapabilities: caps.size > 0 ? caps : undefined,
|
|
165
166
|
reasoningShape: provider.reasoningShape,
|
package/dist/types.d.ts
CHANGED
|
@@ -67,14 +67,27 @@ export interface LlmSession {
|
|
|
67
67
|
}
|
|
68
68
|
export interface LlmInterface {
|
|
69
69
|
readonly available: boolean;
|
|
70
|
+
/** `model` overrides the globally-configured model for this call only.
|
|
71
|
+
* Provider-specific identifier (e.g. "claude-haiku-4-5"). When omitted,
|
|
72
|
+
* the active provider's configured default is used.
|
|
73
|
+
*
|
|
74
|
+
* `reasoningEffort` controls thinking-model token allocation between
|
|
75
|
+
* reasoning and final content (e.g. "low", "medium", "high", or
|
|
76
|
+
* provider-specific). For non-reasoning models it is ignored. Set to
|
|
77
|
+
* "low" for cheap structured-output calls so reasoning doesn't exhaust
|
|
78
|
+
* the max-tokens budget and leave content empty. */
|
|
70
79
|
ask(opts: {
|
|
71
80
|
query: string;
|
|
72
81
|
system?: string;
|
|
73
82
|
maxTokens?: number;
|
|
83
|
+
model?: string;
|
|
84
|
+
reasoningEffort?: string;
|
|
74
85
|
}): Promise<string>;
|
|
75
86
|
session(opts?: {
|
|
76
87
|
system?: string;
|
|
77
88
|
maxTokens?: number;
|
|
89
|
+
model?: string;
|
|
90
|
+
reasoningEffort?: string;
|
|
78
91
|
}): LlmSession;
|
|
79
92
|
}
|
|
80
93
|
export interface AgentShellConfig {
|
|
@@ -158,7 +171,7 @@ export interface ExtensionContext {
|
|
|
158
171
|
}) => () => void;
|
|
159
172
|
providers: {
|
|
160
173
|
configure: (id: string, opts: {
|
|
161
|
-
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
174
|
+
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
162
175
|
}) => void;
|
|
163
176
|
};
|
|
164
177
|
llm: LlmInterface;
|