@evalops/maestro 0.10.29 → 0.10.31
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/compaction/cut-points.d.ts +157 -0
- package/dist/agent/compaction/cut-points.d.ts.map +1 -0
- package/dist/agent/compaction/cut-points.js +341 -0
- package/dist/agent/compaction/cut-points.js.map +1 -0
- package/dist/agent/compaction.d.ts +4 -144
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +5 -340
- package/dist/agent/compaction.js.map +1 -1
- package/dist/agent/transport/events.d.ts +23 -0
- package/dist/agent/transport/events.d.ts.map +1 -0
- package/dist/agent/transport/events.js +69 -0
- package/dist/agent/transport/events.js.map +1 -0
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +1 -68
- package/dist/agent/transport.js.map +1 -1
- package/dist/cli.js +247 -203
- package/dist/node_modules/@evalops/contracts/package.json +1 -1
- package/dist/node_modules/@evalops/tui/package.json +1 -1
- package/dist/server/agent-runtime-ledger.d.ts +18 -3
- package/dist/server/agent-runtime-ledger.d.ts.map +1 -1
- package/dist/server/agent-runtime-ledger.js +45 -1
- package/dist/server/agent-runtime-ledger.js.map +1 -1
- package/dist/tools/parallel-ripgrep.d.ts.map +1 -1
- package/dist/tools/parallel-ripgrep.js +1 -10
- package/dist/tools/parallel-ripgrep.js.map +1 -1
- package/dist/tools/ripgrep-utils.d.ts +1 -0
- package/dist/tools/ripgrep-utils.d.ts.map +1 -1
- package/dist/tools/ripgrep-utils.js +9 -0
- package/dist/tools/ripgrep-utils.js.map +1 -1
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +4 -4
- package/dist/tools/search.js.map +1 -1
- package/dist/version.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { SessionEntry } from "../../session/types.js";
|
|
2
|
+
import type { AppMessage, Usage } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for compaction behavior.
|
|
5
|
+
*/
|
|
6
|
+
export interface CompactionSettings {
|
|
7
|
+
/** Whether auto-compaction is enabled */
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Tokens to reserve for summary generation and safety margin.
|
|
11
|
+
* Default: 16384 (~13k for summary + ~3k safety margin)
|
|
12
|
+
*/
|
|
13
|
+
reserveTokens: number;
|
|
14
|
+
/**
|
|
15
|
+
* Approximate number of recent tokens to preserve verbatim.
|
|
16
|
+
* Default: 20000 (recent context preserved without summarization)
|
|
17
|
+
*/
|
|
18
|
+
keepRecentTokens: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Default compaction settings based on research into Claude Code, Codex, and OpenCode.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
24
|
+
/**
|
|
25
|
+
* Calculate total context tokens from usage metadata.
|
|
26
|
+
*
|
|
27
|
+
* This includes all token types that contribute to context window consumption:
|
|
28
|
+
* - input: Tokens from user/system messages
|
|
29
|
+
* - output: Tokens generated by the model
|
|
30
|
+
* - cacheRead: Tokens read from prompt cache
|
|
31
|
+
* - cacheWrite: Tokens written to prompt cache
|
|
32
|
+
*
|
|
33
|
+
* @param usage - Usage metadata from an assistant message
|
|
34
|
+
* @returns Total context token count
|
|
35
|
+
*/
|
|
36
|
+
export declare function calculateContextTokens(usage: Usage): number;
|
|
37
|
+
/**
|
|
38
|
+
* Find the last non-aborted assistant message usage from a message array.
|
|
39
|
+
*
|
|
40
|
+
* Used to determine current context window consumption. Walks backwards
|
|
41
|
+
* to find the most recent valid usage metadata.
|
|
42
|
+
*
|
|
43
|
+
* @param messages - Array of application messages
|
|
44
|
+
* @returns Usage metadata from the last assistant message, or null
|
|
45
|
+
*/
|
|
46
|
+
export declare function getLastAssistantUsage(messages: AppMessage[]): Usage | null;
|
|
47
|
+
/**
|
|
48
|
+
* Find the last non-aborted assistant message usage from session entries.
|
|
49
|
+
*
|
|
50
|
+
* @param entries - Array of session entries
|
|
51
|
+
* @returns Usage metadata from the last assistant message, or null
|
|
52
|
+
*/
|
|
53
|
+
export declare function getLastAssistantUsageFromEntries(entries: SessionEntry[]): Usage | null;
|
|
54
|
+
/**
|
|
55
|
+
* Determine if compaction should trigger based on current context usage.
|
|
56
|
+
*
|
|
57
|
+
* Compaction triggers when context usage exceeds `contextWindow - reserveTokens`,
|
|
58
|
+
* leaving enough room for summary generation plus a safety margin.
|
|
59
|
+
*
|
|
60
|
+
* @param contextTokens - Current context window usage in tokens
|
|
61
|
+
* @param contextWindow - Model's total context window size
|
|
62
|
+
* @param settings - Compaction configuration
|
|
63
|
+
* @returns true if compaction should trigger
|
|
64
|
+
*/
|
|
65
|
+
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Calculate context usage percentage.
|
|
68
|
+
*
|
|
69
|
+
* @param contextTokens - Current context window usage in tokens
|
|
70
|
+
* @param contextWindow - Model's total context window size
|
|
71
|
+
* @returns Percentage of context window used (0-100)
|
|
72
|
+
*/
|
|
73
|
+
export declare function calculateUsagePercent(contextTokens: number, contextWindow: number): number;
|
|
74
|
+
/**
|
|
75
|
+
* Result of finding a cut point, including split turn information.
|
|
76
|
+
*/
|
|
77
|
+
export interface CutPointResult {
|
|
78
|
+
/** Index of first message to keep (messages before this are summarized) */
|
|
79
|
+
firstKeptIndex: number;
|
|
80
|
+
/** Index of user message that starts the turn being split, or -1 if not splitting */
|
|
81
|
+
turnStartIndex: number;
|
|
82
|
+
/** Whether this cut splits a turn (cut point is not a user message) */
|
|
83
|
+
isSplitTurn: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Find indices of user messages (turn boundaries) in a message array.
|
|
87
|
+
*
|
|
88
|
+
* Turn boundaries are important because we never want to split a turn
|
|
89
|
+
* (user message + assistant response + tool results). Cutting at a user
|
|
90
|
+
* message ensures turn integrity.
|
|
91
|
+
*
|
|
92
|
+
* @param messages - Array of messages to scan
|
|
93
|
+
* @param startIndex - First index to consider (inclusive)
|
|
94
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
95
|
+
* @returns Array of indices where user messages appear
|
|
96
|
+
*/
|
|
97
|
+
export declare function findTurnBoundaries(messages: AppMessage[], startIndex: number, endIndex: number): number[];
|
|
98
|
+
/**
|
|
99
|
+
* Find the user message that starts the turn containing the given index.
|
|
100
|
+
*
|
|
101
|
+
* @param messages - Array of messages
|
|
102
|
+
* @param entryIndex - Index of the message to find the turn start for
|
|
103
|
+
* @param startIndex - First index to consider (inclusive)
|
|
104
|
+
* @returns Index of the user message that starts this turn, or -1 if not found
|
|
105
|
+
*/
|
|
106
|
+
export declare function findTurnStartIndex(messages: AppMessage[], entryIndex: number, startIndex: number): number;
|
|
107
|
+
/**
|
|
108
|
+
* Find the optimal cut point in messages that preserves `keepRecentTokens`.
|
|
109
|
+
*
|
|
110
|
+
* The algorithm:
|
|
111
|
+
* 1. Find all turn boundaries (user message indices)
|
|
112
|
+
* 2. Walk backwards collecting assistant token usage
|
|
113
|
+
* 3. Find where cumulative token difference exceeds keepRecentTokens
|
|
114
|
+
* 4. Return the nearest turn boundary at or before that point
|
|
115
|
+
*
|
|
116
|
+
* This ensures:
|
|
117
|
+
* - Recent context is preserved based on actual token usage, not message count
|
|
118
|
+
* - Turn integrity is maintained (user/assistant/toolResult groups stay together)
|
|
119
|
+
* - More context is kept when messages are small, less when large
|
|
120
|
+
*
|
|
121
|
+
* @param messages - Array of messages to analyze
|
|
122
|
+
* @param startIndex - First index to consider for cutting
|
|
123
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
124
|
+
* @param keepRecentTokens - Approximate tokens to preserve
|
|
125
|
+
* @returns Index of first message to keep (messages before this are summarized)
|
|
126
|
+
*/
|
|
127
|
+
export declare function findCutPoint(messages: AppMessage[], startIndex: number, endIndex: number, keepRecentTokens: number): number;
|
|
128
|
+
/**
|
|
129
|
+
* Find the optimal cut point with split turn information.
|
|
130
|
+
*
|
|
131
|
+
* Similar to findCutPoint but:
|
|
132
|
+
* - Can cut at assistant messages (not just user messages)
|
|
133
|
+
* - Returns information about whether we're splitting a turn
|
|
134
|
+
*
|
|
135
|
+
* This allows for more aggressive compaction when turns are very large,
|
|
136
|
+
* while providing the context needed to summarize the split turn prefix.
|
|
137
|
+
*
|
|
138
|
+
* @param messages - Array of messages to analyze
|
|
139
|
+
* @param startIndex - First index to consider for cutting
|
|
140
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
141
|
+
* @param keepRecentTokens - Approximate tokens to preserve
|
|
142
|
+
* @returns CutPointResult with index and split turn information
|
|
143
|
+
*/
|
|
144
|
+
export declare function findCutPointWithSplitInfo(messages: AppMessage[], startIndex: number, endIndex: number, keepRecentTokens: number): CutPointResult;
|
|
145
|
+
/**
|
|
146
|
+
* Adjust a cut boundary to preserve tool call/result integrity.
|
|
147
|
+
*
|
|
148
|
+
* Tool results must stay paired with their originating assistant message.
|
|
149
|
+
* If a toolResult would be kept but its toolCall would be cut, this
|
|
150
|
+
* moves the boundary back to include the toolCall.
|
|
151
|
+
*
|
|
152
|
+
* @param messages - Array of messages
|
|
153
|
+
* @param boundary - Initial cut point
|
|
154
|
+
* @returns Adjusted boundary that preserves tool integrity
|
|
155
|
+
*/
|
|
156
|
+
export declare function adjustBoundaryForToolResults(messages: AppMessage[], boundary: number): number;
|
|
157
|
+
//# sourceMappingURL=cut-points.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cut-points.d.ts","sourceRoot":"","sources":["../../../src/agent/compaction/cut-points.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAoB,KAAK,EAAE,MAAM,aAAa,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,yCAAyC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,2BAA2B,EAAE,kBAIzC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAsBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,GAAG,IAAI,CAM1E;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC/C,OAAO,EAAE,YAAY,EAAE,GACrB,KAAK,GAAG,IAAI,CAUd;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC5B,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,kBAAkB,GAC1B,OAAO,CAGT;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACpC,aAAa,EAAE,MAAM,EACrB,aAAa,EAAE,MAAM,GACnB,MAAM,CAGR;AAMD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,UAAU,EAAE,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACd,MAAM,EAAE,CAQV;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,UAAU,EAAE,EACtB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GAChB,MAAM,CAOR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAC3B,QAAQ,EAAE,UAAU,EAAE,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACtB,MAAM,CA8CR;AA4BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,UAAU,EAAE,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACtB,cAAc,CAkDhB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,UAAU,EAAE,EACtB,QAAQ,EAAE,MAAM,GACd,MAAM,CA4CR"}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default compaction settings based on research into Claude Code, Codex, and OpenCode.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_COMPACTION_SETTINGS = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
reserveTokens: 16384,
|
|
7
|
+
keepRecentTokens: 20000,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Calculate total context tokens from usage metadata.
|
|
11
|
+
*
|
|
12
|
+
* This includes all token types that contribute to context window consumption:
|
|
13
|
+
* - input: Tokens from user/system messages
|
|
14
|
+
* - output: Tokens generated by the model
|
|
15
|
+
* - cacheRead: Tokens read from prompt cache
|
|
16
|
+
* - cacheWrite: Tokens written to prompt cache
|
|
17
|
+
*
|
|
18
|
+
* @param usage - Usage metadata from an assistant message
|
|
19
|
+
* @returns Total context token count
|
|
20
|
+
*/
|
|
21
|
+
export function calculateContextTokens(usage) {
|
|
22
|
+
return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract usage metadata from a message if it's a non-aborted assistant message.
|
|
26
|
+
*
|
|
27
|
+
* @param msg - Any application message
|
|
28
|
+
* @returns Usage metadata or null if not available/applicable
|
|
29
|
+
*/
|
|
30
|
+
function getAssistantUsage(msg) {
|
|
31
|
+
if (msg.role === "assistant") {
|
|
32
|
+
const assistantMsg = msg;
|
|
33
|
+
if (assistantMsg.stopReason !== "aborted" &&
|
|
34
|
+
assistantMsg.stopReason !== "error" &&
|
|
35
|
+
assistantMsg.usage) {
|
|
36
|
+
return assistantMsg.usage;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find the last non-aborted assistant message usage from a message array.
|
|
43
|
+
*
|
|
44
|
+
* Used to determine current context window consumption. Walks backwards
|
|
45
|
+
* to find the most recent valid usage metadata.
|
|
46
|
+
*
|
|
47
|
+
* @param messages - Array of application messages
|
|
48
|
+
* @returns Usage metadata from the last assistant message, or null
|
|
49
|
+
*/
|
|
50
|
+
export function getLastAssistantUsage(messages) {
|
|
51
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
52
|
+
const usage = getAssistantUsage(messages[i]);
|
|
53
|
+
if (usage)
|
|
54
|
+
return usage;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find the last non-aborted assistant message usage from session entries.
|
|
60
|
+
*
|
|
61
|
+
* @param entries - Array of session entries
|
|
62
|
+
* @returns Usage metadata from the last assistant message, or null
|
|
63
|
+
*/
|
|
64
|
+
export function getLastAssistantUsageFromEntries(entries) {
|
|
65
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
66
|
+
const entry = entries[i];
|
|
67
|
+
if (!entry)
|
|
68
|
+
continue;
|
|
69
|
+
if (entry.type === "message") {
|
|
70
|
+
const usage = getAssistantUsage(entry.message);
|
|
71
|
+
if (usage)
|
|
72
|
+
return usage;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Compaction Trigger Detection
|
|
79
|
+
// ============================================================================
|
|
80
|
+
/**
|
|
81
|
+
* Determine if compaction should trigger based on current context usage.
|
|
82
|
+
*
|
|
83
|
+
* Compaction triggers when context usage exceeds `contextWindow - reserveTokens`,
|
|
84
|
+
* leaving enough room for summary generation plus a safety margin.
|
|
85
|
+
*
|
|
86
|
+
* @param contextTokens - Current context window usage in tokens
|
|
87
|
+
* @param contextWindow - Model's total context window size
|
|
88
|
+
* @param settings - Compaction configuration
|
|
89
|
+
* @returns true if compaction should trigger
|
|
90
|
+
*/
|
|
91
|
+
export function shouldCompact(contextTokens, contextWindow, settings) {
|
|
92
|
+
if (!settings.enabled)
|
|
93
|
+
return false;
|
|
94
|
+
return contextTokens > contextWindow - settings.reserveTokens;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Calculate context usage percentage.
|
|
98
|
+
*
|
|
99
|
+
* @param contextTokens - Current context window usage in tokens
|
|
100
|
+
* @param contextWindow - Model's total context window size
|
|
101
|
+
* @returns Percentage of context window used (0-100)
|
|
102
|
+
*/
|
|
103
|
+
export function calculateUsagePercent(contextTokens, contextWindow) {
|
|
104
|
+
if (contextWindow <= 0)
|
|
105
|
+
return 0;
|
|
106
|
+
return (contextTokens / contextWindow) * 100;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Find indices of user messages (turn boundaries) in a message array.
|
|
110
|
+
*
|
|
111
|
+
* Turn boundaries are important because we never want to split a turn
|
|
112
|
+
* (user message + assistant response + tool results). Cutting at a user
|
|
113
|
+
* message ensures turn integrity.
|
|
114
|
+
*
|
|
115
|
+
* @param messages - Array of messages to scan
|
|
116
|
+
* @param startIndex - First index to consider (inclusive)
|
|
117
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
118
|
+
* @returns Array of indices where user messages appear
|
|
119
|
+
*/
|
|
120
|
+
export function findTurnBoundaries(messages, startIndex, endIndex) {
|
|
121
|
+
const boundaries = [];
|
|
122
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
123
|
+
if (messages[i].role === "user") {
|
|
124
|
+
boundaries.push(i);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return boundaries;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Find the user message that starts the turn containing the given index.
|
|
131
|
+
*
|
|
132
|
+
* @param messages - Array of messages
|
|
133
|
+
* @param entryIndex - Index of the message to find the turn start for
|
|
134
|
+
* @param startIndex - First index to consider (inclusive)
|
|
135
|
+
* @returns Index of the user message that starts this turn, or -1 if not found
|
|
136
|
+
*/
|
|
137
|
+
export function findTurnStartIndex(messages, entryIndex, startIndex) {
|
|
138
|
+
for (let i = entryIndex; i >= startIndex; i--) {
|
|
139
|
+
if (messages[i].role === "user") {
|
|
140
|
+
return i;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Find the optimal cut point in messages that preserves `keepRecentTokens`.
|
|
147
|
+
*
|
|
148
|
+
* The algorithm:
|
|
149
|
+
* 1. Find all turn boundaries (user message indices)
|
|
150
|
+
* 2. Walk backwards collecting assistant token usage
|
|
151
|
+
* 3. Find where cumulative token difference exceeds keepRecentTokens
|
|
152
|
+
* 4. Return the nearest turn boundary at or before that point
|
|
153
|
+
*
|
|
154
|
+
* This ensures:
|
|
155
|
+
* - Recent context is preserved based on actual token usage, not message count
|
|
156
|
+
* - Turn integrity is maintained (user/assistant/toolResult groups stay together)
|
|
157
|
+
* - More context is kept when messages are small, less when large
|
|
158
|
+
*
|
|
159
|
+
* @param messages - Array of messages to analyze
|
|
160
|
+
* @param startIndex - First index to consider for cutting
|
|
161
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
162
|
+
* @param keepRecentTokens - Approximate tokens to preserve
|
|
163
|
+
* @returns Index of first message to keep (messages before this are summarized)
|
|
164
|
+
*/
|
|
165
|
+
export function findCutPoint(messages, startIndex, endIndex, keepRecentTokens) {
|
|
166
|
+
const boundaries = findTurnBoundaries(messages, startIndex, endIndex);
|
|
167
|
+
if (boundaries.length === 0) {
|
|
168
|
+
return startIndex; // No user messages, keep everything in range
|
|
169
|
+
}
|
|
170
|
+
// Collect assistant usages walking backwards from endIndex
|
|
171
|
+
const assistantUsages = [];
|
|
172
|
+
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
173
|
+
const usage = getAssistantUsage(messages[i]);
|
|
174
|
+
if (usage) {
|
|
175
|
+
assistantUsages.push({
|
|
176
|
+
index: i,
|
|
177
|
+
tokens: calculateContextTokens(usage),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (assistantUsages.length === 0) {
|
|
182
|
+
// No usage info, keep last turn only
|
|
183
|
+
return boundaries[boundaries.length - 1];
|
|
184
|
+
}
|
|
185
|
+
// Walk through and find where cumulative token difference exceeds keepRecentTokens
|
|
186
|
+
const newestTokens = assistantUsages[0].tokens;
|
|
187
|
+
let cutIndex = startIndex; // Default: keep everything in range
|
|
188
|
+
for (let i = 1; i < assistantUsages.length; i++) {
|
|
189
|
+
const tokenDiff = newestTokens - assistantUsages[i].tokens;
|
|
190
|
+
if (tokenDiff >= keepRecentTokens) {
|
|
191
|
+
// Find the turn boundary at or before the assistant we want to keep
|
|
192
|
+
const lastKeptAssistantIndex = assistantUsages[i - 1].index;
|
|
193
|
+
for (let b = boundaries.length - 1; b >= 0; b--) {
|
|
194
|
+
const boundary = boundaries[b];
|
|
195
|
+
if (boundary !== undefined && boundary <= lastKeptAssistantIndex) {
|
|
196
|
+
cutIndex = boundary;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return cutIndex;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Find valid cut points (user or assistant messages, not tool results).
|
|
207
|
+
* Tool results must stay with their preceding tool call.
|
|
208
|
+
*
|
|
209
|
+
* @param messages - Array of messages to scan
|
|
210
|
+
* @param startIndex - First index to consider (inclusive)
|
|
211
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
212
|
+
* @returns Array of indices where valid cut points exist
|
|
213
|
+
*/
|
|
214
|
+
function findValidCutPoints(messages, startIndex, endIndex) {
|
|
215
|
+
const cutPoints = [];
|
|
216
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
217
|
+
const role = messages[i].role;
|
|
218
|
+
// user and assistant are valid cut points
|
|
219
|
+
// toolResult must stay with its preceding tool call
|
|
220
|
+
if (role === "user" || role === "assistant") {
|
|
221
|
+
cutPoints.push(i);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return cutPoints;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Find the optimal cut point with split turn information.
|
|
228
|
+
*
|
|
229
|
+
* Similar to findCutPoint but:
|
|
230
|
+
* - Can cut at assistant messages (not just user messages)
|
|
231
|
+
* - Returns information about whether we're splitting a turn
|
|
232
|
+
*
|
|
233
|
+
* This allows for more aggressive compaction when turns are very large,
|
|
234
|
+
* while providing the context needed to summarize the split turn prefix.
|
|
235
|
+
*
|
|
236
|
+
* @param messages - Array of messages to analyze
|
|
237
|
+
* @param startIndex - First index to consider for cutting
|
|
238
|
+
* @param endIndex - Last index to consider (exclusive)
|
|
239
|
+
* @param keepRecentTokens - Approximate tokens to preserve
|
|
240
|
+
* @returns CutPointResult with index and split turn information
|
|
241
|
+
*/
|
|
242
|
+
export function findCutPointWithSplitInfo(messages, startIndex, endIndex, keepRecentTokens) {
|
|
243
|
+
const cutPoints = findValidCutPoints(messages, startIndex, endIndex);
|
|
244
|
+
if (cutPoints.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
firstKeptIndex: startIndex,
|
|
247
|
+
turnStartIndex: -1,
|
|
248
|
+
isSplitTurn: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Estimate message sizes for token-based cutting
|
|
252
|
+
let accumulatedTokens = 0;
|
|
253
|
+
let cutIndex = startIndex;
|
|
254
|
+
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
255
|
+
const usage = getAssistantUsage(messages[i]);
|
|
256
|
+
if (usage) {
|
|
257
|
+
accumulatedTokens = calculateContextTokens(usage);
|
|
258
|
+
// Using cumulative tokens from the last assistant message
|
|
259
|
+
if (accumulatedTokens >= keepRecentTokens) {
|
|
260
|
+
// Find the closest valid cut point at or after this index
|
|
261
|
+
for (const cp of cutPoints) {
|
|
262
|
+
if (cp >= i) {
|
|
263
|
+
cutIndex = cp;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// If no cut point found via tokens, use last assistant's token-based cut
|
|
272
|
+
if (cutIndex === startIndex) {
|
|
273
|
+
// Fall back to original algorithm
|
|
274
|
+
cutIndex = findCutPoint(messages, startIndex, endIndex, keepRecentTokens);
|
|
275
|
+
}
|
|
276
|
+
// Determine if this is a split turn
|
|
277
|
+
const isUserMessage = messages[cutIndex]?.role === "user";
|
|
278
|
+
const turnStartIndex = isUserMessage
|
|
279
|
+
? -1
|
|
280
|
+
: findTurnStartIndex(messages, cutIndex, startIndex);
|
|
281
|
+
return {
|
|
282
|
+
firstKeptIndex: cutIndex,
|
|
283
|
+
turnStartIndex,
|
|
284
|
+
isSplitTurn: !isUserMessage && turnStartIndex !== -1,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Adjust a cut boundary to preserve tool call/result integrity.
|
|
289
|
+
*
|
|
290
|
+
* Tool results must stay paired with their originating assistant message.
|
|
291
|
+
* If a toolResult would be kept but its toolCall would be cut, this
|
|
292
|
+
* moves the boundary back to include the toolCall.
|
|
293
|
+
*
|
|
294
|
+
* @param messages - Array of messages
|
|
295
|
+
* @param boundary - Initial cut point
|
|
296
|
+
* @returns Adjusted boundary that preserves tool integrity
|
|
297
|
+
*/
|
|
298
|
+
export function adjustBoundaryForToolResults(messages, boundary) {
|
|
299
|
+
let adjusted = boundary;
|
|
300
|
+
const seenToolCalls = new Set();
|
|
301
|
+
const missingToolCalls = new Set();
|
|
302
|
+
// Process assistant message to collect tool calls
|
|
303
|
+
const processAssistantMessage = (message) => {
|
|
304
|
+
if (message.role !== "assistant")
|
|
305
|
+
return;
|
|
306
|
+
const content = message.content;
|
|
307
|
+
if (!content)
|
|
308
|
+
return;
|
|
309
|
+
for (const part of content) {
|
|
310
|
+
if (part?.type === "toolCall") {
|
|
311
|
+
seenToolCalls.add(part.id);
|
|
312
|
+
if (missingToolCalls.has(part.id)) {
|
|
313
|
+
missingToolCalls.delete(part.id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
// Process tool result to check for missing tool calls
|
|
319
|
+
const processToolResultMessage = (message) => {
|
|
320
|
+
if (message.role !== "toolResult")
|
|
321
|
+
return;
|
|
322
|
+
const toolCallId = message.toolCallId;
|
|
323
|
+
if (!seenToolCalls.has(toolCallId)) {
|
|
324
|
+
missingToolCalls.add(toolCallId);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
// First pass: process messages we're keeping
|
|
328
|
+
for (const message of messages.slice(adjusted)) {
|
|
329
|
+
processAssistantMessage(message);
|
|
330
|
+
processToolResultMessage(message);
|
|
331
|
+
}
|
|
332
|
+
// Walk backwards to find missing tool calls
|
|
333
|
+
while (missingToolCalls.size > 0 && adjusted > 0) {
|
|
334
|
+
adjusted -= 1;
|
|
335
|
+
const candidate = messages[adjusted];
|
|
336
|
+
processAssistantMessage(candidate);
|
|
337
|
+
processToolResultMessage(candidate);
|
|
338
|
+
}
|
|
339
|
+
return adjusted;
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=cut-points.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cut-points.js","sourceRoot":"","sources":["../../../src/agent/compaction/cut-points.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SessionEntry } from \"../../session/types.js\";\nimport type { AppMessage, AssistantMessage, Usage } from \"../types.js\";\n\n/**\n * Configuration for compaction behavior.\n */\nexport interface CompactionSettings {\n\t/** Whether auto-compaction is enabled */\n\tenabled: boolean;\n\t/**\n\t * Tokens to reserve for summary generation and safety margin.\n\t * Default: 16384 (~13k for summary + ~3k safety margin)\n\t */\n\treserveTokens: number;\n\t/**\n\t * Approximate number of recent tokens to preserve verbatim.\n\t * Default: 20000 (recent context preserved without summarization)\n\t */\n\tkeepRecentTokens: number;\n}\n\n/**\n * Default compaction settings based on research into Claude Code, Codex, and OpenCode.\n */\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n/**\n * Calculate total context tokens from usage metadata.\n *\n * This includes all token types that contribute to context window consumption:\n * - input: Tokens from user/system messages\n * - output: Tokens generated by the model\n * - cacheRead: Tokens read from prompt cache\n * - cacheWrite: Tokens written to prompt cache\n *\n * @param usage - Usage metadata from an assistant message\n * @returns Total context token count\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Extract usage metadata from a message if it's a non-aborted assistant message.\n *\n * @param msg - Any application message\n * @returns Usage metadata or null if not available/applicable\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\") {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (\n\t\t\tassistantMsg.stopReason !== \"aborted\" &&\n\t\t\tassistantMsg.stopReason !== \"error\" &&\n\t\t\tassistantMsg.usage\n\t\t) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from a message array.\n *\n * Used to determine current context window consumption. Walks backwards\n * to find the most recent valid usage metadata.\n *\n * @param messages - Array of application messages\n * @returns Usage metadata from the last assistant message, or null\n */\nexport function getLastAssistantUsage(messages: AppMessage[]): Usage | null {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]!);\n\t\tif (usage) return usage;\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n *\n * @param entries - Array of session entries\n * @returns Usage metadata from the last assistant message, or null\n */\nexport function getLastAssistantUsageFromEntries(\n\tentries: SessionEntry[],\n): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (!entry) continue;\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message as AppMessage);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n// ============================================================================\n// Compaction Trigger Detection\n// ============================================================================\n\n/**\n * Determine if compaction should trigger based on current context usage.\n *\n * Compaction triggers when context usage exceeds `contextWindow - reserveTokens`,\n * leaving enough room for summary generation plus a safety margin.\n *\n * @param contextTokens - Current context window usage in tokens\n * @param contextWindow - Model's total context window size\n * @param settings - Compaction configuration\n * @returns true if compaction should trigger\n */\nexport function shouldCompact(\n\tcontextTokens: number,\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n/**\n * Calculate context usage percentage.\n *\n * @param contextTokens - Current context window usage in tokens\n * @param contextWindow - Model's total context window size\n * @returns Percentage of context window used (0-100)\n */\nexport function calculateUsagePercent(\n\tcontextTokens: number,\n\tcontextWindow: number,\n): number {\n\tif (contextWindow <= 0) return 0;\n\treturn (contextTokens / contextWindow) * 100;\n}\n\n// ============================================================================\n// Cut Point Detection\n// ============================================================================\n\n/**\n * Result of finding a cut point, including split turn information.\n */\nexport interface CutPointResult {\n\t/** Index of first message to keep (messages before this are summarized) */\n\tfirstKeptIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find indices of user messages (turn boundaries) in a message array.\n *\n * Turn boundaries are important because we never want to split a turn\n * (user message + assistant response + tool results). Cutting at a user\n * message ensures turn integrity.\n *\n * @param messages - Array of messages to scan\n * @param startIndex - First index to consider (inclusive)\n * @param endIndex - Last index to consider (exclusive)\n * @returns Array of indices where user messages appear\n */\nexport function findTurnBoundaries(\n\tmessages: AppMessage[],\n\tstartIndex: number,\n\tendIndex: number,\n): number[] {\n\tconst boundaries: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tif (messages[i]!.role === \"user\") {\n\t\t\tboundaries.push(i);\n\t\t}\n\t}\n\treturn boundaries;\n}\n\n/**\n * Find the user message that starts the turn containing the given index.\n *\n * @param messages - Array of messages\n * @param entryIndex - Index of the message to find the turn start for\n * @param startIndex - First index to consider (inclusive)\n * @returns Index of the user message that starts this turn, or -1 if not found\n */\nexport function findTurnStartIndex(\n\tmessages: AppMessage[],\n\tentryIndex: number,\n\tstartIndex: number,\n): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tif (messages[i]!.role === \"user\") {\n\t\t\treturn i;\n\t\t}\n\t}\n\treturn -1;\n}\n\n/**\n * Find the optimal cut point in messages that preserves `keepRecentTokens`.\n *\n * The algorithm:\n * 1. Find all turn boundaries (user message indices)\n * 2. Walk backwards collecting assistant token usage\n * 3. Find where cumulative token difference exceeds keepRecentTokens\n * 4. Return the nearest turn boundary at or before that point\n *\n * This ensures:\n * - Recent context is preserved based on actual token usage, not message count\n * - Turn integrity is maintained (user/assistant/toolResult groups stay together)\n * - More context is kept when messages are small, less when large\n *\n * @param messages - Array of messages to analyze\n * @param startIndex - First index to consider for cutting\n * @param endIndex - Last index to consider (exclusive)\n * @param keepRecentTokens - Approximate tokens to preserve\n * @returns Index of first message to keep (messages before this are summarized)\n */\nexport function findCutPoint(\n\tmessages: AppMessage[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): number {\n\tconst boundaries = findTurnBoundaries(messages, startIndex, endIndex);\n\n\tif (boundaries.length === 0) {\n\t\treturn startIndex; // No user messages, keep everything in range\n\t}\n\n\t// Collect assistant usages walking backwards from endIndex\n\tconst assistantUsages: Array<{ index: number; tokens: number }> = [];\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]!);\n\t\tif (usage) {\n\t\t\tassistantUsages.push({\n\t\t\t\tindex: i,\n\t\t\t\ttokens: calculateContextTokens(usage),\n\t\t\t});\n\t\t}\n\t}\n\n\tif (assistantUsages.length === 0) {\n\t\t// No usage info, keep last turn only\n\t\treturn boundaries[boundaries.length - 1]!;\n\t}\n\n\t// Walk through and find where cumulative token difference exceeds keepRecentTokens\n\tconst newestTokens = assistantUsages[0]!.tokens;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = 1; i < assistantUsages.length; i++) {\n\t\tconst tokenDiff = newestTokens - assistantUsages[i]!.tokens;\n\t\tif (tokenDiff >= keepRecentTokens) {\n\t\t\t// Find the turn boundary at or before the assistant we want to keep\n\t\t\tconst lastKeptAssistantIndex = assistantUsages[i - 1]!.index;\n\n\t\t\tfor (let b = boundaries.length - 1; b >= 0; b--) {\n\t\t\t\tconst boundary = boundaries[b];\n\t\t\t\tif (boundary !== undefined && boundary <= lastKeptAssistantIndex) {\n\t\t\t\t\tcutIndex = boundary;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn cutIndex;\n}\n\n/**\n * Find valid cut points (user or assistant messages, not tool results).\n * Tool results must stay with their preceding tool call.\n *\n * @param messages - Array of messages to scan\n * @param startIndex - First index to consider (inclusive)\n * @param endIndex - Last index to consider (exclusive)\n * @returns Array of indices where valid cut points exist\n */\nfunction findValidCutPoints(\n\tmessages: AppMessage[],\n\tstartIndex: number,\n\tendIndex: number,\n): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst role = messages[i]!.role;\n\t\t// user and assistant are valid cut points\n\t\t// toolResult must stay with its preceding tool call\n\t\tif (role === \"user\" || role === \"assistant\") {\n\t\t\tcutPoints.push(i);\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the optimal cut point with split turn information.\n *\n * Similar to findCutPoint but:\n * - Can cut at assistant messages (not just user messages)\n * - Returns information about whether we're splitting a turn\n *\n * This allows for more aggressive compaction when turns are very large,\n * while providing the context needed to summarize the split turn prefix.\n *\n * @param messages - Array of messages to analyze\n * @param startIndex - First index to consider for cutting\n * @param endIndex - Last index to consider (exclusive)\n * @param keepRecentTokens - Approximate tokens to preserve\n * @returns CutPointResult with index and split turn information\n */\nexport function findCutPointWithSplitInfo(\n\tmessages: AppMessage[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): CutPointResult {\n\tconst cutPoints = findValidCutPoints(messages, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn {\n\t\t\tfirstKeptIndex: startIndex,\n\t\t\tturnStartIndex: -1,\n\t\t\tisSplitTurn: false,\n\t\t};\n\t}\n\n\t// Estimate message sizes for token-based cutting\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = startIndex;\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst usage = getAssistantUsage(messages[i]!);\n\t\tif (usage) {\n\t\t\taccumulatedTokens = calculateContextTokens(usage);\n\t\t\t// Using cumulative tokens from the last assistant message\n\t\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t\t// Find the closest valid cut point at or after this index\n\t\t\t\tfor (const cp of cutPoints) {\n\t\t\t\t\tif (cp >= i) {\n\t\t\t\t\t\tcutIndex = cp;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// If no cut point found via tokens, use last assistant's token-based cut\n\tif (cutIndex === startIndex) {\n\t\t// Fall back to original algorithm\n\t\tcutIndex = findCutPoint(messages, startIndex, endIndex, keepRecentTokens);\n\t}\n\n\t// Determine if this is a split turn\n\tconst isUserMessage = messages[cutIndex]?.role === \"user\";\n\tconst turnStartIndex = isUserMessage\n\t\t? -1\n\t\t: findTurnStartIndex(messages, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\n}\n\n/**\n * Adjust a cut boundary to preserve tool call/result integrity.\n *\n * Tool results must stay paired with their originating assistant message.\n * If a toolResult would be kept but its toolCall would be cut, this\n * moves the boundary back to include the toolCall.\n *\n * @param messages - Array of messages\n * @param boundary - Initial cut point\n * @returns Adjusted boundary that preserves tool integrity\n */\nexport function adjustBoundaryForToolResults(\n\tmessages: AppMessage[],\n\tboundary: number,\n): number {\n\tlet adjusted = boundary;\n\tconst seenToolCalls = new Set<string>();\n\tconst missingToolCalls = new Set<string>();\n\n\t// Process assistant message to collect tool calls\n\tconst processAssistantMessage = (message: AppMessage) => {\n\t\tif (message.role !== \"assistant\") return;\n\t\tconst content = (message as AssistantMessage).content;\n\t\tif (!content) return;\n\t\tfor (const part of content) {\n\t\t\tif (part?.type === \"toolCall\") {\n\t\t\t\tseenToolCalls.add(part.id);\n\t\t\t\tif (missingToolCalls.has(part.id)) {\n\t\t\t\t\tmissingToolCalls.delete(part.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t// Process tool result to check for missing tool calls\n\tconst processToolResultMessage = (message: AppMessage) => {\n\t\tif (message.role !== \"toolResult\") return;\n\t\tconst toolCallId = (message as { toolCallId: string }).toolCallId;\n\t\tif (!seenToolCalls.has(toolCallId)) {\n\t\t\tmissingToolCalls.add(toolCallId);\n\t\t}\n\t};\n\n\t// First pass: process messages we're keeping\n\tfor (const message of messages.slice(adjusted)) {\n\t\tprocessAssistantMessage(message);\n\t\tprocessToolResultMessage(message);\n\t}\n\n\t// Walk backwards to find missing tool calls\n\twhile (missingToolCalls.size > 0 && adjusted > 0) {\n\t\tadjusted -= 1;\n\t\tconst candidate = messages[adjusted]!;\n\t\tprocessAssistantMessage(candidate);\n\t\tprocessToolResultMessage(candidate);\n\t}\n\n\treturn adjusted;\n}\n"]}
|