@mono-agent/agent-runtime 0.3.0 → 0.4.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/ARCHITECTURE.md +75 -9
- package/MIGRATION.md +293 -0
- package/README.md +46 -20
- package/package.json +104 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +231 -654
- package/src/agent/index.js +1 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +272 -54
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +65 -18
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/index.js +1 -0
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +6 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-errors.js +65 -0
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +754 -0
- package/src/ai/runtime/capabilities.js +3 -0
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +33 -2
- package/src/ai/runtime/router.js +119 -19
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +19 -2
- package/src/ai/types.js +252 -20
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +75 -10
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -1310
package/src/agent/compaction.js
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// Context-compaction policy + heuristics for the pi-native bridge.
|
|
4
|
+
//
|
|
5
|
+
// The hand-rolled in-loop compaction manager (transformContext/afterToolCall)
|
|
6
|
+
// was retired with the legacy pi-sdk Agent path — the sole pi bridge uses
|
|
7
|
+
// pi-agent-core's AgentHarness, which owns compaction via harness.compact().
|
|
8
|
+
// What remains here are two pure helpers the bridge still consumes:
|
|
9
|
+
// - resolveAgentCompactionPolicy: derives the context-window compaction
|
|
10
|
+
// trigger and the tool-output payload limits from settings + the running
|
|
11
|
+
// model. Pure (no Agent loop), so the bridge computes it directly.
|
|
12
|
+
// - isLikelyContextTermination: classifies a provider error/termination as a
|
|
13
|
+
// context-pressure event.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} AgentCompactionPolicy
|
|
17
|
+
* @property {boolean} enabled
|
|
18
|
+
* @property {number} contextWindow
|
|
19
|
+
* @property {number} triggerRatio
|
|
20
|
+
* @property {number} triggerTokens
|
|
21
|
+
* @property {number} keepRecentTokens
|
|
22
|
+
* @property {number} summaryMaxTokens
|
|
23
|
+
* @property {boolean} fixedOverheadEnabled
|
|
24
|
+
* @property {number} compactionMinSavingsTokens
|
|
25
|
+
* @property {number} toolPayloadCompactionTriggerChars
|
|
26
|
+
* @property {number} toolPruneTriggerTokens
|
|
27
|
+
* @property {number} toolTextLimitChars
|
|
28
|
+
* @property {number} bashOutputLimitChars
|
|
29
|
+
* @property {number} mcpTextLimitChars
|
|
30
|
+
* @property {number} searchResultLimit
|
|
31
|
+
* @property {number} imageInlineMaxBytes
|
|
32
|
+
* @property {number} toolPayloadMaxBytes
|
|
33
|
+
* @property {number} mcpCallTimeoutMs
|
|
34
|
+
* @property {number} mcpCallMaxTotalTimeoutMs
|
|
35
|
+
*/
|
|
4
36
|
|
|
5
37
|
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
6
38
|
const DEFAULT_TRIGGER_RATIO = 0.85;
|
|
@@ -17,95 +49,49 @@ const DEFAULT_TOOL_TEXT_LIMIT_CHARS = 64000;
|
|
|
17
49
|
const DEFAULT_BASH_OUTPUT_LIMIT_CHARS = 64000;
|
|
18
50
|
const DEFAULT_MCP_TEXT_LIMIT_CHARS = 48000;
|
|
19
51
|
const DEFAULT_SEARCH_RESULT_LIMIT = 100;
|
|
20
|
-
|
|
52
|
+
// Images are returned to vision models whole (a Read of an image attachment, an
|
|
53
|
+
// MCP screenshot). The byte size is large but token cost is driven by image
|
|
54
|
+
// tokens, not base64 length, so allow multi-MB screenshots through instead of
|
|
55
|
+
// clipping them to a "[truncated]" summary the model can't see. Clamp ceiling
|
|
56
|
+
// (10MB) is enforced in resolveAgentCompactionPolicy.
|
|
57
|
+
const DEFAULT_IMAGE_INLINE_MAX_BYTES = 5_000_000;
|
|
21
58
|
const DEFAULT_TOOL_PAYLOAD_MAX_BYTES = 262144;
|
|
22
59
|
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
|
|
23
|
-
|
|
60
|
+
// Hard wall clock for a single MCP tool call. Progress notifications reset the
|
|
61
|
+
// inactivity timeout above but must never extend a call past this cap (45 min) —
|
|
62
|
+
// sized for legitimately long tools (audio transcription, ask-the-user waits).
|
|
63
|
+
const DEFAULT_MCP_CALL_MAX_TOTAL_TIMEOUT_MS = 2_700_000;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {*} value
|
|
67
|
+
* @param {number} fallback
|
|
68
|
+
* @param {number} min
|
|
69
|
+
* @param {number} max
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
24
72
|
function clampNumber(value, fallback, min, max) {
|
|
25
73
|
const n = Number(value);
|
|
26
74
|
if (!Number.isFinite(n)) return fallback;
|
|
27
75
|
return Math.min(Math.max(n, min), max);
|
|
28
76
|
}
|
|
29
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @param {*} value
|
|
80
|
+
* @param {number} fallback
|
|
81
|
+
* @param {number} min
|
|
82
|
+
* @param {number} max
|
|
83
|
+
* @returns {number}
|
|
84
|
+
*/
|
|
30
85
|
function clampInteger(value, fallback, min, max) {
|
|
31
86
|
return Math.floor(clampNumber(value, fallback, min, max));
|
|
32
87
|
}
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!text) return 0;
|
|
41
|
-
const clean = text.includes(",") ? text.slice(text.indexOf(",") + 1) : text;
|
|
42
|
-
return Math.floor(clean.length * 0.75);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function textPart(value) {
|
|
46
|
-
if (typeof value === "string") return value;
|
|
47
|
-
if (!value || typeof value !== "object") return String(value ?? "");
|
|
48
|
-
if (typeof value.text === "string") return value.text;
|
|
49
|
-
if (typeof value.thinking === "string") return value.thinking;
|
|
50
|
-
if (value.type === "image") return `[image ${base64Bytes(value.data)} bytes]`;
|
|
51
|
-
if (value.type === "toolCall") return `${value.name || "tool"} ${jsonString(value.arguments || value.input || {})}`;
|
|
52
|
-
return jsonString(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function contentText(content) {
|
|
56
|
-
if (typeof content === "string") return content;
|
|
57
|
-
if (Array.isArray(content)) return content.map(textPart).join("\n");
|
|
58
|
-
return textPart(content);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function messageText(message) {
|
|
62
|
-
if (!message) return "";
|
|
63
|
-
const base = contentText(message.content);
|
|
64
|
-
if (message.role === "toolResult") {
|
|
65
|
-
return [
|
|
66
|
-
`Tool result: ${message.toolName || "unknown"}`,
|
|
67
|
-
message.isError ? "Status: error" : "",
|
|
68
|
-
base,
|
|
69
|
-
message.details ? jsonString(message.details) : "",
|
|
70
|
-
].filter(Boolean).join("\n");
|
|
71
|
-
}
|
|
72
|
-
return base;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function estimateAgentMessageTokens(message) {
|
|
76
|
-
const text = messageText(message);
|
|
77
|
-
let chars = text.length + 12;
|
|
78
|
-
let imageBytes = 0;
|
|
79
|
-
const parts = Array.isArray(message?.content) ? message.content : [];
|
|
80
|
-
for (const part of parts) {
|
|
81
|
-
if (part?.type === "image") imageBytes += base64Bytes(part.data);
|
|
82
|
-
}
|
|
83
|
-
const tokens = Math.ceil(chars / 4) + Math.ceil(imageBytes / 3);
|
|
84
|
-
return { tokens, chars, imageBytes };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function estimateAgentMessages(messages = []) {
|
|
88
|
-
return messages.reduce((acc, message) => {
|
|
89
|
-
const next = estimateAgentMessageTokens(message);
|
|
90
|
-
acc.tokens += next.tokens;
|
|
91
|
-
acc.chars += next.chars;
|
|
92
|
-
acc.imageBytes += next.imageBytes;
|
|
93
|
-
return acc;
|
|
94
|
-
}, { tokens: 0, chars: 0, imageBytes: 0 });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function estimateFirstTurnInput({ systemPrompt = "", messages = [] } = {}) {
|
|
98
|
-
const overheadChars = String(systemPrompt || "").length;
|
|
99
|
-
const overheadTokens = Math.ceil(overheadChars / 4);
|
|
100
|
-
const messageEstimate = estimateAgentMessages(messages);
|
|
101
|
-
return {
|
|
102
|
-
overheadTokens,
|
|
103
|
-
overheadChars,
|
|
104
|
-
inputTokens: overheadTokens + messageEstimate.tokens,
|
|
105
|
-
inputChars: overheadChars + messageEstimate.chars,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
89
|
+
/**
|
|
90
|
+
* @param {Object<string, *>} [settings]
|
|
91
|
+
* @param {Object} [model]
|
|
92
|
+
* @param {number} [model.contextWindow]
|
|
93
|
+
* @returns {AgentCompactionPolicy}
|
|
94
|
+
*/
|
|
109
95
|
export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
110
96
|
const contextWindow = clampInteger(model?.contextWindow, DEFAULT_CONTEXT_WINDOW, 32000, 10_000_000);
|
|
111
97
|
const triggerRatio = clampNumber(
|
|
@@ -124,6 +110,11 @@ export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
|
124
110
|
triggerTokens: Math.min(ratioTrigger, reserveTrigger),
|
|
125
111
|
keepRecentTokens: clampInteger(settings.agent_compaction_keep_recent_tokens, DEFAULT_KEEP_RECENT_TOKENS, 4000, 200000),
|
|
126
112
|
summaryMaxTokens: clampInteger(settings.agent_compaction_summary_max_tokens, DEFAULT_SUMMARY_MAX_TOKENS, 1000, 64000),
|
|
113
|
+
// ON by default; the proactive fixed-overhead correction (system prompt +
|
|
114
|
+
// tool schemas + per-turn message) is disabled only when explicitly false.
|
|
115
|
+
// Read by the compaction driver off the resolved policy so it never has to
|
|
116
|
+
// re-sniff the raw settings/policy inputs.
|
|
117
|
+
fixedOverheadEnabled: settings.agent_compaction_fixed_overhead_enabled !== false,
|
|
127
118
|
compactionMinSavingsTokens: clampInteger(settings.agent_compaction_min_savings_tokens, DEFAULT_MIN_SAVINGS_TOKENS, 0, 500000),
|
|
128
119
|
toolPayloadCompactionTriggerChars: clampInteger(
|
|
129
120
|
settings.agent_tool_payload_compaction_trigger_chars,
|
|
@@ -139,318 +130,194 @@ export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
|
|
|
139
130
|
imageInlineMaxBytes: clampInteger(settings.agent_image_inline_max_bytes, DEFAULT_IMAGE_INLINE_MAX_BYTES, 0, 10 * 1024 * 1024),
|
|
140
131
|
toolPayloadMaxBytes: clampInteger(settings.agent_tool_payload_max_bytes, DEFAULT_TOOL_PAYLOAD_MAX_BYTES, 0, 16 * 1024 * 1024),
|
|
141
132
|
mcpCallTimeoutMs: clampInteger(settings.agent_mcp_call_timeout_ms, DEFAULT_MCP_CALL_TIMEOUT_MS, 1000, Number.MAX_SAFE_INTEGER),
|
|
133
|
+
mcpCallMaxTotalTimeoutMs: clampInteger(
|
|
134
|
+
settings.agent_mcp_call_max_total_timeout_ms,
|
|
135
|
+
DEFAULT_MCP_CALL_MAX_TOTAL_TIMEOUT_MS,
|
|
136
|
+
1000,
|
|
137
|
+
Number.MAX_SAFE_INTEGER,
|
|
138
|
+
),
|
|
142
139
|
};
|
|
143
140
|
}
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
142
|
+
// --- Typed policy objects <-> deprecated `settings` shim -------------------
|
|
143
|
+
//
|
|
144
|
+
// RuntimeRunOptions now carries typed `toolLimits` / `compaction` policy
|
|
145
|
+
// objects. The DEPRECATED `settings` bag remains a per-group fallback: it is
|
|
146
|
+
// consumed only when the corresponding typed object is ABSENT, and consuming it
|
|
147
|
+
// surfaces one `deprecated_settings_option` runtime_warning per run.
|
|
148
|
+
//
|
|
149
|
+
// resolveAgentCompactionPolicy stays the canonical settings->policy clamp/mapper
|
|
150
|
+
// (its signature is unchanged — worklab deep-imports it). The helpers below
|
|
151
|
+
// project the typed objects back onto the same snake_case settings keys so the
|
|
152
|
+
// resolution goes through that one clamp path unchanged, whether the values came
|
|
153
|
+
// from a typed object or the legacy settings bag.
|
|
154
|
+
|
|
155
|
+
// Typed toolLimits field -> settings key. `bashTimeoutMs` is intentionally
|
|
156
|
+
// ABSENT: no `agent_bash_*_timeout` setting exists today and this phase does not
|
|
157
|
+
// invent new timeout behavior, so the field is documented on the RuntimeToolLimits
|
|
158
|
+
// typedef but not wired through the settings shim or any tool.
|
|
159
|
+
const TOOL_LIMIT_SETTINGS_KEYS = /** @type {const} */ ({
|
|
160
|
+
toolTextLimitChars: "agent_tool_text_limit_chars",
|
|
161
|
+
bashOutputLimitChars: "agent_bash_output_limit_chars",
|
|
162
|
+
mcpTextLimitChars: "agent_mcp_text_limit_chars",
|
|
163
|
+
searchResultLimit: "agent_search_result_limit",
|
|
164
|
+
imageInlineMaxBytes: "agent_image_inline_max_bytes",
|
|
165
|
+
toolPayloadMaxBytes: "agent_tool_payload_max_bytes",
|
|
166
|
+
mcpCallTimeoutMs: "agent_mcp_call_timeout_ms",
|
|
167
|
+
mcpCallMaxTotalTimeoutMs: "agent_mcp_call_max_total_timeout_ms",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Typed compaction field -> settings key. `contextWindowOverride` is ABSENT: it
|
|
171
|
+
// has no legacy settings equivalent and is applied directly at the live-window
|
|
172
|
+
// resolution site (resolveLiveCompactionPolicy), not through this shim.
|
|
173
|
+
const COMPACTION_SETTINGS_KEYS = /** @type {const} */ ({
|
|
174
|
+
enabled: "agent_compaction_enabled",
|
|
175
|
+
triggerRatio: "agent_compaction_trigger_ratio",
|
|
176
|
+
keepRecentTokens: "agent_compaction_keep_recent_tokens",
|
|
177
|
+
summaryMaxTokens: "agent_compaction_summary_max_tokens",
|
|
178
|
+
minSavingsTokens: "agent_compaction_min_savings_tokens",
|
|
179
|
+
fixedOverheadEnabled: "agent_compaction_fixed_overhead_enabled",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
export const DEPRECATED_SETTINGS_WARNING_KIND = "deprecated_settings_option";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {Object<string, *>|null|undefined} group
|
|
186
|
+
* @param {Record<string, string>} keyMap
|
|
187
|
+
* @param {Object<string, *>} out
|
|
188
|
+
*/
|
|
189
|
+
function copyTypedGroup(group, keyMap, out) {
|
|
190
|
+
for (const [field, settingKey] of Object.entries(keyMap)) {
|
|
191
|
+
if (group && group[field] !== undefined) out[settingKey] = group[field];
|
|
164
192
|
}
|
|
165
|
-
if (start <= 0 || start >= messages.length) return -1;
|
|
166
|
-
|
|
167
|
-
while (start > 0 && messages[start]?.role === "toolResult") start -= 1;
|
|
168
|
-
if (start > 0 && messages[start]?.role === "assistant" && !hasToolCalls(messages[start])) {
|
|
169
|
-
for (let i = start - 1; i >= 0; i -= 1) {
|
|
170
|
-
if (messages[i]?.role === "user") {
|
|
171
|
-
start = i;
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
if (messages[i]?.role === "toolResult") break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return start <= 0 ? -1 : start;
|
|
178
193
|
}
|
|
179
194
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const userNotes = [];
|
|
193
|
-
const assistantNotes = [];
|
|
194
|
-
const toolActions = [];
|
|
195
|
-
const toolErrors = [];
|
|
196
|
-
const changedFiles = new Set();
|
|
197
|
-
|
|
198
|
-
for (const message of messages) {
|
|
199
|
-
const text = messageText(message);
|
|
200
|
-
if (text.includes(COMPACTED_CONTEXT_MARKER)) {
|
|
201
|
-
previousSummaries.push(oneLine(text, 1000));
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (message.role === "user") {
|
|
205
|
-
userNotes.push(oneLine(text));
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
if (message.role === "assistant") {
|
|
209
|
-
const toolCalls = assistantToolCalls(message);
|
|
210
|
-
if (toolCalls.length) {
|
|
211
|
-
for (const call of toolCalls) {
|
|
212
|
-
const args = call.arguments || call.input || {};
|
|
213
|
-
const pathish = extractPathish(args);
|
|
214
|
-
toolActions.push(`${call.name || "tool"}${pathish ? `: ${oneLine(pathish, 180)}` : ""}`);
|
|
215
|
-
if (["Write", "Edit"].includes(call.name) && args.file_path) changedFiles.add(args.file_path);
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
assistantNotes.push(oneLine(text));
|
|
219
|
-
}
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (message.role === "toolResult") {
|
|
223
|
-
const details = message.details || {};
|
|
224
|
-
const params = details.params || details.input || {};
|
|
225
|
-
const pathish = extractPathish(params);
|
|
226
|
-
toolActions.push(`${message.toolName || details.tool || "tool"} result${pathish ? `: ${oneLine(pathish, 180)}` : ""}`);
|
|
227
|
-
const changes = details.changes || params.changes || [];
|
|
228
|
-
for (const change of Array.isArray(changes) ? changes : []) {
|
|
229
|
-
if (change?.path) changedFiles.add(change.path);
|
|
230
|
-
}
|
|
231
|
-
if (message.isError) toolErrors.push(oneLine(text, 300));
|
|
195
|
+
/**
|
|
196
|
+
* @param {Object<string, *>|null|undefined} settings
|
|
197
|
+
* @param {Record<string, string>} keyMap
|
|
198
|
+
* @param {Object<string, *>} out
|
|
199
|
+
* @param {Array<string>} consumed
|
|
200
|
+
*/
|
|
201
|
+
function copySettingsGroup(settings, keyMap, out, consumed) {
|
|
202
|
+
if (!settings || typeof settings !== "object") return;
|
|
203
|
+
for (const settingKey of Object.values(keyMap)) {
|
|
204
|
+
if (settings[settingKey] !== undefined) {
|
|
205
|
+
out[settingKey] = settings[settingKey];
|
|
206
|
+
consumed.push(settingKey);
|
|
232
207
|
}
|
|
233
208
|
}
|
|
234
|
-
|
|
235
|
-
const sections = [
|
|
236
|
-
previousSummaries.length ? ["Historical context from previous compactions:", ...previousSummaries.slice(-6).map((item) => `- ${item}`)].join("\n") : "",
|
|
237
|
-
userNotes.length ? ["Recent user/task instructions from compacted prefix:", ...userNotes.slice(-8).map((item) => `- ${item}`)].join("\n") : "",
|
|
238
|
-
assistantNotes.length ? ["Agent progress from compacted prefix:", ...assistantNotes.slice(-8).map((item) => `- ${item}`)].join("\n") : "",
|
|
239
|
-
toolActions.length ? ["Tool activity from compacted prefix:", ...toolActions.slice(-20).map((item) => `- ${item}`)].join("\n") : "",
|
|
240
|
-
changedFiles.size ? ["Files likely touched before compaction:", ...[...changedFiles].slice(0, 20).map((item) => `- ${item}`)].join("\n") : "",
|
|
241
|
-
toolErrors.length ? ["Tool errors observed before compaction:", ...toolErrors.slice(-8).map((item) => `- ${item}`)].join("\n") : "",
|
|
242
|
-
].filter(Boolean);
|
|
243
|
-
|
|
244
|
-
const summary = sections.join("\n\n") || "Prior context contained no compactable text.";
|
|
245
|
-
return summary.length <= maxChars
|
|
246
|
-
? summary
|
|
247
|
-
: `${summary.slice(0, Math.max(0, maxChars - 40)).trimEnd()}\n[compaction summary truncated]`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function buildCompactionMessage({ id, seq, metrics, firstKeptIndex, compactedCount, summary }) {
|
|
251
|
-
return {
|
|
252
|
-
role: "user",
|
|
253
|
-
timestamp: Date.now(),
|
|
254
|
-
content: [
|
|
255
|
-
`# ${COMPACTED_CONTEXT_MARKER}`,
|
|
256
|
-
"",
|
|
257
|
-
`Compaction id: ${id}`,
|
|
258
|
-
`Compaction sequence: ${seq}`,
|
|
259
|
-
`Compacted messages: ${compactedCount}`,
|
|
260
|
-
`First kept message index: ${firstKeptIndex}`,
|
|
261
|
-
`Estimated tokens before compaction: ${metrics.before.tokens}`,
|
|
262
|
-
`Estimated tokens after compaction: ${metrics.after.tokens}`,
|
|
263
|
-
"",
|
|
264
|
-
"The raw earlier transcript was compacted to keep this long autonomous session within the model context window. Continue from the current workspace state and the recent uncompressed messages below.",
|
|
265
|
-
"",
|
|
266
|
-
summary,
|
|
267
|
-
].join("\n"),
|
|
268
|
-
};
|
|
269
209
|
}
|
|
270
210
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Fold the typed `toolLimits` / `compaction` policy objects and the deprecated
|
|
213
|
+
* `settings` bag into ONE settings-like object resolveAgentCompactionPolicy
|
|
214
|
+
* consumes, honoring PER-GROUP precedence: when a typed object is present its
|
|
215
|
+
* fields win and the legacy settings keys for that group are ignored entirely;
|
|
216
|
+
* when the typed object is absent, that group's settings keys are consumed (and
|
|
217
|
+
* reported in `consumedSettingsKeys` so the caller can emit exactly one
|
|
218
|
+
* deprecation warning per run).
|
|
219
|
+
* @param {{toolLimits?: Object<string, *>, compaction?: Object<string, *>, settings?: Object<string, *>}} [options]
|
|
220
|
+
* @returns {{settingsLike: Object<string, *>, consumedSettingsKeys: Array<string>}}
|
|
221
|
+
*/
|
|
222
|
+
export function resolveRuntimePolicyInputs({ toolLimits, compaction, settings } = {}) {
|
|
223
|
+
/** @type {Object<string, *>} */
|
|
224
|
+
const settingsLike = {};
|
|
225
|
+
/** @type {Array<string>} */
|
|
226
|
+
const consumedSettingsKeys = [];
|
|
227
|
+
|
|
228
|
+
if (toolLimits && typeof toolLimits === "object") {
|
|
229
|
+
copyTypedGroup(toolLimits, TOOL_LIMIT_SETTINGS_KEYS, settingsLike);
|
|
230
|
+
} else {
|
|
231
|
+
copySettingsGroup(settings, TOOL_LIMIT_SETTINGS_KEYS, settingsLike, consumedSettingsKeys);
|
|
275
232
|
}
|
|
276
|
-
return "";
|
|
277
|
-
}
|
|
278
233
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
let imageCount = 0;
|
|
284
|
-
let imageBytes = 0;
|
|
285
|
-
for (const part of parts) {
|
|
286
|
-
if (part?.type === "text") {
|
|
287
|
-
const t = String(part.text || "");
|
|
288
|
-
textChars += t.length;
|
|
289
|
-
if (!firstSnippet) firstSnippet = firstNonEmptyLine(t);
|
|
290
|
-
} else if (part?.type === "image") {
|
|
291
|
-
imageCount += 1;
|
|
292
|
-
imageBytes += base64Bytes(part.data);
|
|
293
|
-
}
|
|
234
|
+
if (compaction && typeof compaction === "object") {
|
|
235
|
+
copyTypedGroup(compaction, COMPACTION_SETTINGS_KEYS, settingsLike);
|
|
236
|
+
} else {
|
|
237
|
+
copySettingsGroup(settings, COMPACTION_SETTINGS_KEYS, settingsLike, consumedSettingsKeys);
|
|
294
238
|
}
|
|
295
|
-
const facts = [];
|
|
296
|
-
if (textChars > 0) facts.push(`${textChars} text chars`);
|
|
297
|
-
if (imageCount > 0) facts.push(`${imageCount} image${imageCount === 1 ? "" : "s"} (~${imageBytes} bytes)`);
|
|
298
|
-
if (message?.isError) facts.push("status: error");
|
|
299
|
-
if (firstSnippet) {
|
|
300
|
-
const snip = firstSnippet.length > 160 ? `${firstSnippet.slice(0, 160).trimEnd()}…` : firstSnippet;
|
|
301
|
-
facts.push(`first line: "${snip}"`);
|
|
302
|
-
}
|
|
303
|
-
return facts.join("; ");
|
|
304
|
-
}
|
|
305
239
|
|
|
306
|
-
|
|
307
|
-
// instead of dropping them. Keeps the agent able to reason about prior
|
|
308
|
-
// work (filename, byte count, error status, first-line excerpt) without
|
|
309
|
-
// paying the original payload cost. The full payload remains on disk
|
|
310
|
-
// under <runArtifactDir>/tool-output/ for explicit re-fetch via the
|
|
311
|
-
// artifact path that tool-bloat.js records in details.
|
|
312
|
-
function pruneToolResultContent(message, metrics) {
|
|
313
|
-
const details = {
|
|
314
|
-
...(message.details || {}),
|
|
315
|
-
context_pruned: true,
|
|
316
|
-
pruned_tokens_estimate: metrics.tokens,
|
|
317
|
-
pruned_chars_estimate: metrics.chars,
|
|
318
|
-
};
|
|
319
|
-
const toolLabel = message.toolName || details.tool || "tool";
|
|
320
|
-
const artifact = details.artifact_path || details.full_output_path || details.path || null;
|
|
321
|
-
const summary = summarizeToolResultBody(message);
|
|
322
|
-
const lines = [
|
|
323
|
-
`[older ${toolLabel} result, summarized to free ~${metrics.tokens} tokens of context]`,
|
|
324
|
-
];
|
|
325
|
-
if (summary) lines.push(summary);
|
|
326
|
-
if (artifact) lines.push(`Full output preserved at: ${artifact}`);
|
|
327
|
-
else lines.push("Re-issue a targeted tool call (narrower query / smaller range) if the raw payload is needed again.");
|
|
328
|
-
const content = [{ type: "text", text: lines.join("\n") }];
|
|
329
|
-
return { ...message, content, details };
|
|
240
|
+
return { settingsLike, consumedSettingsKeys };
|
|
330
241
|
}
|
|
331
242
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
let prunableTokens = 0;
|
|
342
|
-
const metricsByIndex = new Map();
|
|
343
|
-
for (let i = 0; i < firstProtectedIndex; i += 1) {
|
|
344
|
-
if (messages[i]?.role !== "toolResult") continue;
|
|
345
|
-
if (messages[i]?.details?.context_pruned) continue;
|
|
346
|
-
const metrics = estimateAgentMessageTokens(messages[i]);
|
|
347
|
-
prunableTokens += metrics.tokens;
|
|
348
|
-
metricsByIndex.set(i, metrics);
|
|
349
|
-
}
|
|
350
|
-
if (prunableTokens < policy.toolPruneTriggerTokens) {
|
|
351
|
-
return { changed: false, messages, prunedCount: 0, tokensBefore: prunableTokens, tokensAfter: prunableTokens };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
let prunedCount = 0;
|
|
355
|
-
const next = messages.map((message, index) => {
|
|
356
|
-
const metrics = metricsByIndex.get(index);
|
|
357
|
-
if (!metrics) return message;
|
|
358
|
-
prunedCount += 1;
|
|
359
|
-
return pruneToolResultContent(message, metrics);
|
|
360
|
-
});
|
|
361
|
-
const tokensAfter = [...metricsByIndex.keys()]
|
|
362
|
-
.reduce((sum, index) => sum + estimateAgentMessageTokens(next[index]).tokens, 0);
|
|
243
|
+
/**
|
|
244
|
+
* Build the one-per-run deprecation warning fired when the legacy `settings`
|
|
245
|
+
* bag was consumed as a policy fallback. Shape matches the other pi/claude
|
|
246
|
+
* bridge runtime warnings ({warning_kind, source, message}).
|
|
247
|
+
* @param {ReadonlyArray<string>} consumedKeys
|
|
248
|
+
* @returns {{warning_kind: string, source: string, message: string, settings_keys: Array<string>}}
|
|
249
|
+
*/
|
|
250
|
+
export function deprecatedSettingsWarning(consumedKeys) {
|
|
251
|
+
const keys = Array.from(consumedKeys || []);
|
|
363
252
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
253
|
+
warning_kind: DEPRECATED_SETTINGS_WARNING_KIND,
|
|
254
|
+
source: "runtime",
|
|
255
|
+
message:
|
|
256
|
+
"runOptions.settings is deprecated; pass the typed `toolLimits` / `compaction` policy objects instead "
|
|
257
|
+
+ "(host migration helper: resolveRuntimePolicies in @mono-agent/runtime-adapter). Consumed settings keys: "
|
|
258
|
+
+ `${keys.join(", ")}.`,
|
|
259
|
+
settings_keys: keys,
|
|
369
260
|
};
|
|
370
261
|
}
|
|
371
262
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
263
|
+
// Estimate the FIXED per-request overhead the provider meters but the raw
|
|
264
|
+
// transcript estimate excludes: the system prompt, the tool/MCP schemas, and the
|
|
265
|
+
// per-turn user message(s). estimateCurrentContextTokens' raw branch sums ONLY
|
|
266
|
+
// session.buildContext().messages (the transcript), so on a seeded session whose
|
|
267
|
+
// last-assistant usage is stale/0 the proactive-compaction trigger under-counts
|
|
268
|
+
// and under-fires, letting the real request overflow the window. Adding this
|
|
269
|
+
// overhead to the raw estimate makes the trigger reflect what the provider counts.
|
|
270
|
+
//
|
|
271
|
+
// Uses Math.ceil(len/4) to mirror pi-ai's chars/4 heuristic — consistency with
|
|
272
|
+
// the transcript estimate matters more than precision. Pure + dependency-free.
|
|
273
|
+
/**
|
|
274
|
+
* @param {Object} [options]
|
|
275
|
+
* @param {string} [options.systemPrompt]
|
|
276
|
+
* @param {Array<Object>} [options.tools]
|
|
277
|
+
* @param {Array<Object>} [options.messages]
|
|
278
|
+
* @returns {{systemPromptTokens: number, toolSchemaTokens: number, userMessageTokens: number, fixedOverheadTokens: number}}
|
|
279
|
+
*/
|
|
280
|
+
export function estimateFixedOverheadTokens({ systemPrompt, tools, messages } = {}) {
|
|
281
|
+
const tokensForChars = (value) => Math.ceil(String(value ?? "").length / 4);
|
|
282
|
+
|
|
283
|
+
const systemPromptTokens = tokensForChars(systemPrompt);
|
|
284
|
+
|
|
285
|
+
let toolSchemaTokens = 0;
|
|
286
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
287
|
+
try {
|
|
288
|
+
const serialized = JSON.stringify({
|
|
289
|
+
name: tool?.name,
|
|
290
|
+
description: tool?.description,
|
|
291
|
+
parameters: tool?.parameters ?? tool?.inputSchema ?? {},
|
|
292
|
+
});
|
|
293
|
+
toolSchemaTokens += tokensForChars(serialized);
|
|
294
|
+
} catch {
|
|
295
|
+
// Circular/unserializable tool schema — count it as 0 rather than throw.
|
|
296
|
+
}
|
|
382
297
|
}
|
|
383
|
-
// First-class truncation marker: tells the model exactly what was cut and
|
|
384
|
-
// what it should do about it. The audit found agents failing tasks because
|
|
385
|
-
// they didn't notice silent ellipses — be loud and actionable.
|
|
386
|
-
const totalKb = Math.round(value.length / 1024);
|
|
387
|
-
const shownKb = Math.round(limit / 1024);
|
|
388
|
-
const marker = [
|
|
389
|
-
"",
|
|
390
|
-
`[!!! ${label || "tool"} OUTPUT TRUNCATED — ${shownKb}KB of ${totalKb}KB shown above. The tool returned ${value.length - limit} more characters that you cannot see.]`,
|
|
391
|
-
"If those characters matter for the task, narrow the query (smaller range, more specific pattern) or paginate. Don't assume the missing tail is empty.",
|
|
392
|
-
].join("\n");
|
|
393
|
-
return {
|
|
394
|
-
text: `${value.slice(0, Math.max(0, limit - marker.length))}${marker}`,
|
|
395
|
-
truncated: true,
|
|
396
|
-
originalLength: value.length,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
298
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
let omittedImages = 0;
|
|
407
|
-
const content = result.content.map((part) => {
|
|
408
|
-
if (part?.type === "text") {
|
|
409
|
-
const truncated = truncateText(part.text || "", limit, toolName);
|
|
410
|
-
originalTextChars += truncated.originalLength;
|
|
411
|
-
keptTextChars += truncated.text.length;
|
|
412
|
-
if (truncated.truncated) changed = true;
|
|
413
|
-
return { ...part, text: truncated.text };
|
|
414
|
-
}
|
|
415
|
-
if (part?.type === "image") {
|
|
416
|
-
const bytes = base64Bytes(part.data);
|
|
417
|
-
if (policy.imageInlineMaxBytes >= 0 && bytes > policy.imageInlineMaxBytes) {
|
|
418
|
-
changed = true;
|
|
419
|
-
omittedImages += 1;
|
|
420
|
-
return {
|
|
421
|
-
type: "text",
|
|
422
|
-
text: `[omitted inline image from ${toolName}: ${bytes} bytes exceeds ${policy.imageInlineMaxBytes} byte context budget]`,
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
return part;
|
|
299
|
+
let userMessageTokens = 0;
|
|
300
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
301
|
+
try {
|
|
302
|
+
userMessageTokens += tokensForChars(JSON.stringify(message?.content ?? ""));
|
|
303
|
+
} catch {
|
|
304
|
+
// Unserializable content — count it as 0 rather than throw.
|
|
426
305
|
}
|
|
427
|
-
|
|
428
|
-
const truncated = truncateText(serialized, limit, toolName);
|
|
429
|
-
originalTextChars += truncated.originalLength;
|
|
430
|
-
keptTextChars += truncated.text.length;
|
|
431
|
-
if (truncated.truncated || truncated.text !== serialized) changed = true;
|
|
432
|
-
return { type: "text", text: truncated.text };
|
|
433
|
-
});
|
|
306
|
+
}
|
|
434
307
|
|
|
435
|
-
if (!changed) return { changed: false, result };
|
|
436
308
|
return {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
details: {
|
|
442
|
-
...(result.details || {}),
|
|
443
|
-
context_compacted: true,
|
|
444
|
-
original_text_chars: originalTextChars || null,
|
|
445
|
-
kept_text_chars: keptTextChars || null,
|
|
446
|
-
omitted_images: omittedImages || null,
|
|
447
|
-
text_limit_chars: limit,
|
|
448
|
-
image_inline_max_bytes: policy.imageInlineMaxBytes,
|
|
449
|
-
},
|
|
450
|
-
},
|
|
309
|
+
systemPromptTokens,
|
|
310
|
+
toolSchemaTokens,
|
|
311
|
+
userMessageTokens,
|
|
312
|
+
fixedOverheadTokens: systemPromptTokens + toolSchemaTokens + userMessageTokens,
|
|
451
313
|
};
|
|
452
314
|
}
|
|
453
315
|
|
|
316
|
+
/**
|
|
317
|
+
* @param {string} message
|
|
318
|
+
* @param {Object<string, *>} [diagnostics]
|
|
319
|
+
* @returns {boolean}
|
|
320
|
+
*/
|
|
454
321
|
export function isLikelyContextTermination(message, diagnostics = {}) {
|
|
455
322
|
const text = String(message || "");
|
|
456
323
|
if (!/terminated|aborted before final output|aborted before final|stream.*aborted|context window|context budget/i.test(text)) return false;
|
|
@@ -460,293 +327,3 @@ export function isLikelyContextTermination(message, diagnostics = {}) {
|
|
|
460
327
|
const trigger = Number(diagnostics.context_compaction_trigger_tokens || 0);
|
|
461
328
|
return Boolean(trigger > 0 && estimate >= trigger * 0.85);
|
|
462
329
|
}
|
|
463
|
-
|
|
464
|
-
export function createAgentCompactionManager({
|
|
465
|
-
runId,
|
|
466
|
-
providerKind,
|
|
467
|
-
modelReference,
|
|
468
|
-
model,
|
|
469
|
-
settings = {},
|
|
470
|
-
onEvent,
|
|
471
|
-
onCompactionRecorded,
|
|
472
|
-
} = {}) {
|
|
473
|
-
const policy = resolveAgentCompactionPolicy(settings, model);
|
|
474
|
-
let compactionCount = 0;
|
|
475
|
-
let toolResultsCompacted = 0;
|
|
476
|
-
let toolResultsPruned = 0;
|
|
477
|
-
let toolPayloadCharsSinceCompaction = 0;
|
|
478
|
-
let maxToolPayloadCharsSinceCompaction = 0;
|
|
479
|
-
let forcedCompactionReason = null;
|
|
480
|
-
let maxContextTokensEstimate = 0;
|
|
481
|
-
let lastCompactionId = null;
|
|
482
|
-
let lastError = null;
|
|
483
|
-
let skippedLowSavings = 0;
|
|
484
|
-
let lastLowSavingsSkipTokens = 0;
|
|
485
|
-
|
|
486
|
-
function emit(event) {
|
|
487
|
-
onEvent?.(event);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// The host owns persistence: it receives the structured
|
|
491
|
-
// record below and writes it into `run_compactions`. The kernel emits a
|
|
492
|
-
// runtime_warning if the host's callback throws.
|
|
493
|
-
function record(row) {
|
|
494
|
-
if (!onCompactionRecorded || !runId) return;
|
|
495
|
-
const persisted = {
|
|
496
|
-
id: row.id,
|
|
497
|
-
task_run_id: runId,
|
|
498
|
-
seq: row.seq,
|
|
499
|
-
trigger: row.trigger,
|
|
500
|
-
provider_kind: providerKind || null,
|
|
501
|
-
model: modelReference || model?.id || null,
|
|
502
|
-
tokens_before: row.tokensBefore || null,
|
|
503
|
-
tokens_after: row.tokensAfter || null,
|
|
504
|
-
chars_before: row.charsBefore || null,
|
|
505
|
-
chars_after: row.charsAfter || null,
|
|
506
|
-
first_kept_index: row.firstKeptIndex ?? null,
|
|
507
|
-
summary: row.summary || "",
|
|
508
|
-
metadata_json: JSON.stringify(row.metadata || {}),
|
|
509
|
-
status: row.status || "succeeded",
|
|
510
|
-
error_text: row.errorText || null,
|
|
511
|
-
created_at: Date.now(),
|
|
512
|
-
};
|
|
513
|
-
try {
|
|
514
|
-
onCompactionRecorded(persisted);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
lastError = err?.message || String(err);
|
|
517
|
-
emit({
|
|
518
|
-
type: "runtime_warning",
|
|
519
|
-
warning_kind: "context_compaction_record_failed",
|
|
520
|
-
message: lastError,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async function transformContext(messages = [], signal) {
|
|
526
|
-
const original = estimateAgentMessages(messages);
|
|
527
|
-
maxContextTokensEstimate = Math.max(maxContextTokensEstimate, original.tokens);
|
|
528
|
-
if (!policy.enabled || signal?.aborted) return messages;
|
|
529
|
-
|
|
530
|
-
let workingMessages = messages;
|
|
531
|
-
const pruned = pruneOldToolResults(workingMessages, policy);
|
|
532
|
-
if (pruned.changed) {
|
|
533
|
-
workingMessages = replaceMessagesInPlace(messages, pruned.messages);
|
|
534
|
-
toolResultsPruned += pruned.prunedCount;
|
|
535
|
-
toolPayloadCharsSinceCompaction = 0;
|
|
536
|
-
const afterPrune = estimateAgentMessages(workingMessages);
|
|
537
|
-
emit({
|
|
538
|
-
type: "tool_context_pruned",
|
|
539
|
-
run_id: runId || null,
|
|
540
|
-
pruned_tool_results: pruned.prunedCount,
|
|
541
|
-
tokens_before: original.tokens,
|
|
542
|
-
tokens_after: afterPrune.tokens,
|
|
543
|
-
tokens_saved: original.tokens - afterPrune.tokens,
|
|
544
|
-
pruned_tool_tokens_before: pruned.tokensBefore,
|
|
545
|
-
pruned_tool_tokens_after: pruned.tokensAfter,
|
|
546
|
-
pruned_tool_tokens_saved: pruned.tokensBefore - pruned.tokensAfter,
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const before = estimateAgentMessages(workingMessages);
|
|
551
|
-
const trigger = forcedCompactionReason || (before.tokens >= policy.triggerTokens ? "token_budget" : null);
|
|
552
|
-
const lowSavingsBackoff = policy.compactionMinSavingsTokens > 0
|
|
553
|
-
&& before.tokens <= lastLowSavingsSkipTokens + Math.max(1000, Math.floor(policy.compactionMinSavingsTokens / 2));
|
|
554
|
-
if (!trigger || lowSavingsBackoff) return workingMessages;
|
|
555
|
-
|
|
556
|
-
const firstKeptIndex = chooseFirstKeptIndex(workingMessages, policy.keepRecentTokens);
|
|
557
|
-
if (firstKeptIndex <= 0) return workingMessages;
|
|
558
|
-
|
|
559
|
-
const id = `cmp_${randomUUID()}`;
|
|
560
|
-
const seq = compactionCount + 1;
|
|
561
|
-
emit({
|
|
562
|
-
type: "context_compaction_started",
|
|
563
|
-
id,
|
|
564
|
-
run_id: runId || null,
|
|
565
|
-
seq,
|
|
566
|
-
trigger,
|
|
567
|
-
tokens_before: before.tokens,
|
|
568
|
-
trigger_tokens: policy.triggerTokens,
|
|
569
|
-
keep_recent_tokens: policy.keepRecentTokens,
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
const compacted = workingMessages.slice(0, firstKeptIndex);
|
|
574
|
-
const recent = workingMessages.slice(firstKeptIndex);
|
|
575
|
-
const summaryMaxChars = Math.max(4000, policy.summaryMaxTokens * 4);
|
|
576
|
-
const summary = summarizeCompactedMessages(compacted, { maxChars: summaryMaxChars });
|
|
577
|
-
const provisional = [
|
|
578
|
-
{
|
|
579
|
-
role: "user",
|
|
580
|
-
content: summary,
|
|
581
|
-
timestamp: Date.now(),
|
|
582
|
-
},
|
|
583
|
-
...recent,
|
|
584
|
-
];
|
|
585
|
-
const after = estimateAgentMessages(provisional);
|
|
586
|
-
const summaryMessage = buildCompactionMessage({
|
|
587
|
-
id,
|
|
588
|
-
seq,
|
|
589
|
-
metrics: { before, after },
|
|
590
|
-
firstKeptIndex,
|
|
591
|
-
compactedCount: compacted.length,
|
|
592
|
-
summary,
|
|
593
|
-
});
|
|
594
|
-
const nextMessages = [summaryMessage, ...recent];
|
|
595
|
-
const finalAfter = estimateAgentMessages(nextMessages);
|
|
596
|
-
const savingsTokens = before.tokens - finalAfter.tokens;
|
|
597
|
-
const emergencyTriggerTokens = Math.floor(policy.contextWindow * 0.95);
|
|
598
|
-
if (
|
|
599
|
-
policy.compactionMinSavingsTokens > 0
|
|
600
|
-
&& savingsTokens < policy.compactionMinSavingsTokens
|
|
601
|
-
&& before.tokens < emergencyTriggerTokens
|
|
602
|
-
) {
|
|
603
|
-
skippedLowSavings += 1;
|
|
604
|
-
lastLowSavingsSkipTokens = before.tokens;
|
|
605
|
-
forcedCompactionReason = null;
|
|
606
|
-
emit({
|
|
607
|
-
type: "runtime_warning",
|
|
608
|
-
warning_kind: "context_compaction_skipped_low_savings",
|
|
609
|
-
message: `Skipped context compaction because estimated savings were ${savingsTokens} tokens below the ${policy.compactionMinSavingsTokens} token minimum.`,
|
|
610
|
-
diagnostics: {
|
|
611
|
-
trigger,
|
|
612
|
-
tokens_before: before.tokens,
|
|
613
|
-
tokens_after: finalAfter.tokens,
|
|
614
|
-
savings_tokens: savingsTokens,
|
|
615
|
-
min_savings_tokens: policy.compactionMinSavingsTokens,
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
return workingMessages;
|
|
619
|
-
}
|
|
620
|
-
compactionCount += 1;
|
|
621
|
-
lastCompactionId = id;
|
|
622
|
-
record({
|
|
623
|
-
id,
|
|
624
|
-
seq,
|
|
625
|
-
trigger,
|
|
626
|
-
tokensBefore: before.tokens,
|
|
627
|
-
tokensAfter: finalAfter.tokens,
|
|
628
|
-
charsBefore: before.chars,
|
|
629
|
-
charsAfter: finalAfter.chars,
|
|
630
|
-
firstKeptIndex,
|
|
631
|
-
summary,
|
|
632
|
-
metadata: {
|
|
633
|
-
context_window: policy.contextWindow,
|
|
634
|
-
trigger_tokens: policy.triggerTokens,
|
|
635
|
-
keep_recent_tokens: policy.keepRecentTokens,
|
|
636
|
-
min_savings_tokens: policy.compactionMinSavingsTokens,
|
|
637
|
-
savings_tokens: savingsTokens,
|
|
638
|
-
tool_payload_chars_since_compaction: toolPayloadCharsSinceCompaction,
|
|
639
|
-
compacted_messages: compacted.length,
|
|
640
|
-
kept_messages: recent.length,
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
toolPayloadCharsSinceCompaction = 0;
|
|
644
|
-
forcedCompactionReason = null;
|
|
645
|
-
emit({
|
|
646
|
-
type: "context_compaction_completed",
|
|
647
|
-
id,
|
|
648
|
-
run_id: runId || null,
|
|
649
|
-
seq,
|
|
650
|
-
tokens_before: before.tokens,
|
|
651
|
-
tokens_after: finalAfter.tokens,
|
|
652
|
-
tokens_saved: savingsTokens,
|
|
653
|
-
chars_before: before.chars,
|
|
654
|
-
chars_after: finalAfter.chars,
|
|
655
|
-
compacted_messages: compacted.length,
|
|
656
|
-
kept_messages: recent.length,
|
|
657
|
-
first_kept_index: firstKeptIndex,
|
|
658
|
-
});
|
|
659
|
-
return replaceMessagesInPlace(messages, nextMessages);
|
|
660
|
-
} catch (err) {
|
|
661
|
-
lastError = err?.message || String(err);
|
|
662
|
-
record({
|
|
663
|
-
id,
|
|
664
|
-
seq,
|
|
665
|
-
trigger,
|
|
666
|
-
tokensBefore: before.tokens,
|
|
667
|
-
charsBefore: before.chars,
|
|
668
|
-
firstKeptIndex,
|
|
669
|
-
summary: "",
|
|
670
|
-
status: "failed",
|
|
671
|
-
errorText: lastError,
|
|
672
|
-
});
|
|
673
|
-
emit({
|
|
674
|
-
type: "runtime_warning",
|
|
675
|
-
warning_kind: "context_compaction_failed",
|
|
676
|
-
message: lastError,
|
|
677
|
-
});
|
|
678
|
-
return workingMessages;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async function afterToolCall({ toolCall, result }, signal) {
|
|
683
|
-
if (signal?.aborted) return undefined;
|
|
684
|
-
const compacted = compactToolResultForContext(result, policy, { toolName: toolCall?.name || "tool" });
|
|
685
|
-
const visibleResult = compacted.changed ? compacted.result : result;
|
|
686
|
-
const payloadChars = estimateAgentMessageTokens({
|
|
687
|
-
role: "toolResult",
|
|
688
|
-
toolName: toolCall?.name || "tool",
|
|
689
|
-
content: visibleResult?.content || [],
|
|
690
|
-
details: visibleResult?.details || null,
|
|
691
|
-
}).chars;
|
|
692
|
-
toolPayloadCharsSinceCompaction += payloadChars;
|
|
693
|
-
maxToolPayloadCharsSinceCompaction = Math.max(maxToolPayloadCharsSinceCompaction, toolPayloadCharsSinceCompaction);
|
|
694
|
-
if (
|
|
695
|
-
policy.enabled
|
|
696
|
-
&& policy.toolPayloadCompactionTriggerChars > 0
|
|
697
|
-
&& toolPayloadCharsSinceCompaction >= policy.toolPayloadCompactionTriggerChars
|
|
698
|
-
) {
|
|
699
|
-
forcedCompactionReason = forcedCompactionReason || "tool_payload_budget";
|
|
700
|
-
}
|
|
701
|
-
if (!compacted.changed) return undefined;
|
|
702
|
-
toolResultsCompacted += 1;
|
|
703
|
-
emit({
|
|
704
|
-
type: "tool_result_compacted",
|
|
705
|
-
tool_use_id: toolCall?.id || null,
|
|
706
|
-
name: toolCall?.name || null,
|
|
707
|
-
text_limit_chars: compacted.result.details?.text_limit_chars || null,
|
|
708
|
-
original_text_chars: compacted.result.details?.original_text_chars || null,
|
|
709
|
-
kept_text_chars: compacted.result.details?.kept_text_chars || null,
|
|
710
|
-
omitted_images: compacted.result.details?.omitted_images || null,
|
|
711
|
-
});
|
|
712
|
-
return {
|
|
713
|
-
content: compacted.result.content,
|
|
714
|
-
details: compacted.result.details,
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function diagnostics() {
|
|
719
|
-
return {
|
|
720
|
-
context_compaction_enabled: policy.enabled,
|
|
721
|
-
context_compactions: compactionCount,
|
|
722
|
-
context_compaction_last_id: lastCompactionId,
|
|
723
|
-
context_compaction_last_error: lastError,
|
|
724
|
-
context_compactions_skipped_low_savings: skippedLowSavings,
|
|
725
|
-
context_window_tokens: policy.contextWindow,
|
|
726
|
-
context_compaction_trigger_tokens: policy.triggerTokens,
|
|
727
|
-
context_keep_recent_tokens: policy.keepRecentTokens,
|
|
728
|
-
context_compaction_min_savings_tokens: policy.compactionMinSavingsTokens,
|
|
729
|
-
context_tokens_estimate_max: maxContextTokensEstimate,
|
|
730
|
-
tool_results_compacted: toolResultsCompacted,
|
|
731
|
-
tool_results_pruned: toolResultsPruned,
|
|
732
|
-
tool_payload_chars_since_compaction: toolPayloadCharsSinceCompaction,
|
|
733
|
-
tool_payload_chars_since_compaction_max: maxToolPayloadCharsSinceCompaction,
|
|
734
|
-
tool_payload_compaction_trigger_chars: policy.toolPayloadCompactionTriggerChars,
|
|
735
|
-
tool_prune_trigger_tokens: policy.toolPruneTriggerTokens,
|
|
736
|
-
context_compaction_pending_reason: forcedCompactionReason,
|
|
737
|
-
tool_text_limit_chars: policy.toolTextLimitChars,
|
|
738
|
-
bash_output_limit_chars: policy.bashOutputLimitChars,
|
|
739
|
-
mcp_text_limit_chars: policy.mcpTextLimitChars,
|
|
740
|
-
search_result_limit: policy.searchResultLimit,
|
|
741
|
-
image_inline_max_bytes: policy.imageInlineMaxBytes,
|
|
742
|
-
mcp_call_timeout_ms: policy.mcpCallTimeoutMs,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
return {
|
|
747
|
-
policy,
|
|
748
|
-
transformContext,
|
|
749
|
-
afterToolCall,
|
|
750
|
-
diagnostics,
|
|
751
|
-
};
|
|
752
|
-
}
|