agent-sh 0.15.5 → 0.15.7

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/agent/agent-loop.js +2 -5
  4. package/dist/agent/extensions/rolling-history/index.js +20 -8
  5. package/dist/agent/extensions/rolling-history/recall.d.ts +2 -2
  6. package/dist/agent/extensions/rolling-history/recall.js +17 -7
  7. package/dist/agent/providers/openai-compatible.d.ts +8 -0
  8. package/dist/agent/providers/openai-compatible.js +9 -2
  9. package/dist/agent/store.js +6 -1
  10. package/dist/agent/token-budget.d.ts +2 -1
  11. package/dist/agent/token-budget.js +6 -1
  12. package/dist/agent/types.d.ts +4 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/core/event-bus.d.ts +16 -1
  15. package/dist/core/event-bus.js +73 -11
  16. package/dist/core/index.js +18 -0
  17. package/dist/shell/tui-renderer.js +116 -174
  18. package/dist/utils/diff-renderer.js +65 -30
  19. package/dist/utils/executor.js +19 -11
  20. package/dist/utils/floating-panel.d.ts +1 -0
  21. package/dist/utils/floating-panel.js +28 -26
  22. package/dist/utils/markdown.js +56 -44
  23. package/dist/utils/palette.d.ts +11 -0
  24. package/dist/utils/palette.js +11 -0
  25. package/docs/agent.md +13 -11
  26. package/docs/architecture.md +3 -5
  27. package/docs/extensions.md +21 -20
  28. package/docs/library.md +6 -3
  29. package/docs/troubleshooting.md +2 -2
  30. package/docs/tui-composition.md +11 -3
  31. package/docs/usage.md +70 -50
  32. package/examples/extensions/ashi/src/chat/assistant.ts +6 -4
  33. package/examples/extensions/ashi/src/compaction.ts +4 -7
  34. package/examples/extensions/ashi/src/frontend.ts +2 -0
  35. package/examples/extensions/ashi/src/schema.ts +8 -2
  36. package/examples/extensions/command-suggest.ts +90 -0
  37. package/examples/extensions/solarized-theme.ts +11 -0
  38. package/package.json +5 -5
  39. package/src/agent/agent-loop.ts +2 -5
  40. package/src/agent/extensions/rolling-history/index.ts +20 -8
  41. package/src/agent/extensions/rolling-history/recall.ts +28 -7
  42. package/src/agent/providers/openai-compatible.ts +19 -4
  43. package/src/agent/store.ts +5 -1
  44. package/src/agent/token-budget.ts +10 -1
  45. package/src/agent/types.ts +4 -1
  46. package/src/cli/index.ts +1 -1
  47. package/src/core/event-bus.ts +67 -12
  48. package/src/core/index.ts +18 -0
  49. package/src/shell/tui-renderer.ts +131 -207
  50. package/src/utils/diff-renderer.ts +62 -29
  51. package/src/utils/executor.ts +17 -14
  52. package/src/utils/floating-panel.ts +24 -22
  53. package/src/utils/markdown.ts +49 -40
  54. package/src/utils/palette.ts +30 -5
@@ -28,17 +28,32 @@ export default function activate(ctx: AgentContext): void {
28
28
  id,
29
29
  apiKey,
30
30
  baseURL,
31
- defaultModel: models[0],
31
+ defaultModel: models[0]!.id,
32
32
  models,
33
33
  });
34
34
  }).catch(() => { /* leave empty — user supplies via --model */ });
35
35
  }
36
36
 
