@roj-ai/sdk 0.1.20 → 0.1.22

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 (98) hide show
  1. package/dist/core/agents/agent.d.ts.map +1 -1
  2. package/dist/core/agents/agent.js +13 -3
  3. package/dist/core/agents/agent.js.map +1 -1
  4. package/dist/core/context/state.d.ts +8 -0
  5. package/dist/core/context/state.d.ts.map +1 -1
  6. package/dist/core/context/state.js +10 -0
  7. package/dist/core/context/state.js.map +1 -1
  8. package/dist/core/events/base-event-store.d.ts.map +1 -1
  9. package/dist/core/events/base-event-store.js +2 -0
  10. package/dist/core/events/base-event-store.js.map +1 -1
  11. package/dist/core/events/metadata-utils.d.ts.map +1 -1
  12. package/dist/core/events/metadata-utils.js +2 -0
  13. package/dist/core/events/metadata-utils.js.map +1 -1
  14. package/dist/core/llm/anthropic.test.js +27 -0
  15. package/dist/core/llm/anthropic.test.js.map +1 -1
  16. package/dist/core/llm/cache-breakpoints.d.ts +19 -5
  17. package/dist/core/llm/cache-breakpoints.d.ts.map +1 -1
  18. package/dist/core/llm/cache-breakpoints.js +40 -23
  19. package/dist/core/llm/cache-breakpoints.js.map +1 -1
  20. package/dist/core/llm/cache-breakpoints.test.d.ts +2 -0
  21. package/dist/core/llm/cache-breakpoints.test.d.ts.map +1 -0
  22. package/dist/core/llm/cache-breakpoints.test.js +45 -0
  23. package/dist/core/llm/cache-breakpoints.test.js.map +1 -0
  24. package/dist/core/llm/state.d.ts +22 -0
  25. package/dist/core/llm/state.d.ts.map +1 -1
  26. package/dist/core/llm/state.js +23 -11
  27. package/dist/core/llm/state.js.map +1 -1
  28. package/dist/index.d.ts +5 -4
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/plugins/agent-status/agent-status.test.d.ts +2 -0
  33. package/dist/plugins/agent-status/agent-status.test.d.ts.map +1 -0
  34. package/dist/plugins/agent-status/agent-status.test.js +136 -0
  35. package/dist/plugins/agent-status/agent-status.test.js.map +1 -0
  36. package/dist/plugins/agent-status/plugin.d.ts +27 -0
  37. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  38. package/dist/plugins/agent-status/plugin.js +46 -0
  39. package/dist/plugins/agent-status/plugin.js.map +1 -1
  40. package/dist/plugins/agents/plugin.d.ts.map +1 -1
  41. package/dist/plugins/agents/plugin.js +7 -1
  42. package/dist/plugins/agents/plugin.js.map +1 -1
  43. package/dist/plugins/context-compact/context-compact.integration.test.js +54 -0
  44. package/dist/plugins/context-compact/context-compact.integration.test.js.map +1 -1
  45. package/dist/plugins/context-compact/context-compactor.d.ts +2 -0
  46. package/dist/plugins/context-compact/context-compactor.d.ts.map +1 -1
  47. package/dist/plugins/context-compact/context-compactor.js +29 -0
  48. package/dist/plugins/context-compact/context-compactor.js.map +1 -1
  49. package/dist/plugins/context-compact/context-compactor.test.js +6 -0
  50. package/dist/plugins/context-compact/context-compactor.test.js.map +1 -1
  51. package/dist/plugins/limits-guard/config.d.ts +30 -0
  52. package/dist/plugins/limits-guard/config.d.ts.map +1 -1
  53. package/dist/plugins/limits-guard/index.d.ts +3 -3
  54. package/dist/plugins/limits-guard/index.d.ts.map +1 -1
  55. package/dist/plugins/limits-guard/index.js +1 -1
  56. package/dist/plugins/limits-guard/index.js.map +1 -1
  57. package/dist/plugins/limits-guard/limit-guard.d.ts +27 -1
  58. package/dist/plugins/limits-guard/limit-guard.d.ts.map +1 -1
  59. package/dist/plugins/limits-guard/limit-guard.js +67 -0
  60. package/dist/plugins/limits-guard/limit-guard.js.map +1 -1
  61. package/dist/plugins/limits-guard/limit-guard.test.js +65 -1
  62. package/dist/plugins/limits-guard/limit-guard.test.js.map +1 -1
  63. package/dist/plugins/limits-guard/limits-guard.integration.test.js +295 -1
  64. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +1 -1
  65. package/dist/plugins/limits-guard/plugin.d.ts +23 -2
  66. package/dist/plugins/limits-guard/plugin.d.ts.map +1 -1
  67. package/dist/plugins/limits-guard/plugin.js +107 -2
  68. package/dist/plugins/limits-guard/plugin.js.map +1 -1
  69. package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
  70. package/dist/plugins/mailbox/plugin.js +18 -0
  71. package/dist/plugins/mailbox/plugin.js.map +1 -1
  72. package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
  73. package/dist/plugins/session-stats/plugin.js +5 -1
  74. package/dist/plugins/session-stats/plugin.js.map +1 -1
  75. package/package.json +2 -2
  76. package/src/core/agents/agent.ts +18 -2
  77. package/src/core/context/state.ts +10 -0
  78. package/src/core/events/base-event-store.ts +2 -0
  79. package/src/core/events/metadata-utils.ts +2 -0
  80. package/src/core/llm/anthropic.test.ts +34 -0
  81. package/src/core/llm/cache-breakpoints.test.ts +55 -0
  82. package/src/core/llm/cache-breakpoints.ts +39 -21
  83. package/src/core/llm/state.ts +25 -11
  84. package/src/index.ts +5 -4
  85. package/src/plugins/agent-status/agent-status.test.ts +164 -0
  86. package/src/plugins/agent-status/plugin.ts +49 -0
  87. package/src/plugins/agents/plugin.ts +7 -1
  88. package/src/plugins/context-compact/context-compact.integration.test.ts +62 -0
  89. package/src/plugins/context-compact/context-compactor.test.ts +6 -0
  90. package/src/plugins/context-compact/context-compactor.ts +31 -0
  91. package/src/plugins/limits-guard/config.ts +35 -0
  92. package/src/plugins/limits-guard/index.ts +3 -3
  93. package/src/plugins/limits-guard/limit-guard.test.ts +80 -1
  94. package/src/plugins/limits-guard/limit-guard.ts +98 -1
  95. package/src/plugins/limits-guard/limits-guard.integration.test.ts +331 -1
  96. package/src/plugins/limits-guard/plugin.ts +153 -3
  97. package/src/plugins/mailbox/plugin.ts +18 -0
  98. package/src/plugins/session-stats/plugin.ts +5 -1
