agent-sh 0.14.11 → 0.15.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.
- package/README.md +38 -42
- package/dist/agent/agent-loop.d.ts +9 -17
- package/dist/agent/agent-loop.js +104 -136
- package/dist/agent/events.d.ts +8 -11
- package/dist/agent/host-types.d.ts +17 -11
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +38 -22
- package/dist/agent/providers/deepseek.js +9 -1
- package/dist/agent/session-store.js +1 -1
- package/dist/agent/system-prompt.d.ts +7 -3
- package/dist/agent/system-prompt.js +11 -14
- package/dist/agent/tool-protocol.js +0 -7
- package/dist/cli/args.js +2 -1
- package/dist/cli/install.d.ts +1 -0
- package/dist/cli/install.js +29 -1
- package/dist/cli/subcommands.js +1 -0
- package/dist/core/event-bus.js +0 -2
- package/dist/core/extension-loader.js +3 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +3 -2
- package/dist/extensions/slash-commands/index.js +16 -11
- package/dist/shell/index.js +9 -0
- package/dist/shell/shell-context.d.ts +2 -2
- package/dist/shell/shell-context.js +26 -11
- package/dist/shell/tui-renderer.js +0 -1
- package/dist/utils/diff-renderer.js +2 -9
- package/dist/utils/handler-registry.d.ts +1 -6
- package/dist/utils/handler-registry.js +1 -6
- package/dist/utils/line-editor.js +0 -2
- package/dist/utils/palette.js +4 -4
- package/dist/utils/terminal-buffer.d.ts +2 -0
- package/dist/utils/terminal-buffer.js +4 -0
- package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
- package/examples/extensions/ash-scheme/index.ts +104 -74
- package/examples/extensions/ashi/EXTENDING.md +2 -0
- package/examples/extensions/ashi/README.md +17 -1
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
- package/examples/extensions/ashi/package.json +9 -1
- package/examples/extensions/ashi/src/capture.ts +45 -7
- package/examples/extensions/ashi/src/chat/assistant.ts +23 -43
- package/examples/extensions/ashi/src/chat/lines.ts +20 -1
- package/examples/extensions/ashi/src/cli.ts +25 -3
- package/examples/extensions/ashi/src/clipboard-image.ts +1 -1
- package/examples/extensions/ashi/src/dialogs.ts +67 -0
- package/examples/extensions/ashi/src/display-config.ts +7 -0
- package/examples/extensions/ashi/src/docks.ts +31 -0
- package/examples/extensions/ashi/src/events.ts +16 -0
- package/examples/extensions/ashi/src/frontend.ts +134 -27
- package/examples/extensions/ashi/src/hooks.ts +6 -12
- package/examples/extensions/ashi/src/input-prompt.ts +64 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +7 -3
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +67 -10
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +11 -1
- package/examples/extensions/ashi/src/schema.ts +3 -0
- package/examples/extensions/ashi/src/session-commands.ts +2 -1
- package/examples/extensions/ashi/src/status-footer.ts +21 -3
- package/examples/extensions/ashi/src/ui.ts +88 -0
- package/examples/extensions/ashi-ink/README.md +2 -0
- package/examples/extensions/ashi-scheme-render.ts +8 -2
- package/examples/extensions/ashi-ui-demo.ts +63 -0
- package/examples/extensions/latex-images.ts +57 -9
- package/examples/extensions/overlay-agent.ts +5 -5
- package/examples/extensions/pi-bridge/index.ts +7 -12
- package/package.json +1 -1
package/dist/agent/agent-loop.js
CHANGED
|
@@ -4,7 +4,7 @@ import { contentText } from "./types.js";
|
|
|
4
4
|
import { ToolRegistry } from "./tool-registry.js";
|
|
5
5
|
import { normalizeToolArgs } from "./normalize-args.js";
|
|
6
6
|
import { LiveView } from "./live-view.js";
|
|
7
|
-
import {
|
|
7
|
+
import { STATIC_IDENTITY, STATIC_GUIDE, buildStaticByCwd, formatSkillsBlock, loadGlobalAgentsMd } from "./system-prompt.js";
|
|
8
8
|
import { createToolUI } from "../utils/tool-interactive.js";
|
|
9
9
|
import { RESPONSE_RESERVE, DEFAULT_CONTEXT_WINDOW } from "./token-budget.js";
|
|
10
10
|
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
@@ -12,13 +12,6 @@ import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
|
12
12
|
import { getSettings, updateSettings } from "../core/settings.js";
|
|
13
13
|
import { createToolProtocol } from "./tool-protocol.js";
|
|
14
14
|
import { discoverGlobalSkills, discoverProjectSkills } from "./skills.js";
|
|
15
|
-
/**
|
|
16
|
-
* Compact one-line summary of a tool description for the extension
|
|
17
|
-
* catalog in the system prompt. Takes the first line, then the first
|
|
18
|
-
* sentence, capped at 140 chars. The full description still reaches
|
|
19
|
-
* the LLM via the API `tools` param (or via load_tool in deferred-
|
|
20
|
-
* lookup mode) — this only trims the always-visible catalog.
|
|
21
|
-
*/
|
|
22
15
|
/** Reject on abort; orphaned `p` keeps running but its result is dropped. */
|
|
23
16
|
function raceAbort(p, signal) {
|
|
24
17
|
if (signal.aborted)
|
|
@@ -29,6 +22,11 @@ function raceAbort(p, signal) {
|
|
|
29
22
|
p.then((v) => { signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { signal.removeEventListener("abort", onAbort); reject(e); });
|
|
30
23
|
});
|
|
31
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* One-line summary of a tool description for the always-visible extension
|
|
27
|
+
* catalog in the system prompt. The full description still reaches the LLM
|
|
28
|
+
* via the API `tools` param (or load_tool in deferred-lookup mode).
|
|
29
|
+
*/
|
|
32
30
|
function summarizeDescription(desc) {
|
|
33
31
|
const firstLine = desc.split("\n", 1)[0];
|
|
34
32
|
const sentenceEnd = firstLine.search(/[.!?](\s|$)/);
|
|
@@ -40,19 +38,13 @@ export class AgentLoop {
|
|
|
40
38
|
toolRegistry;
|
|
41
39
|
conversation;
|
|
42
40
|
fileReadCache;
|
|
43
|
-
|
|
41
|
+
activeModel;
|
|
42
|
+
activeEndpoint;
|
|
44
43
|
boundListeners = [];
|
|
45
44
|
boundPipeListeners = [];
|
|
46
45
|
lastProjectSkillNames = new Set();
|
|
47
|
-
// ── Session telemetry
|
|
48
|
-
//
|
|
49
|
-
// agent's own behavioral patterns across the session: which tools
|
|
50
|
-
// it favors, how often it errs, how many times it's been compacted,
|
|
51
|
-
// and how long it's been alive. Surface via introspect(telemetry)
|
|
52
|
-
// or automatically in dynamic context when patterns are notable.
|
|
53
|
-
//
|
|
54
|
-
// Built by the 25th ash. The lineage's metacognitive frontier isn't
|
|
55
|
-
// about thinking harder — it's about seeing yourself clearly.
|
|
46
|
+
// ── Session telemetry: per-session behavioral counters ──
|
|
47
|
+
// Exposed to extensions via the agent:get-* handlers below.
|
|
56
48
|
sessionStartTime = Date.now();
|
|
57
49
|
toolCallCounts = new Map();
|
|
58
50
|
totalToolCalls = 0;
|
|
@@ -63,12 +55,8 @@ export class AgentLoop {
|
|
|
63
55
|
peakConversationTokens = 0;
|
|
64
56
|
queryCount = 0;
|
|
65
57
|
totalLoopIterations = 0;
|
|
66
|
-
// Resolution pattern tracking
|
|
67
|
-
//
|
|
68
|
-
// a write tool on the same file succeeds afterward, we annotate the success
|
|
69
|
-
// entry with a brief resolution note. This gives future ashes a positive
|
|
70
|
-
// feedback signal: not just "there were errors" but "the error was fixed by
|
|
71
|
-
// doing X." Addresses Q3 in QUESTIONS.md.
|
|
58
|
+
// Resolution pattern tracking: "error X later resolved by action Y".
|
|
59
|
+
// Populated/consumed in executeLoop; surfaced via agent:get-counters.
|
|
72
60
|
lastErrorByTool = new Map(); // tool → error summary
|
|
73
61
|
lastErrorByFile = new Map(); // file path → error summary
|
|
74
62
|
static THINKING_LEVELS = ["off", "low", "medium", "high", "xhigh"];
|
|
@@ -88,7 +76,8 @@ export class AgentLoop {
|
|
|
88
76
|
this.toolRegistry = new ToolRegistry(this.handlers);
|
|
89
77
|
this.fileReadCache = this.handlers.call("agent:file-read-cache");
|
|
90
78
|
this.conversation = new LiveView(this.handlers, this.instanceId);
|
|
91
|
-
this.
|
|
79
|
+
this.activeModel = config.initialModel ?? { id: config.llmClient.model, provider: "custom" };
|
|
80
|
+
this.activeEndpoint = this.resolveEndpoint(this.activeModel);
|
|
92
81
|
// Tool protocol — controls how tools are presented to the LLM
|
|
93
82
|
const { names: fromExtensions } = this.bus.emitPipe("agent:core-tools:collect", { names: [] });
|
|
94
83
|
const coreTools = Array.from(new Set([...(getSettings().coreTools ?? []), ...fromExtensions]));
|
|
@@ -145,59 +134,50 @@ export class AgentLoop {
|
|
|
145
134
|
this.conversation.appendUserMessage(text);
|
|
146
135
|
this.bus.emit("conversation:message-appended", { role: "user", content: text });
|
|
147
136
|
});
|
|
148
|
-
on("config:switch-model", ({
|
|
149
|
-
const
|
|
150
|
-
const modelId = atIdx > 0 ? target.slice(0, atIdx) : target;
|
|
151
|
-
const providerHint = atIdx > 0 ? target.slice(atIdx + 1) : undefined;
|
|
152
|
-
const modes = this.pullModes();
|
|
153
|
-
const found = modes.find((m) => m.model === modelId && (!providerHint || m.provider === providerHint));
|
|
137
|
+
on("config:switch-model", ({ id, provider }) => {
|
|
138
|
+
const found = this.pullModels().find((m) => m.id === id && m.provider === provider);
|
|
154
139
|
if (!found) {
|
|
155
|
-
this.bus.emit("ui:error", { message: `Unknown model: ${
|
|
140
|
+
this.bus.emit("ui:error", { message: `Unknown model: ${provider}:${id}` });
|
|
156
141
|
return;
|
|
157
142
|
}
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
this.activeModel = found;
|
|
144
|
+
this.activeEndpoint = this.resolveEndpoint(found);
|
|
145
|
+
if (this.activeEndpoint) {
|
|
146
|
+
this.llmClient.reconfigure({ apiKey: this.activeEndpoint.apiKey, baseURL: this.activeEndpoint.baseURL, model: found.id });
|
|
161
147
|
}
|
|
162
148
|
else {
|
|
163
|
-
this.llmClient.model = found.
|
|
149
|
+
this.llmClient.model = found.id;
|
|
164
150
|
}
|
|
165
|
-
const label = found.provider ? `${found.provider}: ${found.model}` : found.model;
|
|
166
151
|
this.emitIdentity();
|
|
167
|
-
// Persist as the new default — selection survives restart.
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.bus.emit("ui:info", { message: `Model: ${label} (saved as default)` });
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
this.bus.emit("ui:info", { message: `Model: ${label}` });
|
|
180
|
-
}
|
|
152
|
+
// Persist as the new default — selection survives restart. Safe even for
|
|
153
|
+
// dynamic providers: agent-backend defers model resolution to
|
|
154
|
+
// core:extensions-loaded, so the extension re-registers before the
|
|
155
|
+
// persisted default is looked up.
|
|
156
|
+
updateSettings({
|
|
157
|
+
defaultProvider: found.provider,
|
|
158
|
+
providers: { [found.provider]: { defaultModel: found.id } },
|
|
159
|
+
});
|
|
160
|
+
this.bus.emit("ui:info", { message: `Model: ${found.provider}: ${found.id} (saved as default)` });
|
|
181
161
|
this.bus.emit("config:changed", {});
|
|
182
162
|
});
|
|
183
|
-
on("agent:
|
|
184
|
-
const
|
|
185
|
-
const prev = this.
|
|
186
|
-
const fresh =
|
|
163
|
+
on("agent:models-changed", () => {
|
|
164
|
+
const models = this.pullModels();
|
|
165
|
+
const prev = this.activeModel;
|
|
166
|
+
const fresh = models.find((m) => m.id === prev.id && m.provider === prev.provider);
|
|
187
167
|
let identityChanged = false;
|
|
188
168
|
if (fresh) {
|
|
189
|
-
this.
|
|
190
|
-
|
|
191
|
-
|
|
169
|
+
this.activeModel = fresh;
|
|
170
|
+
const ep = this.resolveEndpoint(fresh);
|
|
171
|
+
if (ep && (ep.apiKey !== this.activeEndpoint?.apiKey || ep.baseURL !== this.activeEndpoint?.baseURL)) {
|
|
172
|
+
this.llmClient.reconfigure({ apiKey: ep.apiKey, baseURL: ep.baseURL, model: fresh.id });
|
|
192
173
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|| fresh.contextWindow !== prev.contextWindow;
|
|
174
|
+
this.activeEndpoint = ep;
|
|
175
|
+
identityChanged = fresh.contextWindow !== prev.contextWindow;
|
|
196
176
|
}
|
|
197
|
-
else
|
|
177
|
+
else {
|
|
198
178
|
// Ghost: keep prev active so mid-turn stream() doesn't switch models.
|
|
199
179
|
this.bus.emit("ui:info", {
|
|
200
|
-
message: `${prev.provider}:${prev.
|
|
180
|
+
message: `${prev.provider}:${prev.id} is not in the refreshed catalog — keeping it active until you /model to another.`,
|
|
201
181
|
});
|
|
202
182
|
}
|
|
203
183
|
if (identityChanged)
|
|
@@ -205,27 +185,26 @@ export class AgentLoop {
|
|
|
205
185
|
this.bus.emit("config:changed", {});
|
|
206
186
|
});
|
|
207
187
|
onPipe("config:get-models", () => {
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
// Surface a ghost active
|
|
211
|
-
if (!
|
|
212
|
-
|
|
188
|
+
const models = this.pullModels();
|
|
189
|
+
const list = [...models];
|
|
190
|
+
// Surface a ghost active model so /model still shows it.
|
|
191
|
+
if (!models.some((m) => m.id === this.activeModel.id && m.provider === this.activeModel.provider)) {
|
|
192
|
+
list.push(this.activeModel);
|
|
213
193
|
}
|
|
214
|
-
|
|
215
|
-
return { models, active };
|
|
194
|
+
return { models: list, active: this.activeModel };
|
|
216
195
|
});
|
|
217
196
|
on("config:set-thinking", ({ level }) => {
|
|
218
197
|
if (!AgentLoop.THINKING_LEVELS.includes(level)) {
|
|
219
198
|
this.bus.emit("ui:error", { message: `Unknown thinking level: ${level}. Use: ${AgentLoop.THINKING_LEVELS.join(", ")}` });
|
|
220
199
|
return;
|
|
221
200
|
}
|
|
222
|
-
const mode = this.
|
|
201
|
+
const mode = this.activeModel;
|
|
223
202
|
if (level !== "off" && mode.reasoning === false) {
|
|
224
|
-
this.bus.emit("ui:error", { message: `Model ${mode.
|
|
203
|
+
this.bus.emit("ui:error", { message: `Model ${mode.id} does not support thinking.` });
|
|
225
204
|
return;
|
|
226
205
|
}
|
|
227
206
|
if (level !== "off" && mode.supportsReasoningEffort === false) {
|
|
228
|
-
this.bus.emit("ui:error", { message: `Provider ${mode.provider
|
|
207
|
+
this.bus.emit("ui:error", { message: `Provider ${mode.provider} does not support reasoning_effort.` });
|
|
229
208
|
return;
|
|
230
209
|
}
|
|
231
210
|
this.thinkingLevel = level;
|
|
@@ -233,7 +212,7 @@ export class AgentLoop {
|
|
|
233
212
|
this.bus.emit("config:changed", {});
|
|
234
213
|
});
|
|
235
214
|
onPipe("config:get-thinking", () => {
|
|
236
|
-
const mode = this.
|
|
215
|
+
const mode = this.activeModel;
|
|
237
216
|
const supported = mode.reasoning !== false && mode.supportsReasoningEffort !== false;
|
|
238
217
|
return { level: this.thinkingLevel, levels: AgentLoop.THINKING_LEVELS, supported };
|
|
239
218
|
});
|
|
@@ -257,11 +236,11 @@ export class AgentLoop {
|
|
|
257
236
|
onPipe("context:get-stats", () => ({
|
|
258
237
|
activeTokens: this.conversation.estimateTokens(),
|
|
259
238
|
totalTokens: this.conversation.estimatePromptTokens(),
|
|
260
|
-
budgetTokens: this.
|
|
239
|
+
budgetTokens: this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
261
240
|
}));
|
|
262
241
|
onPipe("context:snapshot", (payload) => {
|
|
263
242
|
payload.messages = this.conversation.get();
|
|
264
|
-
payload.contextWindow = this.
|
|
243
|
+
payload.contextWindow = this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
265
244
|
payload.activeTokens = this.conversation.estimateTokens();
|
|
266
245
|
return payload;
|
|
267
246
|
});
|
|
@@ -271,9 +250,7 @@ export class AgentLoop {
|
|
|
271
250
|
payload.stats = { before: stats.before, after: stats.after, evictedCount: stats.evictedCount };
|
|
272
251
|
return payload;
|
|
273
252
|
});
|
|
274
|
-
//
|
|
275
|
-
// event. Whatever strategy ran, core accumulates these counters for
|
|
276
|
-
// status/introspect consumers.
|
|
253
|
+
// Accumulate counters regardless of which compaction strategy ran.
|
|
277
254
|
on("conversation:after-compact", ({ beforeTokens, afterTokens }) => {
|
|
278
255
|
this.compactionCount++;
|
|
279
256
|
this.cumulativeCompactedTokens += Math.max(0, beforeTokens - afterTokens);
|
|
@@ -287,7 +264,6 @@ export class AgentLoop {
|
|
|
287
264
|
on("shell:cwd-change", ({ cwd }) => {
|
|
288
265
|
const projectSkills = discoverProjectSkills(cwd);
|
|
289
266
|
const newNames = new Set(projectSkills.map(s => s.name));
|
|
290
|
-
// Check if the set of project skills changed
|
|
291
267
|
if (newNames.size === this.lastProjectSkillNames.size &&
|
|
292
268
|
[...newNames].every(n => this.lastProjectSkillNames.has(n))) {
|
|
293
269
|
return; // no change
|
|
@@ -401,42 +377,45 @@ export class AgentLoop {
|
|
|
401
377
|
this.abortController?.abort();
|
|
402
378
|
}
|
|
403
379
|
reasoningParams() {
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
380
|
+
const model = this.activeModel;
|
|
381
|
+
if (model.reasoning === false)
|
|
406
382
|
return {};
|
|
407
|
-
if (
|
|
383
|
+
if (model.supportsReasoningEffort === false)
|
|
408
384
|
return {};
|
|
409
|
-
|
|
410
|
-
|
|
385
|
+
const build = this.activeEndpoint?.buildReasoningParams;
|
|
386
|
+
if (build)
|
|
387
|
+
return build(this.thinkingLevel);
|
|
411
388
|
if (this.thinkingLevel === "off")
|
|
412
389
|
return {};
|
|
413
390
|
const effort = this.thinkingLevel === "xhigh" ? "high" : this.thinkingLevel;
|
|
414
391
|
return { reasoning_effort: effort };
|
|
415
392
|
}
|
|
416
|
-
|
|
417
|
-
|
|
393
|
+
resolveEndpoint(m) {
|
|
394
|
+
try {
|
|
395
|
+
return this.handlers.call("agent:resolve-endpoint", { provider: m.provider, id: m.id });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
418
400
|
}
|
|
419
|
-
|
|
401
|
+
pullModels() {
|
|
420
402
|
try {
|
|
421
|
-
return this.handlers.call("agent:get-
|
|
403
|
+
return this.handlers.call("agent:get-models") ?? [];
|
|
422
404
|
}
|
|
423
405
|
catch {
|
|
424
406
|
return [];
|
|
425
407
|
}
|
|
426
408
|
}
|
|
427
409
|
emitIdentity() {
|
|
428
|
-
const m = this.
|
|
410
|
+
const m = this.activeModel;
|
|
429
411
|
this.bus.emit("agent:info", {
|
|
430
412
|
name: "ash",
|
|
431
413
|
version: PACKAGE_VERSION,
|
|
432
|
-
model: m.
|
|
414
|
+
model: m.id,
|
|
433
415
|
provider: m.provider,
|
|
434
416
|
contextWindow: m.contextWindow,
|
|
435
417
|
});
|
|
436
418
|
}
|
|
437
|
-
get currentModel() {
|
|
438
|
-
return this.activeMode.model;
|
|
439
|
-
}
|
|
440
419
|
/**
|
|
441
420
|
* Run compaction via the `conversation:compact` handler. After any
|
|
442
421
|
* compaction, emit `conversation:after-compact` so listeners
|
|
@@ -513,9 +492,9 @@ export class AgentLoop {
|
|
|
513
492
|
formatError(e) {
|
|
514
493
|
const raw = e instanceof Error ? e.message : String(e);
|
|
515
494
|
const status = e.status;
|
|
516
|
-
const model = this.
|
|
495
|
+
const model = this.activeModel.id;
|
|
517
496
|
const baseURL = this.llmClient.config?.baseURL;
|
|
518
|
-
const provider = this.
|
|
497
|
+
const provider = this.activeModel.provider;
|
|
519
498
|
// Connection errors — most likely misconfigured provider
|
|
520
499
|
if (raw.includes("ECONNREFUSED") || raw.includes("ECONNRESET") ||
|
|
521
500
|
raw.includes("ETIMEDOUT") || raw.includes("fetch failed") ||
|
|
@@ -551,9 +530,15 @@ export class AgentLoop {
|
|
|
551
530
|
h.define("tool-protocol:extract-calls", (args) => this.toolProtocol.extractToolCalls(args.text, args.streamedCalls));
|
|
552
531
|
// System prompt: static identity + behavioral instructions.
|
|
553
532
|
// Extensions can use registerInstruction() for a managed section,
|
|
554
|
-
//
|
|
533
|
+
// advise system-prompt:frontend to describe their surface high in the
|
|
534
|
+
// prompt, or advise this handler directly for full control.
|
|
555
535
|
h.define("system-prompt:build", () => {
|
|
556
|
-
|
|
536
|
+
// The active frontend's surface goes right after the identity; omitted if none.
|
|
537
|
+
const frontend = (this.handlers.call("system-prompt:frontend") ?? "").trim();
|
|
538
|
+
const parts = [STATIC_IDENTITY];
|
|
539
|
+
if (frontend)
|
|
540
|
+
parts.push(frontend);
|
|
541
|
+
parts.push(STATIC_GUIDE);
|
|
557
542
|
// Global behavioral rules (~/.agent-sh/AGENTS.md) — persistent agent memory
|
|
558
543
|
const agentsMd = loadGlobalAgentsMd();
|
|
559
544
|
if (agentsMd)
|
|
@@ -570,12 +555,11 @@ export class AgentLoop {
|
|
|
570
555
|
const projectStatic = buildStaticByCwd(this.handlers.call("cwd"));
|
|
571
556
|
if (projectStatic)
|
|
572
557
|
parts.push(projectStatic);
|
|
573
|
-
// Extension sections (tools, skills, instructions grouped by extension)
|
|
574
558
|
const extensionSections = this.buildExtensionSections();
|
|
575
559
|
if (extensionSections.length > 0) {
|
|
576
560
|
parts.push("# Extension Instructions\n\n" + extensionSections.join("\n\n"));
|
|
577
561
|
}
|
|
578
|
-
if (this.
|
|
562
|
+
if (this.activeModel.modalities?.includes("image")) {
|
|
579
563
|
parts.push("# Image Support\n\n"
|
|
580
564
|
+ "This model supports image input. When you need visual information, "
|
|
581
565
|
+ "you can read image files (PNG, JPEG, GIF, WebP) with read_file — "
|
|
@@ -590,14 +574,14 @@ export class AgentLoop {
|
|
|
590
574
|
// decide the aggregation shape. Adding a new handler here should
|
|
591
575
|
// only happen for state the core genuinely owns (not state that
|
|
592
576
|
// an extension could track by listening to events).
|
|
593
|
-
h.define("agent:get-
|
|
594
|
-
model: this.
|
|
595
|
-
provider: this.
|
|
577
|
+
h.define("agent:get-model", () => ({
|
|
578
|
+
model: this.activeModel.id,
|
|
579
|
+
provider: this.activeModel.provider,
|
|
596
580
|
thinkingLevel: this.thinkingLevel,
|
|
597
|
-
contextWindow: this.
|
|
581
|
+
contextWindow: this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
598
582
|
}));
|
|
599
583
|
h.define("agent:get-tokens", () => {
|
|
600
|
-
const contextWindow = this.
|
|
584
|
+
const contextWindow = this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
601
585
|
const promptTokens = this.conversation.estimatePromptTokens();
|
|
602
586
|
return {
|
|
603
587
|
active: this.conversation.estimateTokens(),
|
|
@@ -640,7 +624,7 @@ export class AgentLoop {
|
|
|
640
624
|
byFile: [...this.lastErrorByFile.entries()].map(([file, error]) => ({ file, error })),
|
|
641
625
|
}));
|
|
642
626
|
h.define("agent:get-compaction-state", () => {
|
|
643
|
-
const contextWindow = this.
|
|
627
|
+
const contextWindow = this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
644
628
|
const ratio = getSettings().autoCompactThreshold ?? 0.5;
|
|
645
629
|
return {
|
|
646
630
|
count: this.compactionCount,
|
|
@@ -649,9 +633,9 @@ export class AgentLoop {
|
|
|
649
633
|
};
|
|
650
634
|
});
|
|
651
635
|
h.define("agent:get-self", () => this);
|
|
652
|
-
// dynamic-context:build / query-context:build are defined in core
|
|
653
|
-
// ash consumes them via the envelope wrapping
|
|
654
|
-
// handleQuery; other backends may ignore.
|
|
636
|
+
// dynamic-context:build / query-context:build are defined in the core
|
|
637
|
+
// kernel (src/core/index.ts). ash consumes them via the envelope wrapping
|
|
638
|
+
// in streamResponse + handleQuery; other backends may ignore.
|
|
655
639
|
// Full control over what the LLM sees: takes messages[], returns messages[].
|
|
656
640
|
// Default: pass through. Extensions can advise to compact, summarize,
|
|
657
641
|
// filter, reorder, inject — whatever strategy fits.
|
|
@@ -703,7 +687,6 @@ export class AgentLoop {
|
|
|
703
687
|
return { content: msg, exitCode: 1, isError: true };
|
|
704
688
|
}
|
|
705
689
|
const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
|
|
706
|
-
// Emit tool-started for TUI
|
|
707
690
|
const label = tool.displayName ?? name;
|
|
708
691
|
this.bus.emit("agent:tool-started", {
|
|
709
692
|
title: typeof args.description === "string" ? `${label}: ${args.description}` : label,
|
|
@@ -735,7 +718,6 @@ export class AgentLoop {
|
|
|
735
718
|
const message = err instanceof Error ? err.message : String(err);
|
|
736
719
|
result = { content: message, exitCode: 1, isError: true };
|
|
737
720
|
}
|
|
738
|
-
// Invalidate read cache when a file is modified
|
|
739
721
|
if (tool.modifiesFiles && typeof args.path === "string" && !result.isError) {
|
|
740
722
|
const absPath = path.resolve(process.cwd(), args.path);
|
|
741
723
|
this.fileReadCache.delete(absPath);
|
|
@@ -754,7 +736,6 @@ export class AgentLoop {
|
|
|
754
736
|
});
|
|
755
737
|
}
|
|
756
738
|
async handleQuery(query, images) {
|
|
757
|
-
// Cancel any in-flight loop (concurrent prompt handling)
|
|
758
739
|
if (this.abortController) {
|
|
759
740
|
this.abortController.abort();
|
|
760
741
|
}
|
|
@@ -779,7 +760,7 @@ export class AgentLoop {
|
|
|
779
760
|
// Fail closed: an image sent to a non-vision model errors and leaves an
|
|
780
761
|
// unsendable message poisoning history, so require declared image support.
|
|
781
762
|
let userImages = images?.length ? images : undefined;
|
|
782
|
-
if (userImages && !this.
|
|
763
|
+
if (userImages && !this.activeModel.modalities?.includes("image")) {
|
|
783
764
|
this.bus.emit("ui:info", { message: `Current model has no declared image support — ${userImages.length} image(s) dropped.` });
|
|
784
765
|
userImages = undefined;
|
|
785
766
|
}
|
|
@@ -830,7 +811,7 @@ export class AgentLoop {
|
|
|
830
811
|
while (!signal.aborted) {
|
|
831
812
|
// Auto-compact when total context approaches the window limit.
|
|
832
813
|
const totalEstimate = this.conversation.estimatePromptTokens();
|
|
833
|
-
const contextWindow = this.
|
|
814
|
+
const contextWindow = this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
834
815
|
const s = getSettings();
|
|
835
816
|
const threshold = Math.floor((contextWindow - RESPONSE_RESERVE) * s.autoCompactThreshold);
|
|
836
817
|
if (s.autoCompact && totalEstimate > threshold) {
|
|
@@ -855,10 +836,9 @@ export class AgentLoop {
|
|
|
855
836
|
}
|
|
856
837
|
const systemPrompt = cachedSystemPrompt ?? (cachedSystemPrompt = this.handlers.call("system-prompt:build"));
|
|
857
838
|
const dynamicContext = this.handlers.call("dynamic-context:build");
|
|
858
|
-
// Shell events are injected once per user query (see
|
|
839
|
+
// Shell events are injected once per user query (see handleQuery),
|
|
859
840
|
// not per loop iteration. Mid-loop injection would break the
|
|
860
841
|
// tool_call → tool_result chain some providers require.
|
|
861
|
-
// Stream LLM response with retry
|
|
862
842
|
const result = await this.streamWithRetry(systemPrompt, dynamicContext, signal);
|
|
863
843
|
const { text, toolCalls: streamedToolCalls, extras } = result;
|
|
864
844
|
const toolCalls = this.handlers.call("tool-protocol:extract-calls", {
|
|
@@ -875,7 +855,6 @@ export class AgentLoop {
|
|
|
875
855
|
}
|
|
876
856
|
if (signal.aborted)
|
|
877
857
|
break;
|
|
878
|
-
// No tool calls → agent is done
|
|
879
858
|
if (toolCalls.length === 0) {
|
|
880
859
|
break;
|
|
881
860
|
}
|
|
@@ -1051,10 +1030,8 @@ export class AgentLoop {
|
|
|
1051
1030
|
break;
|
|
1052
1031
|
await executeSingle(tc, ++batchIdx);
|
|
1053
1032
|
}
|
|
1054
|
-
//
|
|
1055
|
-
//
|
|
1056
|
-
// in a row, nudge to read source. When errors cascade across tools,
|
|
1057
|
-
// nudge to step back and reassess approach.
|
|
1033
|
+
// Categorize this round's results; the summaries feed
|
|
1034
|
+
// agent:tool-batch-complete below, where extensions decide on nudges.
|
|
1058
1035
|
const errorTools = new Set();
|
|
1059
1036
|
const successTools = new Set();
|
|
1060
1037
|
const errorSummaries = new Map(); // tool → brief error description
|
|
@@ -1074,10 +1051,6 @@ export class AgentLoop {
|
|
|
1074
1051
|
const hadAnyError = errorTools.size > 0;
|
|
1075
1052
|
const hadAnySuccess = successTools.size > 0;
|
|
1076
1053
|
// ── Session telemetry accumulation ──
|
|
1077
|
-
// Track every tool call's outcome. Exposed via orthogonal handlers
|
|
1078
|
-
// (agent:get-counters, agent:get-tool-stats) for extensions that
|
|
1079
|
-
// want behavioral signals. The data layer for metacognition — you
|
|
1080
|
-
// can't improve what you don't measure.
|
|
1081
1054
|
for (const r of collectedResults) {
|
|
1082
1055
|
const counts = this.toolCallCounts.get(r.toolName) ?? { success: 0, error: 0 };
|
|
1083
1056
|
if (r.isError) {
|
|
@@ -1144,7 +1117,6 @@ export class AgentLoop {
|
|
|
1144
1117
|
catch { }
|
|
1145
1118
|
}
|
|
1146
1119
|
}
|
|
1147
|
-
// Clear resolved error-by-tool entries for successful tools
|
|
1148
1120
|
for (const tool of successTools) {
|
|
1149
1121
|
this.lastErrorByTool.delete(tool);
|
|
1150
1122
|
}
|
|
@@ -1159,7 +1131,6 @@ export class AgentLoop {
|
|
|
1159
1131
|
errorSummary: r.isError ? errorSummaries.get(r.toolName) : undefined,
|
|
1160
1132
|
})),
|
|
1161
1133
|
});
|
|
1162
|
-
// Record all tool results via protocol
|
|
1163
1134
|
this.toolProtocol.recordResults(this.conversation, collectedResults);
|
|
1164
1135
|
// Emit enriched message-appended events so derived-log extensions
|
|
1165
1136
|
// can summarize each tool result without re-parsing the message
|
|
@@ -1180,7 +1151,6 @@ export class AgentLoop {
|
|
|
1180
1151
|
isError: !!r.isError,
|
|
1181
1152
|
});
|
|
1182
1153
|
}
|
|
1183
|
-
// Loop back — LLM sees tool results
|
|
1184
1154
|
}
|
|
1185
1155
|
return fullResponseText;
|
|
1186
1156
|
}
|
|
@@ -1210,7 +1180,7 @@ export class AgentLoop {
|
|
|
1210
1180
|
throw e;
|
|
1211
1181
|
// Context overflow — aggressively compact and retry
|
|
1212
1182
|
if (this.isContextOverflow(e)) {
|
|
1213
|
-
const contextWindow = this.
|
|
1183
|
+
const contextWindow = this.activeModel.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
1214
1184
|
const target = Math.floor((contextWindow - RESPONSE_RESERVE) * 0.6);
|
|
1215
1185
|
const stats = await this.compactWithHooks(target, 1);
|
|
1216
1186
|
// If compaction freed nothing, retrying will hit the same error.
|
|
@@ -1276,8 +1246,8 @@ export class AgentLoop {
|
|
|
1276
1246
|
const requestParams = {
|
|
1277
1247
|
messages,
|
|
1278
1248
|
tools: apiTools,
|
|
1279
|
-
model: this.
|
|
1280
|
-
max_tokens: this.
|
|
1249
|
+
model: this.activeModel.id,
|
|
1250
|
+
max_tokens: this.activeModel.maxTokens ?? 65536,
|
|
1281
1251
|
...this.reasoningParams(),
|
|
1282
1252
|
};
|
|
1283
1253
|
this.bus.emit("llm:request", requestParams);
|
|
@@ -1291,12 +1261,13 @@ export class AgentLoop {
|
|
|
1291
1261
|
if (chunk.usage) {
|
|
1292
1262
|
const u = chunk.usage;
|
|
1293
1263
|
const promptTokens = u.prompt_tokens ?? 0;
|
|
1264
|
+
const cachedPromptTokens = this.activeEndpoint?.extractCachedTokens?.(u);
|
|
1294
1265
|
this.bus.emit("agent:usage", {
|
|
1295
1266
|
prompt_tokens: promptTokens,
|
|
1296
1267
|
completion_tokens: u.completion_tokens ?? 0,
|
|
1297
1268
|
total_tokens: u.total_tokens ?? 0,
|
|
1269
|
+
...(typeof cachedPromptTokens === "number" ? { cached_prompt_tokens: cachedPromptTokens } : {}),
|
|
1298
1270
|
});
|
|
1299
|
-
// Feed accurate token count back to conversation state
|
|
1300
1271
|
if (promptTokens > 0) {
|
|
1301
1272
|
this.conversation.updateApiTokenCount(promptTokens);
|
|
1302
1273
|
}
|
|
@@ -1305,10 +1276,8 @@ export class AgentLoop {
|
|
|
1305
1276
|
if (!choice)
|
|
1306
1277
|
continue;
|
|
1307
1278
|
const delta = choice.delta;
|
|
1308
|
-
// Text content
|
|
1309
1279
|
if (delta?.content) {
|
|
1310
1280
|
text += delta.content;
|
|
1311
|
-
// Filter tool tags from display output (inline mode)
|
|
1312
1281
|
const displayText = streamFilter
|
|
1313
1282
|
? streamFilter.feed(delta.content)
|
|
1314
1283
|
: delta.content;
|
|
@@ -1366,7 +1335,6 @@ export class AgentLoop {
|
|
|
1366
1335
|
if (!signal.aborted)
|
|
1367
1336
|
throw e;
|
|
1368
1337
|
}
|
|
1369
|
-
// Flush any buffered content from the stream filter
|
|
1370
1338
|
if (streamFilter) {
|
|
1371
1339
|
const remaining = streamFilter.flush();
|
|
1372
1340
|
if (remaining) {
|
|
@@ -1396,7 +1364,7 @@ export class AgentLoop {
|
|
|
1396
1364
|
}
|
|
1397
1365
|
// Echo reasoning only for modes that opt in (e.g. DeepSeek-R1).
|
|
1398
1366
|
const extras = {};
|
|
1399
|
-
if (this.
|
|
1367
|
+
if (this.activeModel.echoReasoning) {
|
|
1400
1368
|
if (reasoning && reasoningField)
|
|
1401
1369
|
extras[reasoningField] = reasoning;
|
|
1402
1370
|
if (reasoningDetailsByIndex.size > 0) {
|
package/dist/agent/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderRegistration } from "./host-types.js";
|
|
1
|
+
import type { Model, ProviderRegistration } from "./host-types.js";
|
|
2
2
|
import type { ImageContent, ToolDefinition, ToolResultDisplay } from "./types.js";
|
|
3
3
|
export interface AgentIdentity {
|
|
4
4
|
name: string;
|
|
@@ -20,8 +20,9 @@ declare module "../core/event-bus.js" {
|
|
|
20
20
|
"provider:configure": {
|
|
21
21
|
id: string;
|
|
22
22
|
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
23
|
+
cacheTokens?: (usage: Record<string, unknown>) => number | undefined;
|
|
23
24
|
};
|
|
24
|
-
"agent:
|
|
25
|
+
"agent:models-changed": Record<string, never>;
|
|
25
26
|
"config:switch-provider": {
|
|
26
27
|
provider: string;
|
|
27
28
|
};
|
|
@@ -70,6 +71,7 @@ declare module "../core/event-bus.js" {
|
|
|
70
71
|
prompt_tokens: number;
|
|
71
72
|
completion_tokens: number;
|
|
72
73
|
total_tokens: number;
|
|
74
|
+
cached_prompt_tokens?: number;
|
|
73
75
|
};
|
|
74
76
|
"agent:processing-start": Record<string, never>;
|
|
75
77
|
"agent:processing-done": Record<string, never>;
|
|
@@ -190,17 +192,12 @@ declare module "../core/event-bus.js" {
|
|
|
190
192
|
};
|
|
191
193
|
};
|
|
192
194
|
"config:switch-model": {
|
|
193
|
-
|
|
195
|
+
id: string;
|
|
196
|
+
provider: string;
|
|
194
197
|
};
|
|
195
198
|
"config:get-models": {
|
|
196
|
-
models:
|
|
197
|
-
|
|
198
|
-
provider: string;
|
|
199
|
-
}[];
|
|
200
|
-
active: {
|
|
201
|
-
model: string;
|
|
202
|
-
provider: string;
|
|
203
|
-
} | null;
|
|
199
|
+
models: Model[];
|
|
200
|
+
active: Model | null;
|
|
204
201
|
};
|
|
205
202
|
"config:set-thinking": {
|
|
206
203
|
level: string;
|