jeopi-agent-core 16.2.13

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 (66) hide show
  1. package/CHANGELOG.md +1016 -0
  2. package/README.md +473 -0
  3. package/dist/types/agent-loop.d.ts +66 -0
  4. package/dist/types/agent.d.ts +427 -0
  5. package/dist/types/append-only-context.d.ts +133 -0
  6. package/dist/types/compaction/branch-summarization.d.ts +101 -0
  7. package/dist/types/compaction/compaction-v2-streaming.d.ts +82 -0
  8. package/dist/types/compaction/compaction.d.ts +283 -0
  9. package/dist/types/compaction/entries.d.ts +110 -0
  10. package/dist/types/compaction/errors.d.ts +26 -0
  11. package/dist/types/compaction/index.d.ts +12 -0
  12. package/dist/types/compaction/messages.d.ts +77 -0
  13. package/dist/types/compaction/openai.d.ts +77 -0
  14. package/dist/types/compaction/pruning.d.ts +105 -0
  15. package/dist/types/compaction/shake.d.ts +92 -0
  16. package/dist/types/compaction/tool-protection.d.ts +17 -0
  17. package/dist/types/compaction/utils.d.ts +58 -0
  18. package/dist/types/compaction.d.ts +1 -0
  19. package/dist/types/index.d.ts +12 -0
  20. package/dist/types/proxy.d.ts +85 -0
  21. package/dist/types/replay-policy.d.ts +5 -0
  22. package/dist/types/run-collector.d.ts +196 -0
  23. package/dist/types/telemetry.d.ts +590 -0
  24. package/dist/types/thinking.d.ts +17 -0
  25. package/dist/types/tokenizer.d.ts +1 -0
  26. package/dist/types/types.d.ts +640 -0
  27. package/dist/types/utils/yield.d.ts +71 -0
  28. package/package.json +78 -0
  29. package/src/agent-loop.ts +2188 -0
  30. package/src/agent.ts +1457 -0
  31. package/src/append-only-context.ts +348 -0
  32. package/src/compaction/branch-summarization.ts +370 -0
  33. package/src/compaction/compaction-v2-streaming.ts +719 -0
  34. package/src/compaction/compaction.ts +1553 -0
  35. package/src/compaction/entries.ts +142 -0
  36. package/src/compaction/errors.ts +31 -0
  37. package/src/compaction/index.ts +13 -0
  38. package/src/compaction/messages.ts +237 -0
  39. package/src/compaction/openai.ts +581 -0
  40. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  41. package/src/compaction/prompts/branch-summary-context.md +5 -0
  42. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  43. package/src/compaction/prompts/branch-summary.md +30 -0
  44. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  45. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  46. package/src/compaction/prompts/compaction-summary.md +38 -0
  47. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  48. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  49. package/src/compaction/prompts/file-operations.md +5 -0
  50. package/src/compaction/prompts/handoff-document.md +49 -0
  51. package/src/compaction/prompts/snapcompact-archive-context.md +3 -0
  52. package/src/compaction/prompts/summarization-system.md +3 -0
  53. package/src/compaction/pruning.ts +424 -0
  54. package/src/compaction/shake.ts +429 -0
  55. package/src/compaction/tool-protection.ts +55 -0
  56. package/src/compaction/utils.ts +323 -0
  57. package/src/compaction.ts +1 -0
  58. package/src/index.ts +24 -0
  59. package/src/proxy.ts +376 -0
  60. package/src/replay-policy.ts +13 -0
  61. package/src/run-collector.ts +631 -0
  62. package/src/telemetry.ts +2034 -0
  63. package/src/thinking.ts +19 -0
  64. package/src/tokenizer.ts +17 -0
  65. package/src/types.ts +718 -0
  66. package/src/utils/yield.ts +183 -0
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Append-only context mode — stabilizes the byte prefix sent to the LLM
3
+ * across turns so provider prefix caches (DeepSeek, Anthropic, etc.)
4
+ * hit at the maximum possible rate.
5
+ *
6
+ * Two mechanisms:
7
+ *
8
+ * 1. **StablePrefix** — system prompt + tool specs are computed once
9
+ * and frozen. Subsequent turns reuse the exact same byte sequence
10
+ * unless `invalidate()` is called (e.g. after MCP reconnect).
11
+ *
12
+ * 2. **AppendOnlyLog** — messages only grow; prior turns are never
13
+ * re-serialized. Combined with a stable prefix, only the user's new
14
+ * message delta is a cache miss each turn.
15
+ */
16
+
17
+ import type { Context, Message, Tool } from "jeopi-ai";
18
+ import type { Dialect } from "jeopi-ai/dialect";
19
+ import { normalizeTools } from "./agent-loop";
20
+ import type { AgentContext } from "./types";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // StablePrefix (formerly ImmutablePrefix)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Frozen system prompt + tool spec snapshot. */
27
+ export interface StablePrefixSnapshot {
28
+ systemPrompt: string[];
29
+ tools: Tool[];
30
+ fingerprint: string;
31
+ }
32
+
33
+ /** Options threaded through `build()` so the snapshot reflects loop-time settings. */
34
+ export interface BuildOptions {
35
+ /** Inject the `i` intent field into tool schemas (must match agent-loop's normalizeTools). */
36
+ intentTracing: boolean;
37
+ exampleDialect?: Dialect;
38
+ /** Strip tool descriptions from the provider-bound specs (must match normalizeTools). */
39
+ pruneToolDescriptions?: boolean;
40
+ }
41
+
42
+ /**
43
+ * A frozen prefix (system prompt + tools) that produces stable byte
44
+ * sequences across `build()` calls.
45
+ *
46
+ * The first `build()` snapshots the live state. Subsequent calls reuse
47
+ * the cached copy until `invalidate()` is called or the live state's
48
+ * fingerprint changes.
49
+ */
50
+ export class StablePrefix {
51
+ #snapshot: StablePrefixSnapshot | null = null;
52
+ #version = 0;
53
+
54
+ get fingerprint(): string {
55
+ return this.#snapshot?.fingerprint ?? "<unbuilt>";
56
+ }
57
+ get version(): number {
58
+ return this.#version;
59
+ }
60
+ get built(): boolean {
61
+ return this.#snapshot !== null;
62
+ }
63
+
64
+ /**
65
+ * Build or rebuild from live context.
66
+ * Returns `true` if the prefix actually changed (cache miss imminent).
67
+ */
68
+ build(context: AgentContext, options: BuildOptions): boolean {
69
+ const snapshot = takeSnapshot(context, options);
70
+ if (this.#snapshot && this.#snapshot.fingerprint === snapshot.fingerprint) {
71
+ return false;
72
+ }
73
+ this.#snapshot = snapshot;
74
+ this.#version++;
75
+ return true;
76
+ }
77
+
78
+ /** Force rebuild on the next `build()` call. */
79
+ invalidate(): void {
80
+ this.#snapshot = null;
81
+ }
82
+
83
+ /**
84
+ * Returns the cached prefix.
85
+ * @throws if `build()` was never called.
86
+ */
87
+ toContext(): { systemPrompt: string[]; tools: Tool[] } {
88
+ const s = this.#snapshot;
89
+ if (!s) throw new Error("StablePrefix.toContext() called before build()");
90
+ return { systemPrompt: s.systemPrompt, tools: s.tools };
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // AppendOnlyLog
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Append-only message log at the `Message[]` (provider-level) layer.
100
+ *
101
+ * The only mutation path is `replaceTail()`, reserved for compaction.
102
+ * Every other operation is append-only.
103
+ */
104
+ export class AppendOnlyLog {
105
+ #entries: Message[] = [];
106
+
107
+ get length(): number {
108
+ return this.#entries.length;
109
+ }
110
+
111
+ append(message: any): void {
112
+ this.#entries.push(message);
113
+ }
114
+
115
+ extend(messages: any[]): void {
116
+ for (const m of messages) this.#entries.push(m);
117
+ }
118
+
119
+ /** Replace the last entry — only legal for compaction. */
120
+ replaceTail(replacement: any): void {
121
+ const idx = this.#entries.length - 1;
122
+ if (idx >= 0) this.#entries[idx] = replacement;
123
+ }
124
+
125
+ /** Returns a shallow copy of all entries. */
126
+ toMessages(): Message[] {
127
+ return this.#entries.slice();
128
+ }
129
+
130
+ /** Direct readonly access for in-place inspection. */
131
+ entries(): readonly Message[] {
132
+ return this.#entries;
133
+ }
134
+
135
+ /** Drop entries past index `count`, keeping the first `count` byte-stable.
136
+ * Used by {@link AppendOnlyContextManager.syncMessages} to preserve the
137
+ * already-on-the-wire prefix when a later message diverges. */
138
+ truncate(count: number): void {
139
+ if (count < 0) count = 0;
140
+ if (count >= this.#entries.length) return;
141
+ this.#entries.length = count;
142
+ }
143
+
144
+ clear(): void {
145
+ this.#entries = [];
146
+ }
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // AppendOnlyContextManager
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * Manages a stable prefix + append-only log for the agent loop.
155
+ *
156
+ * Call `build(context)` each turn to get a `Context` with stable
157
+ * `systemPrompt` and `tools` and append-only messages. Call
158
+ * `syncMessages(normalizedMessages)` after `convertToLlm` each
159
+ * turn to keep the log in sync.
160
+ *
161
+ * Example:
162
+ * ```
163
+ * const mgr = new AppendOnlyContextManager();
164
+ * const ctx = mgr.build(context); // first call snapshots prefix
165
+ * mgr.syncMessages(normalized); // grow the log
166
+ * ctx = mgr.build(context); // subsequent calls use cache
167
+ * ```
168
+ */
169
+ export class AppendOnlyContextManager {
170
+ readonly prefix = new StablePrefix();
171
+ readonly log = new AppendOnlyLog();
172
+ /** How many normalized messages were synced into the log as of the last sync. */
173
+ #lastSyncCount = 0;
174
+ /**
175
+ * Per-message digests of the synced log. Lets a deep or tail rewrite
176
+ * (per-turn pruning, image strip, transformContext re-render) preserve
177
+ * the byte-stable prefix instead of re-sending the entire conversation
178
+ * — keeps the provider's prompt-cache hit rate up to the divergence
179
+ * point on every subsequent turn.
180
+ */
181
+ #messageDigests: number[] = [];
182
+
183
+ build(context: AgentContext, options: BuildOptions): Context {
184
+ this.prefix.build(context, options);
185
+ const { systemPrompt, tools } = this.prefix.toContext();
186
+ return { systemPrompt, messages: this.log.toMessages(), tools };
187
+ }
188
+
189
+ /**
190
+ * Sync normalized (provider-level) messages into the append-only log.
191
+ *
192
+ * Three cases:
193
+ *
194
+ * 1. **Append**: same prefix, new tail → push the new entries.
195
+ * 2. **Compaction**: shorter array → clear the log and replay.
196
+ * 3. **In-place rewrite** (per-turn pruning, transformContext re-render,
197
+ * image strip, etc.): find the longest byte-stable prefix between
198
+ * the previously-synced messages and the new ones, drop the log
199
+ * down to that prefix, then append the diverged tail. Earlier
200
+ * revisions cleared the whole log on any digest change, which on
201
+ * llama.cpp / local backends forced a full ~40k-token re-prefill
202
+ * every turn that an extension, prune pass, or steering re-wrap
203
+ * rewrote a single message (#3406). Preserving the stable prefix
204
+ * lets the provider's KV cache stay warm up to the divergence
205
+ * point — the model only re-prefills from the changed message on.
206
+ */
207
+ syncMessages(normalizedMessages: any[]): void {
208
+ // Compaction (array shrunk) — every previously-synced message is gone,
209
+ // so the log can't carry any byte-stable bytes forward.
210
+ if (normalizedMessages.length < this.#lastSyncCount) {
211
+ this.log.clear();
212
+ this.#lastSyncCount = 0;
213
+ this.#messageDigests = [];
214
+ }
215
+
216
+ // In-place rewrite: trim the log down to the longest byte-stable prefix
217
+ // that both the previous sync and the new messages share. Bound it by
218
+ // the current log length because `log.clear()` is public; direct clears
219
+ // (advisor reset) can leave the sync cursor ahead of the physical log.
220
+ // Anything past that point will be re-appended below with the new bytes.
221
+ if (this.#lastSyncCount > 0) {
222
+ const stableCount = Math.min(this.#longestStablePrefix(normalizedMessages), this.log.length);
223
+ if (stableCount < this.#lastSyncCount) {
224
+ this.log.truncate(stableCount);
225
+ this.#lastSyncCount = stableCount;
226
+ this.#messageDigests.length = stableCount;
227
+ }
228
+ }
229
+
230
+ // Append the diverged tail (or the full delta on a normal turn).
231
+ for (let i = this.#lastSyncCount; i < normalizedMessages.length; i++) {
232
+ const msg = normalizedMessages[i];
233
+ this.log.append(msg);
234
+ this.#messageDigests.push(this.#messageDigest(msg));
235
+ }
236
+ this.#lastSyncCount = normalizedMessages.length;
237
+ }
238
+
239
+ /** Reset prefix + log for a model/provider switch while mode stays active. */
240
+ invalidateForModelChange(): void {
241
+ this.prefix.invalidate();
242
+ this.log.clear();
243
+ this.#lastSyncCount = 0;
244
+ this.#messageDigests = [];
245
+ }
246
+
247
+ /** Reset the sync cursor AND clear the log. */
248
+ resetSyncCursor(): void {
249
+ this.log.clear();
250
+ this.#lastSyncCount = 0;
251
+ this.#messageDigests = [];
252
+ }
253
+
254
+ appendMessage(message: any): void {
255
+ this.log.append(message);
256
+ }
257
+
258
+ replaceTailMessage(message: any): void {
259
+ this.log.replaceTail(message);
260
+ }
261
+
262
+ invalidate(): void {
263
+ this.prefix.invalidate();
264
+ }
265
+
266
+ reset(context: AgentContext, options: BuildOptions): void {
267
+ this.prefix.invalidate();
268
+ this.log.clear();
269
+ this.#lastSyncCount = 0;
270
+ this.#messageDigests = [];
271
+ this.prefix.build(context, options);
272
+ }
273
+
274
+ /** Index of the first message whose serialized bytes differ from the
275
+ * previously-synced log; equals `min(lastSyncCount, normalizedMessages.length)`
276
+ * when nothing diverged. */
277
+ #longestStablePrefix(normalizedMessages: readonly unknown[]): number {
278
+ const bound = Math.min(this.#lastSyncCount, normalizedMessages.length);
279
+ for (let i = 0; i < bound; i++) {
280
+ if (this.#messageDigest(normalizedMessages[i]) !== this.#messageDigests[i]) {
281
+ return i;
282
+ }
283
+ }
284
+ return bound;
285
+ }
286
+
287
+ /** Deterministic digest over every field the provider may serialize — role,
288
+ * content, provider-native replay payloads, tool calls (both `toolCalls` and
289
+ * OpenAI-wire `tool_calls`), tool-result ids/names/error flags (both internal
290
+ * camelCase and wire snake_case), and assistant `id` — so an in-place rewrite
291
+ * of *any* of these fields is visible to {@link #longestStablePrefix}. */
292
+ #messageDigest(msg: unknown): number {
293
+ if (!msg || typeof msg !== "object") return 0;
294
+ const m = msg as Record<string, unknown>;
295
+ const payload = JSON.stringify({
296
+ r: m.role ?? null,
297
+ c: m.content ?? null,
298
+ pp: m.providerPayload ?? null,
299
+ tc: m.toolCalls ?? m.tool_calls ?? null,
300
+ tcid: m.toolCallId ?? m.tool_call_id ?? null,
301
+ tn: m.toolName ?? m.name ?? null,
302
+ err: m.isError ?? null,
303
+ id: m.id ?? null,
304
+ });
305
+ let hash = 0;
306
+ for (let j = 0; j < payload.length; j++) {
307
+ hash = ((hash << 5) - hash + payload.charCodeAt(j)) | 0;
308
+ }
309
+ return hash >>> 0;
310
+ }
311
+ }
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Snapshot helpers
315
+ // ---------------------------------------------------------------------------
316
+
317
+ function takeSnapshot(context: AgentContext, options: BuildOptions): StablePrefixSnapshot {
318
+ const systemPrompt = [...context.systemPrompt];
319
+ const tools =
320
+ normalizeTools(context.tools, options.intentTracing, options.exampleDialect, options.pruneToolDescriptions) ?? [];
321
+ return {
322
+ systemPrompt,
323
+ tools,
324
+ fingerprint: computeFingerprint(systemPrompt, tools, options),
325
+ };
326
+ }
327
+
328
+ function computeFingerprint(systemPrompt: string[], tools: Tool[], options: BuildOptions): string {
329
+ const payload = JSON.stringify({
330
+ s: systemPrompt,
331
+ t: tools.map(t => ({
332
+ n: t.name,
333
+ d: t.description,
334
+ p: t.parameters,
335
+ s: t.strict,
336
+ cf: t.customFormat,
337
+ cw: t.customWireName,
338
+ })),
339
+ i: options.intentTracing,
340
+ ex: options.exampleDialect,
341
+ pd: options.pruneToolDescriptions,
342
+ });
343
+ let hash = 0;
344
+ for (let i = 0; i < payload.length; i++) {
345
+ hash = ((hash << 5) - hash + payload.charCodeAt(i)) | 0;
346
+ }
347
+ return (hash >>> 0).toString(36);
348
+ }