@@ -1,5 +1,6 @@
1
1
  import type { AgentId } from '~/core/agents/schema.js'
2
2
  import { agentEvents } from '~/core/agents/state.js'
3
+ import { contextEvents } from '~/core/context/state.js'
3
4
  import { llmEvents } from '~/core/llm/state.js'
4
5
  import { definePlugin } from '~/core/plugins/plugin-builder.js'
5
6
  import { selectPluginState } from '~/core/sessions/reducer.js'
@@ -7,8 +8,15 @@ import type { SessionState } from '~/core/sessions/state.js'
7
8
  import { toolEvents } from '~/core/tools/state.js'
8
9
  import { responseFingerprint, toolCallFingerprint } from '~/lib/utils/hash.js'
9
10
  import { mailboxEvents } from '~/plugins/mailbox/state.js'
10
- import type { AgentLimits } from './config.js'
11
- import { checkLimits, countConsecutiveTailDuplicates, resolveAgentLimits } from './limit-guard.js'
11
+ import type { AgentLimits, LimitsSessionConfig } from './config.js'
12
+ import {
13
+ type BudgetSpend,
14
+ checkBudget,
15
+ checkLimits,
16
+ countConsecutiveTailDuplicates,
17
+ resolveAgentLimits,
18
+ resolveSessionLimits,
19
+ } from './limit-guard.js'
12
20
 
13
21
  // ============================================================================
14
22
  // Agent counters (state)
