agent-sh 0.14.7 → 0.14.9

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/dist/agent/agent-loop.d.ts +0 -4
  2. package/dist/agent/agent-loop.js +8 -166
  3. package/dist/agent/entry-format.d.ts +5 -0
  4. package/dist/agent/entry-format.js +9 -0
  5. package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
  6. package/dist/agent/extensions/rolling-history/constants.js +1 -0
  7. package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
  8. package/dist/agent/extensions/rolling-history/index.js +203 -0
  9. package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
  10. package/dist/agent/extensions/rolling-history/recall.js +122 -0
  11. package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
  12. package/dist/agent/extensions/rolling-history/strategy.js +336 -0
  13. package/dist/agent/host-types.d.ts +0 -3
  14. package/dist/agent/index.js +46 -5
  15. package/dist/agent/live-view.d.ts +57 -0
  16. package/dist/agent/live-view.js +238 -0
  17. package/dist/agent/llm-client.d.ts +1 -0
  18. package/dist/agent/llm-client.js +1 -1
  19. package/dist/agent/session-store.d.ts +90 -0
  20. package/dist/agent/session-store.js +288 -0
  21. package/dist/agent/store.d.ts +74 -0
  22. package/dist/agent/store.js +284 -0
  23. package/dist/agent/subagent.js +2 -2
  24. package/dist/agent/tool-protocol.d.ts +11 -11
  25. package/dist/cli/auth/discover.js +18 -1
  26. package/dist/cli/index.js +4 -2
  27. package/dist/core/index.d.ts +0 -1
  28. package/dist/core/index.js +0 -1
  29. package/dist/core/settings.d.ts +5 -1
  30. package/dist/core/settings.js +62 -1
  31. package/dist/extensions/index.d.ts +1 -0
  32. package/dist/shell/events.d.ts +1 -0
  33. package/dist/shell/input-handler.js +4 -0
  34. package/dist/shell/strategies/bash.js +6 -2
  35. package/dist/shell/tui-renderer.js +5 -2
  36. package/dist/utils/diff-renderer.js +9 -7
  37. package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
  38. package/examples/extensions/ashi/package.json +2 -2
  39. package/examples/extensions/ashi/src/capture.ts +1 -1
  40. package/examples/extensions/ashi/src/cli.ts +3 -4
  41. package/examples/extensions/ashi/src/compaction.ts +6 -2
  42. package/examples/extensions/ashi/src/frontend.ts +13 -10
  43. package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
  44. package/examples/extensions/ashi/src/session-commands.ts +1 -1
  45. package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
  46. package/examples/extensions/ollama.ts +3 -2
  47. package/examples/extensions/opencode-provider.ts +1 -2
  48. package/examples/extensions/zai-coding-plan.ts +1 -2
  49. package/package.json +13 -1
  50. package/dist/agent/conversation-state.d.ts +0 -142
  51. package/dist/agent/conversation-state.js +0 -788
  52. package/dist/agent/history-file.d.ts +0 -81
  53. package/dist/agent/history-file.js +0 -271
  54. package/examples/extensions/ashi/src/session-store.ts +0 -363
