skimpyclaw 0.3.14 → 0.4.0

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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
@@ -4,33 +4,205 @@
4
4
  //
5
5
  // Falls back to mechanical truncation if the LLM call fails.
6
6
  //
7
- // Key constraint: tool_use/tool_result pairs (Anthropic) and
8
- // function_call/function_call_output pairs (Codex) must stay structurally intact.
7
+ // Uses a generic compactMessages() driven by MessageFormatHelper adapters,
8
+ // so the compaction algorithm is written once regardless of provider format.
9
9
  const DEFAULT_MAX_CONTEXT_TOKENS = 200_000;
10
10
  const KEEP_TAIL = 8; // always keep last N messages/items untouched
11
- const RESULT_MAX_CHARS = 500; // fallback truncation length
12
- const SUMMARY_MAX_TOKENS = 2048; // max tokens for summary response
11
+ const RESULT_MAX_CHARS = 200; // fallback truncation length
12
+ const SUMMARY_MAX_TOKENS = 1024; // max tokens for summary response
13
13
  // Preferred compaction models in priority order (cheap & fast).
14
14
  // Can be overridden via contextManagement.compactionModel in config.
15
15
  const COMPACTION_MODEL_CANDIDATES = [
16
- 'anthropic/claude-haiku-3-5',
17
- 'openai/gpt-4o-mini',
18
- 'groq/llama-3.1-8b-instant',
16
+ 'anthropic/claude-haiku-4-5',
19
17
  ];
20
18
  /** Rough token estimate: 1 token ≈ 4 chars of JSON. */
21
19
  export function estimateTokens(data) {
22
20
  return Math.ceil(JSON.stringify(data).length / 4);
23
21
  }
24
22
  // --- LLM Summarization ---
