@roj-ai/sdk 0.1.14 → 0.1.16

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 (109) hide show
  1. package/dist/bootstrap.d.ts +1 -0
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/core/agents/agent.d.ts +25 -1
  4. package/dist/core/agents/agent.d.ts.map +1 -1
  5. package/dist/core/agents/agent.js +117 -21
  6. package/dist/core/agents/agent.js.map +1 -1
  7. package/dist/core/agents/config.d.ts +7 -0
  8. package/dist/core/agents/config.d.ts.map +1 -1
  9. package/dist/core/agents/context.d.ts +10 -0
  10. package/dist/core/agents/context.d.ts.map +1 -1
  11. package/dist/core/agents/state.d.ts +11 -3
  12. package/dist/core/agents/state.d.ts.map +1 -1
  13. package/dist/core/agents/state.js.map +1 -1
  14. package/dist/core/file-store/file-store.d.ts +5 -1
  15. package/dist/core/file-store/file-store.d.ts.map +1 -1
  16. package/dist/core/file-store/file-store.js +31 -21
  17. package/dist/core/file-store/file-store.js.map +1 -1
  18. package/dist/core/image/vips-resizer.test.js +26 -14
  19. package/dist/core/image/vips-resizer.test.js.map +1 -1
  20. package/dist/core/llm/anthropic.d.ts.map +1 -1
  21. package/dist/core/llm/anthropic.js +11 -8
  22. package/dist/core/llm/anthropic.js.map +1 -1
  23. package/dist/core/llm/cache-breakpoints.d.ts +5 -1
  24. package/dist/core/llm/cache-breakpoints.d.ts.map +1 -1
  25. package/dist/core/llm/cache-breakpoints.js +10 -5
  26. package/dist/core/llm/cache-breakpoints.js.map +1 -1
  27. package/dist/core/sessions/session.d.ts.map +1 -1
  28. package/dist/core/sessions/session.js +3 -0
  29. package/dist/core/sessions/session.js.map +1 -1
  30. package/dist/core/sessions/session.test.js +5 -0
  31. package/dist/core/sessions/session.test.js.map +1 -1
  32. package/dist/core/sessions/state.d.ts.map +1 -1
  33. package/dist/core/sessions/state.js +5 -1
  34. package/dist/core/sessions/state.js.map +1 -1
  35. package/dist/core/tools/executor.test.js +1 -0
  36. package/dist/core/tools/executor.test.js.map +1 -1
  37. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  38. package/dist/plugins/agent-status/plugin.js +18 -26
  39. package/dist/plugins/agent-status/plugin.js.map +1 -1
  40. package/dist/plugins/context-compact/compaction-live.test.d.ts +17 -0
  41. package/dist/plugins/context-compact/compaction-live.test.d.ts.map +1 -0
  42. package/dist/plugins/context-compact/compaction-live.test.js +177 -0
  43. package/dist/plugins/context-compact/compaction-live.test.js.map +1 -0
  44. package/dist/plugins/context-compact/context-compact.integration.test.js +123 -3
  45. package/dist/plugins/context-compact/context-compact.integration.test.js.map +1 -1
  46. package/dist/plugins/context-compact/context-compactor.d.ts +47 -17
  47. package/dist/plugins/context-compact/context-compactor.d.ts.map +1 -1
  48. package/dist/plugins/context-compact/context-compactor.js +60 -36
  49. package/dist/plugins/context-compact/context-compactor.js.map +1 -1
  50. package/dist/plugins/context-compact/context-compactor.test.js +69 -103
  51. package/dist/plugins/context-compact/context-compactor.test.js.map +1 -1
  52. package/dist/plugins/context-compact/plugin.d.ts +9 -2
  53. package/dist/plugins/context-compact/plugin.d.ts.map +1 -1
  54. package/dist/plugins/context-compact/plugin.js +8 -4
  55. package/dist/plugins/context-compact/plugin.js.map +1 -1
  56. package/dist/plugins/filesystem/filesystem.integration.test.js +36 -0
  57. package/dist/plugins/filesystem/filesystem.integration.test.js.map +1 -1
  58. package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
  59. package/dist/plugins/filesystem/plugin.js +8 -6
  60. package/dist/plugins/filesystem/plugin.js.map +1 -1
  61. package/dist/plugins/mailbox/mailbox.integration.test.js +9 -16
  62. package/dist/plugins/mailbox/mailbox.integration.test.js.map +1 -1
  63. package/dist/plugins/resources/plugin.d.ts.map +1 -1
  64. package/dist/plugins/resources/plugin.js +4 -1
  65. package/dist/plugins/resources/plugin.js.map +1 -1
  66. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
  67. package/dist/plugins/uploads/preprocessors/image-classifier.js +15 -2
  68. package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
  69. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
  70. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +72 -19
  71. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
  72. package/dist/plugins/user-chat/plugin.d.ts +2 -0
  73. package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
  74. package/dist/plugins/user-chat/plugin.js +47 -3
  75. package/dist/plugins/user-chat/plugin.js.map +1 -1
  76. package/dist/plugins/user-chat/schema.d.ts +10 -0
  77. package/dist/plugins/user-chat/schema.d.ts.map +1 -1
  78. package/dist/plugins/user-chat/schema.js +1 -0
  79. package/dist/plugins/user-chat/schema.js.map +1 -1
  80. package/dist/plugins/user-chat/user-chat.integration.test.js +86 -0
  81. package/dist/plugins/user-chat/user-chat.integration.test.js.map +1 -1
  82. package/package.json +2 -2
  83. package/src/core/agents/agent.ts +134 -20
  84. package/src/core/agents/config.ts +7 -0
  85. package/src/core/agents/context.ts +11 -0
  86. package/src/core/agents/state.ts +11 -4
  87. package/src/core/file-store/file-store.ts +38 -18
  88. package/src/core/image/vips-resizer.test.ts +26 -15
  89. package/src/core/llm/anthropic.ts +19 -12
  90. package/src/core/llm/cache-breakpoints.ts +15 -6
  91. package/src/core/sessions/session.test.ts +6 -0
  92. package/src/core/sessions/session.ts +4 -0
  93. package/src/core/sessions/state.ts +5 -1
  94. package/src/core/tools/executor.test.ts +1 -0
  95. package/src/plugins/agent-status/plugin.ts +18 -25
  96. package/src/plugins/context-compact/compaction-live.test.ts +221 -0
  97. package/src/plugins/context-compact/context-compact.integration.test.ts +135 -3
  98. package/src/plugins/context-compact/context-compactor.test.ts +71 -110
  99. package/src/plugins/context-compact/context-compactor.ts +88 -43
  100. package/src/plugins/context-compact/plugin.ts +19 -10
  101. package/src/plugins/filesystem/filesystem.integration.test.ts +44 -0
  102. package/src/plugins/filesystem/plugin.ts +8 -6
  103. package/src/plugins/mailbox/mailbox.integration.test.ts +12 -18
  104. package/src/plugins/resources/plugin.ts +4 -1
  105. package/src/plugins/uploads/preprocessors/image-classifier.ts +15 -2
  106. package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +89 -20
  107. package/src/plugins/user-chat/plugin.ts +60 -3
  108. package/src/plugins/user-chat/schema.ts +10 -1
  109. package/src/plugins/user-chat/user-chat.integration.test.ts +99 -0
