@roj-ai/sdk 0.1.29 → 0.1.30

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 (114) hide show
  1. package/dist/core/agents/agent.d.ts +28 -0
  2. package/dist/core/agents/agent.d.ts.map +1 -1
  3. package/dist/core/agents/agent.js +116 -39
  4. package/dist/core/agents/agent.js.map +1 -1
  5. package/dist/core/agents/config.d.ts +9 -0
  6. package/dist/core/agents/config.d.ts.map +1 -1
  7. package/dist/core/agents/state.d.ts +9 -0
  8. package/dist/core/agents/state.d.ts.map +1 -1
  9. package/dist/core/agents/state.js +4 -0
  10. package/dist/core/agents/state.js.map +1 -1
  11. package/dist/core/context/state.d.ts +22 -13
  12. package/dist/core/context/state.d.ts.map +1 -1
  13. package/dist/core/context/state.js +78 -22
  14. package/dist/core/context/state.js.map +1 -1
  15. package/dist/core/error-recovery.integration.test.d.ts +2 -0
  16. package/dist/core/error-recovery.integration.test.d.ts.map +1 -0
  17. package/dist/core/error-recovery.integration.test.js +288 -0
  18. package/dist/core/error-recovery.integration.test.js.map +1 -0
  19. package/dist/core/llm/anthropic.d.ts.map +1 -1
  20. package/dist/core/llm/anthropic.js +6 -1
  21. package/dist/core/llm/anthropic.js.map +1 -1
  22. package/dist/core/llm/anthropic.test.js +7 -4
  23. package/dist/core/llm/anthropic.test.js.map +1 -1
  24. package/dist/core/llm/message-sanitization.d.ts +10 -0
  25. package/dist/core/llm/message-sanitization.d.ts.map +1 -0
  26. package/dist/core/llm/message-sanitization.js +53 -0
  27. package/dist/core/llm/message-sanitization.js.map +1 -0
  28. package/dist/core/llm/message-sanitization.test.d.ts +2 -0
  29. package/dist/core/llm/message-sanitization.test.d.ts.map +1 -0
  30. package/dist/core/llm/message-sanitization.test.js +181 -0
  31. package/dist/core/llm/message-sanitization.test.js.map +1 -0
  32. package/dist/core/llm/middleware.d.ts +3 -3
  33. package/dist/core/llm/middleware.js +3 -3
  34. package/dist/core/llm/openrouter-mapping.test.js +21 -3
  35. package/dist/core/llm/openrouter-mapping.test.js.map +1 -1
  36. package/dist/core/llm/openrouter.d.ts.map +1 -1
  37. package/dist/core/llm/openrouter.js +6 -1
  38. package/dist/core/llm/openrouter.js.map +1 -1
  39. package/dist/core/llm/provider.d.ts +13 -10
  40. package/dist/core/llm/provider.d.ts.map +1 -1
  41. package/dist/core/llm/provider.js.map +1 -1
  42. package/dist/core/llm/state.d.ts +10 -0
  43. package/dist/core/llm/state.d.ts.map +1 -1
  44. package/dist/core/llm/state.js +9 -0
  45. package/dist/core/llm/state.js.map +1 -1
  46. package/dist/core/plugins/plugin-builder.d.ts +2 -0
  47. package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
  48. package/dist/core/plugins/plugin-builder.js.map +1 -1
  49. package/dist/core/sessions/session.d.ts.map +1 -1
  50. package/dist/core/sessions/session.js +6 -0
  51. package/dist/core/sessions/session.js.map +1 -1
  52. package/dist/core/sessions/state.d.ts.map +1 -1
  53. package/dist/core/sessions/state.js +43 -5
  54. package/dist/core/sessions/state.js.map +1 -1
  55. package/dist/plugins/context-compact/context-compact.integration.test.js +36 -20
  56. package/dist/plugins/context-compact/context-compact.integration.test.js.map +1 -1
  57. package/dist/plugins/context-compact/context-compactor.d.ts.map +1 -1
  58. package/dist/plugins/context-compact/context-compactor.js +54 -9
  59. package/dist/plugins/context-compact/context-compactor.js.map +1 -1
  60. package/dist/plugins/context-compact/context-compactor.test.js +127 -4
  61. package/dist/plugins/context-compact/context-compactor.test.js.map +1 -1
  62. package/dist/plugins/limits-guard/config.d.ts +2 -0
  63. package/dist/plugins/limits-guard/config.d.ts.map +1 -1
  64. package/dist/plugins/limits-guard/limit-guard.d.ts +1 -0
  65. package/dist/plugins/limits-guard/limit-guard.d.ts.map +1 -1
  66. package/dist/plugins/limits-guard/limit-guard.js +11 -1
  67. package/dist/plugins/limits-guard/limit-guard.js.map +1 -1
  68. package/dist/plugins/limits-guard/limit-guard.test.js +9 -0
  69. package/dist/plugins/limits-guard/limit-guard.test.js.map +1 -1
  70. package/dist/plugins/limits-guard/limits-guard.integration.test.js +216 -1
  71. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +1 -1
  72. package/dist/plugins/limits-guard/plugin.d.ts +3 -1
  73. package/dist/plugins/limits-guard/plugin.d.ts.map +1 -1
  74. package/dist/plugins/limits-guard/plugin.js +48 -2
  75. package/dist/plugins/limits-guard/plugin.js.map +1 -1
  76. package/dist/plugins/uploads/plugin.d.ts.map +1 -1
  77. package/dist/plugins/uploads/plugin.js +10 -1
  78. package/dist/plugins/uploads/plugin.js.map +1 -1
  79. package/dist/plugins/uploads/uploads.integration.test.js +74 -28
  80. package/dist/plugins/uploads/uploads.integration.test.js.map +1 -1
  81. package/dist/testing/test-harness.d.ts +8 -0
  82. package/dist/testing/test-harness.d.ts.map +1 -1
  83. package/dist/testing/test-harness.js +18 -0
  84. package/dist/testing/test-harness.js.map +1 -1
  85. package/dist/transport/http/middleware/error-handler.d.ts +1 -1
  86. package/package.json +2 -2
  87. package/src/core/agents/agent.ts +124 -36
  88. package/src/core/agents/config.ts +6 -0
  89. package/src/core/agents/state.ts +9 -0
  90. package/src/core/context/state.ts +86 -27
  91. package/src/core/error-recovery.integration.test.ts +343 -0
  92. package/src/core/llm/anthropic.test.ts +7 -4
  93. package/src/core/llm/anthropic.ts +6 -1
  94. package/src/core/llm/message-sanitization.test.ts +217 -0
  95. package/src/core/llm/message-sanitization.ts +70 -0
  96. package/src/core/llm/middleware.ts +3 -3
  97. package/src/core/llm/openrouter-mapping.test.ts +24 -3
  98. package/src/core/llm/openrouter.ts +7 -8
  99. package/src/core/llm/provider.ts +14 -10
  100. package/src/core/llm/state.ts +10 -0
  101. package/src/core/plugins/plugin-builder.ts +6 -2
  102. package/src/core/sessions/session.ts +6 -0
  103. package/src/core/sessions/state.ts +48 -9
  104. package/src/plugins/context-compact/context-compact.integration.test.ts +38 -21
  105. package/src/plugins/context-compact/context-compactor.test.ts +150 -4
  106. package/src/plugins/context-compact/context-compactor.ts +64 -10
  107. package/src/plugins/limits-guard/config.ts +2 -0
  108. package/src/plugins/limits-guard/limit-guard.test.ts +13 -0
  109. package/src/plugins/limits-guard/limit-guard.ts +13 -1
  110. package/src/plugins/limits-guard/limits-guard.integration.test.ts +246 -1
  111. package/src/plugins/limits-guard/plugin.ts +53 -3
  112. package/src/plugins/uploads/plugin.ts +11 -1
  113. package/src/plugins/uploads/uploads.integration.test.ts +82 -28
  114. package/src/testing/test-harness.ts +20 -0
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, it } from 'bun:test'
2
2
  import z from 'zod/v4'
