@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.
Files changed (123) hide show
  1. package/ARCHITECTURE.md +75 -9
  2. package/MIGRATION.md +293 -0
  3. package/README.md +46 -20
  4. package/package.json +104 -10
  5. package/src/agent/allowlists.js +3 -0
  6. package/src/agent/approval.js +3 -0
  7. package/src/agent/compaction.js +231 -654
  8. package/src/agent/index.js +1 -1
  9. package/src/agent/prompt/skill-index.js +6 -0
  10. package/src/agent/sandbox-seam.js +227 -0
  11. package/src/agent/tool-bloat.js +8 -2
  12. package/src/agent/tools/bash.js +16 -8
  13. package/src/agent/tools/edit.js +7 -3
  14. package/src/agent/tools/glob.js +11 -5
  15. package/src/agent/tools/grep.js +11 -5
  16. package/src/agent/tools/pi-bridge.js +272 -54
  17. package/src/agent/tools/read.js +28 -3
  18. package/src/agent/tools/shared/output-truncation.js +8 -3
  19. package/src/agent/tools/shared/path-resolver.js +24 -18
  20. package/src/agent/tools/shared/ripgrep.js +33 -6
  21. package/src/agent/tools/shared/runtime-context.js +60 -50
  22. package/src/agent/tools/shared/tool-context.js +157 -0
  23. package/src/agent/tools/web-fetch.js +65 -18
  24. package/src/agent/tools/web-search.js +11 -4
  25. package/src/agent/tools/write.js +7 -3
  26. package/src/agent/transcript.js +16 -1
  27. package/src/ai/cost.js +128 -3
  28. package/src/ai/failure.js +96 -4
  29. package/src/ai/index.js +1 -0
  30. package/src/ai/live-input-prompt.js +11 -1
  31. package/src/ai/providers/claude-cli.js +6 -2
  32. package/src/ai/providers/claude-sdk.js +55 -12
  33. package/src/ai/providers/codex-app.js +36 -23
  34. package/src/ai/providers/opencode-app.js +2 -2
  35. package/src/ai/providers/opencode-discovery.js +2 -2
  36. package/src/ai/providers/pi-errors.js +65 -0
  37. package/src/ai/providers/pi-events.js +5 -0
  38. package/src/ai/providers/pi-models.js +21 -4
  39. package/src/ai/providers/pi-native/compaction-driver.js +394 -0
  40. package/src/ai/providers/pi-native/result-builder.js +312 -0
  41. package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
  42. package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
  43. package/src/ai/providers/pi-native/structured-output.js +130 -0
  44. package/src/ai/providers/pi-native/turn-runner.js +278 -0
  45. package/src/ai/providers/pi-native.js +754 -0
  46. package/src/ai/runtime/capabilities.js +3 -0
  47. package/src/ai/runtime/model-refs.js +30 -0
  48. package/src/ai/runtime/registry.js +33 -2
  49. package/src/ai/runtime/router.js +119 -19
  50. package/src/ai/runtime/session-liveness.js +110 -0
  51. package/src/ai/runtime/sessions.js +19 -2
  52. package/src/ai/types.js +252 -20
  53. package/src/index.js +1 -1
  54. package/src/pi-auth.js +147 -16
  55. package/src/runtime-brand.js +21 -1
  56. package/src/runtime.js +75 -10
  57. package/types/agent/allowlists.d.ts +25 -0
  58. package/types/agent/approval.d.ts +30 -0
  59. package/types/agent/compaction.d.ts +97 -0
  60. package/types/agent/index.d.ts +5 -0
  61. package/types/agent/prompt/skill-index.d.ts +19 -0
  62. package/types/agent/sandbox-seam.d.ts +148 -0
  63. package/types/agent/tool-bloat.d.ts +22 -0
  64. package/types/agent/tools/bash.d.ts +16 -0
  65. package/types/agent/tools/edit.d.ts +14 -0
  66. package/types/agent/tools/glob.d.ts +16 -0
  67. package/types/agent/tools/grep.d.ts +22 -0
  68. package/types/agent/tools/index.d.ts +10 -0
  69. package/types/agent/tools/pi-bridge.d.ts +144 -0
  70. package/types/agent/tools/read.d.ts +19 -0
  71. package/types/agent/tools/shared/constants.d.ts +13 -0
  72. package/types/agent/tools/shared/dedup.d.ts +8 -0
  73. package/types/agent/tools/shared/output-truncation.d.ts +22 -0
  74. package/types/agent/tools/shared/path-resolver.d.ts +6 -0
  75. package/types/agent/tools/shared/ripgrep.d.ts +38 -0
  76. package/types/agent/tools/shared/runtime-context.d.ts +27 -0
  77. package/types/agent/tools/shared/tool-context.d.ts +48 -0
  78. package/types/agent/tools/web-fetch.d.ts +13 -0
  79. package/types/agent/tools/web-search.d.ts +11 -0
  80. package/types/agent/tools/write.d.ts +12 -0
  81. package/types/agent/transcript.d.ts +45 -0
  82. package/types/ai/backend.d.ts +41 -0
  83. package/types/ai/cost.d.ts +96 -0
  84. package/types/ai/failure.d.ts +117 -0
  85. package/types/ai/file-change-stats.d.ts +75 -0
  86. package/types/ai/index.d.ts +7 -0
  87. package/types/ai/live-input-prompt.d.ts +6 -0
  88. package/types/ai/observer.d.ts +68 -0
  89. package/types/ai/providers/claude-cli.d.ts +211 -0
  90. package/types/ai/providers/claude-sdk.d.ts +66 -0
  91. package/types/ai/providers/claude-subagents.d.ts +2 -0
  92. package/types/ai/providers/codex-app.d.ts +151 -0
  93. package/types/ai/providers/opencode-app.d.ts +95 -0
  94. package/types/ai/providers/opencode-discovery.d.ts +4 -0
  95. package/types/ai/providers/pi-errors.d.ts +3 -0
  96. package/types/ai/providers/pi-events.d.ts +24 -0
  97. package/types/ai/providers/pi-messages.d.ts +5 -0
  98. package/types/ai/providers/pi-models.d.ts +57 -0
  99. package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
  100. package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
  101. package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
  102. package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
  103. package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
  104. package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
  105. package/types/ai/providers/pi-native.d.ts +18 -0
  106. package/types/ai/registry.d.ts +1 -0
  107. package/types/ai/runtime/capabilities-used.d.ts +21 -0
  108. package/types/ai/runtime/capabilities.d.ts +33 -0
  109. package/types/ai/runtime/context-windows.d.ts +8 -0
  110. package/types/ai/runtime/fast-mode.d.ts +2 -0
  111. package/types/ai/runtime/model-refs.d.ts +35 -0
  112. package/types/ai/runtime/registry.d.ts +38 -0
  113. package/types/ai/runtime/router.d.ts +56 -0
  114. package/types/ai/runtime/session-liveness.d.ts +55 -0
  115. package/types/ai/runtime/sessions.d.ts +38 -0
  116. package/types/ai/streaming/codex-events.d.ts +30 -0
  117. package/types/ai/streaming/opencode-events.d.ts +41 -0
  118. package/types/ai/types.d.ts +702 -0
  119. package/types/index.d.ts +7 -0
  120. package/types/pi-auth.d.ts +28 -0
  121. package/types/runtime-brand.d.ts +30 -0
  122. package/types/runtime.d.ts +10 -0
  123. package/src/ai/providers/pi-sdk.js +0 -1310