@@ -0,0 +1,70 @@
1
+ import type { Store, Entry } from "../../store.js";
2
+ import type { AgentShMessage } from "../../llm-client.js";
3
+ import { type NuclearEntry } from "../../nuclear-form.js";
4
+ export interface SummaryCtx {
5
+ store: Store;
6
+ bus: {
7
+ on(event: "conversation:message-appended", fn: MessageAppendedHandler): void;
8
+ };
9
+ advise(op: "conversation:compact", fn: CompactAdvisor): void;
10
+ /** Process id — disambiguates cross-instance entries in `nucleate()`. */
11
+ iid: string;
12
+ getMessages(): AgentShMessage[];
13
+ replaceMessages(msgs: AgentShMessage[]): void;
14
+ estimateTokens(): number;
15
+ estimatePromptTokens(): number;
16
+ linkMessage(index: number, entryId: string): void;
17
+ }
18
+ export interface MessageAppendedEvent {
19
+ role: "user" | "assistant" | "tool" | "system";
20
+ content: string;
21
+ toolName?: string;
22
+ toolArgs?: Record<string, unknown>;
23
+ isError?: boolean;
24
+ }
25
+ export type MessageAppendedHandler = (e: MessageAppendedEvent) => Promise<void> | void;
26
+ export interface CompactEvent {
27
+ target?: number;
28
+ force?: boolean;
29
+ /** `rewind`/`replace` are kernel-owned manual edits — the summary
30
+ * strategy delegates them via `next()`. */
31
+ strategy?: {
32
+ kind: "two-tier-pin";
33
+ target: number;
34
+ keepRecent?: number;
35
+ force?: boolean;
36
+ } | {
37
+ kind: "rewind";
38
+ toIndex: number;
39
+ } | {
40
+ kind: "replace";
41
+ messages: unknown[];
42
+ };
43
+ }
44
+ export interface CompactResult {
45
+ before: number;
46
+ after: number;
47
+ evictedCount: number;
48
+ }
49
+ export type CompactAdvisor = (next: (e: CompactEvent) => Promise<CompactResult | null>, event: CompactEvent) => Promise<CompactResult | null>;
50
+ export declare function activate(ctx: SummaryCtx): void;
51
+ export declare function makeCaptureHandler(ctx: SummaryCtx): MessageAppendedHandler;
52
+ export declare function nuclearToEntry(ne: NuclearEntry, id: string): Entry;
53
+ export declare function makeCompactAdvisor(ctx: SummaryCtx): CompactAdvisor;
54
+ export declare enum Priority {
55
+ LOWEST = 0,// large read-only tool results
56
+ LOW = 1,// successful tool results
57
+ MEDIUM = 2,// write/edit tool results, plain assistant turns
58
+ HIGH = 3,// user messages, errors
59
+ PINNED = 4
60
+ }
61
+ export interface Turn {
62
+ messages: AgentShMessage[];
63
+ priority: Priority;
64
+ }
65
+ /** A new turn starts at each user message; the first may be headless
66
+ * (system, preamble). */
67
+ export declare function parseTurns(msgs: AgentShMessage[]): Turn[];
68
+ export declare function inferPriority(msgs: AgentShMessage[]): Priority;
69
+ export declare function slimTurn(messages: AgentShMessage[]): AgentShMessage[];
70
+ export declare function readSummaryLines(store: Store, n?: number): Promise<string[]>;
@@ -0,0 +1,336 @@
1
+ import { newEntryId } from "../../store.js";
2
+ import { nucleate, READ_ONLY_TOOLS, WRITE_TOOLS, isReadOnly, } from "../../nuclear-form.js";
3
+ import { formatEntryLine } from "../../entry-format.js";
4
+ import { RECALL_CACHE_KIND } from "./constants.js";
5
+ export function activate(ctx) {
6
+ ctx.bus.on("conversation:message-appended", makeCaptureHandler(ctx));
7
+ ctx.advise("conversation:compact", makeCompactAdvisor(ctx));
8
+ }
9
+ export function makeCaptureHandler(ctx) {
10
+ return async (e) => {
11
+ const store = ctx.store;
12
+ // Capture the index synchronously — later async ticks may grow the array.
13
+ const msgs = ctx.getMessages();
14
+ const liveIdx = msgs.length - 1;
15
+ const m = msgs[liveIdx];
16
+ if (!m)
17
+ return;
18
+ // Stamp meta.tool so compact's inferPriority can read it without
19
+ // walking back to the event payload.
20
+ if (e.role === "tool" && e.toolName !== undefined) {
21
+ m.meta = {
22
+ ...m.meta,
23
+ tool: { toolName: e.toolName, args: e.toolArgs ?? {}, isError: !!e.isError },
24
+ };
25
+ }
26
+ const ne = nucleateFromEvent(e, ctx.iid);
27
+ if (!ne)
28
+ return;
29
+ const id = newEntryId();
30
+ await store.append([nuclearToEntry(ne, id)]);
31
+ await store.append([{
32
+ id: newEntryId(), parentId: id, ts: Date.now(),
33
+ kind: RECALL_CACHE_KIND,
34
+ payload: { fullMessage: m },
35
+ }], { ephemeral: true });
36
+ ctx.linkMessage(liveIdx, id);
37
+ };
38
+ }
39
+ function nucleateFromEvent(e, iid) {
40
+ if (e.role === "user") {
41
+ if (!e.content)
42
+ return null;
43
+ return nucleate("user", e.content, iid, 0);
44
+ }
45
+ if (e.role === "assistant") {
46
+ if (!e.content)
47
+ return null;
48
+ return nucleate("agent", e.content, iid, 0);
49
+ }
50
+ if (e.role === "tool") {
51
+ if (e.toolName === undefined)
52
+ return null;
53
+ return nucleate("tool", e.toolName, e.toolArgs ?? {}, e.content, !!e.isError, iid, 0);
54
+ }
55
+ return null;
56
+ }
57
+ /** Used during compact when topping up summaries for messages that
58
+ * missed eager capture (e.g. injected system notes that bypassed the
59
+ * event). Reads from message structure rather than an event payload. */
60
+ function nucleateFromMessage(m, iid) {
61
+ if (m.role === "user") {
62
+ const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
63
+ if (!text)
64
+ return null;
65
+ return nucleate("user", text, iid, 0);
66
+ }
67
+ if (m.role === "assistant") {
68
+ if ("tool_calls" in m && m.tool_calls && m.tool_calls.length > 0)
69
+ return null;
70
+ const text = typeof m.content === "string" ? m.content : "";
71
+ if (!text)
72
+ return null;
73
+ return nucleate("agent", text, iid, 0);
74
+ }
75
+ if (m.role === "tool") {
76
+ const tool = m.meta?.tool;
77
+ if (!tool)
78
+ return null;
79
+ const content = typeof m.content === "string" ? m.content : "";
80
+ return nucleate("tool", tool.toolName, tool.args, content, tool.isError ?? false, iid, 0);
81
+ }
82
+ return null;
83
+ }
84
+ export function nuclearToEntry(ne, id) {
85
+ const { seq: _seq, ts, kind, ...rest } = ne;
86
+ return { id, ts, kind, payload: rest };
87
+ }
88
+ const DEFAULT_RECENT_TURNS_KEEP = 10;
89
+ export function makeCompactAdvisor(ctx) {
90
+ return async (next, event) => {
91
+ if (event.strategy?.kind === "rewind" || event.strategy?.kind === "replace") {
92
+ return next(event);
93
+ }
94
+ const promptBefore = ctx.estimatePromptTokens();
95
+ const convBefore = ctx.estimateTokens();
96
+ const overhead = Math.max(0, promptBefore - convBefore);
97
+ const promptTarget = event.target ?? Infinity;
98
+ const convTarget = Math.max(0, promptTarget - overhead);
99
+ if (!event.force && promptBefore <= promptTarget)
100
+ return null;
101
+ const msgs = ctx.getMessages();
102
+ const turns = parseTurns(msgs);
103
+ const minTurns = event.force ? 1 : 2;
104
+ if (turns.length <= minTurns)
105
+ return null;
106
+ const maxPinnedFraction = event.force ? 0.4 : 0.6;
107
+ const maxPinned = Math.max(2, Math.floor(turns.length * maxPinnedFraction));
108
+ const recentKeep = Math.min(DEFAULT_RECENT_TURNS_KEEP, turns.length - 1, Math.max(1, event.force ? Math.min(maxPinned, turns.length - 2) : maxPinned));
109
+ for (const t of turns)
110
+ t.priority = inferPriority(t.messages);
111
+ const verbatimCount = 1;
112
+ const slimmedCount = Math.max(0, recentKeep - verbatimCount);
113
+ const slimStart = turns.length - recentKeep;
114
+ const slimEnd = slimStart + slimmedCount;
115
+ const slimmedIndices = new Set();
116
+ for (let i = slimStart; i < slimEnd; i++)
117
+ slimmedIndices.add(i);
118
+ turns[0].priority = Priority.PINNED;
119
+ for (let i = turns.length - verbatimCount; i < turns.length; i++)
120
+ turns[i].priority = Priority.PINNED;
121
+ for (const i of slimmedIndices)
122
+ turns[i].priority = Priority.PINNED;
123
+ const candidates = turns
124
+ .map((t, idx) => ({ turn: t, idx }))
125
+ .filter((c) => c.turn.priority !== Priority.PINNED)
126
+ .sort((a, b) => {
127
+ const wA = a.turn.priority * recencyWeight(a.idx, turns.length);
128
+ const wB = b.turn.priority * recencyWeight(b.idx, turns.length);
129
+ return wA - wB || a.idx - b.idx;
130
+ });
131
+ const evicted = new Set();
132
+ let tokens = convBefore;
133
+ const newSummaryEntries = [];
134
+ const newCacheEntries = [];
135
+ const store = ctx.store;
136
+ for (const c of candidates) {
137
+ if (tokens <= convTarget)
138
+ break;
139
+ evicted.add(c.idx);
140
+ tokens -= estimateTurnTokens(c.turn.messages);
141
+ // Top up summaries for messages that missed eager capture.
142
+ // Read-only tools are cached ephemerally only — the agent can
143
+ // re-read the source.
144
+ for (const m of c.turn.messages) {
145
+ const existingId = m.meta?.entryId;
146
+ if (existingId) {
147
+ newCacheEntries.push({
148
+ id: newEntryId(), parentId: existingId, ts: Date.now(),
149
+ kind: RECALL_CACHE_KIND,
150
+ payload: { fullMessage: m },
151
+ });
152
+ continue;
153
+ }
154
+ const ne = nucleateFromMessage(m, ctx.iid);
155
+ if (!ne)
156
+ continue;
157
+ const id = newEntryId();
158
+ const entry = nuclearToEntry(ne, id);
159
+ if (!isReadOnly(ne)) {
160
+ newSummaryEntries.push(entry);
161
+ }
162
+ newCacheEntries.push({
163
+ id: newEntryId(), parentId: id, ts: Date.now(),
164
+ kind: RECALL_CACHE_KIND,
165
+ payload: { fullMessage: m },
166
+ });
167
+ }
168
+ }
169
+ if (evicted.size === 0)
170
+ return null;
171
+ if (newSummaryEntries.length > 0)
172
+ await store.append(newSummaryEntries);
173
+ if (newCacheEntries.length > 0)
174
+ await store.append(newCacheEntries, { ephemeral: true });
175
+ const rebuilt = [];
176
+ let blockInserted = false;
177
+ for (let i = 0; i < turns.length; i++) {
178
+ if (evicted.has(i)) {
179
+ if (!blockInserted) {
180
+ rebuilt.push(await buildSummaryBlock(store));
181
+ blockInserted = true;
182
+ }
183
+ continue;
184
+ }
185
+ if (slimmedIndices.has(i)) {
186
+ rebuilt.push(...slimTurn(turns[i].messages));
187
+ }
188
+ else {
189
+ rebuilt.push(...turns[i].messages);
190
+ }
191
+ }
192
+ ctx.replaceMessages(rebuilt);
193
+ const after = ctx.estimatePromptTokens();
194
+ return { before: promptBefore, after, evictedCount: evicted.size };
195
+ };
196
+ }
197
+ export var Priority;
198
+ (function (Priority) {
199
+ Priority[Priority["LOWEST"] = 0] = "LOWEST";
200
+ Priority[Priority["LOW"] = 1] = "LOW";
201
+ Priority[Priority["MEDIUM"] = 2] = "MEDIUM";
202
+ Priority[Priority["HIGH"] = 3] = "HIGH";
203
+ Priority[Priority["PINNED"] = 4] = "PINNED";
204
+ })(Priority || (Priority = {}));
205
+ /** A new turn starts at each user message; the first may be headless
206
+ * (system, preamble). */
207
+ export function parseTurns(msgs) {
208
+ const turns = [];
209
+ let current = [];
210
+ for (const m of msgs) {
211
+ if (m.role === "user" && current.length > 0) {
212
+ turns.push({ messages: current, priority: Priority.MEDIUM });
213
+ current = [];
214
+ }
215
+ current.push(m);
216
+ }
217
+ if (current.length > 0)
218
+ turns.push({ messages: current, priority: Priority.MEDIUM });
219
+ return turns;
220
+ }
221
+ export function inferPriority(msgs) {
222
+ let hasError = false;
223
+ let hasWriteTool = false;
224
+ let allReadOnly = true;
225
+ let hasToolResult = false;
226
+ for (const m of msgs) {
227
+ if (m.role === "user")
228
+ return Priority.HIGH;
229
+ if (m.role === "tool") {
230
+ hasToolResult = true;
231
+ const tool = m.meta?.tool;
232
+ const content = typeof m.content === "string" ? m.content : "";
233
+ if (tool?.isError || content.startsWith("Error:"))
234
+ hasError = true;
235
+ }
236
+ if (m.role === "assistant" && "tool_calls" in m && m.tool_calls) {
237
+ for (const tc of m.tool_calls) {
238
+ const fn = "function" in tc ? tc.function : undefined;
239
+ if (!fn)
240
+ continue;
241
+ if (WRITE_TOOLS.has(fn.name))
242
+ hasWriteTool = true;
243
+ if (!READ_ONLY_TOOLS.has(fn.name))
244
+ allReadOnly = false;
245
+ }
246
+ }
247
+ }
248
+ if (hasError)
249
+ return Priority.HIGH;
250
+ if (hasWriteTool)
251
+ return Priority.MEDIUM;
252
+ if (hasToolResult && allReadOnly)
253
+ return Priority.LOWEST;
254
+ if (hasToolResult)
255
+ return Priority.LOW;
256
+ return Priority.MEDIUM;
257
+ }
258
+ function recencyWeight(idx, total) {
259
+ return Math.max(0.1, 1 - idx / total);
260
+ }
261
+ function estimateTurnTokens(msgs) {
262
+ return Math.ceil(JSON.stringify(msgs).length / 4);
263
+ }
264
+ export function slimTurn(messages) {
265
+ const MAX_RESULT_LEN = 1500;
266
+ const MAX_ASSISTANT_LEN = 1500;
267
+ const result = [];
268
+ const droppedToolIds = new Set();
269
+ for (const msg of messages) {
270
+ if (msg.role === "assistant" && "tool_calls" in msg && msg.tool_calls) {
271
+ const kept = msg.tool_calls.filter((tc) => {
272
+ if (!("function" in tc))
273
+ return true;
274
+ if (READ_ONLY_TOOLS.has(tc.function.name)) {
275
+ droppedToolIds.add(tc.id);
276
+ return false;
277
+ }
278
+ return true;
279
+ });
280
+ if (kept.length === 0) {
281
+ const text = typeof msg.content === "string" ? msg.content.trim() : "";
282
+ if (!text)
283
+ continue;
284
+ const { tool_calls: _tc, ...rest } = msg;
285
+ result.push(rest);
286
+ }
287
+ else {
288
+ result.push({ ...msg, tool_calls: kept });
289
+ }
290
+ continue;
291
+ }
292
+ if (msg.role === "tool") {
293
+ if (droppedToolIds.has(msg.tool_call_id))
294
+ continue;
295
+ const content = typeof msg.content === "string" ? msg.content : "";
296
+ if (content.length > MAX_RESULT_LEN) {
297
+ result.push({ ...msg, content: slimToolContent(content, MAX_RESULT_LEN) });
298
+ }
299
+ else {
300
+ result.push(msg);
301
+ }
302
+ continue;
303
+ }
304
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.length > MAX_ASSISTANT_LEN) {
305
+ const head = msg.content.slice(0, Math.floor(MAX_ASSISTANT_LEN * 0.6));
306
+ const tail = msg.content.slice(-Math.floor(MAX_ASSISTANT_LEN * 0.2));
307
+ const trimmed = msg.content.length - head.length - tail.length;
308
+ result.push({ ...msg, content: `${head}\n... [${trimmed} chars trimmed by compact]\n${tail}` });
309
+ continue;
310
+ }
311
+ result.push(msg);
312
+ }
313
+ return result;
314
+ }
315
+ function slimToolContent(content, maxLen) {
316
+ const exitMatch = content.match(/exit code:?\s*(\d+)/i);
317
+ const exitSuffix = exitMatch ? ` (exit ${exitMatch[1]})` : "";
318
+ const lines = content.split("\n");
319
+ if (lines.length > 6) {
320
+ const head = lines.slice(0, 3).join("\n");
321
+ const tail = lines.slice(-2).join("\n");
322
+ return `${head}\n... [${lines.length - 5} lines trimmed by compact]\n${tail}${exitSuffix}`;
323
+ }
324
+ return `${content.slice(0, maxLen)}\n... [${content.length - maxLen} chars trimmed by compact]${exitSuffix}`;
325
+ }
326
+ export async function readSummaryLines(store, n) {
327
+ const recent = await store.readRecent(n);
328
+ return recent.filter((e) => e.kind !== RECALL_CACHE_KIND).map(formatEntryLine);
329
+ }
330
+ async function buildSummaryBlock(store) {
331
+ const lines = await readSummaryLines(store);
332
+ return {
333
+ role: "user",
334
+ content: `[Conversation history — use conversation_recall to expand any entry]\n${lines.join("\n")}`,
335
+ };
336
+ }
@@ -1,5 +1,4 @@
1
1
  import type { CoreConfig, CoreContext } from "../core/types.js";