@@ -19,6 +27,12 @@ export interface AgentCounters {
19
27
  toolCallCount: number
20
28
  spawnedAgentCount: number
21
29
  messagesSentCount: number
30
+ /** Number of context compaction events for this agent. */
31
+ compactionCount: number
32
+ /** Cumulative LLM cost (USD) summed from inference metrics. NOT reset on resume. */
33
+ costSpent: number
34
+ /** Cumulative total tokens (prompt + completion). NOT reset on resume. */
35
+ tokensUsed: number
22
36
  /** Tool name → consecutive failure count + last error message. Reset on success. */
23
37
  consecutiveToolFailures: Record<string, { count: number; lastError: string }>
24
38
  /** Ring buffer of last 20 tool call fingerprints ("toolName:inputHash") */
@@ -32,11 +46,25 @@ export const createAgentCounters = (): AgentCounters => ({
32
46
  toolCallCount: 0,
33
47
  spawnedAgentCount: 0,
34
48
  messagesSentCount: 0,
49
+ compactionCount: 0,
50
+ costSpent: 0,
51
+ tokensUsed: 0,
35
52
  consecutiveToolFailures: {},
36
53
  recentToolCallHashes: [],
37
54
  recentResponseHashes: [],
38
55
  })
39
56
 
57
+ /** Sum cost + tokens across all agents in the session (for the session-wide budget). */
58
+ export function sumSessionSpend(limits: Map<AgentId, AgentCounters>): BudgetSpend {
59
+ let costSpent = 0
60
+ let tokensUsed = 0
61
+ for (const counters of limits.values()) {
62
+ costSpent += counters.costSpent
63
+ tokensUsed += counters.tokensUsed
64
+ }
65
+ return { costSpent, tokensUsed }
66
+ }
67
+
40
68
  /**
41
69
  * Extract agent counters from session state (for external consumers).
42
70
  */
@@ -61,10 +89,21 @@ export const limitsEvents = createEventsFactory({
61
89
  hardLimit: z.number(),
62
90
  message: z.string(),
63
91
  }),
92
+ budget_exceeded: z.object({
93
+ agentId: agentIdSchema,
94
+ /** Whether the per-agent or the session-wide budget was hit. */
95
+ scope: z.enum(['agent', 'session']),
96
+ /** Which limit tripped: maxCost / maxTokens / maxSessionCost / maxSessionTokens. */
97
+ limitName: z.string(),
98
+ spent: z.number(),
99
+ limit: z.number(),
100
+ message: z.string(),
101
+ }),
64
102
  },
65
103
  })
66
104
 
67
105
  export type LimitWarningEvent = (typeof limitsEvents)['Events']['limit_warning']
106
+ export type BudgetExceededEvent = (typeof limitsEvents)['Events']['budget_exceeded']
68
107
 
69
108
  // ============================================================================
70
109
  // Helper
@@ -95,7 +134,8 @@ export interface LimitsAgentConfig {
95
134
  }
96
135
 
97
136
  export const limitsGuardPlugin = definePlugin('limits-guard')
