agent-sh 0.14.10 → 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.
Files changed (97) hide show
  1. package/README.md +36 -13
  2. package/dist/agent/agent-loop.d.ts +9 -17
  3. package/dist/agent/agent-loop.js +123 -150
  4. package/dist/agent/events.d.ts +10 -12
  5. package/dist/agent/host-types.d.ts +17 -11
  6. package/dist/agent/index.d.ts +1 -1
  7. package/dist/agent/index.js +76 -29
  8. package/dist/agent/live-view.d.ts +3 -3
  9. package/dist/agent/live-view.js +15 -7
  10. package/dist/agent/providers/deepseek.js +9 -1
  11. package/dist/agent/providers/openrouter.js +9 -0
  12. package/dist/agent/session-store.js +1 -1
  13. package/dist/agent/subagent.js +1 -1
  14. package/dist/agent/system-prompt.d.ts +7 -3
  15. package/dist/agent/system-prompt.js +11 -14
  16. package/dist/agent/tool-protocol.js +0 -7
  17. package/dist/cli/args.js +2 -1
  18. package/dist/cli/install.d.ts +1 -0
  19. package/dist/cli/install.js +39 -2
  20. package/dist/cli/subcommands.js +1 -0
  21. package/dist/core/event-bus.js +0 -2
  22. package/dist/core/extension-loader.js +3 -1
  23. package/dist/core/index.d.ts +1 -1
  24. package/dist/core/index.js +3 -2
  25. package/dist/extensions/slash-commands/index.js +16 -11
  26. package/dist/shell/events.d.ts +3 -0
  27. package/dist/shell/index.js +9 -0
  28. package/dist/shell/shell-context.d.ts +2 -2
  29. package/dist/shell/shell-context.js +26 -11
  30. package/dist/shell/shell.js +3 -0
  31. package/dist/shell/tui-renderer.js +0 -1
  32. package/dist/utils/diff-renderer.d.ts +4 -0
  33. package/dist/utils/diff-renderer.js +15 -27
  34. package/dist/utils/handler-registry.d.ts +1 -6
  35. package/dist/utils/handler-registry.js +1 -6
  36. package/dist/utils/line-editor.js +0 -2
  37. package/dist/utils/palette.js +4 -4
  38. package/dist/utils/terminal-buffer.d.ts +2 -0
  39. package/dist/utils/terminal-buffer.js +4 -0
  40. package/examples/extensions/ads/SKILL.md +170 -0
  41. package/examples/extensions/ash-acp-bridge/src/index.ts +11 -7
  42. package/examples/extensions/ash-scheme/index.ts +377 -687
  43. package/examples/extensions/ash-scheme/package.json +1 -1
  44. package/examples/extensions/ashi/EXTENDING.md +118 -0
  45. package/examples/extensions/ashi/README.md +26 -54
  46. package/examples/extensions/ashi/docs/ui-surface-protocol.md +163 -0
  47. package/examples/extensions/ashi/package.json +14 -2
  48. package/examples/extensions/ashi/src/autocomplete-controller.ts +95 -0
  49. package/examples/extensions/ashi/src/autocomplete.ts +1 -23
  50. package/examples/extensions/ashi/src/capture.ts +54 -10
  51. package/examples/extensions/ashi/src/chat/assistant.ts +67 -0
  52. package/examples/extensions/ashi/src/chat/lines.ts +39 -0
  53. package/examples/extensions/ashi/src/chat/thinking.ts +42 -0
  54. package/examples/extensions/ashi/src/chat/tool-group.ts +84 -0
  55. package/examples/extensions/ashi/src/chat/user-message.ts +20 -0
  56. package/examples/extensions/ashi/src/cli.ts +80 -12
  57. package/examples/extensions/ashi/src/clipboard-image.ts +41 -0
  58. package/examples/extensions/ashi/src/commands.ts +11 -1
  59. package/examples/extensions/ashi/src/dialogs.ts +67 -0
  60. package/examples/extensions/ashi/src/display-config.ts +16 -1
  61. package/examples/extensions/ashi/src/docks.ts +31 -0
  62. package/examples/extensions/ashi/src/events.ts +16 -0
  63. package/examples/extensions/ashi/src/frontend.ts +456 -268
  64. package/examples/extensions/ashi/src/hooks.ts +27 -40
  65. package/examples/extensions/ashi/src/input-prompt.ts +64 -0
  66. package/examples/extensions/ashi/src/renderer.ts +222 -0
  67. package/examples/extensions/ashi/src/renderers/pi-tui/app.ts +122 -0
  68. package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +27 -0
  69. package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +190 -0
  70. package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +203 -0
  71. package/examples/extensions/ashi/src/renderers/pi-tui/theme-adapters.ts +48 -0
  72. package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +21 -0
  73. package/examples/extensions/ashi/src/schema.ts +46 -205
  74. package/examples/extensions/ashi/src/session-commands.ts +2 -1
  75. package/examples/extensions/ashi/src/status-footer.ts +35 -25
  76. package/examples/extensions/ashi/src/terminal-mode.ts +9 -0
  77. package/examples/extensions/ashi/src/theme.ts +1 -47
  78. package/examples/extensions/ashi/src/ui.ts +88 -0
  79. package/examples/extensions/ashi-ink/README.md +61 -0
  80. package/examples/extensions/ashi-ink/package.json +30 -0
  81. package/examples/extensions/ashi-ink/src/index.ts +6 -0
  82. package/examples/extensions/ashi-ink/src/ink-renderer.tsx +865 -0
  83. package/examples/extensions/ashi-ink/src/shims.d.ts +5 -0
  84. package/examples/extensions/ashi-ink/test/render.test.tsx +408 -0
  85. package/examples/extensions/ashi-ink/tsconfig.json +14 -0
  86. package/examples/extensions/ashi-scheme-render.ts +10 -10
  87. package/examples/extensions/ashi-shell-passthrough.ts +95 -0
  88. package/examples/extensions/ashi-ui-demo.ts +63 -0
  89. package/examples/extensions/latex-images.ts +70 -19
  90. package/examples/extensions/overlay-agent.ts +5 -5
  91. package/examples/extensions/pi-bridge/index.ts +7 -12
  92. package/examples/extensions/terminal-buffer.ts +4 -2
  93. package/package.json +3 -9
  94. package/examples/extensions/ashi/src/components.ts +0 -238
  95. package/examples/extensions/ollama.ts +0 -108
  96. package/examples/extensions/opencode-provider.ts +0 -251
  97. package/examples/extensions/zai-coding-plan.ts +0 -35
