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.
- package/README.md +47 -37
- package/dist/__tests__/adapter-types.test.d.ts +4 -0
- package/dist/__tests__/adapter-types.test.js +63 -0
- package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
- package/dist/__tests__/anthropic-adapter.test.js +264 -0
- package/dist/__tests__/api.test.js +0 -1
- package/dist/__tests__/cli.integration.test.js +2 -4
- package/dist/__tests__/cli.test.js +0 -1
- package/dist/__tests__/code-agents-notifications.test.js +137 -0
- package/dist/__tests__/code-agents-parser.test.js +19 -1
- package/dist/__tests__/code-agents-preflight.test.js +3 -28
- package/dist/__tests__/code-agents-utils.test.js +34 -9
- package/dist/__tests__/code-agents-worktrees.test.js +116 -0
- package/dist/__tests__/codex-adapter.test.js +184 -0
- package/dist/__tests__/codex-auth.test.js +66 -0
- package/dist/__tests__/codex-provider-gating.test.js +35 -0
- package/dist/__tests__/codex-unified-loop.test.js +111 -0
- package/dist/__tests__/config-security.test.js +127 -0
- package/dist/__tests__/config.test.js +23 -0
- package/dist/__tests__/context-manager.test.js +243 -164
- package/dist/__tests__/cron-run.test.js +250 -0
- package/dist/__tests__/cron.test.js +12 -38
- package/dist/__tests__/digests.test.js +67 -0
- package/dist/__tests__/discord-attachments.test.js +211 -0
- package/dist/__tests__/discord-docs.test.d.ts +1 -0
- package/dist/__tests__/discord-docs.test.js +27 -0
- package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-agents.test.js +115 -0
- package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
- package/dist/__tests__/discord-thread-context.test.js +42 -0
- package/dist/__tests__/doctor.formatters.test.js +4 -4
- package/dist/__tests__/doctor.index.test.js +1 -1
- package/dist/__tests__/doctor.runner.test.js +3 -15
- package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
- package/dist/__tests__/env-sanitizer.test.js +45 -0
- package/dist/__tests__/exec-approval.test.js +61 -0
- package/dist/__tests__/fetch-tool.test.d.ts +1 -0
- package/dist/__tests__/fetch-tool.test.js +85 -0
- package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
- package/dist/__tests__/gateway-status-auth.test.js +72 -0
- package/dist/__tests__/heartbeat.test.js +3 -3
- package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
- package/dist/__tests__/interactive-sessions.test.js +96 -0
- package/dist/__tests__/langfuse.test.js +6 -18
- package/dist/__tests__/model-selection.test.js +3 -4
- package/dist/__tests__/providers-init.test.js +2 -8
- package/dist/__tests__/providers-routing.test.js +1 -1
- package/dist/__tests__/providers-utils.test.js +13 -3
- package/dist/__tests__/sessions.test.js +14 -10
- package/dist/__tests__/setup.test.js +12 -29
- package/dist/__tests__/skills.test.js +10 -7
- package/dist/__tests__/stream-formatter.test.d.ts +1 -0
- package/dist/__tests__/stream-formatter.test.js +114 -0
- package/dist/__tests__/token-efficiency.test.js +131 -15
- package/dist/__tests__/tool-loop.test.d.ts +4 -0
- package/dist/__tests__/tool-loop.test.js +505 -0
- package/dist/__tests__/tools.test.js +101 -276
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +14 -0
- package/dist/__tests__/voice.test.js +21 -0
- package/dist/agent.js +35 -4
- package/dist/api.js +113 -37
- package/dist/channels/discord/attachments.d.ts +50 -0
- package/dist/channels/discord/attachments.js +137 -0
- package/dist/channels/discord/delegation.d.ts +5 -0
- package/dist/channels/discord/delegation.js +136 -0
- package/dist/channels/discord/handlers.js +694 -7
- package/dist/channels/discord/index.d.ts +16 -1
- package/dist/channels/discord/index.js +64 -1
- package/dist/channels/discord/thread-agents.d.ts +54 -0
- package/dist/channels/discord/thread-agents.js +323 -0
- package/dist/channels/discord/threads.d.ts +58 -0
- package/dist/channels/discord/threads.js +192 -0
- package/dist/channels/discord/types.js +4 -2
- package/dist/channels/discord/utils.d.ts +16 -0
- package/dist/channels/discord/utils.js +86 -6
- package/dist/channels/telegram/index.d.ts +1 -1
- package/dist/channels/telegram/types.js +1 -1
- package/dist/channels/telegram/utils.js +9 -3
- package/dist/channels.d.ts +1 -1
- package/dist/cli.js +20 -400
- package/dist/code-agents/executor.d.ts +1 -1
- package/dist/code-agents/executor.js +101 -45
- package/dist/code-agents/index.d.ts +2 -7
- package/dist/code-agents/index.js +111 -80
- package/dist/code-agents/interactive-resume.d.ts +6 -0
- package/dist/code-agents/interactive-resume.js +98 -0
- package/dist/code-agents/interactive-sessions.d.ts +20 -0
- package/dist/code-agents/interactive-sessions.js +132 -0
- package/dist/code-agents/parser.js +5 -1
- package/dist/code-agents/registry.d.ts +7 -1
- package/dist/code-agents/registry.js +11 -23
- package/dist/code-agents/stream-formatter.d.ts +8 -0
- package/dist/code-agents/stream-formatter.js +92 -0
- package/dist/code-agents/types.d.ts +16 -24
- package/dist/code-agents/utils.d.ts +35 -11
- package/dist/code-agents/utils.js +349 -95
- package/dist/code-agents/worktrees.d.ts +37 -0
- package/dist/code-agents/worktrees.js +116 -0
- package/dist/config.d.ts +2 -4
- package/dist/config.js +123 -23
- package/dist/cron.d.ts +1 -6
- package/dist/cron.js +175 -82
- package/dist/dashboard/assets/index-B345aOO-.js +65 -0
- package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/digests.d.ts +1 -0
- package/dist/digests.js +132 -42
- package/dist/doctor/checks.d.ts +0 -3
- package/dist/doctor/checks.js +1 -108
- package/dist/doctor/runner.js +1 -4
- package/dist/env-sanitizer.d.ts +2 -0
- package/dist/env-sanitizer.js +61 -0
- package/dist/exec-approval.d.ts +11 -1
- package/dist/exec-approval.js +17 -4
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.js +17 -7
- package/dist/heartbeat.js +1 -6
- package/dist/langfuse.js +3 -29
- package/dist/model-selection.js +3 -1
- package/dist/providers/adapter.d.ts +118 -0
- package/dist/providers/adapter.js +6 -0
- package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
- package/dist/providers/adapters/anthropic-adapter.js +204 -0
- package/dist/providers/adapters/codex-adapter.d.ts +26 -0
- package/dist/providers/adapters/codex-adapter.js +203 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +10 -272
- package/dist/providers/codex.d.ts +21 -0
- package/dist/providers/codex.js +149 -330
- package/dist/providers/content.d.ts +1 -1
- package/dist/providers/content.js +2 -2
- package/dist/providers/context-manager.d.ts +18 -6
- package/dist/providers/context-manager.js +199 -223
- package/dist/providers/index.d.ts +9 -1
- package/dist/providers/index.js +73 -64
- package/dist/providers/loop-utils.d.ts +20 -0
- package/dist/providers/loop-utils.js +30 -0
- package/dist/providers/tool-loop.d.ts +12 -0
- package/dist/providers/tool-loop.js +251 -0
- package/dist/providers/utils.d.ts +19 -3
- package/dist/providers/utils.js +100 -29
- package/dist/secure-store.d.ts +8 -0
- package/dist/secure-store.js +80 -0
- package/dist/service.js +3 -28
- package/dist/sessions.d.ts +3 -0
- package/dist/sessions.js +147 -18
- package/dist/setup-templates.js +13 -25
- package/dist/setup.d.ts +10 -6
- package/dist/setup.js +84 -292
- package/dist/skills.js +3 -11
- package/dist/tools/agent-delegation.d.ts +19 -0
- package/dist/tools/agent-delegation.js +49 -0
- package/dist/tools/bash-tool.js +89 -34
- package/dist/tools/definitions.d.ts +199 -302
- package/dist/tools/definitions.js +70 -123
- package/dist/tools/execute-context.d.ts +13 -4
- package/dist/tools/fetch-tool.js +109 -13
- package/dist/tools/file-tools.js +7 -1
- package/dist/tools.d.ts +7 -7
- package/dist/tools.js +133 -151
- package/dist/types.d.ts +37 -30
- package/dist/utils.js +4 -6
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +17 -4
- package/package.json +33 -23
- package/templates/TOOLS.md +0 -27
- package/dist/__tests__/audit.test.js +0 -122
- package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
- package/dist/__tests__/code-agents-sandbox.test.js +0 -163
- package/dist/__tests__/orchestrator.test.js +0 -425
- package/dist/__tests__/sandbox-bridge.test.js +0 -116
- package/dist/__tests__/sandbox-manager.test.js +0 -144
- package/dist/__tests__/sandbox-mount-security.test.js +0 -139
- package/dist/__tests__/sandbox-runtime.test.js +0 -176
- package/dist/__tests__/subagent.test.js +0 -240
- package/dist/__tests__/telegram.test.js +0 -42
- package/dist/code-agents/orchestrator.d.ts +0 -29
- package/dist/code-agents/orchestrator.js +0 -694
- package/dist/code-agents/worktree.d.ts +0 -40
- package/dist/code-agents/worktree.js +0 -215
- package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
- package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
- package/dist/dashboard.d.ts +0 -8
- package/dist/dashboard.js +0 -4071
- package/dist/discord.d.ts +0 -8
- package/dist/discord.js +0 -792
- package/dist/mcp-context-a8c.d.ts +0 -13
- package/dist/mcp-context-a8c.js +0 -34
- package/dist/orchestrator.d.ts +0 -15
- package/dist/orchestrator.js +0 -676
- package/dist/providers/openai.d.ts +0 -10
- package/dist/providers/openai.js +0 -355
- package/dist/sandbox/bridge.d.ts +0 -5
- package/dist/sandbox/bridge.js +0 -63
- package/dist/sandbox/index.d.ts +0 -5
- package/dist/sandbox/index.js +0 -4
- package/dist/sandbox/manager.d.ts +0 -7
- package/dist/sandbox/manager.js +0 -100
- package/dist/sandbox/mount-security.d.ts +0 -12
- package/dist/sandbox/mount-security.js +0 -122
- package/dist/sandbox/runtime.d.ts +0 -39
- package/dist/sandbox/runtime.js +0 -192
- package/dist/sandbox-utils.d.ts +0 -6
- package/dist/sandbox-utils.js +0 -36
- package/dist/subagent.d.ts +0 -19
- package/dist/subagent.js +0 -407
- package/dist/telegram.d.ts +0 -2
- package/dist/telegram.js +0 -11
- package/dist/tools/browser-tool.d.ts +0 -3
- package/dist/tools/browser-tool.js +0 -266
- package/sandbox/Dockerfile +0 -40
- /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
- /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
- /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
- /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
- /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
- /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { estimateTokens, compactAnthropicMessages, compactOpenAIMessages, compactCodexMessages, serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages, } from '../providers/context-manager.js';
|
|
2
|
+
import { estimateTokens, compactMessages, compactAnthropicMessages, compactOpenAIMessages, compactCodexMessages, anthropicFormatHelper, openaiFormatHelper, codexFormatHelper, serializeAnthropicMessages, serializeOpenAIMessages, serializeCodexMessages, } from '../providers/context-manager.js';
|
|
3
3
|
// Mock the chat function used for LLM summarization
|
|
4
4
|
vi.mock('../providers/index.js', () => ({
|
|
5
5
|
chat: vi.fn().mockResolvedValue('Summary of the conversation: the user asked to list files and the assistant ran ls.'),
|
|
@@ -51,6 +51,14 @@ function openaiExchange(toolResult) {
|
|
|
51
51
|
{ role: 'tool', tool_call_id: 'tc_1', content: toolResult },
|
|
52
52
|
];
|
|
53
53
|
}
|
|
54
|
+
// Helper: build many items for compaction tests
|
|
55
|
+
function manyItems(factory, content, count = 30) {
|
|
56
|
+
const items = [];
|
|
57
|
+
for (let i = 0; i < count; i++) {
|
|
58
|
+
items.push(...factory(content));
|
|
59
|
+
}
|
|
60
|
+
return items;
|
|
61
|
+
}
|
|
54
62
|
describe('estimateTokens', () => {
|
|
55
63
|
it('returns a positive number for non-empty data', () => {
|
|
56
64
|
expect(estimateTokens([{ role: 'user', content: 'hello' }])).toBeGreaterThan(0);
|
|
@@ -64,25 +72,119 @@ describe('estimateTokens', () => {
|
|
|
64
72
|
expect(large).toBeGreaterThan(small);
|
|
65
73
|
});
|
|
66
74
|
});
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// =====================================================================
|
|
76
|
+
// MessageFormatHelper unit tests
|
|
77
|
+
// =====================================================================
|
|
78
|
+
describe('anthropicFormatHelper', () => {
|
|
79
|
+
it('isToolResult returns true for tool_result content blocks', () => {
|
|
80
|
+
const msg = { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tu_1', content: 'result' }] };
|
|
81
|
+
expect(anthropicFormatHelper.isToolResult(msg)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('isToolResult returns false for text messages', () => {
|
|
84
|
+
const msg = { role: 'user', content: [{ type: 'text', text: 'hello' }] };
|
|
85
|
+
expect(anthropicFormatHelper.isToolResult(msg)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
it('isToolResult returns false for string content', () => {
|
|
88
|
+
expect(anthropicFormatHelper.isToolResult({ role: 'user', content: 'hi' })).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it('truncateToolResult truncates long tool_result content', () => {
|
|
91
|
+
const msg = {
|
|
92
|
+
role: 'user',
|
|
93
|
+
content: [{ type: 'tool_result', tool_use_id: 'tu_1', content: 'x'.repeat(1000) }],
|
|
94
|
+
};
|
|
95
|
+
const truncated = anthropicFormatHelper.truncateToolResult(msg, 100);
|
|
96
|
+
expect(truncated.content[0].content).toContain('[truncated]');
|
|
97
|
+
expect(truncated.content[0].content.length).toBeLessThan(200);
|
|
98
|
+
});
|
|
99
|
+
it('truncateToolResult leaves short content unchanged', () => {
|
|
100
|
+
const msg = {
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: [{ type: 'tool_result', tool_use_id: 'tu_1', content: 'short' }],
|
|
103
|
+
};
|
|
104
|
+
const result = anthropicFormatHelper.truncateToolResult(msg, 500);
|
|
105
|
+
expect(result).toBe(msg); // same reference (no change)
|
|
106
|
+
});
|
|
107
|
+
it('buildSummaryMessage returns Anthropic-format summary', () => {
|
|
108
|
+
const summary = anthropicFormatHelper.buildSummaryMessage('test summary');
|
|
109
|
+
expect(summary.role).toBe('user');
|
|
110
|
+
expect(summary.content[0].type).toBe('text');
|
|
111
|
+
expect(summary.content[0].text).toContain('[Conversation Summary]');
|
|
112
|
+
expect(summary.content[0].text).toContain('test summary');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('openaiFormatHelper', () => {
|
|
116
|
+
it('isToolResult returns true for tool role messages', () => {
|
|
117
|
+
expect(openaiFormatHelper.isToolResult({ role: 'tool', content: 'result' })).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('isToolResult returns false for non-tool messages', () => {
|
|
120
|
+
expect(openaiFormatHelper.isToolResult({ role: 'assistant', content: 'hi' })).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
it('truncateToolResult truncates long content', () => {
|
|
123
|
+
const msg = { role: 'tool', tool_call_id: 'tc_1', content: 'x'.repeat(1000) };
|
|
124
|
+
const truncated = openaiFormatHelper.truncateToolResult(msg, 100);
|
|
125
|
+
expect(truncated.content).toContain('[truncated]');
|
|
126
|
+
expect(truncated.content.length).toBeLessThan(200);
|
|
127
|
+
});
|
|
128
|
+
it('truncateToolResult leaves short content unchanged', () => {
|
|
129
|
+
const msg = { role: 'tool', tool_call_id: 'tc_1', content: 'short' };
|
|
130
|
+
const result = openaiFormatHelper.truncateToolResult(msg, 500);
|
|
131
|
+
expect(result).toBe(msg);
|
|
132
|
+
});
|
|
133
|
+
it('buildSummaryMessage returns OpenAI-format summary', () => {
|
|
134
|
+
const summary = openaiFormatHelper.buildSummaryMessage('test summary');
|
|
135
|
+
expect(summary.role).toBe('user');
|
|
136
|
+
expect(summary.content).toContain('[Conversation Summary]');
|
|
137
|
+
expect(summary.content).toContain('test summary');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('codexFormatHelper', () => {
|
|
141
|
+
it('isToolResult returns true for function_call_output items', () => {
|
|
142
|
+
expect(codexFormatHelper.isToolResult({ type: 'function_call_output', output: 'result' })).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
it('isToolResult returns false for function_call items', () => {
|
|
145
|
+
expect(codexFormatHelper.isToolResult({ type: 'function_call', name: 'Bash' })).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
it('isToolResult returns false for message items', () => {
|
|
148
|
+
expect(codexFormatHelper.isToolResult({ type: 'message', role: 'user' })).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
it('truncateToolResult truncates long output', () => {
|
|
151
|
+
const item = { type: 'function_call_output', call_id: 'fc_1', output: 'x'.repeat(1000) };
|
|
152
|
+
const truncated = codexFormatHelper.truncateToolResult(item, 100);
|
|
153
|
+
expect(truncated.output).toContain('[truncated]');
|
|
154
|
+
expect(truncated.output.length).toBeLessThan(200);
|
|
155
|
+
});
|
|
156
|
+
it('truncateToolResult leaves short output unchanged', () => {
|
|
157
|
+
const item = { type: 'function_call_output', call_id: 'fc_1', output: 'short' };
|
|
158
|
+
const result = codexFormatHelper.truncateToolResult(item, 500);
|
|
159
|
+
expect(result).toBe(item);
|
|
160
|
+
});
|
|
161
|
+
it('buildSummaryMessage returns Codex-format summary', () => {
|
|
162
|
+
const summary = codexFormatHelper.buildSummaryMessage('test summary');
|
|
163
|
+
expect(summary.type).toBe('message');
|
|
164
|
+
expect(summary.role).toBe('user');
|
|
165
|
+
expect(summary.content).toContain('[Conversation Summary]');
|
|
166
|
+
expect(summary.content).toContain('test summary');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// =====================================================================
|
|
170
|
+
// Generic compactMessages() tests
|
|
171
|
+
// =====================================================================
|
|
172
|
+
describe('compactMessages (generic)', () => {
|
|
173
|
+
it('passes through when under threshold', async () => {
|
|
69
174
|
const messages = anthropicExchange('short result');
|
|
70
|
-
const result = await
|
|
71
|
-
expect(result.messages).
|
|
175
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 100_000 });
|
|
176
|
+
expect(result.messages).toBe(messages);
|
|
72
177
|
expect(result.compacted).toBe(false);
|
|
73
178
|
});
|
|
74
|
-
it('
|
|
75
|
-
const messages = anthropicExchange
|
|
76
|
-
const result = await
|
|
179
|
+
it('passes through when disabled', async () => {
|
|
180
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
181
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { enabled: false, maxContextTokens: 1 });
|
|
77
182
|
expect(result.messages).toBe(messages);
|
|
183
|
+
expect(result.compacted).toBe(false);
|
|
78
184
|
});
|
|
79
|
-
it('uses LLM summarization
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
for (let i = 0; i < 30; i++) {
|
|
83
|
-
messages.push(...anthropicExchange(longResult));
|
|
84
|
-
}
|
|
85
|
-
const result = await compactAnthropicMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
185
|
+
it('uses LLM summarization with Anthropic helper', async () => {
|
|
186
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
187
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
86
188
|
expect(result.compacted).toBe(true);
|
|
87
189
|
expect(result.method).toBe('llm');
|
|
88
190
|
expect(result.summary).toBeTruthy();
|
|
@@ -90,86 +192,157 @@ describe('compactAnthropicMessages', () => {
|
|
|
90
192
|
expect(result.tokensAfter).toBeGreaterThan(0);
|
|
91
193
|
expect(result.tokensAfter).toBeLessThan(result.tokensBefore);
|
|
92
194
|
expect(mockChat).toHaveBeenCalledOnce();
|
|
93
|
-
// First message should be the summary
|
|
195
|
+
// First message should be the summary in Anthropic format
|
|
94
196
|
expect(result.messages[0].role).toBe('user');
|
|
95
197
|
expect(result.messages[0].content[0].text).toContain('[Conversation Summary]');
|
|
96
198
|
// Last 8 should be preserved
|
|
97
199
|
expect(result.messages.slice(-8)).toEqual(messages.slice(-8));
|
|
98
200
|
});
|
|
99
|
-
it('
|
|
201
|
+
it('uses LLM summarization with OpenAI helper', async () => {
|
|
202
|
+
const messages = manyItems(openaiExchange, 'x'.repeat(10_000));
|
|
203
|
+
const result = await compactMessages(messages, openaiFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
204
|
+
expect(result.compacted).toBe(true);
|
|
205
|
+
expect(result.method).toBe('llm');
|
|
206
|
+
expect(result.messages[0].role).toBe('user');
|
|
207
|
+
expect(result.messages[0].content).toContain('[Conversation Summary]');
|
|
208
|
+
});
|
|
209
|
+
it('uses LLM summarization with Codex helper', async () => {
|
|
210
|
+
const messages = manyItems(codexExchange, 'x'.repeat(10_000));
|
|
211
|
+
const result = await compactMessages(messages, codexFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
212
|
+
expect(result.compacted).toBe(true);
|
|
213
|
+
expect(result.method).toBe('llm');
|
|
214
|
+
expect(result.messages[0].type).toBe('message');
|
|
215
|
+
expect(result.messages[0].content).toContain('[Conversation Summary]');
|
|
216
|
+
});
|
|
217
|
+
it('falls back to truncation when LLM fails (Anthropic)', async () => {
|
|
100
218
|
mockChat.mockRejectedValueOnce(new Error('API error'));
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
for (let i = 0; i < 30; i++) {
|
|
104
|
-
messages.push(...anthropicExchange(longResult));
|
|
105
|
-
}
|
|
106
|
-
const result = await compactAnthropicMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
219
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
220
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
107
221
|
expect(result.compacted).toBe(true);
|
|
108
222
|
expect(result.method).toBe('truncation');
|
|
109
|
-
// Head messages should have truncated tool results
|
|
110
223
|
const headMessages = result.messages.slice(0, -8);
|
|
111
224
|
const toolResultMessages = headMessages.filter((m) => Array.isArray(m.content) && m.content.some((b) => b.type === 'tool_result'));
|
|
112
225
|
for (const msg of toolResultMessages) {
|
|
113
226
|
const block = msg.content.find((b) => b.type === 'tool_result');
|
|
114
227
|
expect(block.content).toContain('[truncated]');
|
|
115
|
-
expect(block.content.length).toBeLessThan(longResult.length);
|
|
116
228
|
}
|
|
117
229
|
});
|
|
118
|
-
it('falls back to truncation
|
|
119
|
-
|
|
120
|
-
const messages =
|
|
121
|
-
|
|
122
|
-
|
|
230
|
+
it('falls back to truncation when LLM fails (OpenAI)', async () => {
|
|
231
|
+
mockChat.mockRejectedValueOnce(new Error('API error'));
|
|
232
|
+
const messages = manyItems(openaiExchange, 'x'.repeat(10_000));
|
|
233
|
+
const result = await compactMessages(messages, openaiFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
234
|
+
expect(result.method).toBe('truncation');
|
|
235
|
+
const toolMessages = result.messages.slice(0, -8).filter((m) => m.role === 'tool');
|
|
236
|
+
for (const msg of toolMessages) {
|
|
237
|
+
expect(msg.content).toContain('[truncated]');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
it('falls back to truncation when LLM fails (Codex)', async () => {
|
|
241
|
+
mockChat.mockRejectedValueOnce(new Error('API error'));
|
|
242
|
+
const messages = manyItems(codexExchange, 'x'.repeat(10_000));
|
|
243
|
+
const result = await compactMessages(messages, codexFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
244
|
+
expect(result.method).toBe('truncation');
|
|
245
|
+
const outputItems = result.messages.slice(0, -8).filter((item) => item.type === 'function_call_output');
|
|
246
|
+
for (const item of outputItems) {
|
|
247
|
+
expect(item.output).toContain('[truncated]');
|
|
123
248
|
}
|
|
124
|
-
|
|
249
|
+
});
|
|
250
|
+
it('falls back to truncation without fullConfig', async () => {
|
|
251
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
252
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 1_000 });
|
|
125
253
|
expect(result.compacted).toBe(true);
|
|
126
254
|
expect(result.method).toBe('truncation');
|
|
127
255
|
expect(mockChat).not.toHaveBeenCalled();
|
|
128
256
|
});
|
|
129
|
-
it('keeps last 8
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
257
|
+
it('keeps last 8 items intact across all formats', async () => {
|
|
258
|
+
for (const [factory, helper] of [
|
|
259
|
+
[anthropicExchange, anthropicFormatHelper],
|
|
260
|
+
[openaiExchange, openaiFormatHelper],
|
|
261
|
+
[codexExchange, codexFormatHelper],
|
|
262
|
+
]) {
|
|
263
|
+
const items = manyItems(factory, 'x'.repeat(10_000));
|
|
264
|
+
const result = await compactMessages(items, helper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
265
|
+
expect(result.messages.slice(-8)).toEqual(items.slice(-8));
|
|
266
|
+
mockChat.mockClear();
|
|
267
|
+
mockChat.mockResolvedValue('Summary of the conversation.');
|
|
134
268
|
}
|
|
135
|
-
const result = await compactAnthropicMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
136
|
-
// Last 8 messages should be untouched
|
|
137
|
-
const tail = result.messages.slice(-8);
|
|
138
|
-
const originalTail = messages.slice(-8);
|
|
139
|
-
expect(tail).toEqual(originalTail);
|
|
140
269
|
});
|
|
141
270
|
it('does not mutate the input array', async () => {
|
|
142
|
-
const
|
|
143
|
-
const messages = [];
|
|
144
|
-
for (let i = 0; i < 30; i++) {
|
|
145
|
-
messages.push(...anthropicExchange(longResult));
|
|
146
|
-
}
|
|
271
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
147
272
|
const originalJson = JSON.stringify(messages);
|
|
148
|
-
await
|
|
273
|
+
await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
149
274
|
expect(JSON.stringify(messages)).toBe(originalJson);
|
|
150
275
|
});
|
|
151
|
-
it('
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
276
|
+
it('includes token counts in result', async () => {
|
|
277
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
278
|
+
const result = await compactMessages(messages, anthropicFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
279
|
+
expect(result.tokensBefore).toBeGreaterThan(1_000);
|
|
280
|
+
expect(result.tokensAfter).toBeDefined();
|
|
281
|
+
});
|
|
282
|
+
it('preserves non-tool-result items during truncation', async () => {
|
|
283
|
+
mockChat.mockRejectedValueOnce(new Error('fail'));
|
|
284
|
+
const items = manyItems(codexExchange, 'x'.repeat(10_000));
|
|
285
|
+
const result = await compactMessages(items, codexFormatHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
286
|
+
const callItems = result.messages.filter((item) => item.type === 'function_call');
|
|
287
|
+
for (const item of callItems) {
|
|
288
|
+
expect(item.name).toBe('Bash');
|
|
156
289
|
}
|
|
157
|
-
const result = await compactAnthropicMessages(messages, { enabled: false, maxContextTokens: 1 });
|
|
158
|
-
expect(result.messages).toBe(messages);
|
|
159
|
-
expect(result.compacted).toBe(false);
|
|
160
290
|
});
|
|
161
|
-
it('
|
|
162
|
-
|
|
163
|
-
const
|
|
291
|
+
it('works with a custom MessageFormatHelper', async () => {
|
|
292
|
+
// Demonstrate that any format helper works with the generic function
|
|
293
|
+
const customHelper = {
|
|
294
|
+
isToolResult: (item) => item.kind === 'result',
|
|
295
|
+
truncateToolResult: (item, maxChars) => ({
|
|
296
|
+
...item,
|
|
297
|
+
data: item.data.slice(0, maxChars) + ' [truncated]',
|
|
298
|
+
}),
|
|
299
|
+
serialize: (items) => items.map(i => JSON.stringify(i)).join('\n'),
|
|
300
|
+
buildSummaryMessage: (summary) => ({ kind: 'summary', data: summary }),
|
|
301
|
+
};
|
|
302
|
+
const items = [];
|
|
164
303
|
for (let i = 0; i < 30; i++) {
|
|
165
|
-
|
|
304
|
+
items.push({ kind: 'call', name: 'test' });
|
|
305
|
+
items.push({ kind: 'result', data: 'x'.repeat(10_000) });
|
|
166
306
|
}
|
|
307
|
+
const result = await compactMessages(items, customHelper, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
308
|
+
expect(result.compacted).toBe(true);
|
|
309
|
+
expect(result.method).toBe('llm');
|
|
310
|
+
expect(result.messages[0].kind).toBe('summary');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
// =====================================================================
|
|
314
|
+
// Legacy wrapper tests (verify backward compatibility)
|
|
315
|
+
// =====================================================================
|
|
316
|
+
describe('compactAnthropicMessages (legacy wrapper)', () => {
|
|
317
|
+
it('passes through unchanged when under threshold', async () => {
|
|
318
|
+
const messages = anthropicExchange('short result');
|
|
319
|
+
const result = await compactAnthropicMessages(messages, { maxContextTokens: 100_000 });
|
|
320
|
+
expect(result.messages).toEqual(messages);
|
|
321
|
+
expect(result.compacted).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
it('returns same reference when no compaction needed', async () => {
|
|
324
|
+
const messages = anthropicExchange('short result');
|
|
325
|
+
const result = await compactAnthropicMessages(messages, { maxContextTokens: 100_000 });
|
|
326
|
+
expect(result.messages).toBe(messages);
|
|
327
|
+
});
|
|
328
|
+
it('uses LLM summarization when fullConfig is provided', async () => {
|
|
329
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
167
330
|
const result = await compactAnthropicMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
168
|
-
expect(result.
|
|
169
|
-
expect(result.
|
|
331
|
+
expect(result.compacted).toBe(true);
|
|
332
|
+
expect(result.method).toBe('llm');
|
|
333
|
+
expect(result.summary).toBeTruthy();
|
|
334
|
+
expect(result.messages[0].role).toBe('user');
|
|
335
|
+
expect(result.messages[0].content[0].text).toContain('[Conversation Summary]');
|
|
336
|
+
expect(result.messages.slice(-8)).toEqual(messages.slice(-8));
|
|
337
|
+
});
|
|
338
|
+
it('passes through unchanged when disabled', async () => {
|
|
339
|
+
const messages = manyItems(anthropicExchange, 'x'.repeat(10_000));
|
|
340
|
+
const result = await compactAnthropicMessages(messages, { enabled: false, maxContextTokens: 1 });
|
|
341
|
+
expect(result.messages).toBe(messages);
|
|
342
|
+
expect(result.compacted).toBe(false);
|
|
170
343
|
});
|
|
171
344
|
});
|
|
172
|
-
describe('compactOpenAIMessages', () => {
|
|
345
|
+
describe('compactOpenAIMessages (legacy wrapper)', () => {
|
|
173
346
|
it('passes through unchanged when under threshold', async () => {
|
|
174
347
|
const messages = openaiExchange('short result');
|
|
175
348
|
const result = await compactOpenAIMessages(messages, { maxContextTokens: 100_000 });
|
|
@@ -177,63 +350,21 @@ describe('compactOpenAIMessages', () => {
|
|
|
177
350
|
expect(result.compacted).toBe(false);
|
|
178
351
|
});
|
|
179
352
|
it('uses LLM summarization when fullConfig is provided', async () => {
|
|
180
|
-
const
|
|
181
|
-
const messages = [];
|
|
182
|
-
for (let i = 0; i < 30; i++) {
|
|
183
|
-
messages.push(...openaiExchange(longResult));
|
|
184
|
-
}
|
|
353
|
+
const messages = manyItems(openaiExchange, 'x'.repeat(10_000));
|
|
185
354
|
const result = await compactOpenAIMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
186
355
|
expect(result.compacted).toBe(true);
|
|
187
356
|
expect(result.method).toBe('llm');
|
|
188
357
|
expect(result.messages[0].role).toBe('user');
|
|
189
358
|
expect(result.messages[0].content).toContain('[Conversation Summary]');
|
|
190
359
|
});
|
|
191
|
-
it('falls back to truncation when LLM fails', async () => {
|
|
192
|
-
mockChat.mockRejectedValueOnce(new Error('API error'));
|
|
193
|
-
const longResult = 'x'.repeat(10_000);
|
|
194
|
-
const messages = [];
|
|
195
|
-
for (let i = 0; i < 30; i++) {
|
|
196
|
-
messages.push(...openaiExchange(longResult));
|
|
197
|
-
}
|
|
198
|
-
const result = await compactOpenAIMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
199
|
-
expect(result.method).toBe('truncation');
|
|
200
|
-
const headItems = result.messages.slice(0, -8);
|
|
201
|
-
const toolMessages = headItems.filter((m) => m.role === 'tool');
|
|
202
|
-
for (const msg of toolMessages) {
|
|
203
|
-
expect(msg.content).toContain('[truncated]');
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
it('keeps last 8 messages intact', async () => {
|
|
207
|
-
const longResult = 'x'.repeat(10_000);
|
|
208
|
-
const messages = [];
|
|
209
|
-
for (let i = 0; i < 30; i++) {
|
|
210
|
-
messages.push(...openaiExchange(longResult));
|
|
211
|
-
}
|
|
212
|
-
const result = await compactOpenAIMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
213
|
-
expect(result.messages.slice(-8)).toEqual(messages.slice(-8));
|
|
214
|
-
});
|
|
215
|
-
it('does not mutate the input array', async () => {
|
|
216
|
-
const longResult = 'x'.repeat(10_000);
|
|
217
|
-
const messages = [];
|
|
218
|
-
for (let i = 0; i < 30; i++) {
|
|
219
|
-
messages.push(...openaiExchange(longResult));
|
|
220
|
-
}
|
|
221
|
-
const original = JSON.stringify(messages);
|
|
222
|
-
await compactOpenAIMessages(messages, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
223
|
-
expect(JSON.stringify(messages)).toBe(original);
|
|
224
|
-
});
|
|
225
360
|
it('passes through unchanged when disabled', async () => {
|
|
226
|
-
const
|
|
227
|
-
const messages = [];
|
|
228
|
-
for (let i = 0; i < 30; i++) {
|
|
229
|
-
messages.push(...openaiExchange(longResult));
|
|
230
|
-
}
|
|
361
|
+
const messages = manyItems(openaiExchange, 'x'.repeat(10_000));
|
|
231
362
|
const result = await compactOpenAIMessages(messages, { enabled: false, maxContextTokens: 1 });
|
|
232
363
|
expect(result.messages).toBe(messages);
|
|
233
364
|
expect(result.compacted).toBe(false);
|
|
234
365
|
});
|
|
235
366
|
});
|
|
236
|
-
describe('compactCodexMessages', () => {
|
|
367
|
+
describe('compactCodexMessages (legacy wrapper)', () => {
|
|
237
368
|
it('passes through unchanged when under threshold', async () => {
|
|
238
369
|
const items = codexExchange('short result');
|
|
239
370
|
const result = await compactCodexMessages(items, { maxContextTokens: 100_000 });
|
|
@@ -241,75 +372,23 @@ describe('compactCodexMessages', () => {
|
|
|
241
372
|
expect(result.compacted).toBe(false);
|
|
242
373
|
});
|
|
243
374
|
it('uses LLM summarization when fullConfig is provided', async () => {
|
|
244
|
-
const
|
|
245
|
-
const items = [];
|
|
246
|
-
for (let i = 0; i < 30; i++) {
|
|
247
|
-
items.push(...codexExchange(longOutput));
|
|
248
|
-
}
|
|
375
|
+
const items = manyItems(codexExchange, 'x'.repeat(10_000));
|
|
249
376
|
const result = await compactCodexMessages(items, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
250
377
|
expect(result.compacted).toBe(true);
|
|
251
378
|
expect(result.method).toBe('llm');
|
|
252
379
|
expect(result.messages[0].type).toBe('message');
|
|
253
380
|
expect(result.messages[0].content).toContain('[Conversation Summary]');
|
|
254
381
|
});
|
|
255
|
-
it('falls back to truncation when LLM fails', async () => {
|
|
256
|
-
mockChat.mockRejectedValueOnce(new Error('API error'));
|
|
257
|
-
const longOutput = 'x'.repeat(10_000);
|
|
258
|
-
const items = [];
|
|
259
|
-
for (let i = 0; i < 30; i++) {
|
|
260
|
-
items.push(...codexExchange(longOutput));
|
|
261
|
-
}
|
|
262
|
-
const result = await compactCodexMessages(items, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
263
|
-
expect(result.method).toBe('truncation');
|
|
264
|
-
const headItems = result.messages.slice(0, -8);
|
|
265
|
-
const outputItems = headItems.filter((item) => item.type === 'function_call_output');
|
|
266
|
-
for (const item of outputItems) {
|
|
267
|
-
expect(item.output).toContain('[truncated]');
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
it('keeps last 8 items intact when compacting', async () => {
|
|
271
|
-
const longOutput = 'x'.repeat(10_000);
|
|
272
|
-
const items = [];
|
|
273
|
-
for (let i = 0; i < 30; i++) {
|
|
274
|
-
items.push(...codexExchange(longOutput));
|
|
275
|
-
}
|
|
276
|
-
const result = await compactCodexMessages(items, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
277
|
-
expect(result.messages.slice(-8)).toEqual(items.slice(-8));
|
|
278
|
-
});
|
|
279
|
-
it('does not mutate the input array', async () => {
|
|
280
|
-
const longOutput = 'x'.repeat(10_000);
|
|
281
|
-
const items = [];
|
|
282
|
-
for (let i = 0; i < 30; i++) {
|
|
283
|
-
items.push(...codexExchange(longOutput));
|
|
284
|
-
}
|
|
285
|
-
const originalJson = JSON.stringify(items);
|
|
286
|
-
await compactCodexMessages(items, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
287
|
-
expect(JSON.stringify(items)).toBe(originalJson);
|
|
288
|
-
});
|
|
289
|
-
it('preserves function_call items unchanged', async () => {
|
|
290
|
-
mockChat.mockRejectedValueOnce(new Error('fail')); // force truncation
|
|
291
|
-
const longOutput = 'x'.repeat(10_000);
|
|
292
|
-
const items = [];
|
|
293
|
-
for (let i = 0; i < 30; i++) {
|
|
294
|
-
items.push(...codexExchange(longOutput));
|
|
295
|
-
}
|
|
296
|
-
const result = await compactCodexMessages(items, { maxContextTokens: 1_000 }, 1, fullConfig);
|
|
297
|
-
const callItems = result.messages.filter((item) => item.type === 'function_call');
|
|
298
|
-
for (const item of callItems) {
|
|
299
|
-
expect(item.name).toBe('Bash');
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
382
|
it('passes through unchanged when disabled', async () => {
|
|
303
|
-
const
|
|
304
|
-
const items = [];
|
|
305
|
-
for (let i = 0; i < 30; i++) {
|
|
306
|
-
items.push(...codexExchange(longOutput));
|
|
307
|
-
}
|
|
383
|
+
const items = manyItems(codexExchange, 'x'.repeat(10_000));
|
|
308
384
|
const result = await compactCodexMessages(items, { enabled: false, maxContextTokens: 1 });
|
|
309
385
|
expect(result.messages).toBe(items);
|
|
310
386
|
expect(result.compacted).toBe(false);
|
|
311
387
|
});
|
|
312
388
|
});
|
|
389
|
+
// =====================================================================
|
|
390
|
+
// Serializer tests (unchanged — these test the format helpers indirectly)
|
|
391
|
+
// =====================================================================
|
|
313
392
|
describe('serializers', () => {
|
|
314
393
|
it('serializeAnthropicMessages produces readable transcript', () => {
|
|
315
394
|
const messages = [
|