98
- .events([agentEvents, llmEvents, toolEvents, mailboxEvents])
137
+ .events([agentEvents, llmEvents, toolEvents, mailboxEvents, contextEvents, limitsEvents])
138
+ .pluginConfig<LimitsSessionConfig>()
99
139
  .state({
100
140
  key: 'agentLimits',
101
141
  initial: (): Map<AgentId, AgentCounters> => new Map(),
@@ -156,11 +196,41 @@ export const limitsGuardPlugin = definePlugin('limits-guard')
156
196
  newLimits.set(event.agentId, {
157
197
  ...counters,
158
198
  inferenceCount: counters.inferenceCount + 1,
199
+ costSpent: counters.costSpent + (event.metrics.cost ?? 0),
200
+ tokensUsed: counters.tokensUsed + (event.metrics.totalTokens ?? 0),
159
201
  recentResponseHashes: newRecentResponseHashes,
160
202
  })
161
203
  return newLimits
162
204
  }
163
205
 
206
+ case 'auxiliary_inference_completed': {
207
+ // Side-channel calls (e.g. context compaction) are billed but don't
208
+ // touch conversation state, so they count toward cost/token budgets
209
+ // but NOT toward inferenceCount or the anti-looping response hashes.
210
+ const counters = limits.get(event.agentId)
211
+ if (!counters) return limits
212
+
213
+ const newLimits = new Map(limits)
214
+ newLimits.set(event.agentId, {
215
+ ...counters,
216
+ costSpent: counters.costSpent + (event.metrics.cost ?? 0),
217
+ tokensUsed: counters.tokensUsed + (event.metrics.totalTokens ?? 0),
218
+ })
219
+ return newLimits
220
+ }
221
+
222
+ case 'context_compacted': {
223
+ const counters = limits.get(event.agentId)
224
+ if (!counters) return limits
225
+
226
+ const newLimits = new Map(limits)
227
+ newLimits.set(event.agentId, {
228
+ ...counters,
229
+ compactionCount: counters.compactionCount + 1,
230
+ })
231
+ return newLimits
232
+ }
233
+
164
234
  case 'tool_started': {
165
235
  const counters = limits.get(event.agentId)
166
236
  if (!counters) return limits
@@ -214,6 +284,11 @@ export const limitsGuardPlugin = definePlugin('limits-guard')
214
284
  const counters = limits.get(event.agentId)
215
285
  if (!counters) return limits
216
286
 
287
+ // Reset the anti-looping counters so the agent can make progress
288
+ // again. Budget spend (costSpent/tokensUsed) is deliberately NOT
289
+ // reset — otherwise a per-agent or session cost cap could be bypassed
290
+ // by repeatedly pausing and resuming. To grant more budget, raise the
291
+ // configured limit instead.
217
292
  const newLimits = new Map(limits)
218
293
  newLimits.set(event.agentId, {
219
294
  ...counters,
@@ -221,6 +296,7 @@ export const limitsGuardPlugin = definePlugin('limits-guard')
221
296
  toolCallCount: 0,
222
297
  spawnedAgentCount: 0,
223
298
  messagesSentCount: 0,
299
+ compactionCount: 0,
224
300
  consecutiveToolFailures: {},
225
301
  recentToolCallHashes: [],
226
302
  recentResponseHashes: [],
@@ -234,6 +310,56 @@ export const limitsGuardPlugin = definePlugin('limits-guard')
234
310
  },
235
311
  })
236
312
  .agentConfig<LimitsAgentConfig>()
313
+ .hook('beforeInference', async (ctx) => {
314
+ // Budgets are enforced here (before the call) so an exhausted agent is
315
+ // paused before spending more — cost/tokens of a call aren't known until
316
+ // after it returns, so we stop the *next* call once the threshold is hit.
317
+ const counters = ctx.pluginState.get(ctx.agentId)
318
+ if (!counters) return null
319
+
320
+ const agentLimits = resolveAgentLimits(ctx.pluginAgentConfig?.limits)
321
+ const agentCheck = checkBudget(
322
+ counters,
323
+ agentLimits.maxCost,
324
+ agentLimits.maxTokens,
325
+ agentLimits.softLimitRatio,
326
+ { cost: 'maxCost', tokens: 'maxTokens' },
327
+ )
328
+ if (agentCheck.status === 'hard_limit') {
329
+ await ctx.emitEvent(limitsEvents.create('budget_exceeded', {
330
+ agentId: ctx.agentId,
331
+ scope: 'agent',
332
+ limitName: agentCheck.limitName,
333
+ spent: agentCheck.currentValue,
334
+ limit: agentCheck.hardLimit,
335
+ message: agentCheck.reason,
336
+ }))
337
+ return { action: 'pause', reason: `Agent budget exceeded — ${agentCheck.reason}` }
338
+ }
339
+
340
+ const sessionLimits = resolveSessionLimits(ctx.pluginConfig)
341
+ const sessionSpend = sumSessionSpend(ctx.pluginState)
342
+ const sessionCheck = checkBudget(
343
+ sessionSpend,
344
+ sessionLimits.maxSessionCost,
345
+ sessionLimits.maxSessionTokens,
346
+ sessionLimits.softLimitRatio,
347
+ { cost: 'maxSessionCost', tokens: 'maxSessionTokens' },
348
+ )
349
+ if (sessionCheck.status === 'hard_limit') {
350
+ await ctx.emitEvent(limitsEvents.create('budget_exceeded', {
351
+ agentId: ctx.agentId,
352
+ scope: 'session',
353
+ limitName: sessionCheck.limitName,
354
+ spent: sessionCheck.currentValue,
355
+ limit: sessionCheck.hardLimit,
356
+ message: sessionCheck.reason,
357
+ }))
358
+ return { action: 'pause', reason: `Session budget exceeded — ${sessionCheck.reason}` }
359
+ }
360
+
361
+ return null
362
+ })
237
363
  .hook('afterInference', async (ctx) => {
238
364
  const resolvedLimits = resolveAgentLimits(ctx.pluginAgentConfig?.limits)
239
365
  const counters = ctx.pluginState.get(ctx.agentId)
@@ -301,6 +427,30 @@ export const limitsGuardPlugin = definePlugin('limits-guard')
301
427
  )
302
428
  }
303
429
 
430
+ // Budget soft warnings — per-agent spend, then the session-wide budget.
431
+ const agentBudget = checkBudget(
432
+ counters,
433
+ resolvedLimits.maxCost,
434
+ resolvedLimits.maxTokens,
435
+ resolvedLimits.softLimitRatio,
436
+ { cost: 'maxCost', tokens: 'maxTokens' },
437
+ )
438
+ if (agentBudget.status === 'soft_warning') {
439
+ parts.push(`⚠️ ${agentBudget.message}. You will be paused when the budget is reached — wrap up.`)
440
+ }
441
+
442
+ const sessionLimits = resolveSessionLimits(ctx.pluginConfig)
443
+ const sessionBudget = checkBudget(
444
+ sumSessionSpend(ctx.pluginState),
445
+ sessionLimits.maxSessionCost,
446
+ sessionLimits.maxSessionTokens,
447
+ sessionLimits.softLimitRatio,
448
+ { cost: 'maxSessionCost', tokens: 'maxSessionTokens' },
449
+ )
450
+ if (sessionBudget.status === 'soft_warning') {
451
+ parts.push(`⚠️ Session-wide ${sessionBudget.message}.`)
452
+ }
453
+
304
454
  return parts.length > 0 ? parts.join('\n\n') : null
305
455
  })