2
- import type { HistoryAdapter } from "./history-file.js";
3
2
  import type { SkillView, ToolDefinition, ToolExecutionContext, ToolSchemaView } from "./types.js";
4
3
  /**
5
4
  * Backend-agnostic LLM interface exposed via `ctx.agent.llm`. Fulfilled
@@ -142,7 +141,5 @@ export interface AgentConfigSurface {
142
141
  provider?: string;
143
142
  /** Default model id. */
144
143
  model?: string;
145
- /** Conversation history backend. Defaults to the on-disk HistoryFile. */
146
- history?: HistoryAdapter;
147
144
  }
148
145
  export type AgentConfig = CoreConfig & AgentConfigSurface;
@@ -7,7 +7,8 @@ import { AgentLoop } from "./agent-loop.js";
7
7
  import { LlmClient } from "./llm-client.js";
8
8
  import { createLlmFacade } from "./llm-facade.js";
9
9
  import { registerReadOnlyTool, unregisterReadOnlyTool } from "./nuclear-form.js";
10
- import { resolveProvider, getProviderNames, getSettings } from "../core/settings.js";
10
+ import { resolveProvider, getProviderNames, getSettings, setSessionOverlay, clearSessionOverlay, getSettingSource, } from "../core/settings.js";
11
+ import { resolveApiKey } from "../cli/auth/keys.js";
11
12
  import { discoverSkills } from "./skills.js";
