@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.
- package/dist/core/agents/agent.d.ts.map +1 -1
- package/dist/core/agents/agent.js +13 -3
- package/dist/core/agents/agent.js.map +1 -1
- package/dist/core/context/state.d.ts +8 -0
- package/dist/core/context/state.d.ts.map +1 -1
- package/dist/core/context/state.js +10 -0
- package/dist/core/context/state.js.map +1 -1
- package/dist/core/events/base-event-store.d.ts.map +1 -1
- package/dist/core/events/base-event-store.js +2 -0
- package/dist/core/events/base-event-store.js.map +1 -1
- package/dist/core/events/metadata-utils.d.ts.map +1 -1
- package/dist/core/events/metadata-utils.js +2 -0
- package/dist/core/events/metadata-utils.js.map +1 -1
- package/dist/core/llm/anthropic.test.js +27 -0
- package/dist/core/llm/anthropic.test.js.map +1 -1
- package/dist/core/llm/cache-breakpoints.d.ts +19 -5
- package/dist/core/llm/cache-breakpoints.d.ts.map +1 -1
- package/dist/core/llm/cache-breakpoints.js +40 -23
- package/dist/core/llm/cache-breakpoints.js.map +1 -1
- package/dist/core/llm/cache-breakpoints.test.d.ts +2 -0
- package/dist/core/llm/cache-breakpoints.test.d.ts.map +1 -0
- package/dist/core/llm/cache-breakpoints.test.js +45 -0
- package/dist/core/llm/cache-breakpoints.test.js.map +1 -0
- package/dist/core/llm/state.d.ts +22 -0
- package/dist/core/llm/state.d.ts.map +1 -1
- package/dist/core/llm/state.js +23 -11
- package/dist/core/llm/state.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/agent-status/agent-status.test.d.ts +2 -0
- package/dist/plugins/agent-status/agent-status.test.d.ts.map +1 -0
- package/dist/plugins/agent-status/agent-status.test.js +136 -0
- package/dist/plugins/agent-status/agent-status.test.js.map +1 -0
- package/dist/plugins/agent-status/plugin.d.ts +27 -0
- package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
- package/dist/plugins/agent-status/plugin.js +46 -0
- package/dist/plugins/agent-status/plugin.js.map +1 -1
- package/dist/plugins/agents/plugin.d.ts.map +1 -1
- package/dist/plugins/agents/plugin.js +7 -1
- package/dist/plugins/agents/plugin.js.map +1 -1
- package/dist/plugins/context-compact/context-compact.integration.test.js +54 -0
- package/dist/plugins/context-compact/context-compact.integration.test.js.map +1 -1
- package/dist/plugins/context-compact/context-compactor.d.ts +2 -0
- package/dist/plugins/context-compact/context-compactor.d.ts.map +1 -1
- package/dist/plugins/context-compact/context-compactor.js +29 -0
- package/dist/plugins/context-compact/context-compactor.js.map +1 -1
- package/dist/plugins/context-compact/context-compactor.test.js +6 -0
- package/dist/plugins/context-compact/context-compactor.test.js.map +1 -1
- package/dist/plugins/limits-guard/config.d.ts +30 -0
- package/dist/plugins/limits-guard/config.d.ts.map +1 -1
- package/dist/plugins/limits-guard/index.d.ts +3 -3
- package/dist/plugins/limits-guard/index.d.ts.map +1 -1
- package/dist/plugins/limits-guard/index.js +1 -1
- package/dist/plugins/limits-guard/index.js.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.d.ts +27 -1
- package/dist/plugins/limits-guard/limit-guard.d.ts.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.js +67 -0
- package/dist/plugins/limits-guard/limit-guard.js.map +1 -1
- package/dist/plugins/limits-guard/limit-guard.test.js +65 -1
- package/dist/plugins/limits-guard/limit-guard.test.js.map +1 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.js +295 -1
- package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +1 -1
- package/dist/plugins/limits-guard/plugin.d.ts +23 -2
- package/dist/plugins/limits-guard/plugin.d.ts.map +1 -1
- package/dist/plugins/limits-guard/plugin.js +107 -2
- package/dist/plugins/limits-guard/plugin.js.map +1 -1
- package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
- package/dist/plugins/mailbox/plugin.js +18 -0
- package/dist/plugins/mailbox/plugin.js.map +1 -1
- package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
- package/dist/plugins/session-stats/plugin.js +5 -1
- package/dist/plugins/session-stats/plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/core/agents/agent.ts +18 -2
- package/src/core/context/state.ts +10 -0
- package/src/core/events/base-event-store.ts +2 -0
- package/src/core/events/metadata-utils.ts +2 -0
- package/src/core/llm/anthropic.test.ts +34 -0
- package/src/core/llm/cache-breakpoints.test.ts +55 -0
- package/src/core/llm/cache-breakpoints.ts +39 -21
- package/src/core/llm/state.ts +25 -11
- package/src/index.ts +5 -4
- package/src/plugins/agent-status/agent-status.test.ts +164 -0
- package/src/plugins/agent-status/plugin.ts +49 -0
- package/src/plugins/agents/plugin.ts +7 -1
- package/src/plugins/context-compact/context-compact.integration.test.ts +62 -0
- package/src/plugins/context-compact/context-compactor.test.ts +6 -0
- package/src/plugins/context-compact/context-compactor.ts +31 -0
- package/src/plugins/limits-guard/config.ts +35 -0
- package/src/plugins/limits-guard/index.ts +3 -3
- package/src/plugins/limits-guard/limit-guard.test.ts +80 -1
- package/src/plugins/limits-guard/limit-guard.ts +98 -1
- package/src/plugins/limits-guard/limits-guard.integration.test.ts +331 -1
- package/src/plugins/limits-guard/plugin.ts +153 -3
- package/src/plugins/mailbox/plugin.ts +18 -0
- package/src/plugins/session-stats/plugin.ts +5 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Returns the worst result: hard_limit > soft_warning > ok.
|
|
5
5
|
*/
|
|
6
|
-
import type { AgentLimits } from '../../plugins/limits-guard/config.js';
|
|
6
|
+
import type { AgentLimits, LimitsSessionConfig } from '../../plugins/limits-guard/config.js';
|
|
7
7
|
import type { AgentCounters } from './plugin.js';
|
|
8
8
|
export interface ResolvedAgentLimits {
|
|
9
9
|
maxTurns: number;
|
|
@@ -14,8 +14,34 @@ export interface ResolvedAgentLimits {
|
|
|
14
14
|
softLimitRatio: number;
|
|
15
15
|
maxRepeatedToolCalls: number;
|
|
16
16
|
maxRepeatedResponses: number;
|
|
17
|
+
maxCost: number;
|
|
18
|
+
maxTokens: number;
|
|
19
|
+
maxCompactions: number;
|
|
17
20
|
}
|
|
18
21
|
export declare function resolveAgentLimits(config?: AgentLimits): ResolvedAgentLimits;
|
|
22
|
+
export interface ResolvedSessionLimits {
|
|
23
|
+
maxSessionCost: number;
|
|
24
|
+
maxSessionTokens: number;
|
|
25
|
+
softLimitRatio: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function resolveSessionLimits(config?: LimitsSessionConfig): ResolvedSessionLimits;
|
|
28
|
+
/** Cumulative spend, either for a single agent or summed across the session. */
|
|
29
|
+
export interface BudgetSpend {
|
|
30
|
+
costSpent: number;
|
|
31
|
+
tokensUsed: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Budget check (cost + tokens) shared by per-agent and session-wide budgets.
|
|
35
|
+
*
|
|
36
|
+
* Kept separate from {@link checkLimits} so it can run in `beforeInference` —
|
|
37
|
+
* blocking the *next* call once the budget is exhausted — without also tripping
|
|
38
|
+
* the counter/pattern limits (those are enforced in `afterInference`). Uses
|
|
39
|
+
* float-aware comparisons (no flooring) so sub-dollar budgets behave correctly.
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkBudget(spend: BudgetSpend, costLimit: number, tokenLimit: number, softLimitRatio: number, names: {
|
|
42
|
+
cost: string;
|
|
43
|
+
tokens: string;
|
|
44
|
+
}): LimitCheckResult;
|
|
19
45
|
export type LimitCheckResult = {
|
|
20
46
|
status: 'ok';
|
|
21
47
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limit-guard.d.ts","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;
|
|
1
|
+
{"version":3,"file":"limit-guard.d.ts","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AACxF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAMhD,MAAM,WAAW,mBAAmB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,0BAA0B,EAAE,MAAM,CAAA;IAClC,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACtB;AAiBD,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,mBAAmB,CAe5E;AAMD,MAAM,WAAW,qBAAqB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,cAAc,EAAE,MAAM,CAAA;CACtB;AAQD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,qBAAqB,CAOxF;AAED,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAC1B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC,gBAAgB,CAiClB;AAYD,MAAM,MAAM,gBAAgB,GACzB;IAAE,MAAM,EAAE,IAAI,CAAA;CAAE,GAChB;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvG;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAMvG,wBAAgB,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAE,mBAAmB,GAAG,gBAAgB,CAgFlG;AAMD;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,CAYpE"}
|
|
@@ -12,6 +12,10 @@ const DEFAULTS = {
|
|
|
12
12
|
softLimitRatio: 0.8,
|
|
13
13
|
maxRepeatedToolCalls: 3,
|
|
14
14
|
maxRepeatedResponses: 3,
|
|
15
|
+
// Budgets and the compaction cap are opt-in: unset means unlimited.
|
|
16
|
+
maxCost: Number.POSITIVE_INFINITY,
|
|
17
|
+
maxTokens: Number.POSITIVE_INFINITY,
|
|
18
|
+
maxCompactions: Number.POSITIVE_INFINITY,
|
|
15
19
|
};
|
|
16
20
|
export function resolveAgentLimits(config) {
|
|
17
21
|
if (!config)
|
|
@@ -25,8 +29,70 @@ export function resolveAgentLimits(config) {
|
|
|
25
29
|
softLimitRatio: config.softLimitRatio ?? DEFAULTS.softLimitRatio,
|
|
26
30
|
maxRepeatedToolCalls: config.maxRepeatedToolCalls ?? DEFAULTS.maxRepeatedToolCalls,
|
|
27
31
|
maxRepeatedResponses: config.maxRepeatedResponses ?? DEFAULTS.maxRepeatedResponses,
|
|
32
|
+
maxCost: config.maxCost ?? DEFAULTS.maxCost,
|
|
33
|
+
maxTokens: config.maxTokens ?? DEFAULTS.maxTokens,
|
|
34
|
+
maxCompactions: config.maxCompactions ?? DEFAULTS.maxCompactions,
|
|
28
35
|
};
|
|
29
36
|
}
|
|
37
|
+
const SESSION_DEFAULTS = {
|
|
38
|
+
maxSessionCost: Number.POSITIVE_INFINITY,
|
|
39
|
+
maxSessionTokens: Number.POSITIVE_INFINITY,
|
|
40
|
+
softLimitRatio: 0.8,
|
|
41
|
+
};
|
|
42
|
+
export function resolveSessionLimits(config) {
|
|
43
|
+
if (!config)
|
|
44
|
+
return SESSION_DEFAULTS;
|
|
45
|
+
return {
|
|
46
|
+
maxSessionCost: config.maxSessionCost ?? SESSION_DEFAULTS.maxSessionCost,
|
|
47
|
+
maxSessionTokens: config.maxSessionTokens ?? SESSION_DEFAULTS.maxSessionTokens,
|
|
48
|
+
softLimitRatio: config.softLimitRatio ?? SESSION_DEFAULTS.softLimitRatio,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Budget check (cost + tokens) shared by per-agent and session-wide budgets.
|
|
53
|
+
*
|
|
54
|
+
* Kept separate from {@link checkLimits} so it can run in `beforeInference` —
|
|
55
|
+
* blocking the *next* call once the budget is exhausted — without also tripping
|
|
56
|
+
* the counter/pattern limits (those are enforced in `afterInference`). Uses
|
|
57
|
+
* float-aware comparisons (no flooring) so sub-dollar budgets behave correctly.
|
|
58
|
+
*/
|
|
59
|
+
export function checkBudget(spend, costLimit, tokenLimit, softLimitRatio, names) {
|
|
60
|
+
const checks = [
|
|
61
|
+
{ name: names.cost, current: spend.costSpent, max: costLimit },
|
|
62
|
+
{ name: names.tokens, current: spend.tokensUsed, max: tokenLimit },
|
|
63
|
+
];
|
|
64
|
+
// Hard limits
|
|
65
|
+
for (const check of checks) {
|
|
66
|
+
if (check.current >= check.max) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'hard_limit',
|
|
69
|
+
limitName: check.name,
|
|
70
|
+
currentValue: check.current,
|
|
71
|
+
hardLimit: check.max,
|
|
72
|
+
reason: `${check.name} reached: ${formatBudget(check.current)}/${formatBudget(check.max)}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Soft warnings
|
|
77
|
+
for (const check of checks) {
|
|
78
|
+
if (check.max !== Number.POSITIVE_INFINITY && check.current >= check.max * softLimitRatio) {
|
|
79
|
+
return {
|
|
80
|
+
status: 'soft_warning',
|
|
81
|
+
limitName: check.name,
|
|
82
|
+
currentValue: check.current,
|
|
83
|
+
hardLimit: check.max,
|
|
84
|
+
message: `Approaching ${check.name} limit: ${formatBudget(check.current)}/${formatBudget(check.max)}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { status: 'ok' };
|
|
89
|
+
}
|
|
90
|
+
/** Format a budget value compactly — 4 decimals for fractional (cost), integer otherwise. */
|
|
91
|
+
function formatBudget(value) {
|
|
92
|
+
if (value === Number.POSITIVE_INFINITY)
|
|
93
|
+
return '∞';
|
|
94
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(4);
|
|
95
|
+
}
|
|
30
96
|
// ============================================================================
|
|
31
97
|
// Check logic
|
|
32
98
|
// ============================================================================
|
|
@@ -37,6 +103,7 @@ export function checkLimits(counters, limits) {
|
|
|
37
103
|
{ name: 'maxToolCalls', current: counters.toolCallCount, max: limits.maxToolCalls },
|
|
38
104
|
{ name: 'maxSpawnedAgents', current: counters.spawnedAgentCount, max: limits.maxSpawnedAgents },
|
|
39
105
|
{ name: 'maxMessagesSent', current: counters.messagesSentCount, max: limits.maxMessagesSent },
|
|
106
|
+
{ name: 'maxCompactions', current: counters.compactionCount, max: limits.maxCompactions },
|
|
40
107
|
];
|
|
41
108
|
for (const check of hardChecks) {
|
|
42
109
|
if (check.current >= check.max) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limit-guard.js","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"limit-guard.js","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { checkLimits, resolveAgentLimits } from './limit-guard.js';
|
|
2
|
+
import { checkBudget, checkLimits, resolveAgentLimits, resolveSessionLimits } from './limit-guard.js';
|
|
3
3
|
import { createAgentCounters } from './plugin.js';
|
|
4
4
|
describe('resolveAgentLimits', () => {
|
|
5
5
|
it('returns defaults when no config', () => {
|
|
@@ -12,6 +12,10 @@ describe('resolveAgentLimits', () => {
|
|
|
12
12
|
expect(limits.softLimitRatio).toBe(0.8);
|
|
13
13
|
expect(limits.maxRepeatedToolCalls).toBe(3);
|
|
14
14
|
expect(limits.maxRepeatedResponses).toBe(3);
|
|
15
|
+
// Budgets and compaction cap are opt-in (unlimited by default)
|
|
16
|
+
expect(limits.maxCost).toBe(Number.POSITIVE_INFINITY);
|
|
17
|
+
expect(limits.maxTokens).toBe(Number.POSITIVE_INFINITY);
|
|
18
|
+
expect(limits.maxCompactions).toBe(Number.POSITIVE_INFINITY);
|
|
15
19
|
});
|
|
16
20
|
it('returns defaults when empty config', () => {
|
|
17
21
|
const limits = resolveAgentLimits({});
|
|
@@ -117,5 +121,65 @@ describe('checkLimits', () => {
|
|
|
117
121
|
}), defaultLimits);
|
|
118
122
|
expect(result.status).toBe('hard_limit');
|
|
119
123
|
});
|
|
124
|
+
// --- Compaction limit ---
|
|
125
|
+
it('detects maxCompactions hard limit', () => {
|
|
126
|
+
const limits = resolveAgentLimits({ maxCompactions: 5 });
|
|
127
|
+
const result = checkLimits(makeCounters({ compactionCount: 5 }), limits);
|
|
128
|
+
expect(result.status).toBe('hard_limit');
|
|
129
|
+
if (result.status === 'hard_limit') {
|
|
130
|
+
expect(result.limitName).toBe('maxCompactions');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
it('does not cap compactions by default (unlimited)', () => {
|
|
134
|
+
const result = checkLimits(makeCounters({ compactionCount: 9999 }), defaultLimits);
|
|
135
|
+
expect(result.status).toBe('ok');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('checkBudget', () => {
|
|
139
|
+
const names = { cost: 'maxCost', tokens: 'maxTokens' };
|
|
140
|
+
it('returns ok when under budget', () => {
|
|
141
|
+
const result = checkBudget({ costSpent: 1, tokensUsed: 100 }, 5, 1000, 0.8, names);
|
|
142
|
+
expect(result.status).toBe('ok');
|
|
143
|
+
});
|
|
144
|
+
it('returns ok when unlimited (Infinity)', () => {
|
|
145
|
+
const result = checkBudget({ costSpent: 1_000_000, tokensUsed: 1_000_000 }, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, 0.8, names);
|
|
146
|
+
expect(result.status).toBe('ok');
|
|
147
|
+
});
|
|
148
|
+
it('detects cost hard limit', () => {
|
|
149
|
+
const result = checkBudget({ costSpent: 5.01, tokensUsed: 0 }, 5, Number.POSITIVE_INFINITY, 0.8, names);
|
|
150
|
+
expect(result.status).toBe('hard_limit');
|
|
151
|
+
if (result.status === 'hard_limit')
|
|
152
|
+
expect(result.limitName).toBe('maxCost');
|
|
153
|
+
});
|
|
154
|
+
it('detects token hard limit', () => {
|
|
155
|
+
const result = checkBudget({ costSpent: 0, tokensUsed: 1000 }, Number.POSITIVE_INFINITY, 1000, 0.8, names);
|
|
156
|
+
expect(result.status).toBe('hard_limit');
|
|
157
|
+
if (result.status === 'hard_limit')
|
|
158
|
+
expect(result.limitName).toBe('maxTokens');
|
|
159
|
+
});
|
|
160
|
+
it('emits soft warning approaching cost budget', () => {
|
|
161
|
+
const result = checkBudget({ costSpent: 4.2, tokensUsed: 0 }, 5, Number.POSITIVE_INFINITY, 0.8, names);
|
|
162
|
+
expect(result.status).toBe('soft_warning');
|
|
163
|
+
if (result.status === 'soft_warning')
|
|
164
|
+
expect(result.limitName).toBe('maxCost');
|
|
165
|
+
});
|
|
166
|
+
it('handles sub-dollar budgets without spurious warnings', () => {
|
|
167
|
+
// floor-based logic would warn at $0 for a $0.50 budget — float-aware must not.
|
|
168
|
+
const result = checkBudget({ costSpent: 0.1, tokensUsed: 0 }, 0.5, Number.POSITIVE_INFINITY, 0.8, names);
|
|
169
|
+
expect(result.status).toBe('ok');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('resolveSessionLimits', () => {
|
|
173
|
+
it('defaults to unlimited', () => {
|
|
174
|
+
const limits = resolveSessionLimits();
|
|
175
|
+
expect(limits.maxSessionCost).toBe(Number.POSITIVE_INFINITY);
|
|
176
|
+
expect(limits.maxSessionTokens).toBe(Number.POSITIVE_INFINITY);
|
|
177
|
+
expect(limits.softLimitRatio).toBe(0.8);
|
|
178
|
+
});
|
|
179
|
+
it('overrides specific values', () => {
|
|
180
|
+
const limits = resolveSessionLimits({ maxSessionCost: 10 });
|
|
181
|
+
expect(limits.maxSessionCost).toBe(10);
|
|
182
|
+
expect(limits.maxSessionTokens).toBe(Number.POSITIVE_INFINITY);
|
|
183
|
+
});
|
|
120
184
|
});
|
|
121
185
|
//# sourceMappingURL=limit-guard.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limit-guard.test.js","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"limit-guard.test.js","sourceRoot":"","sources":["../../../src/plugins/limits-guard/limit-guard.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,10 +2,15 @@ import { describe, expect, it } from 'bun:test';
|
|
|
2
2
|
import { AgentId } from '../../core/agents/schema.js';
|
|
3
3
|
import { agentEvents } from '../../core/agents/state.js';
|
|
4
4
|
import { MockLLMProvider } from '../../core/llm/mock.js';
|
|
5
|
+
import { ModelId } from '../../core/llm/schema.js';
|
|
6
|
+
import { llmEvents } from '../../core/llm/state.js';
|
|
5
7
|
import { selectPluginState } from '../../core/sessions/reducer.js';
|
|
6
8
|
import { ToolCallId } from '../../core/tools/schema.js';
|
|
9
|
+
import { contextCompactPlugin } from '../../plugins/context-compact/index.js';
|
|
10
|
+
import { getAgentMailbox, selectMailboxState } from '../../plugins/mailbox/query.js';
|
|
11
|
+
import { mailboxEvents } from '../../plugins/mailbox/state.js';
|
|
7
12
|
import { createMultiAgentPreset, createTestPreset, TestHarness } from '../../testing/index.js';
|
|
8
|
-
import { limitsGuardPlugin } from './plugin.js';
|
|
13
|
+
import { limitsEvents, limitsGuardPlugin } from './plugin.js';
|
|
9
14
|
function createLimitsHarness(options) {
|
|
10
15
|
return new TestHarness({ ...options, systemPlugins: [limitsGuardPlugin] });
|
|
11
16
|
}
|
|
@@ -374,5 +379,294 @@ describe('limits-guard plugin', () => {
|
|
|
374
379
|
await harness.shutdown();
|
|
375
380
|
});
|
|
376
381
|
});
|
|
382
|
+
// =========================================================================
|
|
383
|
+
// budgets (cost / tokens)
|
|
384
|
+
// =========================================================================
|
|
385
|
+
describe('budgets', () => {
|
|
386
|
+
it('agent exceeding cost budget → paused with budget_exceeded event', async () => {
|
|
387
|
+
let n = 0;
|
|
388
|
+
const harness = createLimitsHarness({
|
|
389
|
+
presets: [createTestPreset({
|
|
390
|
+
orchestratorSystem: 'Test agent.',
|
|
391
|
+
// $0.50 per call, $1.00 budget → pauses before the 3rd call.
|
|
392
|
+
orchestratorPlugins: [limitsGuardPlugin.configureAgent({ limits: { maxCost: 1.0, maxTurns: 100 } })],
|
|
393
|
+
})],
|
|
394
|
+
mockHandler: () => {
|
|
395
|
+
n++;
|
|
396
|
+
return {
|
|
397
|
+
content: null,
|
|
398
|
+
toolCalls: [{ id: ToolCallId(`tc${n}`), name: 'tell_user', input: { message: `Turn ${n}` } }],
|
|
399
|
+
finishReason: 'stop',
|
|
400
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(0.5),
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
const session = await harness.createSession('test');
|
|
405
|
+
const entryAgentId = session.getEntryAgentId();
|
|
406
|
+
await session.sendMessage('Start');
|
|
407
|
+
await waitForAgentPaused(session, entryAgentId);
|
|
408
|
+
expect(session.state.agents.get(entryAgentId).status).toBe('paused');
|
|
409
|
+
const counters = selectPluginState(session.state, 'agentLimits')?.get(entryAgentId);
|
|
410
|
+
expect(counters.costSpent).toBeGreaterThanOrEqual(1.0);
|
|
411
|
+
const budgetEvents = await session.getEventsByType(limitsEvents, 'budget_exceeded');
|
|
412
|
+
const evt = budgetEvents.find(e => e.agentId === entryAgentId);
|
|
413
|
+
expect(evt).toBeDefined();
|
|
414
|
+
expect(evt.scope).toBe('agent');
|
|
415
|
+
expect(evt.limitName).toBe('maxCost');
|
|
416
|
+
await harness.shutdown();
|
|
417
|
+
});
|
|
418
|
+
it('costSpent is preserved across resume — budget cannot be bypassed by pausing', async () => {
|
|
419
|
+
let n = 0;
|
|
420
|
+
const harness = createLimitsHarness({
|
|
421
|
+
presets: [createTestPreset({
|
|
422
|
+
orchestratorSystem: 'Test agent.',
|
|
423
|
+
orchestratorPlugins: [limitsGuardPlugin.configureAgent({ limits: { maxCost: 1.0, maxTurns: 100 } })],
|
|
424
|
+
})],
|
|
425
|
+
mockHandler: () => {
|
|
426
|
+
n++;
|
|
427
|
+
return {
|
|
428
|
+
content: null,
|
|
429
|
+
toolCalls: [{ id: ToolCallId(`tc${n}`), name: 'tell_user', input: { message: `Turn ${n}` } }],
|
|
430
|
+
finishReason: 'stop',
|
|
431
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(0.5),
|
|
432
|
+
};
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
const session = await harness.createSession('test');
|
|
436
|
+
const entryAgentId = session.getEntryAgentId();
|
|
437
|
+
await session.sendMessage('Start');
|
|
438
|
+
await waitForAgentPaused(session, entryAgentId);
|
|
439
|
+
const before = selectPluginState(session.state, 'agentLimits')?.get(entryAgentId);
|
|
440
|
+
expect(before).toBeDefined();
|
|
441
|
+
expect(before.costSpent).toBeGreaterThanOrEqual(1.0);
|
|
442
|
+
await session.callPluginMethod('agents.resume', { agentId: String(entryAgentId) });
|
|
443
|
+
// Budget is still exhausted → agent pauses again immediately without inferring.
|
|
444
|
+
await waitForAgentPaused(session, entryAgentId);
|
|
445
|
+
const after = selectPluginState(session.state, 'agentLimits')?.get(entryAgentId);
|
|
446
|
+
expect(after).toBeDefined();
|
|
447
|
+
// Anti-looping counter reset…
|
|
448
|
+
expect(after.inferenceCount).toBe(0);
|
|
449
|
+
// …but spend preserved, so the cap is not bypassable.
|
|
450
|
+
expect(after.costSpent).toBeGreaterThanOrEqual(before.costSpent);
|
|
451
|
+
await harness.shutdown();
|
|
452
|
+
});
|
|
453
|
+
it('child pausing on budget → parent is notified via a child-paused message', async () => {
|
|
454
|
+
let orchestratorCalls = 0;
|
|
455
|
+
let workerCalls = 0;
|
|
456
|
+
const harness = createLimitsHarness({
|
|
457
|
+
presets: [createTestPreset({
|
|
458
|
+
orchestratorSystem: 'Orchestrator agent.',
|
|
459
|
+
agents: [{
|
|
460
|
+
name: 'worker',
|
|
461
|
+
system: 'Worker agent.',
|
|
462
|
+
tools: [],
|
|
463
|
+
agents: [],
|
|
464
|
+
// $0.50 per call, $0.50 budget → pauses at the 2nd inference's
|
|
465
|
+
// beforeInference (after one completed call spent the budget).
|
|
466
|
+
plugins: [limitsGuardPlugin.configureAgent({ limits: { maxCost: 0.5, maxTurns: 100 } })],
|
|
467
|
+
}],
|
|
468
|
+
})],
|
|
469
|
+
mockHandler: (request) => {
|
|
470
|
+
// Worker: keep spending until the budget pauses it.
|
|
471
|
+
if (request.systemPrompt.includes('Worker agent.')) {
|
|
472
|
+
workerCalls++;
|
|
473
|
+
return {
|
|
474
|
+
content: null,
|
|
475
|
+
toolCalls: [{ id: ToolCallId(`w${workerCalls}`), name: 'tell_user', input: { message: `Work ${workerCalls}` } }],
|
|
476
|
+
finishReason: 'stop',
|
|
477
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(0.5),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// Orchestrator: spawn the worker exactly once, then idle.
|
|
481
|
+
orchestratorCalls++;
|
|
482
|
+
if (orchestratorCalls === 1) {
|
|
483
|
+
return {
|
|
484
|
+
content: null,
|
|
485
|
+
toolCalls: [{ id: ToolCallId('spawn'), name: 'start_worker', input: { message: 'Do work' } }],
|
|
486
|
+
finishReason: 'stop',
|
|
487
|
+
metrics: MockLLMProvider.defaultMetrics(),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return { content: 'Waiting', toolCalls: [], finishReason: 'stop', metrics: MockLLMProvider.defaultMetrics() };
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
const session = await harness.createSession('test');
|
|
494
|
+
await session.sendMessage('Start');
|
|
495
|
+
await waitForAgentPaused(session, AgentId('worker_1'));
|
|
496
|
+
const orchestratorId = session.getEntryAgentId();
|
|
497
|
+
// The mailbox plugin's onPause hook reports the pause to the parent.
|
|
498
|
+
// onPause runs *after* the agent_paused event (which flips status to
|
|
499
|
+
// 'paused'), so poll for the notification.
|
|
500
|
+
const findNotice = async () => (await session.getEventsByType(mailboxEvents, 'mailbox_message')).find(m => m.toAgentId === orchestratorId
|
|
501
|
+
&& m.message.from === AgentId('worker_1')
|
|
502
|
+
&& m.message.content.includes('<child-paused')
|
|
503
|
+
&& m.message.content.includes('worker_1'));
|
|
504
|
+
let notice = await findNotice();
|
|
505
|
+
const deadline = Date.now() + 5000;
|
|
506
|
+
while (!notice && Date.now() < deadline) {
|
|
507
|
+
await new Promise(r => setTimeout(r, 20));
|
|
508
|
+
notice = await findNotice();
|
|
509
|
+
}
|
|
510
|
+
expect(notice).toBeDefined();
|
|
511
|
+
await harness.shutdown();
|
|
512
|
+
});
|
|
513
|
+
it('child-paused notice is actually consumed by a parent that already went idle', async () => {
|
|
514
|
+
// Regression guard for the lifecycle: a parent that finished its work is
|
|
515
|
+
// NOT in a terminal "complete" state — it's persisted as `pending` with an
|
|
516
|
+
// empty mailbox. When the child pauses and delivers <child-paused>, the
|
|
517
|
+
// dequeue check flips the parent's decide() from "complete" back to "infer",
|
|
518
|
+
// so the parent wakes and reads the message rather than leaving it unconsumed.
|
|
519
|
+
let workerCalls = 0;
|
|
520
|
+
let orchestratorSawChildPaused = false;
|
|
521
|
+
const requestHasChildPaused = (request) => request.messages.some((m) => {
|
|
522
|
+
const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
523
|
+
return c.includes('<child-paused');
|
|
524
|
+
});
|
|
525
|
+
const harness = createLimitsHarness({
|
|
526
|
+
presets: [createTestPreset({
|
|
527
|
+
orchestratorSystem: 'Orchestrator agent.',
|
|
528
|
+
agents: [{
|
|
529
|
+
name: 'worker',
|
|
530
|
+
system: 'Worker agent.',
|
|
531
|
+
tools: [],
|
|
532
|
+
agents: [],
|
|
533
|
+
plugins: [limitsGuardPlugin.configureAgent({ limits: { maxCost: 0.5, maxTurns: 100 } })],
|
|
534
|
+
}],
|
|
535
|
+
})],
|
|
536
|
+
mockHandler: (request) => {
|
|
537
|
+
if (request.systemPrompt.includes('Worker agent.')) {
|
|
538
|
+
workerCalls++;
|
|
539
|
+
return {
|
|
540
|
+
content: null,
|
|
541
|
+
toolCalls: [{ id: ToolCallId(`w${workerCalls}`), name: 'tell_user', input: { message: `Work ${workerCalls}` } }],
|
|
542
|
+
finishReason: 'stop',
|
|
543
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(0.5),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// Orchestrator: spawn the worker once, then go idle. Any later wake-up
|
|
547
|
+
// is driven by an incoming message — record if it carried the notice.
|
|
548
|
+
if (requestHasChildPaused(request))
|
|
549
|
+
orchestratorSawChildPaused = true;
|
|
550
|
+
if (workerCalls === 0) {
|
|
551
|
+
return {
|
|
552
|
+
content: null,
|
|
553
|
+
toolCalls: [{ id: ToolCallId('spawn'), name: 'start_worker', input: { message: 'Do work' } }],
|
|
554
|
+
finishReason: 'stop',
|
|
555
|
+
metrics: MockLLMProvider.defaultMetrics(),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return { content: 'Acknowledged', toolCalls: [], finishReason: 'stop', metrics: MockLLMProvider.defaultMetrics() };
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
const session = await harness.createSession('test');
|
|
562
|
+
await session.sendMessage('Start');
|
|
563
|
+
await waitForAgentPaused(session, AgentId('worker_1'));
|
|
564
|
+
// The parent should wake from idle and run an inference that includes the
|
|
565
|
+
// <child-paused> message — proving the notice is consumed, not orphaned.
|
|
566
|
+
const deadline = Date.now() + 5000;
|
|
567
|
+
while (!orchestratorSawChildPaused && Date.now() < deadline) {
|
|
568
|
+
await new Promise(r => setTimeout(r, 20));
|
|
569
|
+
}
|
|
570
|
+
expect(orchestratorSawChildPaused).toBe(true);
|
|
571
|
+
// And the message is marked consumed in the parent's mailbox.
|
|
572
|
+
const orchestratorId = session.getEntryAgentId();
|
|
573
|
+
const mailbox = getAgentMailbox(selectMailboxState(session.state), orchestratorId);
|
|
574
|
+
const childPausedMsg = mailbox.find((m) => m.content.includes('<child-paused'));
|
|
575
|
+
expect(childPausedMsg).toBeDefined();
|
|
576
|
+
expect(childPausedMsg.consumed).toBe(true);
|
|
577
|
+
await harness.shutdown();
|
|
578
|
+
});
|
|
579
|
+
it('compaction (auxiliary inference) cost counts toward the budget', async () => {
|
|
580
|
+
// The compaction summarization is a real, billed LLM call routed through
|
|
581
|
+
// runAuxiliaryInference → auxiliary_inference_completed. It must be charged
|
|
582
|
+
// against the cost budget, otherwise an agent could spend unboundedly on
|
|
583
|
+
// compaction without ever tripping its cap.
|
|
584
|
+
const REGULAR_COST = 0.1;
|
|
585
|
+
const SUMMARY_COST = 5.0;
|
|
586
|
+
// Compaction request detection: inline compaction appends a trailing user
|
|
587
|
+
// message containing the summarization marker.
|
|
588
|
+
const isSummarizationRequest = (request) => {
|
|
589
|
+
const last = request.messages[request.messages.length - 1];
|
|
590
|
+
if (!last || last.role !== 'user')
|
|
591
|
+
return false;
|
|
592
|
+
const content = typeof last.content === 'string' ? last.content : JSON.stringify(last.content);
|
|
593
|
+
return content.includes('[CONTEXT COMPACTION REQUEST]');
|
|
594
|
+
};
|
|
595
|
+
const harness = new TestHarness({
|
|
596
|
+
systemPlugins: [contextCompactPlugin, limitsGuardPlugin],
|
|
597
|
+
presets: [createTestPreset({
|
|
598
|
+
orchestratorSystem: 'Test agent.',
|
|
599
|
+
plugins: [
|
|
600
|
+
contextCompactPlugin.configure({
|
|
601
|
+
compaction: { model: ModelId('mock'), maxTokens: 10, keepRecentMessages: 2 },
|
|
602
|
+
}),
|
|
603
|
+
],
|
|
604
|
+
// Budget large enough to survive the cheap regular turns but small
|
|
605
|
+
// enough that one expensive summarization call blows past it.
|
|
606
|
+
orchestratorPlugins: [
|
|
607
|
+
limitsGuardPlugin.configureAgent({ limits: { maxCost: 2.0, maxTurns: 100 } }),
|
|
608
|
+
],
|
|
609
|
+
})],
|
|
610
|
+
mockHandler: (request) => {
|
|
611
|
+
if (isSummarizationRequest(request)) {
|
|
612
|
+
return {
|
|
613
|
+
content: 'Summary of conversation so far.',
|
|
614
|
+
toolCalls: [],
|
|
615
|
+
finishReason: 'stop',
|
|
616
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(SUMMARY_COST),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
content: 'Agent response with some content to increase token count.',
|
|
621
|
+
toolCalls: [],
|
|
622
|
+
finishReason: 'stop',
|
|
623
|
+
metrics: MockLLMProvider.defaultMetricsWithCost(REGULAR_COST),
|
|
624
|
+
};
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
const session = await harness.createSession('test');
|
|
628
|
+
const entryAgentId = session.getEntryAgentId();
|
|
629
|
+
// Returns once the agent is either idle or paused — used because we don't
|
|
630
|
+
// know up front whether the compaction cost trips the budget on the same
|
|
631
|
+
// turn (depends on beforeInference hook ordering) or on the next one.
|
|
632
|
+
const waitForIdleOrPaused = async (timeoutMs = 10000) => {
|
|
633
|
+
const deadline = Date.now() + timeoutMs;
|
|
634
|
+
while (Date.now() < deadline) {
|
|
635
|
+
const st = session.state.agents.get(entryAgentId);
|
|
636
|
+
if (st?.status === 'paused')
|
|
637
|
+
return 'paused';
|
|
638
|
+
if (st?.status === 'pending' && st.pendingToolCalls.length === 0 && st.pendingToolResults.length === 0) {
|
|
639
|
+
return 'idle';
|
|
640
|
+
}
|
|
641
|
+
await new Promise(r => setTimeout(r, 10));
|
|
642
|
+
}
|
|
643
|
+
throw new Error('waitForIdleOrPaused timed out');
|
|
644
|
+
};
|
|
645
|
+
await session.sendAndWaitForIdle('First message');
|
|
646
|
+
await session.sendAndWaitForIdle('Second message');
|
|
647
|
+
// Third message triggers compaction (the expensive summarization call).
|
|
648
|
+
// It may pause on this turn or settle idle and pause on the next one.
|
|
649
|
+
await session.sendMessage('Third message to trigger compaction');
|
|
650
|
+
if (await waitForIdleOrPaused() === 'idle') {
|
|
651
|
+
await session.sendMessage('Fourth message');
|
|
652
|
+
}
|
|
653
|
+
await waitForAgentPaused(session, entryAgentId);
|
|
654
|
+
// Compaction genuinely ran and was billed.
|
|
655
|
+
const auxEvents = await session.getEventsByType(llmEvents, 'auxiliary_inference_completed');
|
|
656
|
+
expect(auxEvents.some((e) => e.metrics.cost === SUMMARY_COST)).toBe(true);
|
|
657
|
+
// The summarization cost is reflected in the agent's tracked spend…
|
|
658
|
+
const counters = selectPluginState(session.state, 'agentLimits')?.get(entryAgentId);
|
|
659
|
+
expect(counters).toBeDefined();
|
|
660
|
+
expect(counters.costSpent).toBeGreaterThanOrEqual(SUMMARY_COST);
|
|
661
|
+
// …and it tripped the cost budget (the regular turns alone, at 0.1 each,
|
|
662
|
+
// could never reach the 2.0 cap on their own here).
|
|
663
|
+
const budgetEvents = await session.getEventsByType(limitsEvents, 'budget_exceeded');
|
|
664
|
+
const evt = budgetEvents.find((e) => e.agentId === entryAgentId);
|
|
665
|
+
expect(evt).toBeDefined();
|
|
666
|
+
expect(evt.scope).toBe('agent');
|
|
667
|
+
expect(evt.limitName).toBe('maxCost');
|
|
668
|
+
await harness.shutdown();
|
|
669
|
+
});
|
|
670
|
+
});
|
|
377
671
|
});
|
|
378
672
|
//# sourceMappingURL=limits-guard.integration.test.js.map
|