25
- const COMPACTION_SYSTEM_PROMPT = `You are a conversation summarizer for an AI coding assistant. Your job is to produce a concise summary of a conversation between a user and an assistant that used tools (file reads, bash commands, file writes, etc.).
26
-
27
- Rules:
28
- - Preserve ALL important context: file paths, variable names, error messages, decisions made, code changes
29
- - Summarize tool results (e.g. "Read package.json — found dependencies X, Y, Z") rather than reproducing full output
30
- - Keep the summary structured with bullet points or short paragraphs
31
- - Note any unresolved issues or ongoing tasks
32
- - Be concise but don't lose critical information that the assistant needs to continue working
33
- - Output ONLY the summary, no preamble`;
23
+ const COMPACTION_SYSTEM_PROMPT = `Summarize this AI coding assistant conversation concisely.
24
+ Preserve: file paths, variable names, error messages, decisions, code changes.
25
+ Summarize tool results briefly. Note unresolved issues. Use bullet points.
26
+ Output ONLY the summary.`;
27
+ /**
28
+ * Pick the best available compaction model from candidates.
29
+ * Checks which providers are initialized and returns the first match.
30
+ */
31
+ async function pickCompactionModel(config) {
32
+ const { isAnthropicAvailable } = await import('./anthropic.js');
33
+ for (const candidate of COMPACTION_MODEL_CANDIDATES) {
34
+ const provider = candidate.split('/')[0];
35
+ if (provider === 'anthropic' && isAnthropicAvailable())
36
+ return candidate;
37
+ }
38
+ // Last resort: return the first candidate and let chat() fail → fallback to truncation
39
+ return COMPACTION_MODEL_CANDIDATES[0];
40
+ }
41
+ /**
42
+ * Call the LLM to summarize a conversation transcript.
43
+ * Returns the summary text, or null if the call fails.
44
+ */
45
+ async function llmSummarize(transcript, config, compactionModel) {
46
+ try {
47
+ // Dynamically import to avoid circular dependency
48
+ const { chat } = await import('./index.js');
49
+ const model = compactionModel || await pickCompactionModel(config);
50
+ const messages = [
51
+ { role: 'system', content: COMPACTION_SYSTEM_PROMPT },
52
+ {
53
+ role: 'user',
54
+ content: `Summarize the following conversation between an AI coding assistant and a user. This summary will replace the old messages in the context window so the assistant can continue working.\n\n---\n${transcript}\n---`,
55
+ },
56
+ ];
57
+ console.log(`[context-manager] Requesting LLM summary via ${model}`);
58
+ const summary = await chat(messages, {
59
+ model,
60
+ maxTokens: SUMMARY_MAX_TOKENS,
61
+ }, config);
62
+ if (!summary || summary.trim().length === 0) {
63
+ console.warn('[context-manager] LLM returned empty summary, falling back to truncation');
64
+ return null;
65
+ }
66
+ console.log(`[context-manager] LLM summary: ${summary.length} chars`);
67
+ return summary.trim();
68
+ }
69
+ catch (err) {
70
+ console.warn(`[context-manager] LLM summarization failed, falling back to truncation: ${err instanceof Error ? err.message : err}`);
71
+ return null;
72
+ }
73
+ }
74
+ // --- Track whether we already compacted for a given conversation ---
75
+ const compactedMarker = new WeakSet();
76
+ // =====================================================================
77
+ // Generic compaction — single algorithm, format-agnostic via helper
78
+ // =====================================================================
79
+ /**
80
+ * Generic compaction function for any message format.
81
+ * Delegates format-specific concerns (truncation, serialization, summary building)
82
+ * to the provided MessageFormatHelper.
83
+ *
84
+ * Does NOT mutate the input array — returns a new array.
85
+ */
86
+ export async function compactMessages(items, helper, config, iteration = 0, fullConfig) {
87
+ if (config?.enabled === false)
88
+ return { messages: items, compacted: false };
89
+ const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
90
+ const estimated = estimateTokens(items);
91
+ if (estimated <= maxTokens)
92
+ return { messages: items, compacted: false };
93
+ const tail = items.slice(-KEEP_TAIL);
94
+ const head = items.slice(0, -KEEP_TAIL);
95
+ // If we already compacted this array, use truncation fallback
96
+ // to progressively shrink rather than re-summarizing repeatedly.
97
+ if (compactedMarker.has(items)) {
98
+ console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
99
+ const truncatedHead = head.map(item => helper.isToolResult(item) ? helper.truncateToolResult(item, RESULT_MAX_CHARS) : item);
100
+ const result = [...truncatedHead, ...tail];
101
+ return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
102
+ }
103
+ console.log(`[context-manager] Compacting at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
104
+ // Attempt LLM summarization
105
+ if (fullConfig) {
106
+ const transcript = helper.serialize(head);
107
+ const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
108
+ if (summary) {
109
+ const summaryItem = helper.buildSummaryMessage(summary);
110
+ const result = [summaryItem, ...tail];
111
+ compactedMarker.add(result);
112
+ const tokensAfter = estimateTokens(result);
113
+ return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
114
+ }
115
+ }
116
+ // Fallback: mechanical truncation
117
+ const truncatedHead = head.map(item => helper.isToolResult(item) ? helper.truncateToolResult(item, RESULT_MAX_CHARS) : item);
118
+ const result = [...truncatedHead, ...tail];
119
+ return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
120
+ }
121
+ // =====================================================================
122
+ // Provider-specific MessageFormatHelper implementations
123
+ // =====================================================================
124
+ /** Anthropic message format helper. */
125
+ export const anthropicFormatHelper = {
126
+ isToolResult(item) {
127
+ if (!Array.isArray(item.content))
128
+ return false;
129
+ return item.content.some((block) => block.type === 'tool_result');
130
+ },
131
+ truncateToolResult(item, maxChars) {
132
+ if (!Array.isArray(item.content))
133
+ return item;
134
+ let changed = false;
135
+ const newContent = item.content.map((block) => {
136
+ if (block.type !== 'tool_result')
137
+ return block;
138
+ const raw = typeof block.content === 'string'
139
+ ? block.content
140
+ : JSON.stringify(block.content);
141
+ if (raw.length <= maxChars)
142
+ return block;
143
+ changed = true;
144
+ return { ...block, content: raw.slice(0, maxChars) + ' [truncated]' };
145
+ });
146
+ return changed ? { ...item, content: newContent } : item;
147
+ },
148
+ serialize(items) {
149
+ return serializeAnthropicMessages(items);
150
+ },
151
+ buildSummaryMessage(summary) {
152
+ return {
153
+ role: 'user',
154
+ content: [{ type: 'text', text: `[Conversation Summary]\n${summary}` }],
155
+ };
156
+ },
157
+ };
158
+ /** OpenAI message format helper. */
159
+ export const openaiFormatHelper = {
160
+ isToolResult(item) {
161
+ return item.role === 'tool';
162
+ },
163
+ truncateToolResult(item, maxChars) {
164
+ if (typeof item.content !== 'string')
165
+ return item;
166
+ if (item.content.length <= maxChars)
167
+ return item;
168
+ return { ...item, content: item.content.slice(0, maxChars) + ' [truncated]' };
169
+ },
170
+ serialize(items) {
171
+ return serializeOpenAIMessages(items);
172
+ },
173
+ buildSummaryMessage(summary) {
174
+ return {
175
+ role: 'user',
176
+ content: `[Conversation Summary]\n${summary}`,
177
+ };
178
+ },
179
+ };
180
+ /** Codex message format helper. */
181
+ export const codexFormatHelper = {
182
+ isToolResult(item) {
183
+ return item.type === 'function_call_output';
184
+ },
185
+ truncateToolResult(item, maxChars) {
186
+ if (typeof item.output !== 'string')
187
+ return item;
188
+ if (item.output.length <= maxChars)
189
+ return item;
190
+ return { ...item, output: item.output.slice(0, maxChars) + ' [truncated]' };
191
+ },
192
+ serialize(items) {
193
+ return serializeCodexMessages(items);
194
+ },
195
+ buildSummaryMessage(summary) {
196
+ return {
197
+ type: 'message',
198
+ role: 'user',
199
+ content: `[Conversation Summary]\n${summary}`,
200
+ };
201
+ },
202
+ };
203
+ // =====================================================================
204
+ // Serialization helpers (used by format helpers and exported for tests)
205
+ // =====================================================================
34
206
  /**
35
207
  * Serialize Anthropic-format messages into a human-readable conversation transcript
36
208
  * suitable for LLM summarization.
@@ -125,227 +297,31 @@ function serializeCodexMessages(items) {
125
297
  }
126
298
  return lines.join('\n');
127
299
  }
128
- /**
129
- * Pick the best available compaction model from candidates.
130
- * Checks which providers are initialized and returns the first match.
131
- */
132
- async function pickCompactionModel(config) {
133
- const { isAnthropicAvailable } = await import('./anthropic.js');
134
- const { isOpenAIAvailable } = await import('./openai.js');
135
- for (const candidate of COMPACTION_MODEL_CANDIDATES) {
136
- const provider = candidate.split('/')[0];
137
- if (provider === 'anthropic' && isAnthropicAvailable())
138
- return candidate;
139
- if (isOpenAIAvailable(provider))
140
- return candidate;
141
- }
142
- // Last resort: return the first candidate and let chat() fail → fallback to truncation
143
- return COMPACTION_MODEL_CANDIDATES[0];
144
- }
145
- /**
146
- * Call the LLM to summarize a conversation transcript.
147
- * Returns the summary text, or null if the call fails.
148
- */
149
- async function llmSummarize(transcript, config, compactionModel) {
150
- try {
151
- // Dynamically import to avoid circular dependency
152
- const { chat } = await import('./index.js');
153
- const model = compactionModel || await pickCompactionModel(config);
154
- const messages = [
155
- { role: 'system', content: COMPACTION_SYSTEM_PROMPT },
156
- {
157
- role: 'user',
158
- content: `Summarize the following conversation between an AI coding assistant and a user. This summary will replace the old messages in the context window so the assistant can continue working.\n\n---\n${transcript}\n---`,
159
- },
160
- ];
161
- console.log(`[context-manager] Requesting LLM summary via ${model}`);
162
- const summary = await chat(messages, {
163
- model,
164
- maxTokens: SUMMARY_MAX_TOKENS,
165
- }, config);
166
- if (!summary || summary.trim().length === 0) {
167
- console.warn('[context-manager] LLM returned empty summary, falling back to truncation');
168
- return null;
169
- }
170
- console.log(`[context-manager] LLM summary: ${summary.length} chars`);
171
- return summary.trim();
172
- }
173
- catch (err) {
174
- console.warn(`[context-manager] LLM summarization failed, falling back to truncation: ${err instanceof Error ? err.message : err}`);
175
- return null;
176
- }
177
- }
178
- // --- Fallback truncation (original mechanical approach) ---
179
- function truncateAnthropicHead(head) {
180
- return head.map(msg => {
181
- if (!Array.isArray(msg.content))
182
- return msg;
183
- let changed = false;
184
- const newContent = msg.content.map((block) => {
185
- if (block.type !== 'tool_result')
186
- return block;
187
- const raw = typeof block.content === 'string'
188
- ? block.content
189
- : JSON.stringify(block.content);
190
- if (raw.length <= RESULT_MAX_CHARS)
191
- return block;
192
- changed = true;
193
- return { ...block, content: raw.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
194
- });
195
- return changed ? { ...msg, content: newContent } : msg;
196
- });
197
- }
198
- function truncateOpenAIHead(head) {
199
- return head.map(msg => {
200
- if (msg.role !== 'tool')
201
- return msg;
202
- if (typeof msg.content !== 'string')
203
- return msg;
204
- if (msg.content.length <= RESULT_MAX_CHARS)
205
- return msg;
206
- return { ...msg, content: msg.content.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
207
- });
208
- }
209
- function truncateCodexHead(head) {
210
- return head.map(item => {
211
- if (item.type !== 'function_call_output')
212
- return item;
213
- if (typeof item.output !== 'string')
214
- return item;
215
- if (item.output.length <= RESULT_MAX_CHARS)
216
- return item;
217
- return { ...item, output: item.output.slice(0, RESULT_MAX_CHARS) + ' [truncated]' };
218
- });
219
- }
220
- // --- Track whether we already compacted for a given conversation ---
221
- // Key: a hash of the tail messages to avoid re-summarizing the same head repeatedly.
222
- // This is a WeakMap so we don't leak memory across conversations.
223
- const compactedMarker = new WeakSet();
300
+ // =====================================================================
301
+ // Legacy wrapper functions delegate to generic compactMessages()
302
+ // These preserve backward compatibility for the old provider tool loops
303
+ // (anthropic.ts, codex.ts) until Phase 5 removes them.
304
+ // =====================================================================
224
305
  /**
225
306
  * Compact Anthropic-format apiMessages when over threshold.
226
- * Uses LLM summarization for old messages; falls back to truncation on failure.
227
- * Does NOT mutate the input array — returns a new array.
307
+ * @deprecated Use compactMessages() with anthropicFormatHelper instead.
228
308
  */
229
309
  export async function compactAnthropicMessages(messages, config, iteration = 0, fullConfig) {
230
- if (config?.enabled === false)
231
- return { messages, compacted: false };
232
- const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
233
- const estimated = estimateTokens(messages);
234
- if (estimated <= maxTokens)
235
- return { messages, compacted: false };
236
- // If we already compacted this array (it has a summary message), use truncation fallback
237
- // to progressively shrink rather than re-summarizing repeatedly.
238
- if (compactedMarker.has(messages)) {
239
- console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
240
- const tail = messages.slice(-KEEP_TAIL);
241
- const head = messages.slice(0, -KEEP_TAIL);
242
- const result = [...truncateAnthropicHead(head), ...tail];
243
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
244
- }
245
- console.log(`[context-manager] Compacting at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
246
- const tail = messages.slice(-KEEP_TAIL);
247
- const head = messages.slice(0, -KEEP_TAIL);
248
- // Attempt LLM summarization
249
- if (fullConfig) {
250
- const transcript = serializeAnthropicMessages(head);
251
- const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
252
- if (summary) {
253
- const summaryMessage = {
254
- role: 'user',
255
- content: [{ type: 'text', text: `[Conversation Summary]\n${summary}` }],
256
- };
257
- const result = [summaryMessage, ...tail];
258
- compactedMarker.add(result);
259
- const tokensAfter = estimateTokens(result);
260
- return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
261
- }
262
- }
263
- // Fallback: mechanical truncation
264
- const result = [...truncateAnthropicHead(head), ...tail];
265
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
310
+ return compactMessages(messages, anthropicFormatHelper, config, iteration, fullConfig);
266
311
  }
267
312
  /**
268
313
  * Compact OpenAI-format apiMessages when over threshold.
269
- * Uses LLM summarization for old messages; falls back to truncation on failure.
270
- * Does NOT mutate the input array — returns a new array.
314
+ * @deprecated Use compactMessages() with openaiFormatHelper instead.
271
315
  */
272
316
  export async function compactOpenAIMessages(messages, config, iteration = 0, fullConfig) {
273
- if (config?.enabled === false)
274
- return { messages, compacted: false };
275
- const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
276
- const estimated = estimateTokens(messages);
277
- if (estimated <= maxTokens)
278
- return { messages, compacted: false };
279
- if (compactedMarker.has(messages)) {
280
- console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
281
- const tail = messages.slice(-KEEP_TAIL);
282
- const head = messages.slice(0, -KEEP_TAIL);
283
- const result = [...truncateOpenAIHead(head), ...tail];
284
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
285
- }
286
- console.log(`[context-manager] Compacting OpenAI messages at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
287
- const tail = messages.slice(-KEEP_TAIL);
288
- const head = messages.slice(0, -KEEP_TAIL);
289
- // Attempt LLM summarization
290
- if (fullConfig) {
291
- const transcript = serializeOpenAIMessages(head);
292
- const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
293
- if (summary) {
294
- const summaryMessage = {
295
- role: 'user',
296
- content: `[Conversation Summary]\n${summary}`,
297
- };
298
- const result = [summaryMessage, ...tail];
299
- compactedMarker.add(result);
300
- const tokensAfter = estimateTokens(result);
301
- return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
302
- }
303
- }
304
- // Fallback: mechanical truncation
305
- const result = [...truncateOpenAIHead(head), ...tail];
306
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
317
+ return compactMessages(messages, openaiFormatHelper, config, iteration, fullConfig);
307
318
  }
308
319
  /**
309
320
  * Compact Codex-format input items when over threshold.
310
- * Uses LLM summarization for old items; falls back to truncation on failure.
311
- * Does NOT mutate the input array — returns a new array.
321
+ * @deprecated Use compactMessages() with codexFormatHelper instead.
312
322
  */
313
323
  export async function compactCodexMessages(input, config, iteration = 0, fullConfig) {
314
- if (config?.enabled === false)
315
- return { messages: input, compacted: false };
316
- const maxTokens = config?.maxContextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS;
317
- const estimated = estimateTokens(input);
318
- if (estimated <= maxTokens)
319
- return { messages: input, compacted: false };
320
- if (compactedMarker.has(input)) {
321
- console.log(`[context-manager] Already compacted, using truncation fallback (iteration ${iteration})`);
322
- const tail = input.slice(-KEEP_TAIL);
323
- const head = input.slice(0, -KEEP_TAIL);
324
- const result = [...truncateCodexHead(head), ...tail];
325
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
326
- }
327
- console.log(`[context-manager] Compacting Codex input at iteration ${iteration} (~${Math.round(estimated / 1000)}k tokens > ${Math.round(maxTokens / 1000)}k threshold)`);
328
- const tail = input.slice(-KEEP_TAIL);
329
- const head = input.slice(0, -KEEP_TAIL);
330
- // Attempt LLM summarization
331
- if (fullConfig) {
332
- const transcript = serializeCodexMessages(head);
333
- const summary = await llmSummarize(transcript, fullConfig, config?.compactionModel);
334
- if (summary) {
335
- const summaryItem = {
336
- type: 'message',
337
- role: 'user',
338
- content: `[Conversation Summary]\n${summary}`,
339
- };
340
- const result = [summaryItem, ...tail];
341
- compactedMarker.add(result);
342
- const tokensAfter = estimateTokens(result);
343
- return { messages: result, compacted: true, method: 'llm', summary, tokensBefore: estimated, tokensAfter };
344
- }
345
- }
346
- // Fallback: mechanical truncation
347
- const result = [...truncateCodexHead(head), ...tail];
348
- return { messages: result, compacted: true, method: 'truncation', tokensBefore: estimated, tokensAfter: estimateTokens(result) };
324
+ return compactMessages(input, codexFormatHelper, config, iteration, fullConfig);
349
325
  }
350
326
  // --- Exported helpers for testing ---
351
327
  export { serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages };
@@ -1,13 +1,21 @@
1
1
  import type { Config, ChatMessage, ChatOptions, ToolConfig } from '../types.js';
2
2
  import type { ExecuteToolContext } from '../tools/execute-context.js';
3
3
  import type { ToolChatResult } from './types.js';
4
+ import type { ProviderAdapter } from './adapter.js';
4
5
  export type { ToolChatResult, ProviderChatParams, ProviderToolChatParams } from './types.js';
6
+ export type { ProviderAdapter } from './adapter.js';
5
7
  export { TOOL_GUARD, setUsingOAuth, isUsingOAuth, buildSystemParam, addToolCacheBreakpoint, contentToText, toOpenAITools, resolveModel, resolveProviderRoute, shouldUseCodexAliasProvider, getProvider, stripProvider, buildThinkingConfig, } from './utils.js';
6
8
  export { toOpenAIContent, toCodexContent, toCodexToolDefinitions } from './content.js';
7
9
  export { setLangfuseHelpers, toCostDetails, toAnthropicUsageDetails, toUsageDetails, toNumericUsageDetails, } from './observability.js';
10
+ /**
11
+ * Resolve a provider name to a ProviderAdapter instance.
12
+ * Adapters are lightweight — creating one per call is fine.
13
+ */
14
+ export declare function getAdapter(provider: string): ProviderAdapter;
8
15
  export { setAnthropicClient, isAnthropicAvailable, chatAnthropic, chatWithToolsAnthropic, } from './anthropic.js';
9
- export { addOpenAIClient, getOpenAIClient, hasOpenAIClient, clearOpenAIClients, resetOpenAIProviderState, isOpenAIAvailable, chatOpenAI, chatWithToolsOpenAI, } from './openai.js';
10
16
  export { addResponsesApiProvider, isResponsesApiProvider, setCodexAuthPath, setCodexBaseUrl, initCodexAuth, resetCodexProviderState, loadCodexAuth, getCodexAuth, isCodexAvailable, chatCodex, chatWithToolsCodex, } from './codex.js';
11
17
  export declare function initProviders(config: Config): Promise<void>;
18
+ /** Unified chat function that routes to appropriate provider via adapter. */
12
19
  export declare function chat(messages: ChatMessage[], options: ChatOptions, config: Config): Promise<string>;
20
+ /** Unified chatWithTools function that routes to appropriate provider via adapter. */
13
21
  export declare function chatWithTools(messages: ChatMessage[], options: ChatOptions, config: Config, toolConfig: ToolConfig, toolContext?: ExecuteToolContext): Promise<ToolChatResult>;