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.
Files changed (37) hide show
  1. package/README.md +11 -3
  2. package/dist/agent/agent-loop.d.ts +1 -0
  3. package/dist/agent/agent-loop.js +30 -5
  4. package/dist/agent/conversation-state.d.ts +3 -2
  5. package/dist/agent/conversation-state.js +27 -14
  6. package/dist/agent/normalize-args.d.ts +29 -0
  7. package/dist/agent/normalize-args.js +56 -0
  8. package/dist/agent/subagent.js +2 -0
  9. package/dist/core.d.ts +3 -1
  10. package/dist/core.js +16 -22
  11. package/dist/event-bus.d.ts +9 -2
  12. package/dist/event-bus.js +9 -0
  13. package/dist/extensions/agent-backend.js +104 -24
  14. package/dist/extensions/index.js +8 -3
  15. package/dist/extensions/providers/deepseek.d.ts +8 -0
  16. package/dist/extensions/providers/deepseek.js +23 -0
  17. package/dist/extensions/providers/openai-compatible.d.ts +7 -0
  18. package/dist/extensions/providers/openai-compatible.js +30 -0
  19. package/dist/extensions/providers/openai.d.ts +7 -0
  20. package/dist/extensions/providers/openai.js +39 -0
  21. package/dist/extensions/{openrouter.d.ts → providers/openrouter.d.ts} +1 -1
  22. package/dist/extensions/{openrouter.js → providers/openrouter.js} +5 -3
  23. package/dist/extensions/slash-commands.js +0 -24
  24. package/dist/extensions/tui-renderer.js +28 -15
  25. package/dist/index.js +8 -33
  26. package/dist/settings.d.ts +2 -0
  27. package/dist/settings.js +1 -0
  28. package/dist/types.d.ts +14 -1
  29. package/dist/utils/box-frame.js +14 -8
  30. package/dist/utils/llm-client.d.ts +5 -1
  31. package/dist/utils/llm-client.js +6 -1
  32. package/dist/utils/llm-facade.js +5 -5
  33. package/examples/extensions/pi-bridge/README.md +12 -19
  34. package/examples/extensions/pi-bridge/index.ts +307 -35
  35. package/package.json +1 -1
  36. package/dist/extensions/openai.d.ts +0 -9
  37. 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 Claude Code and pi for serious coding work — this doesn't replace them. 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, bridge extensions let you plug [Claude Code](examples/extensions/claude-code-bridge/) or [pi](examples/extensions/pi-bridge/) in as the backend.
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 — [Claude Code](examples/extensions/claude-code-bridge/) and [pi](examples/extensions/pi-bridge/) run as drop-in backend extensions.
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
 
@@ -38,6 +38,7 @@ export declare class AgentLoop implements AgentBackend {
38
38
  private modes;
39
39
  private currentModeIndex;
40
40
  private boundListeners;
41
+ private boundPipeListeners;
41
42
  private ctorListeners;
42
43
  private ctorPipeListeners;
43
44
  private lastProjectSkillNames;
@@ -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
- this.bus.onPipe("config:get-models", (payload) => {
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
- this.bus.onPipe("config:get-thinking", () => {
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
- this.bus.onPipe("context:get-stats", () => ({
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
- this.bus.onPipe("context:snapshot", (payload) => {
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
- this.bus.onPipeAsync("context:compact", async (payload) => {
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 pendingNotes;
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 flushPendingNotes;
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
- // Notes queued when addSystemNote fires mid-tool-pair; flushed once
60
- // the trailing tool_result lands. Splicing into the gap breaks
61
- // reasoning_content pairing and is rejected by strict providers.
62
- pendingNotes = [];
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.flushPendingNotes();
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.flushPendingNotes();
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.pendingNotes.push(text);
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
- flushPendingNotes() {
155
- if (this.pendingNotes.length === 0)
161
+ flushPendingMessages() {
162
+ if (this.pendingMessages.length === 0)
156
163
  return;
157
164
  if (this.hasOpenToolCalls())
158
165
  return;
159
- for (const text of this.pendingNotes) {
160
- this.messages.push({ role: "user", content: text });
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.flushPendingNotes();
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.pendingNotes = [];
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
+ }
@@ -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, silent = false) => {
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
- if (!silent) {
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 (activeBackendName === name) {
83
- settingsMod.updateSettings({ defaultBackend: name });
84
- bus.emit("ui:info", { message: `Saved '${name}' as default backend.` });
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
- if (preferred && backends.has(preferred)) {
115
- activateByName(preferred, true);
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,
@@ -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": Record<string, never>;
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);