37
- async function fetchModels(baseURL: string, apiKey: string): Promise<string[]> {
37
+ export interface CatalogModel {
38
+ id: string;
39
+ meta?: { n_ctx?: number };
40
+ max_model_len?: number;
41
+ }
42
+
43
+ export function catalogContextWindow(m: CatalogModel): number | undefined {
44
+ if (typeof m.meta?.n_ctx === "number" && m.meta.n_ctx > 0) return m.meta.n_ctx;
45
+ if (typeof m.max_model_len === "number" && m.max_model_len > 0) return m.max_model_len;
46
+ return undefined;
47
+ }
48
+
49
+ async function fetchModels(
50
+ baseURL: string,
51
+ apiKey: string,
52
+ ): Promise<{ id: string; contextWindow?: number }[]> {
38
53
  const headers: Record<string, string> = {};
39
54
  if (apiKey && apiKey !== "no-key") headers.Authorization = `Bearer ${apiKey}`;
40
55
  const res = await fetch(`${baseURL.replace(/\/$/, "")}/models`, { headers });
41
56
  if (!res.ok) return [];
42
- const data = await res.json() as { data?: { id: string }[] };
43
- return (data.data ?? []).map((m) => m.id);
57
+ const data = await res.json() as { data?: CatalogModel[] };
58
+ return (data.data ?? []).map((m) => ({ id: m.id, contextWindow: catalogContextWindow(m) }));
44
59
  }
@@ -54,7 +54,11 @@ function escapeRegex(s: string): string {
54
54
  }
55
55
 
56
56
  function compileSearchRegex(query: string): RegExp {
57
- return new RegExp(escapeRegex(query), "i");
57
+ try {
58
+ return new RegExp(query, "i");
59
+ } catch {
60
+ return new RegExp(escapeRegex(query), "i");
61
+ }
58
62
  }
59
63
 
60
64
  function matchEntry(entry: Entry, re: RegExp): SearchHit | null {
@@ -8,5 +8,14 @@
8
8
  /** Response reserve — tokens reserved for the model's output. */
9
9
  export const RESPONSE_RESERVE = 8192;
10
10
 
11
+ const FALLBACK_CONTEXT_WINDOW = 60_000;
12
+
13
+ export function resolveDefaultContextWindow(
14
+ env: Record<string, string | undefined> = process.env,
15
+ ): number {
16
+ const n = Number(env.AGENT_SH_DEFAULT_CONTEXT_WINDOW);
17
+ return Number.isInteger(n) && n > 0 ? n : FALLBACK_CONTEXT_WINDOW;
18
+ }
19
+
11
20
  /** Fallback when contextWindow is unknown. */
12
- export const DEFAULT_CONTEXT_WINDOW = 60_000;
21
+ export const DEFAULT_CONTEXT_WINDOW = resolveDefaultContextWindow();
@@ -54,7 +54,10 @@ export type ToolResultBody =
54
54
  | { kind: "lines"; lines: string[]; maxLines?: number }
55
55
 
56
56
  export interface ToolDisplayInfo {
57
- kind: "read" | "write" | "execute" | "search";
57
+ /** Verb shown next to the detail (e.g. "execute foo.py"). Omit when a custom
58
+ * `icon` already makes the action self-evident — the renderer then shows
59
+ * icon + detail with no verb. */
60
+ kind?: "read" | "write" | "execute" | "search";
58
61
  locations?: { path: string; line?: number | null }[];
59
62
  /** Custom icon character for TUI display (e.g., "◆", "⌕"). When set, the TUI shows
60
63
  * icon + detail only. When absent, the tool name is shown alongside the detail. */
package/src/cli/index.ts CHANGED
@@ -128,7 +128,6 @@ async function main(): Promise<void> {
128
128
  // Load before spawning the shell so PS1 lands below the banner.
129
129
  const settings = getSettings();
130
130
  await loadBuiltinExtensions(extCtx, settings.disabledBuiltins);
131
- activateRollingHistory(extCtx);
132
131
  const loadExtensionsTimeoutMs = 10000;
133
132
  let loadedExtensions: string[] = [];
134
133
  await Promise.race([
@@ -197,6 +196,7 @@ async function main(): Promise<void> {
197
196
  }
198
197
 
199
198
  await core.activateBackend(config.backend);
199
+ activateRollingHistory(extCtx);
200
200
 
201
201
  // 100ms sidesteps macOS SIGTTOU during fg-pgrp handoff.
202
202
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -1,5 +1,3 @@
1
- import { EventEmitter } from "node:events";
2
-
3
1
  export interface BackendRegistration {
4
2
  name: string;
5
3
  kill: () => void;
@@ -49,6 +47,15 @@ export interface BusMeta {
49
47
 
50
48
  export type AnyListener = (name: string, payload: unknown, meta: BusMeta) => void;
51
49
 
50
+ /** A listener fault routed to the error reporter; `phase` is the callback site. */
51
+ export interface BusFault {
52
+ phase: "on" | "any" | "pipe" | "pipe-async";
53
+ event: string;
54
+ err: unknown;
55
+ }
56
+
57
+ export type ErrorReporter = (fault: BusFault) => void;
58
+
52
59
  /**
53
60
  * Typed event bus with two modes:
54
61
  * - emit/on/off: fire-and-forget notifications
@@ -56,18 +63,54 @@ export type AnyListener = (name: string, payload: unknown, meta: BusMeta) => voi
56
63
  * can modify the payload before passing to the next
57
64
  */
58
65
  export class EventBus {
59
- private emitter = new EventEmitter().setMaxListeners(0);
66
+ private listeners = new Map<string, Listener<any>[]>();
60
67
  private pipeListeners = new Map<string, PipeListener<any>[]>();
61
68
  private asyncPipeListeners = new Map<string, AsyncPipeListener<any>[]>();
62
69
  private source = "0000";
63
70
  private nextSeq = 0;
64
71
  private anyListeners: AnyListener[] = [];
65
72
 
73
+ /** Default fault sink, overridable via setErrorReporter: silent unless DEBUG. */
74
+ private reportError: ErrorReporter = ({ phase, event, err }) => {
75
+ if (process.env.DEBUG) {
76
+ const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
77
+ process.stderr.write(`[event-bus] ${phase} fault on "${event}": ${msg}\n`);
78
+ }
79
+ };
80
+
66
81
  /** Set the source id stamped onto every emitted event. */
67
82
  setSource(src: string): void {
68
83
  this.source = src;
69
84
  }
70
85
 
86
+ /** Install a fault reporter. */
87
+ setErrorReporter(fn: ErrorReporter): void {
88
+ this.reportError = fn;
89
+ }
90
+
91
+ /** Report a fault; guarded so a broken reporter can't break dispatch. */
92
+ private fault(phase: BusFault["phase"], event: string, err: unknown): void {
93
+ try {
94
+ this.reportError({ phase, event, err });
95
+ } catch {
96
+ /* swallow */
97
+ }
98
+ }
99
+
100
+ /** Fire every listener for `name`, isolating faults. */
101
+ private notify(name: string, payload: unknown): void {
102
+ const arr = this.listeners.get(name);
103
+ if (!arr || arr.length === 0) return;
104
+ // snapshot so a listener that (un)subscribes mid-dispatch can't shift iteration
105
+ if (arr.length === 1) {
106
+ try { arr[0](payload); } catch (err) { this.fault("on", name, err); }
107
+ return;
108
+ }
109
+ for (const fn of arr.slice()) {
110
+ try { fn(payload); } catch (err) { this.fault("on", name, err); }
111
+ }
112
+ }
113
+
71
114
  /** Subscribe to every emitted event with full envelope. Returns unsubscribe. */
72
115
  onAny(fn: AnyListener): () => void {
73
116
  this.anyListeners.push(fn);
@@ -87,10 +130,10 @@ export class EventBus {
87
130
  name,
88
131
  };
89
132
  for (const fn of this.anyListeners) {
90
- try { fn(name, payload, meta); } catch { /* swallow */ }
133
+ try { fn(name, payload, meta); } catch (err) { this.fault("any", name, err); }
91
134
  }
92
135
  }
93
- this.emitter.emit(name, payload);
136
+ this.notify(name, payload);
94
137
  }
95
138
 
96
139
  /** Subscribe to a fire-and-forget event. */
@@ -98,7 +141,12 @@ export class EventBus {
98
141
  event: K,
99
142
  fn: Listener<BusEvents[K]>,
100
143
  ): void {
101
- this.emitter.on(event, fn);
144
+ let arr = this.listeners.get(event);
145
+ if (!arr) {
146
+ arr = [];
147
+ this.listeners.set(event, arr);
148
+ }
149
+ arr.push(fn);
102
150
  }
103
151
 
104
152
  /** Unsubscribe from a fire-and-forget event. */
@@ -106,7 +154,10 @@ export class EventBus {
106
154
  event: K,
107
155
  fn: Listener<BusEvents[K]>,
108
156
  ): void {
109
- this.emitter.off(event, fn);
157
+ const arr = this.listeners.get(event);
158
+ if (!arr) return;
159
+ const idx = arr.indexOf(fn);
160
+ if (idx !== -1) arr.splice(idx, 1);
110
161
  }
111
162
 
112
163
  /** Emit a fire-and-forget event. */
@@ -123,10 +174,10 @@ export class EventBus {
123
174
  relay(meta: BusMeta, payload: unknown): void {
124
175
  if (this.anyListeners.length > 0) {
125
176
  for (const fn of this.anyListeners) {
126
- try { fn(meta.name, payload, meta); } catch { /* swallow */ }
177
+ try { fn(meta.name, payload, meta); } catch (err) { this.fault("any", meta.name, err); }
127
178
  }
128
179
  }
129
- this.emitter.emit(meta.name, payload);
180
+ this.notify(meta.name, payload);
130
181
  }
131
182
 
132
183
  /**
@@ -191,12 +242,12 @@ export class EventBus {
191
242
  try {
192
243
  const out = fn(result);
193
244
  if (out && typeof (out as any).then === "function") {
194
- console.error(`[event-bus] Warning: async handler in sync pipe "${String(event)}" — use onPipeAsync instead`);
245
+ this.fault("pipe", String(event), new Error("async handler in sync pipe — use onPipeAsync instead"));
195
246
  continue;
196
247
  }
197
248
  result = out;
198
249
  } catch (err) {
199
- console.error(`[event-bus] Pipe handler error in "${String(event)}":`, err instanceof Error ? err.message : err);
250
+ this.fault("pipe", String(event), err);
200
251
  }
201
252
  }
202
253
  return result;
@@ -245,7 +296,11 @@ export class EventBus {
245
296
  if (!listeners) return payload;
246
297
  let result = payload;
247
298
  for (const fn of listeners) {
248
- result = await fn(result);
299
+ try {
300
+ result = await fn(result);
301
+ } catch (err) {
302
+ this.fault("pipe-async", String(event), err);
303
+ }
249
304
  }
250
305
  return result;
251
306
  }
package/src/core/index.ts CHANGED
@@ -48,6 +48,24 @@ export function createCore(config: AppConfig): AgentShellCore {
48
48
  // should accept ≥6 hex chars.
49
49
  const instanceId = crypto.randomBytes(3).toString("hex");
50
50
  bus.setSource(instanceId);
51
+
52
+ // Surface faults on ui:error; `surfacing` stops a faulting renderer from looping.
53
+ let surfacing = false;
54
+ bus.setErrorReporter(({ phase, event, err }) => {
55
+ const detail = err instanceof Error ? err.message : String(err);
56
+ if (process.env.DEBUG) {
57
+ const full = err instanceof Error ? (err.stack ?? err.message) : detail;
58
+ process.stderr.write(`[event-bus] ${phase} fault on "${event}": ${full}\n`);
59
+ }
60
+ if (surfacing) return;
61
+ surfacing = true;
62
+ try {
63
+ bus.emit("ui:error", { message: `Handler error on "${event}": ${detail}` });
64
+ } finally {
65
+ surfacing = false;
66
+ }
67
+ });
68
+
51
69
  handlers.define("config:get-app-config", () => config);
52
70
  handlers.define("cwd", () => process.cwd());
53
71