12
13
  import activateOpenrouter from "./providers/openrouter.js";
13
14
  import activateOpenai from "./providers/openai.js";
@@ -205,9 +206,9 @@ export default function agentBackend(ctx) {
205
206
  },
206
207
  };
207
208
  ctx.agent = agentSurface;
209
+ ctx.define("provider:resolve-api-key", (id) => resolveApiKey(id));
208
210
  // Core tools register at activate — before extensions load — so
209
211
  // extensions that look them up at activate time (e.g. scheme.ts) find them.
210
- // conversation_recall stays in AgentLoop (needs session state).
211
212
  const fileReadCache = new Map();
212
213
  ctx.define("agent:file-read-cache", () => fileReadCache);
213
214
  const getCwd = () => ctx.call("cwd");
@@ -385,14 +386,54 @@ export default function agentBackend(ctx) {
385
386
  initialMode,
386
387
  compositor: ctx.shell?.compositor,
387
388
  instanceId: ctx.instanceId,
388
- history: config.history,
389
389
  });
390
390
  agentLoop.wire();
391
391
  ashActive = true;
392
392
  bus.emit("command:register", {
393
393
  name: "/compact",
394
- description: "Compact conversation via the active compaction strategy",
395
- handler: () => bus.emit("agent:compact-request", {}),
394
+ description: "Compact now, or: off | on | threshold <0..1> | status",
395
+ handler: (args) => {
396
+ const trimmed = args.trim();
397
+ if (!trimmed) {
398
+ bus.emit("agent:compact-request", {});
399
+ return;
400
+ }
401
+ const [sub, ...rest] = trimmed.split(/\s+/);
402
+ if (sub === "off" || sub === "on") {
403
+ setSessionOverlay({ autoCompact: sub === "on" });
404
+ bus.emit("ui:info", { message: `auto-compact: ${sub} (session)` });
405
+ return;
406
+ }
407
+ if (sub === "threshold") {
408
+ const raw = rest[0];
409
+ const n = Number(raw);
410
+ if (!raw || !Number.isFinite(n) || n < 0 || n > 1) {
411
+ bus.emit("ui:error", { message: "usage: /compact threshold <0.0..1.0>" });
412
+ return;
413
+ }
414
+ setSessionOverlay({ autoCompactThreshold: n });
415
+ bus.emit("ui:info", { message: `auto-compact threshold: ${n} (session)` });
416
+ return;
417
+ }
418
+ if (sub === "reset") {
419
+ clearSessionOverlay("autoCompact", "autoCompactThreshold");
420
+ bus.emit("ui:info", { message: "auto-compact: session overrides cleared" });
421
+ return;
422
+ }
423
+ if (sub === "status") {
424
+ const s = getSettings();
425
+ const enabledSrc = getSettingSource("autoCompact");
426
+ const thSrc = getSettingSource("autoCompactThreshold");
427
+ bus.emit("ui:info", {
428
+ message: `auto-compact: ${s.autoCompact ? "on" : "off"} (${enabledSrc}), ` +
429
+ `threshold: ${s.autoCompactThreshold} (${thSrc})`,
430
+ });
431
+ return;
432
+ }
433
+ bus.emit("ui:error", {
434
+ message: `unknown subcommand: ${sub}. usage: /compact [off|on|threshold <0..1>|reset|status]`,
435
+ });
436
+ },
396
437
  });