306
456
  .build()
@@ -228,6 +228,24 @@ export const mailboxPlugin = definePlugin("mailbox")
228
228
  });
229
229
  return null;
230
230
  })
231
+ .hook("onPause", async (ctx) => {
232
+ // Notify the parent immediately when a child pauses (budget/limit exhaustion,
233
+ // manual pause, …) so it can react: resume after addressing the cause,
234
+ // reassign the work, or stop. Lives here (not in the agents plugin) because
235
+ // the agents plugin is disabled on leaf agents that can't spawn — but those
236
+ // are exactly the agents that pause and need to report upward. mailbox is
237
+ // enabled on every agent, and self.send wakes the parent. Root agents
238
+ // (no parent) have no one to notify.
239
+ const parentId = ctx.agentState.parentId;
240
+ if (!parentId || !ctx.sessionState.agents.has(parentId)) return null;
241
+
242
+ await ctx.self.send({
243
+ fromAgentId: ctx.agentId,
244
+ toAgentId: parentId,
245
+ content: `<child-paused agent="${ctx.agentId}">${ctx.reason ?? "no reason given"}</child-paused>`,
246
+ });
247
+ return null;
248
+ })
231
249
  .systemPrompt((ctx) => {
232
250
  const role = getAgentRole(ctx.agentState, ctx.sessionState);
233
251
  switch (role) {
@@ -79,7 +79,11 @@ export const sessionStatsPlugin = definePlugin('session-stats')
79
79
  case 'agent_spawned':
80
80
  return withTimestamp({ agentCount: stats.agentCount + 1 })
81
81
 
82
- case 'inference_completed': {
82
+ // inference_completed = main agent turns; auxiliary_inference_completed =
83
+ // side-channel calls (e.g. context compaction). Both are billed LLM
84
+ // calls, so both feed the same usage/cost accounting.
85
+ case 'inference_completed':
86
+ case 'auxiliary_inference_completed': {
83
87
  const provider = event.metrics.provider
84
88
  const byProvider = provider
85
89
  ? {