@@ -1,16 +1,15 @@
1
1
  import { beforeEach, describe, expect, it } from 'bun:test'
2
2
  import type { AgentId } from '~/core/agents/schema.js'
3
3
  import { generateTestAgentId } from '~/core/agents/schema.js'
4
- import type { InferenceRequest, InferenceResponse, LLMError, LLMMessage, LLMProvider } from '~/core/llm/provider.js'
4
+ import type { InferenceResponse, LLMMessage } from '~/core/llm/provider.js'
5
5
  import { ModelId } from '~/core/llm/schema.js'
6
6
  import type { SessionId } from '~/core/sessions/schema.js'
7
7
  import { generateSessionId } from '~/core/sessions/schema.js'
8
8
  import { generateToolCallId } from '~/core/tools/schema.js'
9
9
  import { Err, Ok } from '~/lib/utils/result.js'
10
- import type { Result } from '~/lib/utils/result.js'
11
10
  import { silentLogger } from '../../lib/logger/logger.js'
12
11
  import { ContextCompactor, createContextCompactedEvent, formatMessageForSummary } from './context-compactor.js'
13
- import type { CompactionConfig, CompactionResult } from './context-compactor.js'
12
+ import type { CompactionConfig, CompactionResult, RunInferenceFn } from './context-compactor.js'
14
13
 
15
14
  // ============================================================================
16
15
  // Test Constants