@@ -1,6 +1,38 @@
1
- import { randomUUID } from "node:crypto";
2
-
3
- export const COMPACTED_CONTEXT_MARKER = "Compacted prior agent context";
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
- const DEFAULT_IMAGE_INLINE_MAX_BYTES = 250000;
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
- function jsonString(value) {
35
- try { return JSON.stringify(value); } catch { return String(value ?? ""); }
36
- }
37
-
38
- function base64Bytes(data) {
39
- const text = String(data || "");
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
- function assistantToolCalls(message) {
146
- if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return [];
147
- return message.content.filter((part) => part?.type === "toolCall");
148
- }
149
-
150
- function hasToolCalls(message) {
151
- return assistantToolCalls(message).length > 0;
152
- }
153
-
154
- function chooseFirstKeptIndex(messages, keepRecentTokens) {
155
- if (!Array.isArray(messages) || messages.length <= 2) return -1;
156
- let tokens = 0;
157
- let start = messages.length;
158
- for (let i = messages.length - 1; i >= 0; i -= 1) {
159
- tokens += estimateAgentMessageTokens(messages[i]).tokens;
160
- if (tokens >= keepRecentTokens) {
161
- start = i;
162
- break;
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
- function oneLine(value, limit = 220) {
181
- const text = String(value || "").replace(/\s+/g, " ").trim();
182
- return text.length <= limit ? text : `${text.slice(0, limit - 3).trimEnd()}...`;
183
- }
184
-
185
- function extractPathish(value) {
186
- if (!value || typeof value !== "object") return "";
187
- return value.file_path || value.path || value.command || value.pattern || value.url || "";
188
- }
189
-
190
- function summarizeCompactedMessages(messages, { maxChars }) {
191
- const previousSummaries = [];
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
- function firstNonEmptyLine(text) {
272
- for (const raw of String(text || "").split(/\r?\n/)) {
273
- const line = raw.trim();
274
- if (line) return line;
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
- function summarizeToolResultBody(message) {
280
- const parts = Array.isArray(message?.content) ? message.content : [];
281
- let textChars = 0;
282
- let firstSnippet = "";
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
- // Replace older tool_result bodies with a 1–3 sentence lossy summary
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
- function pruneOldToolResults(messages, policy) {
333
- if (!Array.isArray(messages) || !policy?.toolPruneTriggerTokens) {
334
- return { changed: false, messages, prunedCount: 0, tokensBefore: 0, tokensAfter: 0 };
335
- }
336
- const firstProtectedIndex = chooseFirstKeptIndex(messages, policy.keepRecentTokens);
337
- if (firstProtectedIndex <= 0) {
338
- return { changed: false, messages, prunedCount: 0, tokensBefore: 0, tokensAfter: 0 };
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
- changed: prunedCount > 0,
365
- messages: next,
366
- prunedCount,
367
- tokensBefore: prunableTokens,
368
- tokensAfter,
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
- function replaceMessagesInPlace(target, next) {
373
- if (!Array.isArray(target) || target === next) return next;
374
- target.splice(0, target.length, ...next);
375
- return target;
376
- }
377
-
378
- function truncateText(text, limit, label) {
379
- const value = String(text || "");
380
- if (value.length <= limit) {
381
- return { text: value, truncated: false, originalLength: value.length };
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
- export function compactToolResultForContext(result, policy, { toolName = "tool" } = {}) {
401
- if (!Array.isArray(result?.content) || !policy) return { changed: false, result };
402
- const limit = toolName === "Bash" ? policy.bashOutputLimitChars : policy.toolTextLimitChars;
403
- let changed = false;
404
- let originalTextChars = 0;
405
- let keptTextChars = 0;
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
- const serialized = jsonString(part);
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
- changed: true,
438
- result: {
439
- ...result,
440
- content,
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
- }