@prometheus-ai/agent-core 0.5.3 → 0.5.8
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/types/agent-loop.d.ts +7 -0
- package/dist/types/agent.d.ts +41 -13
- package/dist/types/compaction/branch-summarization.d.ts +3 -3
- package/dist/types/compaction/compaction.d.ts +11 -9
- package/dist/types/compaction/messages.d.ts +14 -2
- package/dist/types/compaction/openai.d.ts +18 -3
- package/dist/types/compaction/pruning.d.ts +55 -0
- package/dist/types/compaction/shake.d.ts +3 -1
- package/dist/types/compaction/utils.d.ts +18 -2
- package/dist/types/proxy.d.ts +4 -3
- package/dist/types/telemetry.d.ts +59 -57
- package/dist/types/types.d.ts +60 -16
- package/package.json +6 -4
- package/src/agent-loop.ts +660 -181
- package/src/agent.ts +103 -30
- package/src/compaction/branch-summarization.ts +8 -7
- package/src/compaction/compaction.ts +69 -34
- package/src/compaction/messages.ts +78 -64
- package/src/compaction/openai.ts +88 -74
- package/src/compaction/prompts/branch-summary.md +1 -1
- package/src/compaction/prompts/compaction-summary-context.md +1 -1
- package/src/compaction/prompts/compaction-summary.md +2 -2
- package/src/compaction/prompts/compaction-update-summary.md +3 -3
- package/src/compaction/prompts/file-operations.md +3 -8
- package/src/compaction/prompts/summarization-system.md +1 -1
- package/src/compaction/pruning.ts +240 -8
- package/src/compaction/shake.ts +7 -3
- package/src/compaction/utils.ts +97 -19
- package/src/proxy.ts +13 -7
- package/src/telemetry.ts +126 -113
- package/src/types.ts +65 -16
package/src/compaction/shake.ts
CHANGED
|
@@ -267,7 +267,9 @@ function scanContentBlocks(
|
|
|
267
267
|
* Walks the protect-recent window (most recent `protectTokens` of context is
|
|
268
268
|
* kept intact), collects whole tool-result messages (honoring `protectedTools`
|
|
269
269
|
* and skipping already-pruned results) and large fenced/XML blocks inside
|
|
270
|
-
* user/developer/assistant/custom messages.
|
|
270
|
+
* user/developer/assistant/custom messages. Tool results flagged contextually
|
|
271
|
+
* useless by their tool bypass the protect window — there is nothing recent
|
|
272
|
+
* worth keeping in them. Returns regions in document order.
|
|
271
273
|
*
|
|
272
274
|
* `toolCall` blocks are never touched (tool-call/result pairing is preserved)
|
|
273
275
|
* and regions never span a message boundary. When the combined estimated
|
|
@@ -289,10 +291,12 @@ export function collectShakeRegions(entries: SessionEntry[], config: ShakeConfig
|
|
|
289
291
|
|
|
290
292
|
const regions: ShakeRegion[] = [];
|
|
291
293
|
for (let i = 0; i < n; i++) {
|
|
292
|
-
if (accumulatedAfter[i] < config.protectTokens) continue;
|
|
293
294
|
const entry = entries[i];
|
|
294
|
-
|
|
295
295
|
const toolResult = getToolResultMessage(entry);
|
|
296
|
+
// Useless-flagged results carry no information once consumed; they are
|
|
297
|
+
// eligible even inside the protect-recent window.
|
|
298
|
+
const uselessResult = toolResult !== undefined && toolResult.useless === true && toolResult.isError !== true;
|
|
299
|
+
if (!uselessResult && accumulatedAfter[i] < config.protectTokens) continue;
|
|
296
300
|
if (toolResult) {
|
|
297
301
|
if (toolResult.prunedAt !== undefined) continue;
|
|
298
302
|
if (isProtectedToolResult(toolResult, toolCallsById.get(toolResult.toolCallId), config.protectedTools))
|
package/src/compaction/utils.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Message } from "@prometheus-ai/ai";
|
|
6
|
-
import { prompt } from "@prometheus-ai/utils";
|
|
6
|
+
import { formatGroupedPaths, prompt } from "@prometheus-ai/utils";
|
|
7
7
|
import type { AgentMessage } from "../types";
|
|
8
8
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
9
9
|
import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
|
|
@@ -26,6 +26,55 @@ export function createFileOps(): FileOperations {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Read-tool selector grammar, mirrored from the conservative filesystem splitter in
|
|
30
|
+
// packages/coding-agent/src/tools/path-utils.ts (splitPathAndSel). Keep in sync.
|
|
31
|
+
// A trailing `:chunk` is a selector only when it is a line-range list
|
|
32
|
+
// (`50`, `50-200`, `50+10`, `5-16,960-973`, `..` alias), `raw`, or `conflicts` —
|
|
33
|
+
// alone or as a `range:raw` / `raw:range` compound.
|
|
34
|
+
const RANGE_CHUNK_SRC = String.raw`L?\d+(?:(?:[-+]|\.\.)L?\d+|-|\.\.)?`;
|
|
35
|
+
const RANGE_LIST_SRC = `${RANGE_CHUNK_SRC}(?:,${RANGE_CHUNK_SRC})*`;
|
|
36
|
+
const READ_SELECTOR_RE = new RegExp(`^(?:${RANGE_LIST_SRC}|raw|conflicts)$`, "i");
|
|
37
|
+
const READ_RANGE_ONLY_RE = new RegExp(`^${RANGE_LIST_SRC}$`, "i");
|
|
38
|
+
const READ_RAW_ONLY_RE = /^raw$/i;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Split a read-tool path into its base path and trailing selector, mirroring the
|
|
42
|
+
* read tool's own splitter. Single source of the grammar in this package: the
|
|
43
|
+
* file-operations list strips selectors via {@link stripReadSelector}, and the
|
|
44
|
+
* supersede-prune pass keys on both parts via `readToolSupersedeKey`.
|
|
45
|
+
*/
|
|
46
|
+
export function splitReadSelector(path: string): { path: string; sel?: string } {
|
|
47
|
+
const colon = path.lastIndexOf(":");
|
|
48
|
+
if (colon <= 0) return { path };
|
|
49
|
+
const candidate = path.slice(colon + 1);
|
|
50
|
+
if (!READ_SELECTOR_RE.test(candidate)) return { path };
|
|
51
|
+
let base = path.slice(0, colon);
|
|
52
|
+
let sel = candidate;
|
|
53
|
+
// Compound trailing selector: `path:1-50:raw` or `path:raw:1-50`.
|
|
54
|
+
const inner = base.lastIndexOf(":");
|
|
55
|
+
if (inner > 0) {
|
|
56
|
+
const innerCandidate = base.slice(inner + 1);
|
|
57
|
+
const innerIsRaw = READ_RAW_ONLY_RE.test(innerCandidate);
|
|
58
|
+
const outerIsRaw = READ_RAW_ONLY_RE.test(candidate);
|
|
59
|
+
const innerIsRange = READ_RANGE_ONLY_RE.test(innerCandidate);
|
|
60
|
+
const outerIsRange = READ_RANGE_ONLY_RE.test(candidate);
|
|
61
|
+
if ((innerIsRaw && outerIsRange) || (innerIsRange && outerIsRaw)) {
|
|
62
|
+
sel = `${innerCandidate}:${candidate}`;
|
|
63
|
+
base = base.slice(0, inner);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { path: base, sel };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Strip a trailing read-tool selector (`:50-200`, `:raw`, `:1-50:raw`, `:conflicts`, …)
|
|
71
|
+
* so the same file read with different line ranges dedupes to one `<files>` entry
|
|
72
|
+
* and matches its write/edit path when computing Read/Write/RW markers.
|
|
73
|
+
*/
|
|
74
|
+
export function stripReadSelector(path: string): string {
|
|
75
|
+
return splitReadSelector(path).path;
|
|
76
|
+
}
|
|
77
|
+
|
|
29
78
|
/**
|
|
30
79
|
* Extract file operations from tool calls in an assistant message.
|
|
31
80
|
*/
|
|
@@ -46,7 +95,7 @@ export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOp
|
|
|
46
95
|
|
|
47
96
|
switch (block.name) {
|
|
48
97
|
case "read":
|
|
49
|
-
fileOps.read.add(path);
|
|
98
|
+
fileOps.read.add(stripReadSelector(path));
|
|
50
99
|
break;
|
|
51
100
|
case "write":
|
|
52
101
|
fileOps.written.add(path);
|
|
@@ -70,32 +119,48 @@ export function computeFileLists(fileOps: FileOperations): { readFiles: string[]
|
|
|
70
119
|
}
|
|
71
120
|
|
|
72
121
|
/**
|
|
73
|
-
* Format file operations as
|
|
122
|
+
* Format file operations as one `<files>` tag: a grouped, prefix-folded
|
|
123
|
+
* directory tree (find-tool shape — `# dir/` headers, bare basenames) with a
|
|
124
|
+
* ` (Read)` / ` (Write)` / ` (RW)` marker per file instead of separate
|
|
125
|
+
* read/modified lists. `readSet` is the cumulative read set (`fileOps.read`),
|
|
126
|
+
* used to tell modified files that were also read (RW) from blind writes.
|
|
74
127
|
*/
|
|
75
128
|
const FILE_OPERATION_SUMMARY_LIMIT = 20;
|
|
76
129
|
|
|
77
|
-
function truncateFileList(files: string[]): string[] {
|
|
78
|
-
if (files.length <= FILE_OPERATION_SUMMARY_LIMIT) return files;
|
|
79
|
-
const omitted = files.length - FILE_OPERATION_SUMMARY_LIMIT;
|
|
80
|
-
return [...files.slice(0, FILE_OPERATION_SUMMARY_LIMIT), `… (${omitted} more files omitted)`];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
130
|
function stripFileOperationTags(summary: string): string {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
131
|
+
// Legacy <read-files>/<modified-files> tags are still stripped so summaries
|
|
132
|
+
// written before the combined <files> tag self-heal on the next compaction.
|
|
133
|
+
return summary
|
|
134
|
+
.replace(/<files>[\s\S]*?<\/files>\s*/g, "")
|
|
135
|
+
.replace(/<read-files>[\s\S]*?<\/read-files>\s*/g, "")
|
|
136
|
+
.replace(/<modified-files>[\s\S]*?<\/modified-files>\s*/g, "")
|
|
137
|
+
.trimEnd();
|
|
87
138
|
}
|
|
88
|
-
export function formatFileOperations(
|
|
139
|
+
export function formatFileOperations(
|
|
140
|
+
readFiles: string[],
|
|
141
|
+
modifiedFiles: string[],
|
|
142
|
+
readSet?: ReadonlySet<string>,
|
|
143
|
+
): string {
|
|
89
144
|
if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
145
|
+
const mode = new Map<string, "Read" | "Write" | "RW">();
|
|
146
|
+
for (const file of readFiles) mode.set(file, "Read");
|
|
147
|
+
for (const file of modifiedFiles) mode.set(file, readSet?.has(file) ? "RW" : "Write");
|
|
148
|
+
const all = [...mode.keys()].sort();
|
|
149
|
+
let files = formatGroupedPaths(all.slice(0, FILE_OPERATION_SUMMARY_LIMIT), path => ` (${mode.get(path)})`);
|
|
150
|
+
if (all.length > FILE_OPERATION_SUMMARY_LIMIT) {
|
|
151
|
+
files += `\n… (${all.length - FILE_OPERATION_SUMMARY_LIMIT} more files omitted)`;
|
|
152
|
+
}
|
|
153
|
+
return prompt.render(fileOperationsTemplate, { files });
|
|
94
154
|
}
|
|
95
155
|
|
|
96
|
-
export function upsertFileOperations(
|
|
156
|
+
export function upsertFileOperations(
|
|
157
|
+
summary: string,
|
|
158
|
+
readFiles: string[],
|
|
159
|
+
modifiedFiles: string[],
|
|
160
|
+
readSet?: ReadonlySet<string>,
|
|
161
|
+
): string {
|
|
97
162
|
const baseSummary = stripFileOperationTags(summary);
|
|
98
|
-
const fileOperations = formatFileOperations(readFiles, modifiedFiles);
|
|
163
|
+
const fileOperations = formatFileOperations(readFiles, modifiedFiles, readSet);
|
|
99
164
|
if (!fileOperations) return baseSummary;
|
|
100
165
|
if (!baseSummary) return fileOperations;
|
|
101
166
|
return `${baseSummary}\n\n${fileOperations}`;
|
|
@@ -126,6 +191,17 @@ function truncateForSummary(text: string, maxChars: number): string {
|
|
|
126
191
|
export function serializeConversation(messages: Message[]): string {
|
|
127
192
|
const parts: string[] = [];
|
|
128
193
|
|
|
194
|
+
// Tool results flagged contextually useless (and their paired calls) are
|
|
195
|
+
// dropped from the serialized text: the source region is discarded after
|
|
196
|
+
// summarization anyway, so excluding them costs nothing and keeps garbage
|
|
197
|
+
// out of the summary input.
|
|
198
|
+
const uselessCallIds = new Set<string>();
|
|
199
|
+
for (const msg of messages) {
|
|
200
|
+
if (msg.role === "toolResult" && msg.useless === true && msg.isError !== true) {
|
|
201
|
+
uselessCallIds.add(msg.toolCallId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
129
205
|
for (const msg of messages) {
|
|
130
206
|
if (msg.role === "user") {
|
|
131
207
|
const content =
|
|
@@ -147,6 +223,7 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
147
223
|
} else if (block.type === "thinking") {
|
|
148
224
|
thinkingParts.push(block.thinking);
|
|
149
225
|
} else if (block.type === "toolCall") {
|
|
226
|
+
if (uselessCallIds.has(block.id)) continue;
|
|
150
227
|
const args = block.arguments as Record<string, unknown>;
|
|
151
228
|
const argsStr = Object.entries(args)
|
|
152
229
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
@@ -165,6 +242,7 @@ export function serializeConversation(messages: Message[]): string {
|
|
|
165
242
|
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
166
243
|
}
|
|
167
244
|
} else if (msg.role === "toolResult") {
|
|
245
|
+
if (uselessCallIds.has(msg.toolCallId)) continue;
|
|
168
246
|
const content = msg.content
|
|
169
247
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
170
248
|
.map(c => c.text)
|
package/src/proxy.ts
CHANGED
|
@@ -7,17 +7,18 @@ import {
|
|
|
7
7
|
type AssistantMessageEvent,
|
|
8
8
|
type Context,
|
|
9
9
|
EventStream,
|
|
10
|
+
type FetchImpl,
|
|
10
11
|
type Model,
|
|
11
12
|
type SimpleStreamOptions,
|
|
12
13
|
type StopReason,
|
|
13
14
|
type ToolCall,
|
|
14
15
|
} from "@prometheus-ai/ai";
|
|
15
|
-
import { calculateCost } from "@prometheus-ai/ai/models";
|
|
16
16
|
import { parseStreamingJson } from "@prometheus-ai/ai/utils/json-parse";
|
|
17
|
+
import { calculateCost } from "@prometheus-ai/catalog/models";
|
|
17
18
|
import { readSseJson } from "@prometheus-ai/utils";
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
20
|
+
// Event stream adapter for proxy SSE events
|
|
21
|
+
export class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
21
22
|
constructor() {
|
|
22
23
|
super(
|
|
23
24
|
event => event.type === "done" || event.type === "error",
|
|
@@ -61,6 +62,8 @@ export interface ProxyStreamOptions extends SimpleStreamOptions {
|
|
|
61
62
|
authToken: string;
|
|
62
63
|
/** Proxy server URL (e.g., "https://genai.example.com") */
|
|
63
64
|
proxyUrl: string;
|
|
65
|
+
/** Optional fetch implementation; defaults to global fetch. */
|
|
66
|
+
fetch?: FetchImpl;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -117,7 +120,7 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
try {
|
|
120
|
-
response = await fetch(`${options.proxyUrl}/api/stream`, {
|
|
123
|
+
response = await (options.fetch ?? fetch)(`${options.proxyUrl}/api/stream`, {
|
|
121
124
|
method: "POST",
|
|
122
125
|
headers: {
|
|
123
126
|
Authorization: `Bearer ${options.authToken}`,
|
|
@@ -167,9 +170,12 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
if (!sawTerminalEvent) {
|
|
174
|
+
if (options.signal?.aborted) {
|
|
175
|
+
const reason = options.signal.reason;
|
|
176
|
+
throw reason instanceof Error ? reason : new Error(String(reason ?? "Request aborted"));
|
|
177
|
+
}
|
|
178
|
+
throw new Error("Proxy stream ended without a terminal event (done or error)");
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
stream.end();
|