397
438
  bus.emit("command:register", {
398
439
  name: "/context",
@@ -0,0 +1,57 @@
1
+ import type { ChatCompletionMessageParam, AgentShMessage } from "./llm-client.js";
2
+ import type { HandlerFunctions } from "../utils/handler-registry.js";
3
+ import type { ImageContent } from "./types.js";
4
+ export interface CompactResult {
5
+ before: number;
6
+ after: number;
7
+ evictedCount: number;
8
+ [extra: string]: unknown;
9
+ }
10
+ export declare class LiveView {
11
+ private messages;
12
+ private messagesDirty;
13
+ private cachedMessagesJson;
14
+ readonly instanceId: string;
15
+ private readonly handlers;
16
+ private lastApiTokenCount;
17
+ private lastApiMessageCount;
18
+ private pendingMessages;
19
+ constructor(handlers?: HandlerFunctions, instanceId?: string);
20
+ private getMessagesJson;
21
+ private invalidateMessagesCache;
22
+ addUserMessage(text: string): void;
23
+ addAssistantMessage(content: string | null, toolCalls?: {
24
+ id: string;
25
+ function: {
26
+ name: string;
27
+ arguments: string;
28
+ };
29
+ }[], extras?: Record<string, unknown>): void;
30
+ addToolResult(toolCallId: string, content: string | ImageContent[], isError?: boolean): void;
31
+ addToolResultInline(content: string): void;
32
+ /** Safe from any context: queues if mid-tool-pair, appends otherwise. */
33
+ addSystemNote(text: string): void;
34
+ appendUserMessage(text: string): void;
35
+ private hasOpenToolCalls;
36
+ private flushPendingMessages;
37
+ getMessages(): ChatCompletionMessageParam[];
38
+ get(): AgentShMessage[];
39
+ forLLM(): ChatCompletionMessageParam[];
40
+ replace(msgs: AgentShMessage[]): void;
41
+ link(index: number, entryId: string): void;
42
+ /** DeepSeek 400s on tool messages without a matching tool_call;
43
+ * compaction can leave such orphans. */
44
+ private dropOrphanToolMessages;
45
+ /** Stub missing tool results after a mid-execution interrupt so
46
+ * DeepSeek doesn't 400 on dangling tool_calls. */
47
+ private stubDanglingToolCalls;
48
+ /** DeepSeek 400s if any assistant in a thinking-mode conversation
49
+ * is missing `reasoning_content`. Cross-alias `reasoning` (from
50
+ * OpenRouter) and stub gaps with "". */
51
+ private normalizeReasoningConsistency;
52
+ /** Invalidates the API token baseline since the new array's count is unknown. */
53
+ replaceMessages(messages: ChatCompletionMessageParam[]): void;
54
+ updateApiTokenCount(promptTokens: number): void;
55
+ estimatePromptTokens(): number;
56
+ estimateTokens(): number;
57
+ }