@@ -1,5 +1,5 @@
1
- import type { ProviderRegistration } from "./host-types.js";
2
- import type { ToolDefinition, ToolResultDisplay } from "./types.js";
1
+ import type { Model, ProviderRegistration } from "./host-types.js";
2
+ import type { ImageContent, ToolDefinition, ToolResultDisplay } from "./types.js";
3
3
  export interface AgentIdentity {
4
4
  name: string;
5
5
  version: 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:modes-changed": Record<string, never>;
25
+ "agent:models-changed": Record<string, never>;
25
26
  "config:switch-provider": {
26
27
  provider: string;
27
28
  };
@@ -44,6 +45,7 @@ declare module "../core/event-bus.js" {
44
45
  };
45
46
  "agent:submit": {
46
47
  query: string;
48
+ images?: ImageContent[];
47
49
  };
48
50
  "agent:cancel-request": {
49
51
  silent?: boolean;
@@ -69,6 +71,7 @@ declare module "../core/event-bus.js" {
69
71
  prompt_tokens: number;
70
72
  completion_tokens: number;
71
73
  total_tokens: number;
74
+ cached_prompt_tokens?: number;
72
75
  };
73
76
  "agent:processing-start": Record<string, never>;
74
77
  "agent:processing-done": Record<string, never>;
@@ -189,17 +192,12 @@ declare module "../core/event-bus.js" {
189
192
  };
190
193
  };
191
194
  "config:switch-model": {
192
- model: string;
195
+ id: string;
196
+ provider: string;
193
197
  };
194
198
  "config:get-models": {
195
- models: {
196
- model: string;
197
- provider: string;
198
- }[];
199
- active: {
200
- model: string;
201
- provider: string;
202
- } | null;
199
+ models: Model[];
200
+ active: Model | null;
203
201
  };
204
202
  "config:set-thinking": {
205
203
  level: string;
@@ -56,19 +56,16 @@ export interface ProviderRegistration {
56
56
  /** Local daemons etc. — `auth list/login` shows "no auth required". */
57
57
  noAuth?: boolean;
58
58
  }
59
- /** A model entry in the cycling list, optionally tied to a provider. */
60
- export interface AgentMode {
61
- model: string;
62
- /** Provider id — when cycling changes provider, LlmClient is reconfigured. */
63
- provider?: string;
64
- /** Provider-specific config for reconfiguring LlmClient on switch. */
65
- providerConfig?: {
66
- apiKey: string;
67
- baseURL?: string;
68
- };
59
+ /** A selectable (provider, model) target the frontend lists and switches.
60
+ * Serializable identity + capabilities only; the secret + closures needed to
61
+ * invoke it live in ModelEndpoint, so this can safely cross to frontends and
62
+ * out-of-process bridges. */
63
+ export interface Model {
64
+ id: string;
65
+ provider: string;
69
66
  /** Context window size in tokens (for usage display). */
70
67
  contextWindow?: number;
71
- /** Max output tokens for this mode. */
68
+ /** Max output tokens. */
72
69
  maxTokens?: number;
73
70
  /** Model supports reasoning/thinking tokens. */
74
71
  reasoning?: boolean;
@@ -79,7 +76,15 @@ export interface AgentMode {
79
76
  echoReasoning?: boolean;
80
77
  /** Input modalities the model supports. Defaults to ["text"]. */
81
78
  modalities?: ("text" | "image")[];
79
+ }
80
+ /** Credentials + provider-shape transforms for invoking a Model, resolved by
81
+ * (provider, id). Internal: holds a secret (apiKey) and non-serializable
82
+ * closures, so it must never ride a bus event. */
83
+ export interface ModelEndpoint {
84
+ apiKey: string;
85
+ baseURL?: string;
82
86
  buildReasoningParams?: (level: string) => Record<string, unknown>;
87
+ extractCachedTokens?: (usage: Record<string, unknown>) => number | undefined;
83
88
  }
84
89
  /**
85
90
  * Capabilities the agent host adds on top of CoreContext. Only available
@@ -94,6 +99,7 @@ export interface AgentSurface {
94
99
  unregister: (id: string) => void;
95
100
  configure: (id: string, opts: {
96
101
  reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
102
+ cacheTokens?: (usage: Record<string, unknown>) => number | undefined;
97
103
  }) => void;
98
104
  };
99
105
  registerTool: (tool: ToolDefinition) => void;
@@ -11,5 +11,5 @@ export { AgentLoop } from "./agent-loop.js";
11
11
  export { ToolRegistry } from "./tool-registry.js";
12
12
  export { runSubagent, type SubagentOptions } from "./subagent.js";
13
13
  /** Built-in providers register unconditionally so `auth list` can
14
- * enumerate them; buildModes() skips entries without an apiKey. */
14
+ * enumerate them; buildModels() skips entries without an apiKey. */
15
15
  export declare function activateAgent(ctx: ExtensionContext): void;
@@ -32,11 +32,21 @@ function persistedModelFor(providerName) {
32
32
  return undefined;
33
33
  return getSettings().providers?.[providerName]?.defaultModel;
34
34
  }
35
+ /** The OpenAI SDK silently defaults an empty baseURL to api.openai.com, so a
36
+ * provider with a key but no endpoint would misroute its key there. `openai`
37
+ * is exempt: that default is its endpoint. */
38
+ function usableProvider(p) {
39
+ return !!p?.apiKey && (!!p.baseURL || p.id === "openai");
40
+ }
35
41
  function defaultReasoningBuilder(level) {
36
42
  if (level === "off")
37
43
  return {};
38
44
  return { reasoning_effort: level === "xhigh" ? "high" : level };
39
45
  }
46
+ function defaultCacheTokens(usage) {
47
+ const details = usage.prompt_tokens_details;
48
+ return typeof details?.cached_tokens === "number" ? details.cached_tokens : undefined;
49
+ }
40
50
  function mergeCaps(settingsCaps, payloadCaps, modelIds) {
41
51
  if (!settingsCaps)
42
52
  return payloadCaps.size > 0 ? payloadCaps : undefined;
@@ -85,11 +95,12 @@ export default function agentBackend(ctx) {
85
95
  settingsProviders.set(name, p);
86
96
  }
87
97
  const providerHooks = new Map();
88
- // Bakes model id so AgentMode.buildReasoningParams keeps its (level) signature.
98
+ // Bakes model id so ModelEndpoint.buildReasoningParams keeps its (level) signature.
89
99
  const bindReasoning = (shapeId, model) => {
90
100
  const hook = providerHooks.get(shapeId)?.reasoningParams;
91
101
  return hook ? (level) => hook(level, model) : defaultReasoningBuilder;
92
102
  };
103
+ const bindCacheTokens = (shapeId) => providerHooks.get(shapeId)?.cacheTokens ?? defaultCacheTokens;
93
104
  const agentSurface = {
94
105
  llm: createLlmFacade({ list: ctx.list, call: ctx.call }),
95
106
  providers: {
@@ -274,31 +285,43 @@ export default function agentBackend(ctx) {
274
285
  }
275
286
  return out;
276
287
  };
277
- const buildModes = () => {
288
+ const buildModels = () => {
278
289
  const out = [];
279
290
  for (const [id, p] of resolvedProviders) {
280
291
  if (!p.apiKey)
281
292
  continue;
282
- const shapeId = p.reasoningShape ?? id;
293
+ if (!usableProvider(p))
294
+ continue;
283
295
  for (const model of p.models) {
284
296
  const mc = p.modelCapabilities?.get(model);
285
297
  out.push({
286
- model,
298
+ id: model,
287
299
  provider: id,
288
- providerConfig: { apiKey: p.apiKey, baseURL: p.baseURL },
289
300
  contextWindow: mc?.contextWindow ?? p.contextWindow,
290
301
  maxTokens: mc?.maxTokens ?? (mc?.contextWindow ? Math.min(Math.floor(mc.contextWindow * 0.4), 65536) : undefined),
291
302
  reasoning: mc?.reasoning,
292
303
  supportsReasoningEffort: p.supportsReasoningEffort,
293
304
  echoReasoning: mc?.echoReasoning,
294
305
  modalities: mc?.modalities,
295
- buildReasoningParams: bindReasoning(shapeId, model),
296
306
  });
297
307
  }
298
308
  }
299
309
  return out;
300
310
  };
301
- ctx.define("agent:get-modes", () => buildModes());
311
+ const resolveEndpoint = (providerId, modelId) => {
312
+ const p = resolvedProviders.get(providerId);
313
+ if (!p?.apiKey)
314
+ return undefined;
315
+ const shapeId = p.reasoningShape ?? providerId;
316
+ return {
317
+ apiKey: p.apiKey,
318
+ baseURL: p.baseURL,
319
+ buildReasoningParams: bindReasoning(shapeId, modelId),
320
+ extractCachedTokens: bindCacheTokens(shapeId),
321
+ };
322
+ };
323
+ ctx.define("agent:get-models", () => buildModels());
324
+ ctx.define("agent:resolve-endpoint", ({ provider, id }) => resolveEndpoint(provider, id));
302
325
  // Reconfigured at core:extensions-loaded; start() gates on `resolved`.
303
326
  const llmClient = new LlmClient({ apiKey: "not-configured", model: "not-configured" });
304
327
  ctx.define("llm:get-client", () => llmClient);
@@ -321,10 +344,10 @@ export default function agentBackend(ctx) {
321
344
  resolvedProviders = computeResolvedProviders();
322
345
  if (!resolved)
323
346
  return;
324
- bus.emit("agent:modes-changed", {});
347
+ bus.emit("agent:models-changed", {});
325
348
  if (!ashActive)
326
349
  return;
327
- if (buildModes().some((m) => m.model === llmClient.model))
350
+ if (buildModels().some((m) => m.id === llmClient.model))
328
351
  return;
329
352
  const pendingProvider = getSettings().defaultProvider;
330
353
  if (!pendingProvider)
@@ -334,26 +357,47 @@ export default function agentBackend(ctx) {
334
357
  return;
335
358
  const pendingModel = persistedModelFor(pendingProvider);
336
359
  if (pendingModel && p.models.includes(pendingModel) && llmClient.model !== pendingModel) {
337
- bus.emit("config:switch-model", { model: pendingModel });
360
+ bus.emit("config:switch-model", { id: pendingModel, provider: pendingProvider });
338
361
  }
339
362
  });
340
- bus.on("provider:configure", ({ id, reasoningParams }) => {
363
+ bus.on("provider:configure", ({ id, reasoningParams, cacheTokens }) => {
341
364
  const prev = providerHooks.get(id) ?? {};
342
365
  if (reasoningParams !== undefined)
343
366
  prev.reasoningParams = reasoningParams;
367
+ if (cacheTokens !== undefined)
368
+ prev.cacheTokens = cacheTokens;
344
369
  providerHooks.set(id, prev);
345
370
  });
346
371
  bus.on("core:extensions-loaded", ({ names }) => {
347
372
  loadedExtensionNames = names;
348
373
  resolvedProviders = computeResolvedProviders();
349
374
  const settings = getSettings();
350
- // Built-ins register unconditionally so `auth list` can enumerate them;
351
- // the fallback must skip keyless entries or it lands on openrouter and
352
- // bails at the `!effectiveApiKey` guard below.
353
- const providerName = config.provider
354
- ?? settings.defaultProvider
355
- ?? [...resolvedProviders].find(([, p]) => p.apiKey)?.[0];
356
- const activeProvider = providerName ? resolvedProviders.get(providerName) ?? null : null;
375
+ let providerName = config.provider ?? settings.defaultProvider;
376
+ let activeProvider = providerName ? resolvedProviders.get(providerName) ?? null : null;
377
+ // Inline CLI credentials carry their own endpoint, so they skip the
378
+ // usable-provider fallback that registry-driven selection needs.
379
+ if (!config.apiKey) {
380
+ if (!providerName) {
381
+ const first = [...resolvedProviders].find(([, p]) => usableProvider(p));
382
+ providerName = first?.[0];
383
+ activeProvider = first?.[1] ?? null;
384
+ }
385
+ else if (!usableProvider(activeProvider)) {
386
+ const reason = !activeProvider ? "is not registered"
387
+ : !activeProvider.apiKey ? "has no API key configured"
388
+ : "has no endpoint configured";
389
+ const next = [...resolvedProviders].find(([, p]) => usableProvider(p));
390
+ if (next) {
391
+ bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}; falling back to "${next[0]}".` });
392
+ providerName = next[0];
393
+ activeProvider = next[1];
394
+ }
395
+ else {
396
+ bus.emit("ui:error", { message: `Provider "${providerName}" ${reason}, and no other configured provider has both an API key and an endpoint. Run \`agent-sh auth\` to configure one.` });
397
+ return;
398
+ }
399
+ }
400
+ }
357
401
  // Persisted defaultModel wins over openrouter's hardcoded DEFAULT_MODELS[0].
358
402
  const effectiveApiKey = config.apiKey ?? activeProvider?.apiKey;
359
403
  const effectiveBaseURL = config.baseURL ?? activeProvider?.baseURL;
@@ -361,15 +405,14 @@ export default function agentBackend(ctx) {
361
405
  // No provider → don't register ash; let another backend own activation.
362
406
  if (!effectiveApiKey || !effectiveModel)
363
407
  return;
364
- const foundInModes = buildModes().find((m) => m.model === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
408
+ const foundModel = buildModels().find((m) => m.id === effectiveModel && (!activeProvider || m.provider === activeProvider.id));
365
409
  // Stub when openrouter's async catalog hasn't returned yet; reconciled
366
410
  // later via agent:providers:changed → config:switch-model.
367
- const initialMode = foundInModes ?? (activeProvider ? {
368
- model: effectiveModel,
369
- provider: activeProvider.id,
370
- providerConfig: { apiKey: effectiveApiKey, baseURL: effectiveBaseURL },
371
- supportsReasoningEffort: activeProvider.supportsReasoningEffort,
372
- } : { model: effectiveModel });
411
+ const initialModel = foundModel ?? {
412
+ id: effectiveModel,
413
+ provider: activeProvider?.id ?? providerName ?? "custom",
414
+ supportsReasoningEffort: activeProvider?.supportsReasoningEffort,
415
+ };
373
416
  llmClient.reconfigure({ apiKey: effectiveApiKey, baseURL: effectiveBaseURL, model: effectiveModel });
374
417
  resolved = true;
375
418
  bus.emit("agent:register-backend", {
@@ -386,7 +429,7 @@ export default function agentBackend(ctx) {
386
429
  bus,
387
430
  llmClient,
388
431
  handlers: { define: ctx.define, advise: ctx.advise, call: ctx.call, list: ctx.list },
389
- initialMode,
432
+ initialModel,
390
433
  compositor: ctx.shell?.compositor,
391
434
  instanceId: ctx.instanceId,
392
435
  });
@@ -468,14 +511,18 @@ export default function agentBackend(ctx) {
468
511
  bus.emit("ui:error", { message: `Provider "${name}" has no API key configured` });
469
512
  return;
470
513
  }
514
+ if (!p.baseURL && p.id !== "openai") {
515
+ bus.emit("ui:error", { message: `Provider "${name}" has no endpoint configured` });
516
+ return;
517
+ }
471
518
  const switchModel = p.defaultModel ?? p.models[0];
472
519
  if (!switchModel) {
473
520
  bus.emit("ui:error", { message: `Provider "${name}" has no models configured` });
474
521
  return;
475
522
  }
476
523
  llmClient.reconfigure({ apiKey: p.apiKey, baseURL: p.baseURL, model: switchModel });
477
- bus.emit("agent:modes-changed", {});
478
- bus.emit("config:switch-model", { model: switchModel });
524
+ bus.emit("agent:models-changed", {});
525
+ bus.emit("config:switch-model", { id: switchModel, provider: name });
479
526
  bus.emit("ui:info", { message: `Switched to ${name} (${switchModel})` });
480
527
  });
481
528
  bus.onPipe("banner:collect", (e) => {
@@ -495,7 +542,7 @@ export { AgentLoop } from "./agent-loop.js";
495
542
  export { ToolRegistry } from "./tool-registry.js";
496
543
  export { runSubagent } from "./subagent.js";
497
544
  /** Built-in providers register unconditionally so `auth list` can
498
- * enumerate them; buildModes() skips entries without an apiKey. */
545
+ * enumerate them; buildModels() skips entries without an apiKey. */
499
546
  export function activateAgent(ctx) {
500
547
  agentBackend(ctx);
501
548
  const agentCtx = ctx;
@@ -19,7 +19,7 @@ export declare class LiveView {
19
19
  constructor(handlers?: HandlerFunctions, instanceId?: string);
20
20
  private getMessagesJson;
21
21
  private invalidateMessagesCache;
22
- addUserMessage(text: string): void;
22
+ addUserMessage(text: string, images?: ImageContent[]): void;
23
23
  addAssistantMessage(content: string | null, toolCalls?: {
24
24
  id: string;
25
25
  function: {
@@ -34,9 +34,9 @@ export declare class LiveView {
34
34
  appendUserMessage(text: string): void;
35
35
  private hasOpenToolCalls;
36
36
  private flushPendingMessages;
37
- getMessages(): ChatCompletionMessageParam[];
38
- get(): AgentShMessage[];
37
+ /** Send-shaped; may be longer than get() (dangling calls stubbed) — never link()/replace() by these indices. */
39
38
  forLLM(): ChatCompletionMessageParam[];
39
+ get(): AgentShMessage[];
40
40
  replace(msgs: AgentShMessage[]): void;
41
41
  link(index: number, entryId: string): void;
42
42
  /** DeepSeek 400s on tool messages without a matching tool_call;
@@ -1,4 +1,3 @@
1
- import { stripMeta } from "./llm-client.js";
2
1
  export class LiveView {
3
2
  messages = [];
4
3
  messagesDirty = true;
@@ -26,8 +25,19 @@ export class LiveView {
26
25
  this.messagesDirty = true;
27
26
  this.cachedMessagesJson = null;
28
27
  }
29
- addUserMessage(text) {
30
- this.messages.push({ role: "user", content: text });
28
+ addUserMessage(text, images) {
29
+ if (images?.length) {
30
+ const parts = [];
31
+ if (text)
32
+ parts.push({ type: "text", text });
33
+ for (const img of images) {
34
+ parts.push({ type: "image_url", image_url: { url: `data:${img.mimeType};base64,${img.data}` } });
35
+ }
36
+ this.messages.push({ role: "user", content: parts });
37
+ }
38
+ else {
39
+ this.messages.push({ role: "user", content: text });
40
+ }
31
41
  this.invalidateMessagesCache();
32
42
  }
33
43
  addAssistantMessage(content, toolCalls, extras) {
@@ -131,15 +141,13 @@ export class LiveView {
131
141
  }
132
142
  this.invalidateMessagesCache();
133
143
  }
134
- getMessages() {
144
+ /** Send-shaped; may be longer than get() (dangling calls stubbed) — never link()/replace() by these indices. */
145
+ forLLM() {
135
146
  return this.normalizeReasoningConsistency(this.stubDanglingToolCalls(this.dropOrphanToolMessages(this.messages)));
136
147
  }
137
148
  get() {
138
149
  return this.messages;
139
150
  }
140
- forLLM() {
141
- return this.getMessages().map(stripMeta);
142
- }
143
151
  replace(msgs) {
144
152
  this.replaceMessages(msgs);
145
153
  }
@@ -10,7 +10,15 @@ function buildReasoningParams(level, _model) {
10
10
  : { thinking: { type: "enabled" }, reasoning_effort: level };
11
11
  }
12
12
  export default function activate(ctx) {
13
- ctx.agent.providers.configure("deepseek", { reasoningParams: buildReasoningParams });
13
+ ctx.agent.providers.configure("deepseek", {
14
+ reasoningParams: buildReasoningParams,
15
+ // Native DeepSeek reports caching as flat hit/miss counts, not the
16
+ // OpenAI-standard prompt_tokens_details.cached_tokens the default reads.
17
+ cacheTokens: (u) => {
18
+ const hit = u.prompt_cache_hit_tokens;
19
+ return typeof hit === "number" ? hit : undefined;
20
+ },
21
+ });
14
22
  ctx.agent.providers.register({
15
23
  id: "deepseek",
16
24
  apiKey: resolveApiKey("deepseek").key ?? undefined,
@@ -14,6 +14,14 @@ function buildReasoningParams(level, _model) {
14
14
  ? { reasoning: { effort: "none" } }
15
15
  : { reasoning: { effort: level } };
16
16
  }
17
+ /** OpenRouter's input_modalities → the text/image subset; undefined when absent
18
+ * so the fail-closed image guard treats the model as text-only. */
19
+ function toModalities(input) {
20
+ if (!Array.isArray(input))
21
+ return undefined;
22
+ const out = input.filter((v) => v === "text" || v === "image");
23
+ return out.length ? out : undefined;
24
+ }
17
25
  export default function activate(ctx) {
18
26
  const apiKey = resolveApiKey("openrouter").key;
19
27
  ctx.agent.providers.configure("openrouter", { reasoningParams: buildReasoningParams });
@@ -42,6 +50,7 @@ export default function activate(ctx) {
42
50
  reasoning: m.supported_parameters?.includes("reasoning") ?? false,
43
51
  contextWindow: m.context_length,
44
52
  echoReasoning: userOverrides.get(m.id) ?? patterns.some((re) => re.test(m.id)),
53
+ modalities: toModalities(m.architecture?.input_modalities),
45
54
  })),
46
55
  });
47
56
  }).catch(() => { });
@@ -237,7 +237,7 @@ export class SessionStore {
237
237
  for (const e of this.entries.values()) {
238
238
  if (e.type === "message" && e.message.role === "user") {
239
239
  const raw = typeof e.message.content === "string" ? e.message.content : "";
240
- const txt = stripContextWrappers(raw);
240
+ const txt = stripContextWrappers(raw).replace(/\s+/g, " ").trim();
241
241
  if (txt)
242
242
  return txt.slice(0, 80);
243
243
  }
@@ -109,7 +109,7 @@ async function streamOnce(llmClient, systemPrompt, conversation, apiTools, model
109
109
  const stream = await llmClient.stream({
110
110
  messages: [
111
111
  { role: "system", content: systemPrompt },
112
- ...wrapTrailingWithDynamicContext(conversation.getMessages(), dynamicContext ?? ""),
112
+ ...wrapTrailingWithDynamicContext(conversation.forLLM(), dynamicContext ?? ""),
113
113
  ],
114
114
  tools: apiTools.length > 0 ? apiTools : undefined,
115
115
  model,
@@ -7,10 +7,14 @@ import { type Skill } from "./skills.js";
7
7
  export declare function formatSkillsBlock(skills: Skill[]): string;
8
8
  export declare function loadGlobalAgentsMd(): string | null;
9
9
  /**
10
- * Static system prompt identical across all queries, cacheable.
11
- * Contains only identity and behavioral instructions.
10
+ * Identity paragraph one of the system prompt. Surface-agnostic, cacheable.
12
11
  */
13
- export declare const STATIC_SYSTEM_PROMPT: string;
12
+ export declare const STATIC_IDENTITY = "You are ash, an AI coding assistant running inside agent-sh \u2014 a composable agent runtime with a small core and everything else, including the frontend you're attached to, layered on as extensions.";
13
+ /**
14
+ * The rest of the static prompt — code map, tool guidance, envelope contract.
15
+ * Follows the frontend surface description in the assembled prompt.
16
+ */
17
+ export declare const STATIC_GUIDE: string;
14
18
  /**
15
19
  * CWD-scoped static context: project conventions (CLAUDE.md / AGENT.md)
16
20
  * and discovered skills. Stable for a given cwd — callers should cache
@@ -85,14 +85,14 @@ function loadConventionFiles(dir) {
85
85
  return result;
86
86
  }
87
87
  /**
88
- * Static system prompt identical across all queries, cacheable.
89
- * Contains only identity and behavioral instructions.
88
+ * Identity paragraph one of the system prompt. Surface-agnostic, cacheable.
90
89
  */
91
- export const STATIC_SYSTEM_PROMPT = `You are ash, an AI coding assistant running inside agent-sh — a composable agent runtime with a small core and everything else, including the shell integration, layered on as extensions.
92
-
93
- You may be paired with a terminal shell that shares the user's CWD, environment, and history in that mode you can read shell events and act on the user's session. Otherwise you may be embedded as a library, exposed over a bridge protocol, or running headless, with no shell available; in those modes you operate purely through your registered tools.
94
-
95
- agent-sh source and documentation live at ${CODE_DIR}. Read them when you need to understand how the runtime works, or when the user asks how to modify or extend it:
90
+ export const STATIC_IDENTITY = `You are ash, an AI coding assistant running inside agent-sh — a composable agent runtime with a small core and everything else, including the frontend you're attached to, layered on as extensions.`;
91
+ /**
92
+ * The rest of the static promptcode map, tool guidance, envelope contract.
93
+ * Follows the frontend surface description in the assembled prompt.
94
+ */
95
+ export const STATIC_GUIDE = `agent-sh source and documentation live at ${CODE_DIR}. Read them when you need to understand how the runtime works, or when the user asks how to modify or extend it:
96
96
  - ${path.join(CODE_DIR, "docs")} — start with README.md; architecture.md and extensions.md cover the kernel boundary and extension API
97
97
  - ${path.join(CODE_DIR, "src")} — kernel in src/core, default backend in src/agent, shell host in src/shell, built-in extensions in src/extensions
98
98
  - ${path.join(CODE_DIR, "examples/extensions")} — reference extensions to study or copy when adding functionality
@@ -105,15 +105,12 @@ guidance rather than assuming a particular tool exists. Tool output is
105
105
  returned to you for reasoning — the user doesn't see it directly.
106
106
 
107
107
  # Context Envelopes
108
- - \`<query_context>\` (contains \`<cwd>\` always, and \`<shell_events>\` when there were user shell commands since the last turn): the user's situation when they sent this turn — \`<cwd>\` anchors where they are right now, \`<shell_events>\` grounds "fix this" / "what just happened" requests. Trust the most recent \`<cwd>\` over any cwd referenced in earlier history.
109
- - \`<dynamic_context>\`: current system state — in-flight work, mode markers, warnings.
110
- \`<dynamic_context>\` may be absent on any turn.
111
108
 
112
- # Preference Learning
109
+ A turn may be preceded by either of two wrappers:
110
+ - \`<query_context>\`: the user's situation when they sent this turn — the frontend and extensions inject what grounds the request here. Trust the most recent values over anything referenced earlier in history.
111
+ - \`<dynamic_context>\`: current system state — in-flight work, mode markers, warnings.
113
112
 
114
- Treat the user's past commands as standing preferences. Before acting, check shell history
115
- and conversation context for recurring patterns — apply them proactively and do not wait to
116
- be reminded.`;
113
+ Either may be absent on any turn.`;
117
114
  /**
118
115
  * CWD-scoped static context: project conventions (CLAUDE.md / AGENT.md)
119
116
  * and discovered skills. Stable for a given cwd — callers should cache
@@ -84,7 +84,6 @@ export class InlineToolProtocol {
84
84
  const name = obj.tool;
85
85
  if (typeof name !== "string")
86
86
  continue;
87
- // Separate tool name from args
88
87
  const { tool: _, ...args } = obj;
89
88
  calls.push({
90
89
  id: `inline_${++this.callCounter}`,
@@ -128,7 +127,6 @@ class CodeBlockFilter {
128
127
  let raw = "";
129
128
  while (this.buf.length > 0) {
130
129
  if (this.inFence) {
131
- // Look for closing ```
132
130
  const closeIdx = this.buf.indexOf("```");
133
131
  if (closeIdx !== -1) {
134
132
  // Skip past closing ``` and any trailing whitespace on that line
@@ -142,7 +140,6 @@ class CodeBlockFilter {
142
140
  // No closing yet — keep buffering
143
141
  break;
144
142
  }
145
- // Look for opening ```tool
146
143
  const openIdx = this.buf.indexOf("```tool");
147
144
  if (openIdx !== -1) {
148
145
  // Emit everything before the fence, trimming trailing newline
@@ -184,7 +181,6 @@ class CodeBlockFilter {
184
181
  raw += this.buf;
185
182
  this.buf = "";
186
183
  }
187
- // Collapse runs of 3+ newlines into 2 (one blank line max)
188
184
  return this.collapseNewlines(raw);
189
185
  }
190
186
  flush() {
@@ -209,7 +205,6 @@ class CodeBlockFilter {
209
205
  prefix = "\n".repeat(Math.min(leading, allowed));
210
206
  text = text.slice(leading);
211
207
  }
212
- // Collapse internal runs
213
208
  text = text.replace(/\n{3,}/g, "\n\n");
214
209
  // Track trailing newlines for next call
215
210
  let trailing = 0;
@@ -322,9 +317,7 @@ export class DeferredToolProtocol {
322
317
  if (schemaProps) {
323
318
  const validParams = new Set(Object.keys(schemaProps));
324
319
  const providedParams = Object.keys(targetArgs);
325
- // Check for unknown params (likely wrong names)
326
320
  const unknown = providedParams.filter((p) => !validParams.has(p));
327
- // Check for missing required params
328
321
  const missing = [...requiredParams].filter((p) => !targetArgs[p]);
329
322
  if (unknown.length > 0 || missing.length > 0) {
330
323
  const expected = [...validParams]
package/dist/cli/args.js CHANGED
@@ -3,9 +3,10 @@ const HELP_TEXT = `agent-sh — a shell-first terminal where AI is one keystroke
3
3
 
4
4
  Usage: agent-sh [options]
5
5
  agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
6
- agent-sh install <spec> [--force] [--sync-deps]
6
+ agent-sh install <spec> [--force] [--sync-deps] [--dev]
7
7
  Install an extension (bundled name, file:, npm:, github:)
8
8
  --sync-deps rewrites a stale agent-sh pin to the host version
9
+ --dev links the extension against the running host's core (local development)
9
10
  agent-sh uninstall <name> Remove an installed extension
10
11
  agent-sh list List installed extensions
11
12
  agent-sh auth login [provider] Store an API key for a built-in provider
@@ -1,6 +1,7 @@
1
1
  interface InstallOpts {
2
2
  force?: boolean;
3
3
  syncDeps?: boolean;
4
+ dev?: boolean;
4
5
  }
5
6
  export declare function listBundled(): string[];
6
7
  /** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */