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,323 @@
1
+ /**
2
+ * Shared utilities for compaction and branch summarization.
3
+ */
4
+
5
+ import type { Message, ToolCall } from "jeopi-ai";
6
+ import { type Dialect, getDialectDefinition } from "jeopi-ai/dialect";
7
+ import { formatGroupedPaths, prompt } from "jeopi-utils";
8
+ import type { AgentMessage } from "../types";
9
+ import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
10
+ import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
11
+
12
+ // ============================================================================
13
+ // File Operation Tracking
14
+ // ============================================================================
15
+
16
+ export interface FileOperations {
17
+ read: Set<string>;
18
+ written: Set<string>;
19
+ edited: Set<string>;
20
+ }
21
+
22
+ export function createFileOps(): FileOperations {
23
+ return {
24
+ read: new Set(),
25
+ written: new Set(),
26
+ edited: new Set(),
27
+ };
28
+ }
29
+
30
+ // Read-tool selector grammar, mirrored from the conservative filesystem splitter in
31
+ // packages/coding-agent/src/tools/path-utils.ts (splitPathAndSel). Keep in sync.
32
+ // A trailing `:chunk` is a selector only when it is a line-range list
33
+ // (`50`, `50-200`, `50+10`, `5-16,960-973`, `..` alias), `raw`, or `conflicts` —
34
+ // alone or as a `range:raw` / `raw:range` compound.
35
+ const RANGE_CHUNK_SRC = String.raw`L?\d+(?:(?:[-+]|\.\.)L?\d+|-|\.\.)?`;
36
+ const RANGE_LIST_SRC = `${RANGE_CHUNK_SRC}(?:,${RANGE_CHUNK_SRC})*`;
37
+ const READ_SELECTOR_RE = new RegExp(`^(?:${RANGE_LIST_SRC}|raw|conflicts)$`, "i");
38
+ const READ_RANGE_ONLY_RE = new RegExp(`^${RANGE_LIST_SRC}$`, "i");
39
+ const READ_RAW_ONLY_RE = /^raw$/i;
40
+
41
+ /**
42
+ * Split a read-tool path into its base path and trailing selector, mirroring the
43
+ * read tool's own splitter. Single source of the grammar in this package: the
44
+ * file-operations list strips selectors via {@link stripReadSelector}, and the
45
+ * supersede-prune pass keys on both parts via `readToolSupersedeKey`.
46
+ */
47
+ export function splitReadSelector(path: string): { path: string; sel?: string } {
48
+ const colon = path.lastIndexOf(":");
49
+ if (colon <= 0) return { path };
50
+ const candidate = path.slice(colon + 1);
51
+ if (!READ_SELECTOR_RE.test(candidate)) return { path };
52
+ let base = path.slice(0, colon);
53
+ let sel = candidate;
54
+ // Compound trailing selector: `path:1-50:raw` or `path:raw:1-50`.
55
+ const inner = base.lastIndexOf(":");
56
+ if (inner > 0) {
57
+ const innerCandidate = base.slice(inner + 1);
58
+ const innerIsRaw = READ_RAW_ONLY_RE.test(innerCandidate);
59
+ const outerIsRaw = READ_RAW_ONLY_RE.test(candidate);
60
+ const innerIsRange = READ_RANGE_ONLY_RE.test(innerCandidate);
61
+ const outerIsRange = READ_RANGE_ONLY_RE.test(candidate);
62
+ if ((innerIsRaw && outerIsRange) || (innerIsRange && outerIsRaw)) {
63
+ sel = `${innerCandidate}:${candidate}`;
64
+ base = base.slice(0, inner);
65
+ }
66
+ }
67
+ return { path: base, sel };
68
+ }
69
+
70
+ /**
71
+ * Strip a trailing read-tool selector (`:50-200`, `:raw`, `:1-50:raw`, `:conflicts`, …)
72
+ * so the same file read with different line ranges dedupes to one `<files>` entry
73
+ * and matches its write/edit path when computing Read/Write/RW markers.
74
+ */
75
+ export function stripReadSelector(path: string): string {
76
+ return splitReadSelector(path).path;
77
+ }
78
+
79
+ /**
80
+ * A real filesystem path never contains a `scheme://` URL. Tool-call paths that
81
+ * do — `conflict://1`, `artifact://3`, `local://ctx.md`, `history://…`,
82
+ * `issue://12`, `https://…`, and the tolerated `file.ts:conflict://1` prefix
83
+ * form — are session-scoped or remote resources, not files the post-compaction
84
+ * agent can re-ground on. Keep them out of the `<files>` summary.
85
+ */
86
+ const URL_SCHEME_RE = /[a-z][a-z0-9+.-]*:\/\//i;
87
+
88
+ /**
89
+ * Whether `path` references a `scheme://` URL (internal URI or web URL) rather
90
+ * than a filesystem path that belongs in the compaction `<files>` summary.
91
+ */
92
+ export function isUrlSchemePath(path: string): boolean {
93
+ return URL_SCHEME_RE.test(path);
94
+ }
95
+
96
+ /**
97
+ * Extract file operations from tool calls in an assistant message.
98
+ */
99
+ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
100
+ if (message.role !== "assistant") return;
101
+ if (!("content" in message) || !Array.isArray(message.content)) return;
102
+
103
+ for (const block of message.content) {
104
+ if (typeof block !== "object" || block === null) continue;
105
+ if (!("type" in block) || block.type !== "toolCall") continue;
106
+ if (!("arguments" in block) || !("name" in block)) continue;
107
+
108
+ const args = block.arguments as Record<string, unknown> | undefined;
109
+ if (!args) continue;
110
+
111
+ const path = typeof args.path === "string" ? args.path : undefined;
112
+ if (!path) continue;
113
+
114
+ // Internal URIs (conflict://, artifact://, local://, history://, …) and
115
+ // web URLs are not re-groundable files — keep them out of `<files>`.
116
+ if (isUrlSchemePath(path)) continue;
117
+
118
+ switch (block.name) {
119
+ case "read":
120
+ fileOps.read.add(stripReadSelector(path));
121
+ break;
122
+ case "write":
123
+ fileOps.written.add(path);
124
+ break;
125
+ case "edit":
126
+ fileOps.edited.add(path);
127
+ break;
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Compute final file lists from file operations.
134
+ * Returns readFiles (files only read, not modified) and modifiedFiles.
135
+ */
136
+ export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
137
+ // Drop any `scheme://` URLs (e.g. legacy `conflict://`/`artifact://` entries
138
+ // rehydrated straight into `fileOps` from a pre-fix compaction summary) — only
139
+ // real files belong in `<files>`. New tool-call scans are already filtered.
140
+ const modified = new Set([...fileOps.edited, ...fileOps.written].filter(f => !isUrlSchemePath(f)));
141
+ const readOnly = [...fileOps.read].filter(f => !isUrlSchemePath(f) && !modified.has(f)).sort();
142
+ const modifiedFiles = [...modified].sort();
143
+ return { readFiles: readOnly, modifiedFiles };
144
+ }
145
+
146
+ /**
147
+ * Format file operations as one `<files>` tag: a grouped, prefix-folded
148
+ * directory tree (find-tool shape — `# dir/` headers, bare basenames) with a
149
+ * ` (Read)` / ` (Write)` / ` (RW)` marker per file instead of separate
150
+ * read/modified lists. `readSet` is the cumulative read set (`fileOps.read`),
151
+ * used to tell modified files that were also read (RW) from blind writes.
152
+ */
153
+ const FILE_OPERATION_SUMMARY_LIMIT = 20;
154
+
155
+ function stripFileOperationTags(summary: string): string {
156
+ // Legacy <read-files>/<modified-files> tags are still stripped so summaries
157
+ // written before the combined <files> tag self-heal on the next compaction.
158
+ return summary
159
+ .replace(/<files>[\s\S]*?<\/files>\s*/g, "")
160
+ .replace(/<read-files>[\s\S]*?<\/read-files>\s*/g, "")
161
+ .replace(/<modified-files>[\s\S]*?<\/modified-files>\s*/g, "")
162
+ .trimEnd();
163
+ }
164
+ export function formatFileOperations(
165
+ readFiles: string[],
166
+ modifiedFiles: string[],
167
+ readSet?: ReadonlySet<string>,
168
+ ): string {
169
+ if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
170
+ const mode = new Map<string, "Read" | "Write" | "RW">();
171
+ for (const file of readFiles) mode.set(file, "Read");
172
+ for (const file of modifiedFiles) mode.set(file, readSet?.has(file) ? "RW" : "Write");
173
+ const all = [...mode.keys()].sort();
174
+ let files = formatGroupedPaths(all.slice(0, FILE_OPERATION_SUMMARY_LIMIT), path => ` (${mode.get(path)})`);
175
+ if (all.length > FILE_OPERATION_SUMMARY_LIMIT) {
176
+ files += `\n[…${all.length - FILE_OPERATION_SUMMARY_LIMIT} files elided…]`;
177
+ }
178
+ return prompt.render(fileOperationsTemplate, { files });
179
+ }
180
+
181
+ export function upsertFileOperations(
182
+ summary: string,
183
+ readFiles: string[],
184
+ modifiedFiles: string[],
185
+ readSet?: ReadonlySet<string>,
186
+ ): string {
187
+ const baseSummary = stripFileOperationTags(summary);
188
+ const fileOperations = formatFileOperations(readFiles, modifiedFiles, readSet);
189
+ if (!fileOperations) return baseSummary;
190
+ if (!baseSummary) return fileOperations;
191
+ return `${baseSummary}\n\n${fileOperations}`;
192
+ }
193
+
194
+ // ============================================================================
195
+ // Message Serialization
196
+ // ============================================================================
197
+
198
+ /** Maximum characters for a tool result in serialized summaries. */
199
+ const TOOL_RESULT_MAX_CHARS = 2000;
200
+
201
+ /**
202
+ * Truncate tool results to the same representation used in summarization prompts.
203
+ */
204
+ export function truncateToolResultForSummary(text: string): string {
205
+ if (text.length <= TOOL_RESULT_MAX_CHARS) return text;
206
+ const truncatedChars = text.length - TOOL_RESULT_MAX_CHARS;
207
+ return `${text.slice(0, TOOL_RESULT_MAX_CHARS)}\n\n[... ${truncatedChars} more characters truncated]`;
208
+ }
209
+
210
+ /**
211
+ * Serialize LLM messages to text for summarization.
212
+ * This prevents the model from treating it as a conversation to continue.
213
+ * Call convertToLlm() first to handle custom message types.
214
+ */
215
+ export function serializeConversation(messages: Message[], dialect?: Dialect): string {
216
+ // Tool results flagged contextually useless (and their paired calls) are
217
+ // dropped from the serialized text: the source region is discarded after
218
+ // summarization anyway, so excluding them costs nothing and keeps garbage
219
+ // out of the summary input.
220
+ const uselessCallIds = new Set<string>();
221
+ for (const msg of messages) {
222
+ if (msg.role === "toolResult" && msg.useless === true && msg.isError !== true) {
223
+ uselessCallIds.add(msg.toolCallId);
224
+ }
225
+ }
226
+ if (dialect) {
227
+ const processed: Message[] = [];
228
+ for (const msg of messages) {
229
+ if (msg.role === "assistant") {
230
+ const content = msg.content.filter(block => block.type !== "toolCall" || !uselessCallIds.has(block.id));
231
+ if (content.length > 0) processed.push(content.length === msg.content.length ? msg : { ...msg, content });
232
+ continue;
233
+ }
234
+ if (msg.role === "toolResult") {
235
+ if (uselessCallIds.has(msg.toolCallId)) continue;
236
+ const text = msg.content
237
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
238
+ .map(c => c.text)
239
+ .join("");
240
+ if (!text) continue;
241
+ processed.push({
242
+ ...msg,
243
+ content: [{ type: "text", text: truncateToolResultForSummary(text) }],
244
+ });
245
+ continue;
246
+ }
247
+ processed.push(msg);
248
+ }
249
+ return getDialectDefinition(dialect).renderTranscript(processed);
250
+ }
251
+
252
+ const parts: string[] = [];
253
+ for (const msg of messages) {
254
+ if (msg.role === "user") {
255
+ const content =
256
+ typeof msg.content === "string"
257
+ ? msg.content
258
+ : msg.content
259
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
260
+ .map(c => c.text)
261
+ .join("");
262
+ if (content) parts.push(`[User]: ${content}`);
263
+ } else if (msg.role === "assistant") {
264
+ const textParts: string[] = [];
265
+ const thinkingParts: string[] = [];
266
+ const toolCalls: ToolCall[] = [];
267
+
268
+ for (const block of msg.content) {
269
+ if (block.type === "text") {
270
+ textParts.push(block.text);
271
+ } else if (block.type === "thinking") {
272
+ thinkingParts.push(block.thinking);
273
+ } else if (block.type === "toolCall") {
274
+ if (uselessCallIds.has(block.id)) continue;
275
+ toolCalls.push(block);
276
+ }
277
+ }
278
+
279
+ if (thinkingParts.length > 0) {
280
+ parts.push(`[Think]: ${thinkingParts.join("\n")}`);
281
+ }
282
+ if (textParts.length > 0) {
283
+ parts.push(`[Assistant]: ${textParts.join("\n")}`);
284
+ }
285
+ if (toolCalls.length > 0) {
286
+ parts.push(`[Tool Call]: ${renderToolCalls(toolCalls)}`);
287
+ }
288
+ } else if (msg.role === "toolResult") {
289
+ if (uselessCallIds.has(msg.toolCallId)) continue;
290
+ const content = msg.content
291
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
292
+ .map(c => c.text)
293
+ .join("");
294
+ if (content) {
295
+ const text = truncateToolResultForSummary(content);
296
+ parts.push(`[Tool Result]: ${text}`);
297
+ }
298
+ }
299
+ }
300
+
301
+ return parts.join("\n\n");
302
+ }
303
+
304
+ /**
305
+ * Render an assistant turn's tool calls as a compact `name(args)` list for the
306
+ * legacy serializer.
307
+ */
308
+ function renderToolCalls(calls: ToolCall[]): string {
309
+ return calls
310
+ .map(call => {
311
+ const argsStr = Object.entries(call.arguments as Record<string, unknown>)
312
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
313
+ .join(", ");
314
+ return `${call.name}(${argsStr})`;
315
+ })
316
+ .join("; ");
317
+ }
318
+
319
+ // ============================================================================
320
+ // Summarization System Prompt
321
+ // ============================================================================
322
+
323
+ export const SUMMARIZATION_SYSTEM_PROMPT = prompt.render(summarizationSystemPrompt);
@@ -0,0 +1 @@
1
+ export * from "./compaction/index";
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // Core Agent
2
+ export * from "./agent";
3
+ // Loop functions
4
+ export * from "./agent-loop";
5
+ // Append-only context mode
6
+ export * from "./append-only-context";
7
+ // Compaction
8
+ export * from "./compaction";
9
+ // Proxy utilities
10
+ export * from "./proxy";
11
+ // Replay policy
12
+ export * from "./replay-policy";
13
+ // Run-level telemetry collector + aggregators
14
+ export * from "./run-collector";
15
+ // Telemetry
16
+ export * from "./telemetry";
17
+ // Thinking selectors
18
+ export * from "./thinking";
19
+ // Tokenizer choice
20
+ export * from "./tokenizer";
21
+ // Types
22
+ export * from "./types";
23
+ // Yield utilities for Bun event-loop busy-wait prevention
24
+ export * from "./utils/yield";