agent-sh 0.14.8 → 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.
- package/dist/agent/agent-loop.d.ts +0 -4
- package/dist/agent/agent-loop.js +8 -166
- package/dist/agent/entry-format.d.ts +5 -0
- package/dist/agent/entry-format.js +9 -0
- package/dist/agent/extensions/rolling-history/constants.d.ts +1 -0
- package/dist/agent/extensions/rolling-history/constants.js +1 -0
- package/dist/agent/extensions/rolling-history/index.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/index.js +203 -0
- package/dist/agent/extensions/rolling-history/recall.d.ts +4 -0
- package/dist/agent/extensions/rolling-history/recall.js +122 -0
- package/dist/agent/extensions/rolling-history/strategy.d.ts +70 -0
- package/dist/agent/extensions/rolling-history/strategy.js +336 -0
- package/dist/agent/host-types.d.ts +0 -3
- package/dist/agent/index.js +44 -5
- package/dist/agent/live-view.d.ts +57 -0
- package/dist/agent/live-view.js +238 -0
- package/dist/agent/llm-client.d.ts +1 -0
- package/dist/agent/llm-client.js +1 -1
- package/dist/agent/session-store.d.ts +90 -0
- package/dist/agent/session-store.js +288 -0
- package/dist/agent/store.d.ts +74 -0
- package/dist/agent/store.js +284 -0
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/tool-protocol.d.ts +11 -11
- package/dist/cli/index.js +4 -2
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.js +0 -1
- package/dist/core/settings.d.ts +5 -1
- package/dist/core/settings.js +62 -1
- package/dist/extensions/index.d.ts +1 -0
- package/dist/shell/events.d.ts +1 -0
- package/dist/shell/input-handler.js +4 -0
- package/dist/shell/tui-renderer.js +5 -2
- package/dist/utils/diff-renderer.js +9 -7
- package/examples/extensions/ash-acp-bridge/src/index.ts +1 -2
- package/examples/extensions/ashi/package.json +2 -2
- package/examples/extensions/ashi/src/capture.ts +1 -1
- package/examples/extensions/ashi/src/cli.ts +3 -4
- package/examples/extensions/ashi/src/compaction.ts +6 -2
- package/examples/extensions/ashi/src/frontend.ts +13 -13
- package/examples/extensions/ashi/src/multi-session-store.ts +35 -12
- package/examples/extensions/ashi/src/session-commands.ts +1 -1
- package/examples/extensions/ashi/src/user-shell-intents.ts +17 -0
- package/package.json +13 -1
- package/dist/agent/conversation-state.d.ts +0 -142
- package/dist/agent/conversation-state.js +0 -788
- package/dist/agent/history-file.d.ts +0 -81
- package/dist/agent/history-file.js +0 -271
- 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;
|
package/dist/agent/index.js
CHANGED
|
@@ -7,7 +7,7 @@ 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
11
|
import { resolveApiKey } from "../cli/auth/keys.js";
|
|
12
12
|
import { discoverSkills } from "./skills.js";
|
|
13
13
|
import activateOpenrouter from "./providers/openrouter.js";
|
|
@@ -209,7 +209,6 @@ export default function agentBackend(ctx) {
|
|
|
209
209
|
ctx.define("provider:resolve-api-key", (id) => resolveApiKey(id));
|
|
210
210
|
// Core tools register at activate — before extensions load — so
|
|
211
211
|
// extensions that look them up at activate time (e.g. scheme.ts) find them.
|
|
212
|
-
// conversation_recall stays in AgentLoop (needs session state).
|
|
213
212
|
const fileReadCache = new Map();
|
|
214
213
|
ctx.define("agent:file-read-cache", () => fileReadCache);
|
|
215
214
|
const getCwd = () => ctx.call("cwd");
|
|
@@ -387,14 +386,54 @@ export default function agentBackend(ctx) {
|
|
|
387
386
|
initialMode,
|
|
388
387
|
compositor: ctx.shell?.compositor,
|
|
389
388
|
instanceId: ctx.instanceId,
|
|
390
|
-
history: config.history,
|
|
391
389
|
});
|
|
392
390
|
agentLoop.wire();
|
|
393
391
|
ashActive = true;
|
|
394
392
|
bus.emit("command:register", {
|
|
395
393
|
name: "/compact",
|
|
396
|
-
description: "Compact
|
|
397
|
-
handler: () =>
|
|
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
|
+
},
|
|
398
437
|
});
|
|
399
438
|
bus.emit("command:register", {
|
|
400
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
|
+
}
|