agent-sh 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/README.md +14 -0
- package/docs/agent.md +398 -0
- package/docs/architecture.md +196 -0
- package/docs/context-management.md +200 -0
- package/docs/extensions.md +951 -0
- package/docs/library.md +84 -0
- package/docs/troubleshooting.md +65 -0
- package/docs/tui-composition.md +294 -0
- package/docs/usage.md +306 -0
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +2 -2
- package/examples/extensions/ashi/README.md +2 -2
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
- package/examples/extensions/ashi/package.json +5 -3
- package/examples/extensions/ashi/src/cli.ts +6 -5
- package/examples/extensions/ashi/src/renderer.ts +22 -2
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
- package/examples/extensions/ashi-ink/package.json +2 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/package.json +3 -1
- package/src/agent/agent-loop.ts +1563 -0
- package/src/agent/entry-format.ts +19 -0
- package/src/agent/events.ts +151 -0
- package/src/agent/extensions/rolling-history/constants.ts +1 -0
- package/src/agent/extensions/rolling-history/index.ts +202 -0
- package/src/agent/extensions/rolling-history/recall.ts +131 -0
- package/src/agent/extensions/rolling-history/strategy.ts +404 -0
- package/src/agent/host-types.ts +192 -0
- package/src/agent/index.ts +591 -0
- package/src/agent/live-view.ts +279 -0
- package/src/agent/llm-client.ts +111 -0
- package/src/agent/llm-facade.ts +43 -0
- package/src/agent/normalize-args.ts +61 -0
- package/src/agent/nuclear-form.ts +382 -0
- package/src/agent/providers/deepseek.ts +39 -0
- package/src/agent/providers/ollama.ts +92 -0
- package/src/agent/providers/openai-compatible.ts +36 -0
- package/src/agent/providers/openai.ts +52 -0
- package/src/agent/providers/opencode.ts +142 -0
- package/src/agent/providers/openrouter.ts +105 -0
- package/src/agent/providers/zai-coding-plan.ts +33 -0
- package/src/agent/session-store.ts +336 -0
- package/src/agent/skills.ts +228 -0
- package/src/agent/store.ts +310 -0
- package/src/agent/subagent.ts +305 -0
- package/src/agent/system-prompt.ts +151 -0
- package/src/agent/token-budget.ts +12 -0
- package/src/agent/tool-protocol.ts +722 -0
- package/src/agent/tool-registry.ts +66 -0
- package/src/agent/tools/bash.ts +95 -0
- package/src/agent/tools/edit-file.ts +154 -0
- package/src/agent/tools/expand-home.ts +7 -0
- package/src/agent/tools/glob.ts +108 -0
- package/src/agent/tools/grep.ts +228 -0
- package/src/agent/tools/list-skills.ts +37 -0
- package/src/agent/tools/ls.ts +81 -0
- package/src/agent/tools/pwsh.ts +140 -0
- package/src/agent/tools/read-file.ts +164 -0
- package/src/agent/tools/write-file.ts +72 -0
- package/src/agent/types.ts +149 -0
- package/src/cli/args.ts +91 -0
- package/src/cli/auth/cli.ts +244 -0
- package/src/cli/auth/discover.ts +52 -0
- package/src/cli/auth/keys.ts +143 -0
- package/src/cli/index.ts +295 -0
- package/src/cli/init.ts +74 -0
- package/src/cli/install.ts +439 -0
- package/src/cli/shell-env.ts +68 -0
- package/src/cli/subcommands.ts +24 -0
- package/src/core/event-bus.ts +252 -0
- package/src/core/extension-loader.ts +347 -0
- package/src/core/index.ts +152 -0
- package/src/core/settings.ts +398 -0
- package/src/core/types.ts +61 -0
- package/src/extensions/file-autocomplete.ts +71 -0
- package/src/extensions/index.ts +38 -0
- package/src/extensions/slash-commands/events.ts +14 -0
- package/src/extensions/slash-commands/index.ts +269 -0
- package/src/shell/events.ts +73 -0
- package/src/shell/host-types.ts +150 -0
- package/src/shell/index.ts +159 -0
- package/src/shell/input-handler.ts +505 -0
- package/src/shell/output-parser.ts +156 -0
- package/src/shell/shell-context.ts +193 -0
- package/src/shell/shell.ts +414 -0
- package/src/shell/strategies/bash.ts +83 -0
- package/src/shell/strategies/fish.ts +77 -0
- package/src/shell/strategies/index.ts +24 -0
- package/src/shell/strategies/types.ts +64 -0
- package/src/shell/strategies/zsh.ts +92 -0
- package/src/shell/terminal.ts +124 -0
- package/src/shell/tui-input-view.ts +222 -0
- package/src/shell/tui-renderer.ts +1126 -0
- package/src/utils/ansi.ts +140 -0
- package/src/utils/box-frame.ts +138 -0
- package/src/utils/compositor.ts +157 -0
- package/src/utils/diff-renderer.ts +829 -0
- package/src/utils/diff.ts +244 -0
- package/src/utils/executor.ts +305 -0
- package/src/utils/file-watcher.ts +110 -0
- package/src/utils/floating-panel.ts +1160 -0
- package/src/utils/handler-registry.ts +110 -0
- package/src/utils/line-editor.ts +636 -0
- package/src/utils/markdown.ts +437 -0
- package/src/utils/message-utils.ts +113 -0
- package/src/utils/package-version.ts +12 -0
- package/src/utils/palette.ts +64 -0
- package/src/utils/ref-counter.ts +9 -0
- package/src/utils/ripgrep-path.ts +17 -0
- package/src/utils/shell-output-spill.ts +76 -0
- package/src/utils/stream-transform.ts +292 -0
- package/src/utils/terminal-buffer.ts +213 -0
- package/src/utils/tool-display.ts +315 -0
- package/src/utils/tool-interactive.ts +71 -0
- package/src/utils/tty.ts +14 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import type { Store, Entry } from "../../store.js";
|
|
2
|
+
import { newEntryId } from "../../store.js";
|
|
3
|
+
import type { AgentShMessage } from "../../llm-client.js";
|
|
4
|
+
import {
|
|
5
|
+
type NuclearEntry,
|
|
6
|
+
nucleate,
|
|
7
|
+
READ_ONLY_TOOLS,
|
|
8
|
+
WRITE_TOOLS,
|
|
9
|
+
isReadOnly,
|
|
10
|
+
} from "../../nuclear-form.js";
|
|
11
|
+
import { formatEntryLine } from "../../entry-format.js";
|
|
12
|
+
import { RECALL_CACHE_KIND } from "./constants.js";
|
|
13
|
+
|
|
14
|
+
interface ToolMeta {
|
|
15
|
+
toolName: string;
|
|
16
|
+
args: Record<string, unknown>;
|
|
17
|
+
isError?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SummaryCtx {
|
|
21
|
+
store: Store;
|
|
22
|
+
bus: {
|
|
23
|
+
on(event: "conversation:message-appended", fn: MessageAppendedHandler): void;
|
|
24
|
+
};
|
|
25
|
+
advise(
|
|
26
|
+
op: "conversation:compact",
|
|
27
|
+
fn: CompactAdvisor,
|
|
28
|
+
): void;
|
|
29
|
+
/** Process id — disambiguates cross-instance entries in `nucleate()`. */
|
|
30
|
+
iid: string;
|
|
31
|
+
getMessages(): AgentShMessage[];
|
|
32
|
+
replaceMessages(msgs: AgentShMessage[]): void;
|
|
33
|
+
estimateTokens(): number;
|
|
34
|
+
estimatePromptTokens(): number;
|
|
35
|
+
linkMessage(index: number, entryId: string): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MessageAppendedEvent {
|
|
39
|
+
role: "user" | "assistant" | "tool" | "system";
|
|
40
|
+
content: string;
|
|
41
|
+
toolName?: string;
|
|
42
|
+
toolArgs?: Record<string, unknown>;
|
|
43
|
+
isError?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export type MessageAppendedHandler = (e: MessageAppendedEvent) => Promise<void> | void;
|
|
46
|
+
|
|
47
|
+
export interface CompactEvent {
|
|
48
|
+
target?: number;
|
|
49
|
+
force?: boolean;
|
|
50
|
+
/** `rewind`/`replace` are kernel-owned manual edits — the summary
|
|
51
|
+
* strategy delegates them via `next()`. */
|
|
52
|
+
strategy?:
|
|
53
|
+
| { kind: "two-tier-pin"; target: number; keepRecent?: number; force?: boolean }
|
|
54
|
+
| { kind: "rewind"; toIndex: number }
|
|
55
|
+
| { kind: "replace"; messages: unknown[] };
|
|
56
|
+
}
|
|
57
|
+
export interface CompactResult {
|
|
58
|
+
before: number;
|
|
59
|
+
after: number;
|
|
60
|
+
evictedCount: number;
|
|
61
|
+
}
|
|
62
|
+
export type CompactAdvisor = (
|
|
63
|
+
next: (e: CompactEvent) => Promise<CompactResult | null>,
|
|
64
|
+
event: CompactEvent,
|
|
65
|
+
) => Promise<CompactResult | null>;
|
|
66
|
+
|
|
67
|
+
export function activate(ctx: SummaryCtx): void {
|
|
68
|
+
ctx.bus.on("conversation:message-appended", makeCaptureHandler(ctx));
|
|
69
|
+
ctx.advise("conversation:compact", makeCompactAdvisor(ctx));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function makeCaptureHandler(ctx: SummaryCtx): MessageAppendedHandler {
|
|
73
|
+
return async (e) => {
|
|
74
|
+
const store = ctx.store;
|
|
75
|
+
// Capture the index synchronously — later async ticks may grow the array.
|
|
76
|
+
const msgs = ctx.getMessages();
|
|
77
|
+
const liveIdx = msgs.length - 1;
|
|
78
|
+
const m = msgs[liveIdx];
|
|
79
|
+
if (!m) return;
|
|
80
|
+
|
|
81
|
+
// Stamp meta.tool so compact's inferPriority can read it without
|
|
82
|
+
// walking back to the event payload.
|
|
83
|
+
if (e.role === "tool" && e.toolName !== undefined) {
|
|
84
|
+
m.meta = {
|
|
85
|
+
...m.meta,
|
|
86
|
+
tool: { toolName: e.toolName, args: e.toolArgs ?? {}, isError: !!e.isError },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ne = nucleateFromEvent(e, ctx.iid);
|
|
91
|
+
if (!ne) return;
|
|
92
|
+
const id = newEntryId();
|
|
93
|
+
await store.append([nuclearToEntry(ne, id)]);
|
|
94
|
+
await store.append(
|
|
95
|
+
[{
|
|
96
|
+
id: newEntryId(), parentId: id, ts: Date.now(),
|
|
97
|
+
kind: RECALL_CACHE_KIND,
|
|
98
|
+
payload: { fullMessage: m },
|
|
99
|
+
}],
|
|
100
|
+
{ ephemeral: true },
|
|
101
|
+
);
|
|
102
|
+
ctx.linkMessage(liveIdx, id);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function nucleateFromEvent(e: MessageAppendedEvent, iid: string): NuclearEntry | null {
|
|
107
|
+
if (e.role === "user") {
|
|
108
|
+
if (!e.content) return null;
|
|
109
|
+
return nucleate("user", e.content, iid, 0);
|
|
110
|
+
}
|
|
111
|
+
if (e.role === "assistant") {
|
|
112
|
+
if (!e.content) return null;
|
|
113
|
+
return nucleate("agent", e.content, iid, 0);
|
|
114
|
+
}
|
|
115
|
+
if (e.role === "tool") {
|
|
116
|
+
if (e.toolName === undefined) return null;
|
|
117
|
+
return nucleate("tool", e.toolName, e.toolArgs ?? {}, e.content, !!e.isError, iid, 0);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Used during compact when topping up summaries for messages that
|
|
123
|
+
* missed eager capture (e.g. injected system notes that bypassed the
|
|
124
|
+
* event). Reads from message structure rather than an event payload. */
|
|
125
|
+
function nucleateFromMessage(m: AgentShMessage, iid: string): NuclearEntry | null {
|
|
126
|
+
if (m.role === "user") {
|
|
127
|
+
const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? "");
|
|
128
|
+
if (!text) return null;
|
|
129
|
+
return nucleate("user", text, iid, 0);
|
|
130
|
+
}
|
|
131
|
+
if (m.role === "assistant") {
|
|
132
|
+
if ("tool_calls" in m && m.tool_calls && m.tool_calls.length > 0) return null;
|
|
133
|
+
const text = typeof m.content === "string" ? m.content : "";
|
|
134
|
+
if (!text) return null;
|
|
135
|
+
return nucleate("agent", text, iid, 0);
|
|
136
|
+
}
|
|
137
|
+
if (m.role === "tool") {
|
|
138
|
+
const tool = m.meta?.tool as ToolMeta | undefined;
|
|
139
|
+
if (!tool) return null;
|
|
140
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
141
|
+
return nucleate("tool", tool.toolName, tool.args, content, tool.isError ?? false, iid, 0);
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function nuclearToEntry(ne: NuclearEntry, id: string): Entry {
|
|
147
|
+
const { seq: _seq, ts, kind, ...rest } = ne;
|
|
148
|
+
return { id, ts, kind, payload: rest };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const DEFAULT_RECENT_TURNS_KEEP = 10;
|
|
152
|
+
|
|
153
|
+
export function makeCompactAdvisor(ctx: SummaryCtx): CompactAdvisor {
|
|
154
|
+
return async (next, event) => {
|
|
155
|
+
if (event.strategy?.kind === "rewind" || event.strategy?.kind === "replace") {
|
|
156
|
+
return next(event);
|
|
157
|
+
}
|
|
158
|
+
const promptBefore = ctx.estimatePromptTokens();
|
|
159
|
+
const convBefore = ctx.estimateTokens();
|
|
160
|
+
const overhead = Math.max(0, promptBefore - convBefore);
|
|
161
|
+
const promptTarget = event.target ?? Infinity;
|
|
162
|
+
const convTarget = Math.max(0, promptTarget - overhead);
|
|
163
|
+
|
|
164
|
+
if (!event.force && promptBefore <= promptTarget) return null;
|
|
165
|
+
|
|
166
|
+
const msgs = ctx.getMessages();
|
|
167
|
+
const turns = parseTurns(msgs);
|
|
168
|
+
const minTurns = event.force ? 1 : 2;
|
|
169
|
+
if (turns.length <= minTurns) return null;
|
|
170
|
+
|
|
171
|
+
const maxPinnedFraction = event.force ? 0.4 : 0.6;
|
|
172
|
+
const maxPinned = Math.max(2, Math.floor(turns.length * maxPinnedFraction));
|
|
173
|
+
const recentKeep = Math.min(
|
|
174
|
+
DEFAULT_RECENT_TURNS_KEEP,
|
|
175
|
+
turns.length - 1,
|
|
176
|
+
Math.max(1, event.force ? Math.min(maxPinned, turns.length - 2) : maxPinned),
|
|
177
|
+
);
|
|
178
|
+
for (const t of turns) t.priority = inferPriority(t.messages);
|
|
179
|
+
const verbatimCount = 1;
|
|
180
|
+
const slimmedCount = Math.max(0, recentKeep - verbatimCount);
|
|
181
|
+
const slimStart = turns.length - recentKeep;
|
|
182
|
+
const slimEnd = slimStart + slimmedCount;
|
|
183
|
+
const slimmedIndices = new Set<number>();
|
|
184
|
+
for (let i = slimStart; i < slimEnd; i++) slimmedIndices.add(i);
|
|
185
|
+
turns[0]!.priority = Priority.PINNED;
|
|
186
|
+
for (let i = turns.length - verbatimCount; i < turns.length; i++) turns[i]!.priority = Priority.PINNED;
|
|
187
|
+
for (const i of slimmedIndices) turns[i]!.priority = Priority.PINNED;
|
|
188
|
+
|
|
189
|
+
const candidates = turns
|
|
190
|
+
.map((t, idx) => ({ turn: t, idx }))
|
|
191
|
+
.filter((c) => c.turn.priority !== Priority.PINNED)
|
|
192
|
+
.sort((a, b) => {
|
|
193
|
+
const wA = a.turn.priority * recencyWeight(a.idx, turns.length);
|
|
194
|
+
const wB = b.turn.priority * recencyWeight(b.idx, turns.length);
|
|
195
|
+
return wA - wB || a.idx - b.idx;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const evicted = new Set<number>();
|
|
199
|
+
let tokens = convBefore;
|
|
200
|
+
const newSummaryEntries: Entry[] = [];
|
|
201
|
+
const newCacheEntries: Entry[] = [];
|
|
202
|
+
const store = ctx.store;
|
|
203
|
+
|
|
204
|
+
for (const c of candidates) {
|
|
205
|
+
if (tokens <= convTarget) break;
|
|
206
|
+
evicted.add(c.idx);
|
|
207
|
+
tokens -= estimateTurnTokens(c.turn.messages);
|
|
208
|
+
|
|
209
|
+
// Top up summaries for messages that missed eager capture.
|
|
210
|
+
// Read-only tools are cached ephemerally only — the agent can
|
|
211
|
+
// re-read the source.
|
|
212
|
+
for (const m of c.turn.messages) {
|
|
213
|
+
const existingId = m.meta?.entryId as string | undefined;
|
|
214
|
+
if (existingId) {
|
|
215
|
+
newCacheEntries.push({
|
|
216
|
+
id: newEntryId(), parentId: existingId, ts: Date.now(),
|
|
217
|
+
kind: RECALL_CACHE_KIND,
|
|
218
|
+
payload: { fullMessage: m },
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const ne = nucleateFromMessage(m, ctx.iid);
|
|
223
|
+
if (!ne) continue;
|
|
224
|
+
const id = newEntryId();
|
|
225
|
+
const entry = nuclearToEntry(ne, id);
|
|
226
|
+
if (!isReadOnly(ne)) {
|
|
227
|
+
newSummaryEntries.push(entry);
|
|
228
|
+
}
|
|
229
|
+
newCacheEntries.push({
|
|
230
|
+
id: newEntryId(), parentId: id, ts: Date.now(),
|
|
231
|
+
kind: RECALL_CACHE_KIND,
|
|
232
|
+
payload: { fullMessage: m },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (evicted.size === 0) return null;
|
|
238
|
+
|
|
239
|
+
if (newSummaryEntries.length > 0) await store.append(newSummaryEntries);
|
|
240
|
+
if (newCacheEntries.length > 0) await store.append(newCacheEntries, { ephemeral: true });
|
|
241
|
+
|
|
242
|
+
const rebuilt: AgentShMessage[] = [];
|
|
243
|
+
let blockInserted = false;
|
|
244
|
+
for (let i = 0; i < turns.length; i++) {
|
|
245
|
+
if (evicted.has(i)) {
|
|
246
|
+
if (!blockInserted) {
|
|
247
|
+
rebuilt.push(await buildSummaryBlock(store));
|
|
248
|
+
blockInserted = true;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (slimmedIndices.has(i)) {
|
|
253
|
+
rebuilt.push(...slimTurn(turns[i]!.messages));
|
|
254
|
+
} else {
|
|
255
|
+
rebuilt.push(...turns[i]!.messages);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ctx.replaceMessages(rebuilt);
|
|
260
|
+
const after = ctx.estimatePromptTokens();
|
|
261
|
+
return { before: promptBefore, after, evictedCount: evicted.size };
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export enum Priority {
|
|
266
|
+
LOWEST = 0, // large read-only tool results
|
|
267
|
+
LOW = 1, // successful tool results
|
|
268
|
+
MEDIUM = 2, // write/edit tool results, plain assistant turns
|
|
269
|
+
HIGH = 3, // user messages, errors
|
|
270
|
+
PINNED = 4, // never evicted
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface Turn {
|
|
274
|
+
messages: AgentShMessage[];
|
|
275
|
+
priority: Priority;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A new turn starts at each user message; the first may be headless
|
|
279
|
+
* (system, preamble). */
|
|
280
|
+
export function parseTurns(msgs: AgentShMessage[]): Turn[] {
|
|
281
|
+
const turns: Turn[] = [];
|
|
282
|
+
let current: AgentShMessage[] = [];
|
|
283
|
+
for (const m of msgs) {
|
|
284
|
+
if (m.role === "user" && current.length > 0) {
|
|
285
|
+
turns.push({ messages: current, priority: Priority.MEDIUM });
|
|
286
|
+
current = [];
|
|
287
|
+
}
|
|
288
|
+
current.push(m);
|
|
289
|
+
}
|
|
290
|
+
if (current.length > 0) turns.push({ messages: current, priority: Priority.MEDIUM });
|
|
291
|
+
return turns;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function inferPriority(msgs: AgentShMessage[]): Priority {
|
|
295
|
+
let hasError = false;
|
|
296
|
+
let hasWriteTool = false;
|
|
297
|
+
let allReadOnly = true;
|
|
298
|
+
let hasToolResult = false;
|
|
299
|
+
|
|
300
|
+
for (const m of msgs) {
|
|
301
|
+
if (m.role === "user") return Priority.HIGH;
|
|
302
|
+
if (m.role === "tool") {
|
|
303
|
+
hasToolResult = true;
|
|
304
|
+
const tool = m.meta?.tool as ToolMeta | undefined;
|
|
305
|
+
const content = typeof m.content === "string" ? m.content : "";
|
|
306
|
+
if (tool?.isError || content.startsWith("Error:")) hasError = true;
|
|
307
|
+
}
|
|
308
|
+
if (m.role === "assistant" && "tool_calls" in m && m.tool_calls) {
|
|
309
|
+
for (const tc of m.tool_calls) {
|
|
310
|
+
const fn = "function" in tc ? tc.function : undefined;
|
|
311
|
+
if (!fn) continue;
|
|
312
|
+
if (WRITE_TOOLS.has(fn.name)) hasWriteTool = true;
|
|
313
|
+
if (!READ_ONLY_TOOLS.has(fn.name)) allReadOnly = false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (hasError) return Priority.HIGH;
|
|
319
|
+
if (hasWriteTool) return Priority.MEDIUM;
|
|
320
|
+
if (hasToolResult && allReadOnly) return Priority.LOWEST;
|
|
321
|
+
if (hasToolResult) return Priority.LOW;
|
|
322
|
+
return Priority.MEDIUM;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function recencyWeight(idx: number, total: number): number {
|
|
326
|
+
return Math.max(0.1, 1 - idx / total);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function estimateTurnTokens(msgs: AgentShMessage[]): number {
|
|
330
|
+
return Math.ceil(JSON.stringify(msgs).length / 4);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function slimTurn(messages: AgentShMessage[]): AgentShMessage[] {
|
|
334
|
+
const MAX_RESULT_LEN = 1500;
|
|
335
|
+
const MAX_ASSISTANT_LEN = 1500;
|
|
336
|
+
const result: AgentShMessage[] = [];
|
|
337
|
+
const droppedToolIds = new Set<string>();
|
|
338
|
+
|
|
339
|
+
for (const msg of messages) {
|
|
340
|
+
if (msg.role === "assistant" && "tool_calls" in msg && msg.tool_calls) {
|
|
341
|
+
const kept = msg.tool_calls.filter((tc) => {
|
|
342
|
+
if (!("function" in tc)) return true;
|
|
343
|
+
if (READ_ONLY_TOOLS.has(tc.function.name)) {
|
|
344
|
+
droppedToolIds.add(tc.id);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
});
|
|
349
|
+
if (kept.length === 0) {
|
|
350
|
+
const text = typeof msg.content === "string" ? msg.content.trim() : "";
|
|
351
|
+
if (!text) continue;
|
|
352
|
+
const { tool_calls: _tc, ...rest } = msg;
|
|
353
|
+
result.push(rest as AgentShMessage);
|
|
354
|
+
} else {
|
|
355
|
+
result.push({ ...msg, tool_calls: kept });
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (msg.role === "tool") {
|
|
360
|
+
if (droppedToolIds.has(msg.tool_call_id)) continue;
|
|
361
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
362
|
+
if (content.length > MAX_RESULT_LEN) {
|
|
363
|
+
result.push({ ...msg, content: slimToolContent(content, MAX_RESULT_LEN) });
|
|
364
|
+
} else {
|
|
365
|
+
result.push(msg);
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.length > MAX_ASSISTANT_LEN) {
|
|
370
|
+
const head = msg.content.slice(0, Math.floor(MAX_ASSISTANT_LEN * 0.6));
|
|
371
|
+
const tail = msg.content.slice(-Math.floor(MAX_ASSISTANT_LEN * 0.2));
|
|
372
|
+
const trimmed = msg.content.length - head.length - tail.length;
|
|
373
|
+
result.push({ ...msg, content: `${head}\n... [${trimmed} chars trimmed by compact]\n${tail}` });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
result.push(msg);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function slimToolContent(content: string, maxLen: number): string {
|
|
382
|
+
const exitMatch = content.match(/exit code:?\s*(\d+)/i);
|
|
383
|
+
const exitSuffix = exitMatch ? ` (exit ${exitMatch[1]})` : "";
|
|
384
|
+
const lines = content.split("\n");
|
|
385
|
+
if (lines.length > 6) {
|
|
386
|
+
const head = lines.slice(0, 3).join("\n");
|
|
387
|
+
const tail = lines.slice(-2).join("\n");
|
|
388
|
+
return `${head}\n... [${lines.length - 5} lines trimmed by compact]\n${tail}${exitSuffix}`;
|
|
389
|
+
}
|
|
390
|
+
return `${content.slice(0, maxLen)}\n... [${content.length - maxLen} chars trimmed by compact]${exitSuffix}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function readSummaryLines(store: Store, n?: number): Promise<string[]> {
|
|
394
|
+
const recent = await store.readRecent(n);
|
|
395
|
+
return recent.filter((e) => e.kind !== RECALL_CACHE_KIND).map(formatEntryLine);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function buildSummaryBlock(store: Store): Promise<AgentShMessage> {
|
|
399
|
+
const lines = await readSummaryLines(store);
|
|
400
|
+
return {
|
|
401
|
+
role: "user",
|
|
402
|
+
content: `[Conversation history — use conversation_recall to expand any entry]\n${lines.join("\n")}`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { CoreConfig, CoreContext } from "../core/types.js";
|
|
2
|
+
import type { SkillView, ToolDefinition, ToolExecutionContext, ToolSchemaView } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// ── LLM port ─────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Backend-agnostic LLM interface exposed via `ctx.agent.llm`. Fulfilled
|
|
8
|
+
* by defining an `llm:invoke` handler; backends without an LLM leave
|
|
9
|
+
* `available` false and calls reject.
|
|
10
|
+
*/
|
|
11
|
+
export interface LlmMessage {
|
|
12
|
+
role: "system" | "user" | "assistant";
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LlmSession {
|
|
17
|
+
send(message: string): Promise<string>;
|
|
18
|
+
history(): ReadonlyArray<LlmMessage>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LlmInterface {
|
|
22
|
+
readonly available: boolean;
|
|
23
|
+
/** `model` overrides the globally-configured model for this call only.
|
|
24
|
+
* Provider-specific identifier (e.g. "claude-haiku-4-5"). When omitted,
|
|
25
|
+
* the active provider's configured default is used.
|
|
26
|
+
*
|
|
27
|
+
* `reasoningEffort` controls thinking-model token allocation between
|
|
28
|
+
* reasoning and final content (e.g. "low", "medium", "high", or
|
|
29
|
+
* provider-specific). For non-reasoning models it is ignored. Set to
|
|
30
|
+
* "low" for cheap structured-output calls so reasoning doesn't exhaust
|
|
31
|
+
* the max-tokens budget and leave content empty. */
|
|
32
|
+
ask(opts: {
|
|
33
|
+
query: string;
|
|
34
|
+
system?: string;
|
|
35
|
+
maxTokens?: number;
|
|
36
|
+
model?: string;
|
|
37
|
+
reasoningEffort?: string;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
session(opts?: {
|
|
40
|
+
system?: string;
|
|
41
|
+
maxTokens?: number;
|
|
42
|
+
model?: string;
|
|
43
|
+
reasoningEffort?: string;
|
|
44
|
+
}): LlmSession;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProviderRegistration {
|
|
48
|
+
id: string;
|
|
49
|
+
apiKey?: string;
|
|
50
|
+
baseURL?: string;
|
|
51
|
+
/** Falls back to models[0] when absent. */
|
|
52
|
+
defaultModel?: string;
|
|
53
|
+
models?: (string | {
|
|
54
|
+
id: string;
|
|
55
|
+
reasoning?: boolean;
|
|
56
|
+
contextWindow?: number;
|
|
57
|
+
maxTokens?: number;
|
|
58
|
+
echoReasoning?: boolean;
|
|
59
|
+
modalities?: ("text" | "image")[];
|
|
60
|
+
})[];
|
|
61
|
+
supportsReasoningEffort?: boolean;
|
|
62
|
+
/** Local daemons etc. — `auth list/login` shows "no auth required". */
|
|
63
|
+
noAuth?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A selectable (provider, model) target the frontend lists and switches.
|
|
67
|
+
* Serializable — identity + capabilities only; the secret + closures needed to
|
|
68
|
+
* invoke it live in ModelEndpoint, so this can safely cross to frontends and
|
|
69
|
+
* out-of-process bridges. */
|
|
70
|
+
export interface Model {
|
|
71
|
+
id: string;
|
|
72
|
+
provider: string;
|
|
73
|
+
/** Context window size in tokens (for usage display). */
|
|
74
|
+
contextWindow?: number;
|
|
75
|
+
/** Max output tokens. */
|
|
76
|
+
maxTokens?: number;
|
|
77
|
+
/** Model supports reasoning/thinking tokens. */
|
|
78
|
+
reasoning?: boolean;
|
|
79
|
+
/** Provider supports the reasoning_effort parameter. */
|
|
80
|
+
supportsReasoningEffort?: boolean;
|
|
81
|
+
/** Echo reasoning_content back on assistant turns. Required by DeepSeek;
|
|
82
|
+
* default off (leaky shims may forward it to the model as OOD input). */
|
|
83
|
+
echoReasoning?: boolean;
|
|
84
|
+
/** Input modalities the model supports. Defaults to ["text"]. */
|
|
85
|
+
modalities?: ("text" | "image")[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Credentials + provider-shape transforms for invoking a Model, resolved by
|
|
89
|
+
* (provider, id). Internal: holds a secret (apiKey) and non-serializable
|
|
90
|
+
* closures, so it must never ride a bus event. */
|
|
91
|
+
export interface ModelEndpoint {
|
|
92
|
+
apiKey: string;
|
|
93
|
+
baseURL?: string;
|
|
94
|
+
buildReasoningParams?: (level: string) => Record<string, unknown>;
|
|
95
|
+
extractCachedTokens?: (usage: Record<string, unknown>) => number | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Agent-host extension surface ─────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Capabilities the agent host adds on top of CoreContext. Only available
|
|
102
|
+
* when the built-in agent backend is loaded; bridge backends pass a bare
|
|
103
|
+
* CoreContext, so extensions that need these should type as AgentContext.
|
|
104
|
+
*/
|
|
105
|
+
export interface AgentSurface {
|
|
106
|
+
llm: LlmInterface;
|
|
107
|
+
providers: {
|
|
108
|
+
/** Re-registering the same id replaces the prior contribution. */
|
|
109
|
+
register: (reg: ProviderRegistration) => () => void;
|
|
110
|
+
unregister: (id: string) => void;
|
|
111
|
+
configure: (id: string, opts: {
|
|
112
|
+
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
113
|
+
cacheTokens?: (usage: Record<string, unknown>) => number | undefined;
|
|
114
|
+
}) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ── Tool registration ────────────────────────────────────────
|
|
118
|
+
registerTool: (tool: ToolDefinition) => void;
|
|
119
|
+
unregisterTool: (name: string) => void;
|
|
120
|
+
adviseTool: (
|
|
121
|
+
name: string,
|
|
122
|
+
advisor: (
|
|
123
|
+
next: ToolDefinition["execute"],
|
|
124
|
+
args: Record<string, unknown>,
|
|
125
|
+
onChunk?: (chunk: string) => void,
|
|
126
|
+
ctx?: ToolExecutionContext,
|
|
127
|
+
) => ReturnType<ToolDefinition["execute"]>,
|
|
128
|
+
) => () => void;
|
|
129
|
+
adviseToolSchema: (
|
|
130
|
+
name: string,
|
|
131
|
+
advisor: (next: () => ToolSchemaView) => ToolSchemaView,
|
|
132
|
+
) => () => void;
|
|
133
|
+
getTools: () => ToolDefinition[];
|
|
134
|
+
|
|
135
|
+
// ── System prompt instructions ──────────────────────────────
|
|
136
|
+
registerInstruction: (name: string, text: string) => void;
|
|
137
|
+
removeInstruction: (name: string) => void;
|
|
138
|
+
adviseInstruction: (
|
|
139
|
+
name: string,
|
|
140
|
+
advisor: (next: () => string) => string,
|
|
141
|
+
) => () => void;
|
|
142
|
+
|
|
143
|
+
// ── Skill registration ──────────────────────────────────────
|
|
144
|
+
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
145
|
+
removeSkill: (name: string) => void;
|
|
146
|
+
adviseSkill: (
|
|
147
|
+
name: string,
|
|
148
|
+
advisor: (next: () => SkillView) => SkillView,
|
|
149
|
+
) => () => void;
|
|
150
|
+
|
|
151
|
+
// ── Dynamic context registration ────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Register a context producer — a function that contributes a string
|
|
154
|
+
* (or `null` to skip) into one of two lifecycles:
|
|
155
|
+
*
|
|
156
|
+
* - `mode: "per-request"` (default) — fires on every LLM request,
|
|
157
|
+
* including each tool-loop iteration. Output is ephemerally wrapped
|
|
158
|
+
* in `<dynamic_context>` onto the trailing message at request time;
|
|
159
|
+
* never persisted. Use for "current state" signals.
|
|
160
|
+
*
|
|
161
|
+
* - `mode: "per-query"` — fires once at user-query start. Output is
|
|
162
|
+
* wrapped in `<query_context>` and frozen into the user message;
|
|
163
|
+
* persists in conversation history.
|
|
164
|
+
*
|
|
165
|
+
* Returns a dispose fn that unregisters the producer.
|
|
166
|
+
*/
|
|
167
|
+
registerContextProducer: (
|
|
168
|
+
name: string,
|
|
169
|
+
producer: () => string | null,
|
|
170
|
+
opts?: { mode?: "per-request" | "per-query" },
|
|
171
|
+
) => () => void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Substrate + agent surface. Use this when an extension only touches
|
|
175
|
+
* agent-side features (tools, instructions, LLM) and doesn't need
|
|
176
|
+
* shell rendering. */
|
|
177
|
+
export type AgentContext = CoreContext & { agent: AgentSurface };
|
|
178
|
+
|
|
179
|
+
// ── Agent-host config surface ────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
export interface AgentConfigSurface {
|
|
182
|
+
/** API key for OpenAI-compatible provider. */
|
|
183
|
+
apiKey?: string;
|
|
184
|
+
/** Base URL for OpenAI-compatible API. */
|
|
185
|
+
baseURL?: string;
|
|
186
|
+
/** Named provider to use from settings.json. */
|
|
187
|
+
provider?: string;
|
|
188
|
+
/** Default model id. */
|
|
189
|
+
model?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type AgentConfig = CoreConfig & AgentConfigSurface;
|