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
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ So I built agent-sh. Under the hood it's a normal shell on top of node-pty — y
|
|
|
19
19
|
~ $ > draft a commit message # agent reads your diff and shell history
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
I still use
|
|
22
|
+
I still use a proper coding harness for serious work — this doesn't replace that. But for the quick stuff in the terminal, I reach for agent-sh almost every day now. The built-in agent is lightweight and good enough for most of what I throw at it, and when it isn't, you can swap in [pi](examples/extensions/pi-bridge/) as the backend via a bridge extension.
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
@@ -57,14 +57,22 @@ export OPENAI_API_KEY=sk-...
|
|
|
57
57
|
agent-sh
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
**DeepSeek:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export DEEPSEEK_API_KEY=sk-...
|
|
64
|
+
agent-sh
|
|
65
|
+
```
|
|
66
|
+
|
|
60
67
|
**Local models** (Ollama, llama.cpp server, LM Studio, vLLM — anything OpenAI-compatible):
|
|
61
68
|
|
|
62
69
|
```bash
|
|
63
|
-
export OPENAI_API_KEY=ollama # any value; dummy is fine
|
|
64
70
|
export OPENAI_BASE_URL=http://localhost:11434/v1 # point at your server
|
|
65
71
|
agent-sh
|
|
66
72
|
```
|
|
67
73
|
|
|
74
|
+
Set `OPENAI_API_KEY` too if your server requires auth.
|
|
75
|
+
|
|
68
76
|
Once running, switch models at any time with `/model <name>` (tab-completes; selection persists across sessions).
|
|
69
77
|
|
|
70
78
|
For richer configuration (multiple providers, extensions), run `agent-sh init` to scaffold `~/.agent-sh/settings.json` with copy-pasteable examples. See the [Usage Guide](docs/usage.md) for the full list of supported providers.
|
|
@@ -87,7 +95,7 @@ Requires Node.js 18+. Currently supports **bash** and **zsh**; other shells (fis
|
|
|
87
95
|
|
|
88
96
|
**Context that just works.** Every query includes your cwd, recent commands, and their output. Run a failing test, type `> fix this`, and agent-sh knows exactly what happened. Context management works like shell history — continuous, persistent across restarts, no sessions to manage. See [Context Management](docs/context-management.md).
|
|
89
97
|
|
|
90
|
-
**Any LLM, any backend.** agent-sh works with any OpenAI-compatible API out of the box. Define multiple providers in settings and switch models at runtime with `/model <name>`. Or swap in a completely different agent — [
|
|
98
|
+
**Any LLM, any backend.** agent-sh works with any OpenAI-compatible API out of the box. Define multiple providers in settings and switch models at runtime with `/model <name>`. Or swap in a completely different agent — [pi](examples/extensions/pi-bridge/) runs as a drop-in backend extension.
|
|
91
99
|
|
|
92
100
|
**Extensible by design.** The entire system is built on a typed event bus. Extensions can add custom input modes, content transforms (render LaTeX as images, Mermaid as diagrams), themes, slash commands, or replace the agent backend entirely. The built-in TUI renderer is itself just an extension.
|
|
93
101
|
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import { computeDiff, computeEditDiff, computeInputDiff } from "../utils/diff.js";
|
|
6
6
|
import { ToolRegistry } from "./tool-registry.js";
|
|
7
|
+
import { normalizeToolArgs } from "./normalize-args.js";
|
|
7
8
|
import { ConversationState } from "./conversation-state.js";
|
|
8
9
|
import { HistoryFile } from "./history-file.js";
|
|
9
10
|
import { nucleate, formatNuclearLine, isReadOnly } from "./nuclear-form.js";
|
|
@@ -58,6 +59,7 @@ export class AgentLoop {
|
|
|
58
59
|
modes;
|
|
59
60
|
currentModeIndex = 0;
|
|
60
61
|
boundListeners = [];
|
|
62
|
+
boundPipeListeners = [];
|
|
61
63
|
ctorListeners = [];
|
|
62
64
|
ctorPipeListeners = [];
|
|
63
65
|
lastProjectSkillNames = new Set();
|
|
@@ -215,12 +217,24 @@ export class AgentLoop {
|
|
|
215
217
|
this.bus.on(event, fn);
|
|
216
218
|
this.boundListeners.push({ event, fn });
|
|
217
219
|
};
|
|
220
|
+
const onPipe = (event, fn) => {
|
|
221
|
+
this.bus.onPipe(event, fn);
|
|
222
|
+
this.boundPipeListeners.push({ event, fn, async: false });
|
|
223
|
+
};
|
|
224
|
+
const onPipeAsync = (event, fn) => {
|
|
225
|
+
this.bus.onPipeAsync(event, fn);
|
|
226
|
+
this.boundPipeListeners.push({ event, fn, async: true });
|
|
227
|
+
};
|
|
218
228
|
on("agent:submit", ({ query }) => {
|
|
219
229
|
this.handleQuery(query).catch(() => { });
|
|
220
230
|
});
|
|
221
231
|
on("agent:cancel-request", (e) => {
|
|
222
232
|
this.abortController?.abort(e.silent ? "silent" : undefined);
|
|
223
233
|
});
|
|
234
|
+
on("agent:append-user-message", ({ text }) => {
|
|
235
|
+
this.conversation.appendUserMessage(text);
|
|
236
|
+
this.bus.emit("conversation:message-appended", { role: "user", content: text });
|
|
237
|
+
});
|
|
224
238
|
on("config:switch-model", ({ model: target }) => {
|
|
225
239
|
const atIdx = target.lastIndexOf("@");
|
|
226
240
|
const modelId = atIdx > 0 ? target.slice(0, atIdx) : target;
|
|
@@ -256,7 +270,7 @@ export class AgentLoop {
|
|
|
256
270
|
}
|
|
257
271
|
this.bus.emit("config:changed", {});
|
|
258
272
|
});
|
|
259
|
-
|
|
273
|
+
onPipe("config:get-models", () => {
|
|
260
274
|
const models = this.modes.map((m) => ({ model: m.model, provider: m.provider ?? "" }));
|
|
261
275
|
const cur = this.modes[this.currentModeIndex];
|
|
262
276
|
const active = cur ? { model: cur.model, provider: cur.provider ?? "" } : null;
|
|
@@ -280,7 +294,7 @@ export class AgentLoop {
|
|
|
280
294
|
this.bus.emit("ui:info", { message: `Thinking: ${level}` });
|
|
281
295
|
this.bus.emit("config:changed", {});
|
|
282
296
|
});
|
|
283
|
-
|
|
297
|
+
onPipe("config:get-thinking", () => {
|
|
284
298
|
const mode = this.currentMode;
|
|
285
299
|
const supported = mode.reasoning !== false && mode.supportsReasoningEffort !== false;
|
|
286
300
|
return { level: this.thinkingLevel, levels: AgentLoop.THINKING_LEVELS, supported };
|
|
@@ -302,20 +316,20 @@ export class AgentLoop {
|
|
|
302
316
|
this.bus.emit("ui:info", { message: "(nothing to compact)" });
|
|
303
317
|
}
|
|
304
318
|
});
|
|
305
|
-
|
|
319
|
+
onPipe("context:get-stats", () => ({
|
|
306
320
|
activeTokens: this.conversation.estimateTokens(),
|
|
307
321
|
totalTokens: this.conversation.estimatePromptTokens(),
|
|
308
322
|
nuclearEntries: this.conversation.getNuclearEntryCount(),
|
|
309
323
|
recallArchiveSize: this.conversation.getRecallArchiveSize(),
|
|
310
324
|
budgetTokens: this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
311
325
|
}));
|
|
312
|
-
|
|
326
|
+
onPipe("context:snapshot", (payload) => {
|
|
313
327
|
payload.messages = this.conversation.getMessages();
|
|
314
328
|
payload.contextWindow = this.currentMode.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
|
|
315
329
|
payload.activeTokens = this.conversation.estimateTokens();
|
|
316
330
|
return payload;
|
|
317
331
|
});
|
|
318
|
-
|
|
332
|
+
onPipeAsync("context:compact", async (payload) => {
|
|
319
333
|
const stats = await this.compactWithHooks(0, undefined, false, payload.strategy);
|
|
320
334
|
if (stats)
|
|
321
335
|
payload.stats = { before: stats.before, after: stats.after, evictedCount: stats.evictedCount };
|
|
@@ -365,6 +379,13 @@ export class AgentLoop {
|
|
|
365
379
|
this.bus.off(event, fn);
|
|
366
380
|
}
|
|
367
381
|
this.boundListeners = [];
|
|
382
|
+
for (const { event, fn, async } of this.boundPipeListeners) {
|
|
383
|
+
if (async)
|
|
384
|
+
this.bus.offPipeAsync(event, fn);
|
|
385
|
+
else
|
|
386
|
+
this.bus.offPipe(event, fn);
|
|
387
|
+
}
|
|
388
|
+
this.boundPipeListeners = [];
|
|
368
389
|
}
|
|
369
390
|
/** Register a tool (used by extensions via ctx.registerTool). */
|
|
370
391
|
registerTool(tool) {
|
|
@@ -1188,6 +1209,10 @@ export class AgentLoop {
|
|
|
1188
1209
|
});
|
|
1189
1210
|
return;
|
|
1190
1211
|
}
|
|
1212
|
+
// Normalize against the tool's input_schema: some LLMs stringify
|
|
1213
|
+
// nested object/array args despite the schema. See
|
|
1214
|
+
// normalize-args.ts for the diagnostic that uncovered this.
|
|
1215
|
+
args = normalizeToolArgs(args, tool.input_schema);
|
|
1191
1216
|
// ── Round-scoped cache for cacheable read-only tools ──
|
|
1192
1217
|
const cacheable = !tool.modifiesFiles && !tool.requiresPermission && tool.showOutput !== true;
|
|
1193
1218
|
const cacheKey = cacheable ? `${tc.name}:${JSON.stringify(args)}` : null;
|
|
@@ -38,7 +38,7 @@ export declare class ConversationState {
|
|
|
38
38
|
private nextSeq;
|
|
39
39
|
private lastApiTokenCount;
|
|
40
40
|
private lastApiMessageCount;
|
|
41
|
-
private
|
|
41
|
+
private pendingMessages;
|
|
42
42
|
constructor(handlers?: HandlerFunctions, instanceId?: string);
|
|
43
43
|
/** Get JSON.stringify of messages, cached until next mutation. */
|
|
44
44
|
private getMessagesJson;
|
|
@@ -56,8 +56,9 @@ export declare class ConversationState {
|
|
|
56
56
|
addToolResultInline(content: string): void;
|
|
57
57
|
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
58
58
|
addSystemNote(text: string): void;
|
|
59
|
+
appendUserMessage(text: string): void;
|
|
59
60
|
private hasOpenToolCalls;
|
|
60
|
-
private
|
|
61
|
+
private flushPendingMessages;
|
|
61
62
|
getMessages(): ChatCompletionMessageParam[];
|
|
62
63
|
/** Drop tool messages with no matching preceding tool_call — strict
|
|
63
64
|
* providers (DeepSeek) 400, and compaction can leave such orphans. */
|
|
@@ -56,10 +56,10 @@ export class ConversationState {
|
|
|
56
56
|
nextSeq = 1;
|
|
57
57
|
lastApiTokenCount = null;
|
|
58
58
|
lastApiMessageCount = 0;
|
|
59
|
-
//
|
|
60
|
-
// the trailing tool_result lands. Splicing into the gap
|
|
61
|
-
// reasoning_content pairing and is rejected by strict providers.
|
|
62
|
-
|
|
59
|
+
// Buffered when addSystemNote/appendUserMessage fires mid-tool-pair;
|
|
60
|
+
// flushed once the trailing tool_result lands. Splicing into the gap
|
|
61
|
+
// breaks reasoning_content pairing and is rejected by strict providers.
|
|
62
|
+
pendingMessages = [];
|
|
63
63
|
constructor(handlers, instanceId = "0000") {
|
|
64
64
|
this.handlers = handlers ?? null;
|
|
65
65
|
this.instanceId = instanceId;
|
|
@@ -114,23 +114,30 @@ export class ConversationState {
|
|
|
114
114
|
if (isError)
|
|
115
115
|
this.toolErrors.add(toolCallId);
|
|
116
116
|
this.invalidateMessagesCache();
|
|
117
|
-
this.
|
|
117
|
+
this.flushPendingMessages();
|
|
118
118
|
}
|
|
119
119
|
/** Add tool results as a user message (for inline tool protocol). */
|
|
120
120
|
addToolResultInline(content) {
|
|
121
121
|
this.messages.push({ role: "user", content });
|
|
122
122
|
this.invalidateMessagesCache();
|
|
123
|
-
this.
|
|
123
|
+
this.flushPendingMessages();
|
|
124
124
|
}
|
|
125
125
|
/** Safe from any context: queues if mid-tool-pair, appends otherwise. */
|
|
126
126
|
addSystemNote(text) {
|
|
127
127
|
if (this.hasOpenToolCalls()) {
|
|
128
|
-
this.
|
|
128
|
+
this.pendingMessages.push({ kind: "system", text });
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
this.messages.push({ role: "user", content: text });
|
|
132
132
|
this.invalidateMessagesCache();
|
|
133
133
|
}
|
|
134
|
+
appendUserMessage(text) {
|
|
135
|
+
if (this.hasOpenToolCalls()) {
|
|
136
|
+
this.pendingMessages.push({ kind: "user", text });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.addUserMessage(text);
|
|
140
|
+
}
|
|
134
141
|
hasOpenToolCalls() {
|
|
135
142
|
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
136
143
|
const msg = this.messages[i];
|
|
@@ -151,15 +158,21 @@ export class ConversationState {
|
|
|
151
158
|
}
|
|
152
159
|
return false;
|
|
153
160
|
}
|
|
154
|
-
|
|
155
|
-
if (this.
|
|
161
|
+
flushPendingMessages() {
|
|
162
|
+
if (this.pendingMessages.length === 0)
|
|
156
163
|
return;
|
|
157
164
|
if (this.hasOpenToolCalls())
|
|
158
165
|
return;
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
const pending = this.pendingMessages;
|
|
167
|
+
this.pendingMessages = [];
|
|
168
|
+
for (const m of pending) {
|
|
169
|
+
if (m.kind === "user") {
|
|
170
|
+
this.addUserMessage(m.text);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.messages.push({ role: "user", content: m.text });
|
|
174
|
+
}
|
|
161
175
|
}
|
|
162
|
-
this.pendingNotes = [];
|
|
163
176
|
this.invalidateMessagesCache();
|
|
164
177
|
}
|
|
165
178
|
getMessages() {
|
|
@@ -244,7 +257,7 @@ export class ConversationState {
|
|
|
244
257
|
this.invalidateMessagesCache();
|
|
245
258
|
this.lastApiTokenCount = null;
|
|
246
259
|
this.lastApiMessageCount = 0;
|
|
247
|
-
this.
|
|
260
|
+
this.flushPendingMessages();
|
|
248
261
|
}
|
|
249
262
|
pruneToolErrors() {
|
|
250
263
|
if (this.toolErrors.size === 0)
|
|
@@ -544,7 +557,7 @@ export class ConversationState {
|
|
|
544
557
|
this.nuclearEntries = [];
|
|
545
558
|
this.nuclearBySeq.clear();
|
|
546
559
|
this.recallArchive.clear();
|
|
547
|
-
this.
|
|
560
|
+
this.pendingMessages = [];
|
|
548
561
|
this.invalidateMessagesCache();
|
|
549
562
|
this.lastApiTokenCount = null;
|
|
550
563
|
this.lastApiMessageCount = 0;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-aware tool-arg normalization.
|
|
3
|
+
*
|
|
4
|
+
* Some LLMs (notably Claude) occasionally emit nested object/array
|
|
5
|
+
* tool-call arguments as JSON-encoded strings instead of native
|
|
6
|
+
* objects, despite the schema declaring `type: "object"` /
|
|
7
|
+
* `type: "array"`. The discrepancy was diagnosed by the superash field
|
|
8
|
+
* test (2026-05-03 / commit `b9efd47`):
|
|
9
|
+
*
|
|
10
|
+
* describe_demos: 'task' arrived as a string (length 1267)
|
|
11
|
+
* last char code: 93 (']')
|
|
12
|
+
* truncation suspected: true
|
|
13
|
+
*
|
|
14
|
+
* Tool handlers downstream had to add ad-hoc JSON.parse fallbacks. This
|
|
15
|
+
* helper centralizes the fix at the kernel boundary: after parsing the
|
|
16
|
+
* outer `argumentsJson`, walk each top-level field; for any field whose
|
|
17
|
+
* schema declares `object` or `array` but whose value is a string, run
|
|
18
|
+
* a single JSON.parse pass. On parse failure (e.g. truncated content),
|
|
19
|
+
* the string is left as-is — the tool can produce a clean error.
|
|
20
|
+
*
|
|
21
|
+
* Top-level only by design. Recursing into nested object schemas would
|
|
22
|
+
* change semantics for tools that legitimately accept stringified
|
|
23
|
+
* payloads as inner fields, and the observed wild cases all stringify
|
|
24
|
+
* at the top level.
|
|
25
|
+
*/
|
|
26
|
+
/** Normalize tool-call args against the tool's input_schema. Pure: does
|
|
27
|
+
* not mutate `args`. Returns a new object with stringified-then-decoded
|
|
28
|
+
* fields swapped in where applicable. */
|
|
29
|
+
export declare function normalizeToolArgs(args: Record<string, unknown>, schema: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-aware tool-arg normalization.
|
|
3
|
+
*
|
|
4
|
+
* Some LLMs (notably Claude) occasionally emit nested object/array
|
|
5
|
+
* tool-call arguments as JSON-encoded strings instead of native
|
|
6
|
+
* objects, despite the schema declaring `type: "object"` /
|
|
7
|
+
* `type: "array"`. The discrepancy was diagnosed by the superash field
|
|
8
|
+
* test (2026-05-03 / commit `b9efd47`):
|
|
9
|
+
*
|
|
10
|
+
* describe_demos: 'task' arrived as a string (length 1267)
|
|
11
|
+
* last char code: 93 (']')
|
|
12
|
+
* truncation suspected: true
|
|
13
|
+
*
|
|
14
|
+
* Tool handlers downstream had to add ad-hoc JSON.parse fallbacks. This
|
|
15
|
+
* helper centralizes the fix at the kernel boundary: after parsing the
|
|
16
|
+
* outer `argumentsJson`, walk each top-level field; for any field whose
|
|
17
|
+
* schema declares `object` or `array` but whose value is a string, run
|
|
18
|
+
* a single JSON.parse pass. On parse failure (e.g. truncated content),
|
|
19
|
+
* the string is left as-is — the tool can produce a clean error.
|
|
20
|
+
*
|
|
21
|
+
* Top-level only by design. Recursing into nested object schemas would
|
|
22
|
+
* change semantics for tools that legitimately accept stringified
|
|
23
|
+
* payloads as inner fields, and the observed wild cases all stringify
|
|
24
|
+
* at the top level.
|
|
25
|
+
*/
|
|
26
|
+
/** Normalize tool-call args against the tool's input_schema. Pure: does
|
|
27
|
+
* not mutate `args`. Returns a new object with stringified-then-decoded
|
|
28
|
+
* fields swapped in where applicable. */
|
|
29
|
+
export function normalizeToolArgs(args, schema) {
|
|
30
|
+
if (!schema || typeof schema !== "object")
|
|
31
|
+
return args;
|
|
32
|
+
const properties = schema.properties;
|
|
33
|
+
if (!properties || typeof properties !== "object")
|
|
34
|
+
return args;
|
|
35
|
+
let out = null;
|
|
36
|
+
for (const [field, fieldSchema] of Object.entries(properties)) {
|
|
37
|
+
if (!fieldSchema || typeof fieldSchema !== "object")
|
|
38
|
+
continue;
|
|
39
|
+
const expectedType = fieldSchema.type;
|
|
40
|
+
if (expectedType !== "object" && expectedType !== "array")
|
|
41
|
+
continue;
|
|
42
|
+
const value = args[field];
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(value);
|
|
47
|
+
if (out === null)
|
|
48
|
+
out = { ...args };
|
|
49
|
+
out[field] = parsed;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Leave as string — downstream tool can produce a useful error.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out ?? args;
|
|
56
|
+
}
|
package/dist/agent/subagent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConversationState } from "./conversation-state.js";
|
|
2
|
+
import { normalizeToolArgs } from "./normalize-args.js";
|
|
2
3
|
import { wrapTrailingWithDynamicContext } from "../utils/message-utils.js";
|
|
3
4
|
/**
|
|
4
5
|
* Run a subagent to completion.
|
|
@@ -56,6 +57,7 @@ export async function runSubagent(opts) {
|
|
|
56
57
|
conversation.addToolResult(tc.id, `Error: Invalid JSON arguments for ${tc.name}`, true);
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
60
|
+
args = normalizeToolArgs(args, tool.input_schema);
|
|
59
61
|
// Emit tool events for TUI (if bus provided)
|
|
60
62
|
if (bus) {
|
|
61
63
|
const display = tool.getDisplayInfo?.(args) ?? { kind: "execute" };
|
package/dist/core.d.ts
CHANGED
|
@@ -37,11 +37,13 @@ export interface AgentShellCore {
|
|
|
37
37
|
/** Unique id for this agent process; used for shell-marker tagging and lineage tracking. */
|
|
38
38
|
instanceId: string;
|
|
39
39
|
/** Activate the agent backend (call after extensions load). */
|
|
40
|
-
activateBackend(): void
|
|
40
|
+
activateBackend(): Promise<void>;
|
|
41
41
|
/** Convenience: emit agent:submit and await the response. */
|
|
42
42
|
query(text: string): Promise<string>;
|
|
43
43
|
/** Convenience: emit agent:cancel-request. */
|
|
44
44
|
cancel(): void;
|
|
45
|
+
/** Convenience: emit agent:append-user-message. */
|
|
46
|
+
appendUserMessage(text: string): void;
|
|
45
47
|
/** Build an ExtensionContext for loading extensions against this core. */
|
|
46
48
|
extensionContext(opts: {
|
|
47
49
|
quit: () => void;
|
package/dist/core.js
CHANGED
|
@@ -56,33 +56,30 @@ export function createCore(config) {
|
|
|
56
56
|
handlers.define("query-context:build", () => "");
|
|
57
57
|
const backends = new Map();
|
|
58
58
|
let activeBackendName = null;
|
|
59
|
-
const activateByName = async (name
|
|
59
|
+
const activateByName = async (name) => {
|
|
60
60
|
const backend = backends.get(name);
|
|
61
61
|
if (!backend) {
|
|
62
62
|
bus.emit("ui:error", { message: `Unknown backend: ${name}` });
|
|
63
|
-
return;
|
|
63
|
+
return false;
|
|
64
64
|
}
|
|
65
|
-
// Deactivate current backend
|
|
66
65
|
if (activeBackendName) {
|
|
67
66
|
backends.get(activeBackendName)?.kill();
|
|
68
67
|
}
|
|
69
|
-
// Activate new backend
|
|
70
68
|
await backend.start?.();
|
|
71
69
|
activeBackendName = name;
|
|
72
|
-
|
|
73
|
-
bus.emit("ui:info", { message: `Backend: ${name}` });
|
|
74
|
-
}
|
|
75
|
-
bus.emit("config:changed", {});
|
|
70
|
+
return true;
|
|
76
71
|
};
|
|
77
72
|
bus.on("agent:register-backend", (backend) => {
|
|
78
73
|
backends.set(backend.name, backend);
|
|
79
74
|
});
|
|
80
75
|
bus.on("config:switch-backend", ({ name }) => {
|
|
81
|
-
activateByName(name).then(() => {
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
activateByName(name).then((ok) => {
|
|
77
|
+
if (!ok)
|
|
78
|
+
return;
|
|
79
|
+
settingsMod.updateSettings({ defaultBackend: name });
|
|
80
|
+
// Single ui:info; config:changed (which triggers prompt redraw) follows it.
|
|
81
|
+
bus.emit("ui:info", { message: `Backend: ${name} (saved as default)` });
|
|
82
|
+
bus.emit("config:changed", {});
|
|
86
83
|
});
|
|
87
84
|
});
|
|
88
85
|
bus.on("config:list-backends", () => {
|
|
@@ -105,18 +102,12 @@ export function createCore(config) {
|
|
|
105
102
|
bus,
|
|
106
103
|
handlers,
|
|
107
104
|
instanceId,
|
|
108
|
-
activateBackend() {
|
|
109
|
-
// Silent — backend info is shown in the startup banner.
|
|
110
|
-
// Runtime switches (config:switch-backend) still emit ui:info.
|
|
105
|
+
async activateBackend() {
|
|
111
106
|
if (backends.size === 0)
|
|
112
107
|
return;
|
|
113
108
|
const preferred = settings.defaultBackend;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
activateByName(backends.keys().next().value, true);
|
|
119
|
-
}
|
|
109
|
+
const name = preferred && backends.has(preferred) ? preferred : backends.keys().next().value;
|
|
110
|
+
await activateByName(name);
|
|
120
111
|
},
|
|
121
112
|
async query(text) {
|
|
122
113
|
return new Promise((resolve, reject) => {
|
|
@@ -155,6 +146,9 @@ export function createCore(config) {
|
|
|
155
146
|
cancel() {
|
|
156
147
|
bus.emit("agent:cancel-request", {});
|
|
157
148
|
},
|
|
149
|
+
appendUserMessage(text) {
|
|
150
|
+
bus.emit("agent:append-user-message", { text });
|
|
151
|
+
},
|
|
158
152
|
extensionContext(opts) {
|
|
159
153
|
const ctx = {
|
|
160
154
|
bus,
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ export interface ShellEvents {
|
|
|
47
47
|
"agent:cancel-request": {
|
|
48
48
|
silent?: boolean;
|
|
49
49
|
};
|
|
50
|
+
"agent:append-user-message": {
|
|
51
|
+
text: string;
|
|
52
|
+
};
|
|
50
53
|
"input-mode:register": import("./types.js").InputModeConfig;
|
|
51
54
|
"agent:query": {
|
|
52
55
|
query: string;
|
|
@@ -307,7 +310,9 @@ export interface ShellEvents {
|
|
|
307
310
|
"config:add-modes": {
|
|
308
311
|
modes: AgentMode[];
|
|
309
312
|
};
|
|
310
|
-
"core:extensions-loaded":
|
|
313
|
+
"core:extensions-loaded": {
|
|
314
|
+
names: string[];
|
|
315
|
+
};
|
|
311
316
|
"provider:register": {
|
|
312
317
|
id: string;
|
|
313
318
|
apiKey?: string;
|
|
@@ -327,7 +332,7 @@ export interface ShellEvents {
|
|
|
327
332
|
};
|
|
328
333
|
"provider:configure": {
|
|
329
334
|
id: string;
|
|
330
|
-
reasoningParams?: (level: string) => Record<string, unknown>;
|
|
335
|
+
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
331
336
|
};
|
|
332
337
|
"agent:register-tool": {
|
|
333
338
|
tool: import("./agent/types.js").ToolDefinition;
|
|
@@ -445,6 +450,8 @@ export declare class EventBus {
|
|
|
445
450
|
* If no listeners are registered, returns the original payload unchanged.
|
|
446
451
|
*/
|
|
447
452
|
emitPipe<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): ShellEvents[K];
|
|
453
|
+
/** Remove an async transform listener from a pipeline event. */
|
|
454
|
+
offPipeAsync<K extends keyof ShellEvents>(event: K, fn: AsyncPipeListener<ShellEvents[K]>): void;
|
|
448
455
|
/** Register an async transform listener for a pipeline event. */
|
|
449
456
|
onPipeAsync<K extends keyof ShellEvents>(event: K, fn: AsyncPipeListener<ShellEvents[K]>): void;
|
|
450
457
|
/**
|
package/dist/event-bus.js
CHANGED
|
@@ -131,6 +131,15 @@ export class EventBus {
|
|
|
131
131
|
}
|
|
132
132
|
return result;
|
|
133
133
|
}
|
|
134
|
+
/** Remove an async transform listener from a pipeline event. */
|
|
135
|
+
offPipeAsync(event, fn) {
|
|
136
|
+
const listeners = this.asyncPipeListeners.get(event);
|
|
137
|
+
if (!listeners)
|
|
138
|
+
return;
|
|
139
|
+
const idx = listeners.indexOf(fn);
|
|
140
|
+
if (idx !== -1)
|
|
141
|
+
listeners.splice(idx, 1);
|
|
142
|
+
}
|
|
134
143
|
/** Register an async transform listener for a pipeline event. */
|
|
135
144
|
onPipeAsync(event, fn) {
|
|
136
145
|
let listeners = this.asyncPipeListeners.get(event);
|