@@ -19,24 +18,25 @@ import type { CompactionConfig, CompactionResult } from './context-compactor.js'
19
18
  const TEST_MODEL_ID: ModelId = ModelId('test/model')
20
19
 
21
20
  // ============================================================================
22
- // Mock LLM Provider
21
+ // Mock runInference callback
23
22
  // ============================================================================
24
23
 
25
- class MockLLMProvider implements LLMProvider {
26
- readonly name = 'mock'
24
+ interface InferenceMockCall {
25
+ extraMessages: LLMMessage[]
26
+ }
27
+
28
+ class InferenceMock {
27
29
  private responses: InferenceResponse[] = []
28
30
  private responseIndex = 0
29
- calls: InferenceRequest[] = []
31
+ calls: InferenceMockCall[] = []
30
32
 
31
33
  setResponses(responses: InferenceResponse[]): void {
32
34
  this.responses = responses
33
35
  this.responseIndex = 0
34
36
  }
35
37
 
36
- async inference(
37
- request: InferenceRequest,
38
- ): Promise<Result<InferenceResponse, LLMError>> {
39
- this.calls.push(request)
38
+ readonly run: RunInferenceFn = async (extraMessages) => {
39
+ this.calls.push({ extraMessages })
40
40
  if (this.responseIndex >= this.responses.length) {
41
41
  return Err({ type: 'server_error', message: 'No more mock responses' })
42
42
  }
@@ -49,12 +49,12 @@ class MockLLMProvider implements LLMProvider {
49
49
  // ============================================================================
50
50
 
51
51
  describe('ContextCompactor.needsCompaction', () => {
52
- let mockLLM: MockLLMProvider
52
+ let inference: InferenceMock
53
53
  let compactor: ContextCompactor
54
54
 
55
55
  beforeEach(() => {
56
- mockLLM = new MockLLMProvider()
57
- compactor = new ContextCompactor(mockLLM, silentLogger, {
56
+ inference = new InferenceMock()
57
+ compactor = new ContextCompactor(silentLogger, {
58
58
  model: TEST_MODEL_ID,
59
59
  maxTokens: 100,
60
60
  keepRecentMessages: 2,
@@ -82,14 +82,14 @@ describe('ContextCompactor.needsCompaction', () => {
82
82
  // ============================================================================
83
83
 
84
84
  describe('ContextCompactor.compact', () => {
85
- let mockLLM: MockLLMProvider
85
+ let inference: InferenceMock
86
86
  let compactor: ContextCompactor
87
87
  let sessionId: SessionId
88
88
  let agentId: AgentId
89
89
 
90
90
  beforeEach(() => {
91
- mockLLM = new MockLLMProvider()
92
- compactor = new ContextCompactor(mockLLM, silentLogger, {
91
+ inference = new InferenceMock()
92
+ compactor = new ContextCompactor(silentLogger, {
93
93
  model: TEST_MODEL_ID,
94
94
  maxTokens: 100,
95
95
  keepRecentMessages: 2,
@@ -105,7 +105,7 @@ describe('ContextCompactor.compact', () => {
105
105
  { role: 'assistant', content: 'message 2' },
106
106
  ]
107
107
 
108
- const result = await compactor.compact(sessionId, agentId, messages)
108
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
109
109
  expect(result.ok).toBe(true)
110
110
  if (!result.ok) return
111
111
 
@@ -115,7 +115,7 @@ describe('ContextCompactor.compact', () => {
115
115
  })
116
116
 
117
117
  it('compacts old messages and keeps recent ones', async () => {
118
- mockLLM.setResponses([
118
+ inference.setResponses([
119
119
  {
120
120
  content: 'Summary of the conversation',
121
121
  toolCalls: [],
@@ -138,7 +138,7 @@ describe('ContextCompactor.compact', () => {
138
138
  { role: 'assistant', content: 'recent message 2' },
139
139
  ]
140
140
 
141
- const result = await compactor.compact(sessionId, agentId, messages)
141
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
142
142
  expect(result.ok).toBe(true)
143
143
  if (!result.ok) return
144
144
 
@@ -146,8 +146,8 @@ describe('ContextCompactor.compact', () => {
146
146
  expect(result.value.messagesRemoved).toBe(3)
147
147
  expect(result.value.compactedMessages.length).toBe(3) // summary + 2 recent
148
148
 
149
- // First message is summary
150
- expect(result.value.compactedMessages[0].role).toBe('system')
149
+ // First message is the summary (user-role so it cleanly fits into chat history)
150
+ expect(result.value.compactedMessages[0].role).toBe('user')
151
151
  expect(result.value.compactedMessages[0].content).toContain(
152
152
  '[CONVERSATION SUMMARY]',
153
153
  )
@@ -160,8 +160,8 @@ describe('ContextCompactor.compact', () => {
160
160
  expect(result.value.compactedMessages[2].content).toBe('recent message 2')
161
161
  })
162
162
 
163
- it('calls LLM with formatted conversation and configured model', async () => {
164
- mockLLM.setResponses([
163
+ it('calls runInference with a single trailing summarize-instruction message', async () => {
164
+ inference.setResponses([
165
165
  {
166
166
  content: 'Summary',
167
167
  toolCalls: [],
@@ -183,17 +183,16 @@ describe('ContextCompactor.compact', () => {
183
183
  { role: 'assistant', content: 'recent too' },
184
184
  ]
185
185
 
186
- await compactor.compact(sessionId, agentId, messages)
186
+ await compactor.compact(sessionId, agentId, messages, inference.run)
187
187
 
188
- expect(mockLLM.calls.length).toBe(1)
189
- const request = mockLLM.calls[0]
190
- // Verify model from config is used
191
- expect(request.model).toBe(TEST_MODEL_ID)
192
- expect(request.messages[0].role).toBe('user')
193
- expect(request.messages[0].content).toContain('User: user message')
194
- expect(request.messages[0].content).toContain('Agent: assistant message')
195
- // Recent messages should not be in the summarization request
196
- expect(request.messages[0].content).not.toContain('recent')
188
+ expect(inference.calls.length).toBe(1)
189
+ const call = inference.calls[0]
190
+ // Inline summarization sends ONE trailing user message — the host (agent)
191
+ // is responsible for the full prefix (preamble + history); the compactor
192
+ // only contributes the instruction.
193
+ expect(call.extraMessages.length).toBe(1)
194
+ expect(call.extraMessages[0].role).toBe('user')
195
+ expect(typeof call.extraMessages[0].content).toBe('string')
197
196
  })
198
197
 
199
198
  it('returns error when LLM fails', async () => {
@@ -207,7 +206,7 @@ describe('ContextCompactor.compact', () => {
207
206
  { role: 'assistant', content: 'recent 2' },
208
207
  ]
209
208
 
210
- const result = await compactor.compact(sessionId, agentId, messages)
209
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
211
210
  expect(result.ok).toBe(false)
212
211
  if (result.ok) return
213
212
 
@@ -220,14 +219,14 @@ describe('ContextCompactor.compact', () => {
220
219
  // ============================================================================
221
220
 
222
221
  describe('ContextCompactor.compactIfNeeded', () => {
223
- let mockLLM: MockLLMProvider
222
+ let inference: InferenceMock
224
223
  let compactor: ContextCompactor
225
224
  let sessionId: SessionId
226
225
  let agentId: AgentId
227
226
 
228
227
  beforeEach(() => {
229
- mockLLM = new MockLLMProvider()
230
- compactor = new ContextCompactor(mockLLM, silentLogger, {
228
+ inference = new InferenceMock()
229
+ compactor = new ContextCompactor(silentLogger, {
231
230
  model: TEST_MODEL_ID,
232
231
  maxTokens: 50,
233
232
  keepRecentMessages: 1,
@@ -239,16 +238,16 @@ describe('ContextCompactor.compactIfNeeded', () => {
239
238
  it('returns null when compaction not needed', async () => {
240
239
  const messages: LLMMessage[] = [{ role: 'user', content: 'short' }]
241
240
 
242
- const result = await compactor.compactIfNeeded(sessionId, agentId, messages)
241
+ const result = await compactor.compactIfNeeded(sessionId, agentId, messages, inference.run)
243
242
  expect(result.ok).toBe(true)
244
243
  if (!result.ok) return
245
244
 
246
245
  expect(result.value).toBeNull()
247
- expect(mockLLM.calls.length).toBe(0)
246
+ expect(inference.calls.length).toBe(0)
248
247
  })
249
248
 
250
249
  it('compacts when needed', async () => {
251
- mockLLM.setResponses([
250
+ inference.setResponses([
252
251
  {
253
252
  content: 'Summary',
254
253
  toolCalls: [],
@@ -270,7 +269,7 @@ describe('ContextCompactor.compactIfNeeded', () => {
270
269
  { role: 'user', content: 'c'.repeat(100) },
271
270
  ]
272
271
 
273
- const result = await compactor.compactIfNeeded(sessionId, agentId, messages)
272
+ const result = await compactor.compactIfNeeded(sessionId, agentId, messages, inference.run)
274
273
  expect(result.ok).toBe(true)
275
274
  if (!result.ok) return
276
275
 
@@ -338,13 +337,13 @@ describe('createContextCompactedEvent', () => {
338
337
 
339
338
  describe('ContextCompactor with custom config', () => {
340
339
  it('respects custom maxTokens', () => {
341
- const mockLLM = new MockLLMProvider()
340
+ const inference = new InferenceMock()
342
341
  const config: CompactionConfig = {
343
342
  model: TEST_MODEL_ID,
344
343
  maxTokens: 20,
345
344
  keepRecentMessages: 1,
346
345
  }
347
- const compactor = new ContextCompactor(mockLLM, silentLogger, config)
346
+ const compactor = new ContextCompactor(silentLogger, config)
348
347
 
349
348
  const smallMessages: LLMMessage[] = [{ role: 'user', content: 'hi' }]
350
349
  expect(compactor.needsCompaction(smallMessages)).toBe(false)
@@ -356,8 +355,8 @@ describe('ContextCompactor with custom config', () => {
356
355
  })
357
356
 
358
357
  it('respects custom keepRecentMessages', async () => {
359
- const mockLLM = new MockLLMProvider()
360
- mockLLM.setResponses([
358
+ const inference = new InferenceMock()
359
+ inference.setResponses([
361
360
  {
362
361
  content: 'Summary',
363
362
  toolCalls: [],
@@ -377,7 +376,7 @@ describe('ContextCompactor with custom config', () => {
377
376
  maxTokens: 10,
378
377
  keepRecentMessages: 3,
379
378
  }
380
- const compactor = new ContextCompactor(mockLLM, silentLogger, config)
379
+ const compactor = new ContextCompactor(silentLogger, config)
381
380
 
382
381
  const messages: LLMMessage[] = [
383
382
  { role: 'user', content: 'old 1' },
@@ -391,6 +390,7 @@ describe('ContextCompactor with custom config', () => {
391
390
  generateSessionId(),
392
391
  generateTestAgentId(),
393
392
  messages,
393
+ inference.run,
394
394
  )
395
395
 
396
396
  expect(result.ok).toBe(true)
@@ -402,8 +402,8 @@ describe('ContextCompactor with custom config', () => {
402
402
  })
403
403
 
404
404
  it('uses custom summaryPrompt', async () => {
405
- const mockLLM = new MockLLMProvider()
406
- mockLLM.setResponses([
405
+ const inference = new InferenceMock()
406
+ inference.setResponses([
407
407
  {
408
408
  content: 'Summary',
409
409
  toolCalls: [],
@@ -425,17 +425,18 @@ describe('ContextCompactor with custom config', () => {
425
425
  keepRecentMessages: 1,
426
426
  summaryPrompt: customPrompt,
427
427
  }
428
- const compactor = new ContextCompactor(mockLLM, silentLogger, config)
428
+ const compactor = new ContextCompactor(silentLogger, config)
429
429
 
430
430
  const messages: LLMMessage[] = [
431
431
  { role: 'user', content: 'old' },
432
432
  { role: 'user', content: 'recent' },
433
433
  ]
434
434
 
435
- await compactor.compact(generateSessionId(), generateTestAgentId(), messages)
435
+ await compactor.compact(generateSessionId(), generateTestAgentId(), messages, inference.run)
436
436
 
437
- expect(mockLLM.calls.length).toBe(1)
438
- expect(mockLLM.calls[0].systemPrompt).toBe(customPrompt)
437
+ expect(inference.calls.length).toBe(1)
438
+ // Custom prompt is sent as the content of the trailing user-role instruction.
439
+ expect(inference.calls[0].extraMessages[0].content).toBe(customPrompt)
439
440
  })
440
441
  })
441
442
 
@@ -557,14 +558,14 @@ describe('formatMessageForSummary', () => {
557
558
  // ============================================================================
558
559
 
559
560
  describe('ContextCompactor with tool calls', () => {
560
- let mockLLM: MockLLMProvider
561
+ let inference: InferenceMock
561
562
  let compactor: ContextCompactor
562
563
  let sessionId: SessionId
563
564
  let agentId: AgentId
564
565
 
565
566
  beforeEach(() => {
566
- mockLLM = new MockLLMProvider()
567
- compactor = new ContextCompactor(mockLLM, silentLogger, {
567
+ inference = new InferenceMock()
568
+ compactor = new ContextCompactor(silentLogger, {
568
569
  model: TEST_MODEL_ID,
569
570
  maxTokens: 100,
570
571
  keepRecentMessages: 1,
@@ -574,7 +575,7 @@ describe('ContextCompactor with tool calls', () => {
574
575
  })
575
576
 
576
577
  it('does not leave orphaned tool results at the start of kept messages', async () => {
577
- mockLLM.setResponses([
578
+ inference.setResponses([
578
579
  {
579
580
  content: 'Summary',
580
581
  toolCalls: [],
@@ -597,7 +598,7 @@ describe('ContextCompactor with tool calls', () => {
597
598
  { role: 'tool', content: 'export const foo = 1', toolCallId, toolName: 'read' },
598
599
  ]
599
600
 
600
- const result = await compactor.compact(sessionId, agentId, messages)
601
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
601
602
  expect(result.ok).toBe(true)
602
603
  if (!result.ok) return
603
604
 
@@ -609,41 +610,6 @@ describe('ContextCompactor with tool calls', () => {
609
610
  // All 5 original messages should be compacted (none kept except summary)
610
611
  expect(result.value.messagesRemoved).toBe(5)
611
612
  })
612
-
613
- it('includes tool calls in summarization request', async () => {
614
- mockLLM.setResponses([
615
- {
616
- content: 'Summary',
617
- toolCalls: [],
618
- finishReason: 'stop',
619
- metrics: { promptTokens: 50, completionTokens: 20, totalTokens: 70, latencyMs: 100, model: 'mock' },
620
- },
621
- ])
622
-
623
- const toolCallId = generateToolCallId()
624
- const messages: LLMMessage[] = [
625
- { role: 'user', content: 'Read the file' },
626
- {
627
- role: 'assistant',
628
- content: '',
629
- toolCalls: [{ id: toolCallId, name: 'read', input: { path: '/src/index.ts' } }],
630
- },
631
- { role: 'tool', content: 'export const foo = 1', toolCallId, toolName: 'read' },
632
- { role: 'user', content: 'recent message' },
633
- ]
634
-
635
- await compactor.compact(sessionId, agentId, messages)
636
-
637
- expect(mockLLM.calls.length).toBe(1)
638
- const request = mockLLM.calls[0]
639
- const summaryContent = request.messages[0].content as string
640
-
641
- // Verify tool call is included
642
- expect(summaryContent).toContain('[Called tools: read(path)]')
643
- // Verify tool result includes tool name
644
- expect(summaryContent).toContain('Tool(read):')
645
- expect(summaryContent).toContain('export const foo = 1')
646
- })
647
613
  })
648
614
 
649
615
  // ============================================================================
@@ -651,12 +617,12 @@ describe('ContextCompactor with tool calls', () => {
651
617
  // ============================================================================
652
618
 
653
619
  describe('ContextCompactor with history offloading', () => {
654
- let mockLLM: MockLLMProvider
620
+ let inference: InferenceMock
655
621
  let sessionId: SessionId
656
622
  let agentId: AgentId
657
623
 
658
624
  beforeEach(() => {
659
- mockLLM = new MockLLMProvider()
625
+ inference = new InferenceMock()
660
626
  sessionId = generateSessionId()
661
627
  agentId = generateTestAgentId()
662
628
  })
@@ -670,7 +636,7 @@ describe('ContextCompactor with history offloading', () => {
670
636
  },
671
637
  }
672
638
 
673
- mockLLM.setResponses([
639
+ inference.setResponses([
674
640
  {
675
641
  content: 'Summary',
676
642
  toolCalls: [],
@@ -680,7 +646,6 @@ describe('ContextCompactor with history offloading', () => {
680
646
  ])
681
647
 
682
648
  const compactor = new ContextCompactor(
683
- mockLLM,
684
649
  silentLogger,
685
650
  {
686
651
  model: TEST_MODEL_ID,
@@ -697,7 +662,7 @@ describe('ContextCompactor with history offloading', () => {
697
662
  { role: 'user', content: 'recent message' },
698
663
  ]
699
664
 
700
- const result = await compactor.compact(sessionId, agentId, messages)
665
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
701
666
 
702
667
  expect(result.ok).toBe(true)
703
668
  if (!result.ok) return
@@ -723,7 +688,7 @@ describe('ContextCompactor with history offloading', () => {
723
688
  },
724
689
  }
725
690
 
726
- mockLLM.setResponses([
691
+ inference.setResponses([
727
692
  {
728
693
  content: 'Summary',
729
694
  toolCalls: [],
@@ -733,7 +698,6 @@ describe('ContextCompactor with history offloading', () => {
733
698
  ])
734
699
 
735
700
  const compactor = new ContextCompactor(
736
- mockLLM,
737
701
  silentLogger,
738
702
  {
739
703
  model: TEST_MODEL_ID,
@@ -749,7 +713,7 @@ describe('ContextCompactor with history offloading', () => {
749
713
  { role: 'user', content: 'recent' },
750
714
  ]
751
715
 
752
- const result = await compactor.compact(sessionId, agentId, messages)
716
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
753
717
 
754
718
  expect(result.ok).toBe(true)
755
719
  if (!result.ok) return
@@ -759,7 +723,7 @@ describe('ContextCompactor with history offloading', () => {
759
723
  })
760
724
 
761
725
  it('does not offload history when offloader is not provided', async () => {
762
- mockLLM.setResponses([
726
+ inference.setResponses([
763
727
  {
764
728
  content: 'Summary',
765
729
  toolCalls: [],
@@ -769,7 +733,6 @@ describe('ContextCompactor with history offloading', () => {
769
733
  ])
770
734
 
771
735
  const compactor = new ContextCompactor(
772
- mockLLM,
773
736
  silentLogger,
774
737
  {
775
738
  model: TEST_MODEL_ID,
@@ -785,7 +748,7 @@ describe('ContextCompactor with history offloading', () => {
785
748
  { role: 'user', content: 'recent' },
786
749
  ]
787
750
 
788
- const result = await compactor.compact(sessionId, agentId, messages)
751
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
789
752
 
790
753
  expect(result.ok).toBe(true)
791
754
  if (!result.ok) return
@@ -802,7 +765,7 @@ describe('ContextCompactor with history offloading', () => {
802
765
  },
803
766
  }
804
767
 
805
- mockLLM.setResponses([
768
+ inference.setResponses([
806
769
  {
807
770
  content: 'Summary',
808
771
  toolCalls: [],
@@ -812,7 +775,6 @@ describe('ContextCompactor with history offloading', () => {
812
775
  ])
813
776
 
814
777
  const compactor = new ContextCompactor(
815
- mockLLM,
816
778
  silentLogger,
817
779
  {
818
780
  model: TEST_MODEL_ID,
@@ -829,7 +791,7 @@ describe('ContextCompactor with history offloading', () => {
829
791
  { role: 'user', content: 'recent' },
830
792
  ]
831
793
 
832
- await compactor.compact(sessionId, agentId, messages)
794
+ await compactor.compact(sessionId, agentId, messages, inference.run)
833
795
 
834
796
  expect(offloadedPaths.length).toBe(1)
835
797
  expect(offloadedPaths[0].pathPrefix).toBe('/session/.custom-history/')
@@ -842,7 +804,7 @@ describe('ContextCompactor with history offloading', () => {
842
804
  },
843
805
  }
844
806
 
845
- mockLLM.setResponses([
807
+ inference.setResponses([
846
808
  {
847
809
  content: 'Summary despite offload failure',
848
810
  toolCalls: [],
@@ -852,7 +814,6 @@ describe('ContextCompactor with history offloading', () => {
852
814
  ])
853
815
 
854
816
  const compactor = new ContextCompactor(
855
- mockLLM,
856
817
  silentLogger,
857
818
  {
858
819
  model: TEST_MODEL_ID,
@@ -868,7 +829,7 @@ describe('ContextCompactor with history offloading', () => {
868
829
  { role: 'user', content: 'recent' },
869
830
  ]
870
831
 
871
- const result = await compactor.compact(sessionId, agentId, messages)
832
+ const result = await compactor.compact(sessionId, agentId, messages, inference.run)
872
833
 
873
834
  // Compaction should succeed despite offload failure
874
835
  expect(result.ok).toBe(true)