agent-sh 0.3.0 → 0.3.1

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.
@@ -21,7 +21,7 @@ export class AcpClient {
21
21
  terminalDonePromises = new Map();
22
22
  terminalCounter = 0;
23
23
  fileWatcher;
24
- pendingToolCalls = new Map(); // toolCallId → title
24
+ pendingToolCalls = new Map();
25
25
  autoCancelled = false;
26
26
  pendingToolCounter = 0;
27
27
  agentInfo = null;
@@ -146,19 +146,15 @@ export class AcpClient {
146
146
  const contextBlock = this.contextManager.getContext();
147
147
  try {
148
148
  this.log("sending prompt...");
149
- const promptTimeoutMs = 300000; // 5 minutes timeout for LLM response
150
- const response = await Promise.race([
151
- this.connection.prompt({
152
- sessionId: this.sessionId,
153
- prompt: [
154
- {
155
- type: "text",
156
- text: contextBlock + "\n" + query,
157
- },
158
- ],
159
- }),
160
- new Promise((_, reject) => setTimeout(() => reject(new Error(`Prompt timeout after ${promptTimeoutMs}ms`)), promptTimeoutMs)),
161
- ]);
149
+ const response = await this.connection.prompt({
150
+ sessionId: this.sessionId,
151
+ prompt: [
152
+ {
153
+ type: "text",
154
+ text: contextBlock + "\n" + query,
155
+ },
156
+ ],
157
+ });
162
158
  this.log(`prompt resolved: stopReason=${response.stopReason}`);
163
159
  if (response.stopReason === "cancelled") {
164
160
  cancelled = true;
@@ -176,7 +172,7 @@ export class AcpClient {
176
172
  finally {
177
173
  this.log("restoring shell mode");
178
174
  if (!cancelled) {
179
- this.bus.emit("agent:response-done", {
175
+ this.bus.emitTransform("agent:response-done", {
180
176
  response: this.currentResponseText,
181
177
  });
182
178
  }
@@ -327,8 +323,15 @@ export class AcpClient {
327
323
  createClientHandler() {
328
324
  return {
329
325
  // Required: handle session update notifications (streaming)
326
+ // Errors must not propagate — the ACP SDK returns them as error
327
+ // responses to the agent, which can stall the stream.
330
328
  sessionUpdate: async (params) => {
331
- this.handleSessionUpdate(params);
329
+ try {
330
+ this.handleSessionUpdate(params);
331
+ }
332
+ catch (err) {
333
+ this.log(`Error in sessionUpdate handler: ${err instanceof Error ? err.stack : err}`);
334
+ }
332
335
  },
333
336
  // Required: handle permission requests
334
337
  requestPermission: async (params) => {
@@ -370,40 +373,53 @@ export class AcpClient {
370
373
  const content = update.content;
371
374
  if (content.type === "text") {
372
375
  this.currentResponseText += content.text;
373
- this.bus.emit("agent:response-chunk", { text: content.text });
376
+ this.bus.emitTransform("agent:response-chunk", { text: content.text });
374
377
  }
375
378
  break;
376
379
  }
377
380
  case "agent_thought_chunk": {
378
381
  const thought = update.content;
379
382
  if (thought.type === "text" && thought.text) {
380
- this.bus.emit("agent:thinking-chunk", { text: thought.text });
383
+ this.bus.emitTransform("agent:thinking-chunk", { text: thought.text });
381
384
  }
382
385
  break;
383
386
  }
384
387
  case "tool_call": {
385
388
  const toolId = update.toolCallId || `tool-${this.pendingToolCounter++}`;
386
- this.pendingToolCalls.set(toolId, update.title ?? "");
387
- this.bus.emit("agent:tool-started", {
389
+ const payload = {
388
390
  title: update.title,
389
391
  toolCallId: toolId,
390
392
  kind: update.kind ?? undefined,
391
393
  locations: update.locations?.map((l) => ({ path: l.path, line: l.line })),
392
394
  rawInput: update.rawInput,
395
+ };
396
+ const defer = this.pendingToolCalls.size > 0;
397
+ this.pendingToolCalls.set(toolId, {
398
+ title: update.title ?? "",
399
+ deferredPayload: defer ? payload : undefined,
393
400
  });
401
+ if (!defer) {
402
+ this.bus.emit("agent:tool-started", payload);
403
+ }
394
404
  break;
395
405
  }
396
406
  case "tool_call_update": {
397
407
  const toolId = update.toolCallId;
398
- const toolTitle = toolId ? this.pendingToolCalls.get(toolId) : undefined;
408
+ const toolInfo = toolId ? this.pendingToolCalls.get(toolId) : undefined;
409
+ const toolTitle = toolInfo?.title;
399
410
  if (update.status === "completed" || update.status === "failed") {
411
+ // Emit deferred tool-started before output (parallel tools)
412
+ if (toolInfo?.deferredPayload) {
413
+ this.bus.emit("agent:tool-started", toolInfo.deferredPayload);
414
+ toolInfo.deferredPayload = undefined;
415
+ }
400
416
  // Show content only on final status. Skip tools whose output the
401
417
  // user already sees (user_shell → PTY) or is agent-only (shell_recall).
402
418
  const skipOutput = toolTitle === "user_shell" || toolTitle === "shell_recall";
403
419
  if (!skipOutput && update.content && Array.isArray(update.content)) {
404
420
  for (const block of update.content) {
405
421
  if (block.type === "content" && block.content?.type === "text" && block.content.text) {
406
- this.bus.emit("agent:tool-output-chunk", { chunk: block.content.text });
422
+ this.bus.emitTransform("agent:tool-output-chunk", { chunk: block.content.text });
407
423
  }
408
424
  }
409
425
  }
package/dist/core.js CHANGED
@@ -20,11 +20,15 @@ import { EventBus } from "./event-bus.js";
20
20
  import { ContextManager } from "./context-manager.js";
21
21
  import { AcpClient } from "./acp-client.js";
22
22
  import { setPalette } from "./utils/palette.js";
23
+ import * as streamTransform from "./utils/stream-transform.js";
24
+ import * as settingsMod from "./settings.js";
25
+ import { HandlerRegistry } from "./utils/handler-registry.js";
23
26
  // Re-export types that library consumers need
24
27
  export { EventBus } from "./event-bus.js";
25
28
  export { palette, setPalette, resetPalette } from "./utils/palette.js";
26
29
  export function createCore(config) {
27
30
  const bus = new EventBus();
31
+ const handlers = new HandlerRegistry();
28
32
  const contextManager = new ContextManager(bus);
29
33
  const client = new AcpClient({ bus, contextManager, config });
30
34
  let connected = false;
@@ -67,6 +71,12 @@ export function createCore(config) {
67
71
  getAcpClient: () => client,
68
72
  quit: opts.quit,
69
73
  setPalette,
74
+ createBlockTransform: (o) => streamTransform.createBlockTransform(bus, o),
75
+ createFencedBlockTransform: (o) => streamTransform.createFencedBlockTransform(bus, o),
76
+ getExtensionSettings: settingsMod.getExtensionSettings,
77
+ define: (name, fn) => handlers.define(name, fn),
78
+ advise: (name, wrapper) => handlers.advise(name, wrapper),
79
+ call: (name, ...args) => handlers.call(name, ...args),
70
80
  };
71
81
  },
72
82
  kill() {
@@ -32,6 +32,7 @@ export interface ShellEvents {
32
32
  };
33
33
  "agent:response-chunk": {
34
34
  text: string;
35
+ blocks?: ContentBlock[];
35
36
  };
36
37
  "agent:response-done": {
37
38
  response: string;
@@ -126,6 +127,20 @@ export interface ShellEvents {
126
127
  }[];
127
128
  };
128
129
  }
130
+ export type ContentBlock = {
131
+ type: "text";
132
+ text: string;
133
+ } | {
134
+ type: "code-block";
135
+ language: string;
136
+ code: string;
137
+ } | {
138
+ type: "image";
139
+ data: Buffer;
140
+ } | {
141
+ type: "raw";
142
+ escape: string;
143
+ };
129
144
  type Listener<T> = (payload: T) => void;
130
145
  type PipeListener<T> = (payload: T) => T;
131
146
  type AsyncPipeListener<T> = (payload: T) => T | Promise<T>;
@@ -145,6 +160,13 @@ export declare class EventBus {
145
160
  off<K extends keyof ShellEvents>(event: K, fn: Listener<ShellEvents[K]>): void;
146
161
  /** Emit a fire-and-forget event. */
147
162
  emit<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
163
+ /**
164
+ * Transform-then-notify: run the payload through any registered pipe
165
+ * listeners (transforms), then emit the final result to regular `on`
166
+ * listeners (renderers). This enables content pipelines where extensions
167
+ * modify data (e.g. render LaTeX → terminal image) before renderers see it.
168
+ */
169
+ emitTransform<K extends keyof ShellEvents>(event: K, payload: ShellEvents[K]): void;
148
170
  /** Register a transform listener for a pipeline event. */
149
171
  onPipe<K extends keyof ShellEvents>(event: K, fn: PipeListener<ShellEvents[K]>): void;
150
172
  /**
package/dist/event-bus.js CHANGED
@@ -21,6 +21,16 @@ export class EventBus {
21
21
  emit(event, payload) {
22
22
  this.emitter.emit(event, payload);
23
23
  }
24
+ /**
25
+ * Transform-then-notify: run the payload through any registered pipe
26
+ * listeners (transforms), then emit the final result to regular `on`
27
+ * listeners (renderers). This enables content pipelines where extensions
28
+ * modify data (e.g. render LaTeX → terminal image) before renderers see it.
29
+ */
30
+ emitTransform(event, payload) {
31
+ const transformed = this.emitPipe(event, payload);
32
+ this.emitter.emit(event, transformed);
33
+ }
24
34
  /** Register a transform listener for a pipeline event. */
25
35
  onPipe(event, fn) {
26
36
  let listeners = this.pipeListeners.get(event);
@@ -1,2 +1,2 @@
1
1
  import type { ExtensionContext } from "../types.js";
2
- export default function activate({ bus, getAcpClient }: ExtensionContext): void;
2
+ export default function activate(ctx: ExtensionContext): void;