3
+ import { agentEvents } from '~/core/agents/state.js'
3
4
  import { MockLLMProvider } from '~/core/llm/mock.js'
4
5
  import { selectPluginState } from '~/core/sessions/reducer.js'
5
6
  import { createTestPreset, TestHarness } from '~/testing/index.js'
7
+ import type { TestSession } from '~/testing/index.js'
6
8
  import type { UploadsState } from './state.js'
7
9
  import { uploadEvents } from './state.js'
8
10
 
@@ -17,6 +19,12 @@ function okValue<T>(result: { ok: boolean; value?: unknown }, schema: z.ZodType<
17
19
  return schema.parse(result.value)
18
20
  }
19
21
 
22
+ async function pauseEntryAgent(session: TestSession): Promise<void> {
23
+ const entryAgentId = session.getEntryAgentId()
24
+ if (!entryAgentId) throw new Error('Expected entry agent')
25
+ await session.pauseAgent(entryAgentId, 'Keep upload pending for storage test')
26
+ }
27
+
20
28
  const uploadResultSchema = z.object({
21
29
  uploadId: z.string(),
22
30
  status: z.enum(['ready', 'failed']),
@@ -174,7 +182,7 @@ describe('uploads plugin', () => {
174
182
  // =========================================================================
175
183
 
176
184
  describe('dequeue', () => {
177
- it('pending upload delivered to agent as LLM message with <attachment> XML', async () => {
185
+ it('upload arriving while agent is idle wakes exactly one inference', async () => {
178
186
  const harness = new TestHarness({
179
187
  presets: [createTestPreset()],
180
188
  llmProvider: MockLLMProvider.withFixedResponse({ content: 'Got it', toolCalls: [] }),
@@ -191,8 +199,8 @@ describe('uploads plugin', () => {
191
199
  fileBuffer: fileContent,
192
200
  })
193
201
 
194
- // Send a message to trigger inference — the pending upload should be dequeued
195
- await session.sendAndWaitForIdle('Process the file')
202
+ await session.waitForIdle()
203
+ expect(harness.llmProvider.getCallCount()).toBe(1)
196
204
 
197
205
  // Check that the LLM received the attachment in its messages
198
206
  const lastRequest = harness.llmProvider.getLastRequest()
@@ -209,41 +217,48 @@ describe('uploads plugin', () => {
209
217
  expect(attachmentMessage.content).toContain('filename="doc.txt"')
210
218
  expect(attachmentMessage.content).toContain('type="text/plain"')
211
219
 
220
+ const consumedEvents = await session.getEventsByType(agentEvents, 'agent_input_consumed')
221
+ expect(consumedEvents).toHaveLength(1)
222
+ expect(consumedEvents[0].sourcePlugins).toContain('uploads')
223
+
212
224
  await harness.shutdown()
213
225
  })
214
226
 
215
- it('after delivery, upload consumed (attachments_consumed event, removed from pending)', async () => {
227
+ it('several quick uploads debounce into one inference and one consumption event', async () => {
216
228
  const harness = new TestHarness({
217
229
  presets: [createTestPreset()],
218
230
  llmProvider: MockLLMProvider.withFixedResponse({ content: 'Got it', toolCalls: [] }),
219
231
  })
220
232
 
221
233
  const session = await harness.createSession('test')
222
- const fileContent = Buffer.from('Some data')
223
-
224
- await session.callPluginMethod('uploads.upload', {
225
- sessionId: String(session.sessionId),
226
- filename: 'data.txt',
227
- mimeType: 'text/plain',
228
- size: fileContent.length,
229
- fileBuffer: fileContent,
230
- })
234
+ const filenames = ['one.txt', 'two.txt', 'three.txt']
235
+ await Promise.all(filenames.map((filename) => {
236
+ const fileContent = Buffer.from(`Content of ${filename}`)
237
+ return session.callPluginMethod('uploads.upload', {
238
+ sessionId: String(session.sessionId),
239
+ filename,
240
+ mimeType: 'text/plain',
241
+ size: fileContent.length,
242
+ fileBuffer: fileContent,
243
+ })
244
+ }))
231
245
 
232
- // Before inference, should be in pending
233
- const uploadsBefore = selectPluginState<UploadsState>(session.state, 'uploads')
234
- expect(uploadsBefore).toBeDefined()
235
- if (!uploadsBefore) throw new Error('Expected uploads state')
236
- expect(uploadsBefore.pending).toHaveLength(1)
246
+ await session.waitForIdle()
247
+ expect(harness.llmProvider.getCallCount()).toBe(1)
237
248
 
238
- // Trigger inference
239
- await session.sendAndWaitForIdle('Handle this')
249
+ const lastRequest = harness.llmProvider.getLastRequest()
250
+ expect(lastRequest).toBeDefined()
251
+ const requestText = lastRequest?.messages
252
+ .map((message) => typeof message.content === 'string' ? message.content : '')
253
+ .join('\n') ?? ''
254
+ for (const filename of filenames) {
255
+ expect(requestText).toContain(`filename="${filename}"`)
256
+ }
240
257
 
241
- // After inference, should be consumed
242
258
  const consumedEvents = await session.getEventsByType(uploadEvents, 'attachments_consumed')
243
259
  expect(consumedEvents).toHaveLength(1)
244
- expect(consumedEvents[0].uploadIds).toHaveLength(1)
260
+ expect(consumedEvents[0].uploadIds).toHaveLength(3)
245
261
 
246
- // Pending should be empty
247
262
  const uploadsAfter = selectPluginState<UploadsState>(session.state, 'uploads')
248
263
  expect(uploadsAfter).toBeDefined()
249
264
  if (!uploadsAfter) throw new Error('Expected uploads state')
@@ -251,6 +266,36 @@ describe('uploads plugin', () => {
251
266
 
252
267
  await harness.shutdown()
253
268
  })
269
+
270
+ it('upload does not wake a paused agent', async () => {
271
+ const harness = new TestHarness({
272
+ presets: [createTestPreset()],
273
+ llmProvider: MockLLMProvider.withFixedResponse({ content: 'Got it', toolCalls: [] }),
274
+ })
275
+
276
+ const session = await harness.createSession('test')
277
+ const entryAgentId = session.getEntryAgentId()
278
+ if (!entryAgentId) throw new Error('Expected entry agent')
279
+ await session.pauseAgent(entryAgentId, 'Paused by user')
280
+
281
+ const fileContent = Buffer.from('Wait until resume')
282
+ await session.callPluginMethod('uploads.upload', {
283
+ sessionId: String(session.sessionId),
284
+ filename: 'paused.txt',
285
+ mimeType: 'text/plain',
286
+ size: fileContent.length,
287
+ fileBuffer: fileContent,
288
+ })
289
+
290
+ await new Promise((resolve) => setTimeout(resolve, 25))
291
+ expect(harness.llmProvider.getCallCount()).toBe(0)
292
+ expect(session.state.agents.get(entryAgentId)?.status).toBe('paused')
293
+
294
+ const uploads = selectPluginState<UploadsState>(session.state, 'uploads')
295
+ expect(uploads?.pending).toHaveLength(1)
296
+
297
+ await harness.shutdown()
298
+ })
254
299
  })
255
300
 
256
301
  // =========================================================================
@@ -265,6 +310,7 @@ describe('uploads plugin', () => {
265
310
  })
266
311
 
267
312
  const session = await harness.createSession('test')
313
+ await pauseEntryAgent(session)
268
314
 
269
315
  await session.callPluginMethod('uploads.upload', {
270
316
  sessionId: String(session.sessionId),
@@ -304,6 +350,7 @@ describe('uploads plugin', () => {
304
350
  })
305
351
 
306
352
  const session = await harness.createSession('test')
353
+ await pauseEntryAgent(session)
307
354
 
308
355
  const uploadData = okValue(
309
356
  await session.callPluginMethod('uploads.upload', {
@@ -340,6 +387,7 @@ describe('uploads plugin', () => {
340
387
  })
341
388
 
342
389
  const session = await harness.createSession('test')
390
+ await pauseEntryAgent(session)
343
391
 
344
392
  const uploadData = okValue(
345
393
  await session.callPluginMethod('uploads.upload', {
@@ -381,6 +429,7 @@ describe('uploads plugin', () => {
381
429
  })
382
430
 
383
431
  const session = await harness.createSession('test')
432
+ await pauseEntryAgent(session)
384
433
 
385
434
  const uploadData = okValue(
386
435
  await session.callPluginMethod('uploads.upload', {
@@ -417,6 +466,7 @@ describe('uploads plugin', () => {
417
466
  })
418
467
 
419
468
  const session = await harness.createSession('test')
469
+ await pauseEntryAgent(session)
420
470
 
421
471
  const uploadData = okValue(
422
472
  await session.callPluginMethod('uploads.upload', {
@@ -482,6 +532,7 @@ describe('uploads plugin', () => {
482
532
  })
483
533
 
484
534
  const session = await harness.createSession('test')
535
+ await pauseEntryAgent(session)
485
536
 
486
537
  const uploadData = okValue(
487
538
  await session.callPluginMethod('uploads.upload', {
@@ -617,7 +668,7 @@ describe('uploads plugin', () => {
617
668
  // uploadAsync method
618
669
  // =========================================================================
619
670
 
620
- it('uploadAsync returns processing immediately, then statusChanged → ready', async () => {
671
+ it('uploadAsync returns processing, then ready upload wakes the idle agent', async () => {
621
672
  const harness = new TestHarness({
622
673
  presets: [createTestPreset()],
623
674
  llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
@@ -655,12 +706,13 @@ describe('uploads plugin', () => {
655
706
  expect(own[0].status).toBe('processing')
656
707
  expect(own[1].status).toBe('ready')
657
708
 
658
- // State only carries ready uploads — processing event is filtered by reducer.
709
+ await session.waitForIdle()
710
+ expect(harness.llmProvider.getCallCount()).toBe(1)
711
+
712
+ // The ready upload was delivered and consumed by the woken agent.
659
713
  const uploads = selectPluginState<UploadsState>(session.state, 'uploads')
660
714
  if (!uploads) throw new Error('Expected uploads state')
661
- expect(uploads.pending).toHaveLength(1)
662
- expect(uploads.pending[0]?.uploadId).toBe(data.uploadId)
663
- expect(uploads.pending[0]?.status).toBe('ready')
715
+ expect(uploads.pending).toHaveLength(0)
664
716
 
665
717
  // First notification was processing.
666
718
  const all = harness.notifications.getByType('uploads', 'uploadStatusChanged')
@@ -704,6 +756,7 @@ describe('uploads plugin', () => {
704
756
  })
705
757
 
706
758
  const session = await harness.createSession('test')
759
+ await pauseEntryAgent(session)
707
760
 
708
761
  const result = await session.callPluginMethod('uploads.uploadAsync', {
709
762
  sessionId: String(session.sessionId),
@@ -743,6 +796,7 @@ describe('uploads plugin', () => {
743
796
  })
744
797
 
745
798
  const session = await harness.createSession('test')
799
+ await pauseEntryAgent(session)
746
800
 
747
801
  const uploadData = okValue(
748
802
  await session.callPluginMethod('uploads.upload', {
@@ -275,6 +275,26 @@ export class TestSession {
275
275
  return this.session.getEntryAgentId()
276
276
  }
277
277
 
278
+ /**
279
+ * Pause an agent (the manual-pause API).
280
+ */
281
+ async pauseAgent(agentId: AgentId, message?: string): Promise<void> {
282
+ const result = await this.session.pauseAgent(agentId, message)
283
+ if (!result.ok) {
284
+ throw new Error(`pauseAgent failed: ${result.error.type} — ${result.error.message}`)
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Resume a paused (or errored) agent.
290
+ */
291
+ async resumeAgent(agentId: AgentId): Promise<void> {
292
+ const result = await this.session.resumeAgent(agentId)
293
+ if (!result.ok) {
294
+ throw new Error(`resumeAgent failed: ${result.error.type} — ${result.error.message}`)
295
+ }
296
+ }
297
+
278
298
  /**
279
299
  * Close the session.
280
300
  */