@adhd/agent-plugin-budget 0.0.2
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/CHANGELOG.md +65 -0
- package/README.md +231 -0
- package/index.cjs +26 -0
- package/index.d.ts +323 -0
- package/index.js +480 -0
- package/package.json +25 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@adhd/agent-mcp-budget`. Format based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); this project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [0.0.2] — 2026-06-17
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Package relocated from `packages/node-tools/` to `packages/ai/`** via
|
|
14
|
+
`nx g @nx/workspace:move`. Import path `@adhd/agent-mcp-budget` and all
|
|
15
|
+
runtime behaviour are unchanged. The move aligns this package with the rest of
|
|
16
|
+
the `@adhd/agent-mcp-*` plugin family under `packages/ai/`.
|
|
17
|
+
- **Import of `HookRegistry` changed from `@adhd/agent-mcp` to `@adhd/agent-mcp-types`**
|
|
18
|
+
in tests. The class was relocated to `agent-mcp-types` to eliminate a circular
|
|
19
|
+
Nx build dependency (`agent-mcp:build → agent-mcp-budget:build → agent-mcp:build`).
|
|
20
|
+
No change to production code — plugins depend on `@adhd/agent-mcp-types` as a peer.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`vite.config.ts` now sets `emptyOutDir: true`.** Without this, Vite does not clear
|
|
25
|
+
`dist/` between builds — the old `dist/package.json` (containing the previous version
|
|
26
|
+
number) would survive a version bump and the wrong version would be published.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [0.0.1] — 2026-06-16
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Initial release. Budget enforcement plugin for `@adhd/agent-mcp`.
|
|
35
|
+
- Registers a `pre:model_request` **enforcement** handler via
|
|
36
|
+
`IHookRegistry.registerEnforcement()` — throws `IEnforcementError` when any
|
|
37
|
+
configured limit is breached, aborting the LLM call before it is made.
|
|
38
|
+
- Registers a `post:model_response` **observational** handler that accumulates
|
|
39
|
+
model call count, token totals, and elapsed time.
|
|
40
|
+
- Configurable limits (all optional; omit to leave unbounded):
|
|
41
|
+
- `maxModelCalls` — maximum number of LLM calls per task
|
|
42
|
+
- `maxTotalTokens` — combined input + output token cap
|
|
43
|
+
- `maxInputTokens` — input token cap
|
|
44
|
+
- `maxOutputTokens` — output token cap
|
|
45
|
+
- `maxWallClockMs` — wall-clock duration cap (from task start)
|
|
46
|
+
- `maxModelMs` — cumulative model latency cap
|
|
47
|
+
- `maxCostUSD` — cost cap (requires `inputPricePerMToken` + `outputPricePerMToken`
|
|
48
|
+
in config)
|
|
49
|
+
- Exports `configSchema` (Zod `z.object(...)`) — the server validates the plugin's
|
|
50
|
+
`config` block in `agent-mcp.config.json` against this schema before calling the
|
|
51
|
+
factory. Validation failure skips the plugin and logs a structured error; the server
|
|
52
|
+
continues without it.
|
|
53
|
+
- Exports `createPlugin` as both default and named export (factory signature:
|
|
54
|
+
`(ctx: PluginContext) => Plugin`).
|
|
55
|
+
- Activated by adding the plugin to `agent-mcp.config.json`:
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"plugins": [
|
|
59
|
+
{
|
|
60
|
+
"module": "/abs/path/to/dist/packages/ai/agent-mcp-budget/index.js",
|
|
61
|
+
"config": { "maxModelCalls": 5, "maxTotalTokens": 50000 }
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @adhd/agent-plugin-budget
|
|
2
|
+
|
|
3
|
+
Enforcement plugin for `@adhd/agent-mcp`. Caps token spend, model calls, wall-clock time, and tool usage per task, session, agent, provider, or globally — with configurable time windows and warning/block modes.
|
|
4
|
+
|
|
5
|
+
## Config
|
|
6
|
+
|
|
7
|
+
Place at `.adhd/agent-mcp/config.json` (auto-discovered by the plugin loader):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"module": "@adhd/agent-plugin-budget",
|
|
14
|
+
"config": {
|
|
15
|
+
"defaults": {
|
|
16
|
+
"caps": [
|
|
17
|
+
{ "field": "tokens", "maximum": 50000 },
|
|
18
|
+
{ "field": "calls", "maximum": 8 },
|
|
19
|
+
{ "field": "wallClock", "maximum": 120000 }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Caps
|
|
29
|
+
|
|
30
|
+
Each cap defines one limit. Fields:
|
|
31
|
+
|
|
32
|
+
| Field | Unit | Scope | Description |
|
|
33
|
+
| -------------- | ----- | --------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `tokens` | count | all | Sum of input + output + cache tokens |
|
|
35
|
+
| `inputTokens` | count | all | Input tokens only |
|
|
36
|
+
| `outputTokens` | count | all | Output tokens only |
|
|
37
|
+
| `calls` | count | all | Number of LLM model calls |
|
|
38
|
+
| `wallClock` | ms | task only | Wall-clock time from task start |
|
|
39
|
+
| `modelMs` | ms | task only | Cumulative model response time |
|
|
40
|
+
| `cost` | USD | all | Estimated cost (requires `costPerInputToken` / `costPerOutputToken`) |
|
|
41
|
+
| `toolCalls` | count | tool only | Per-tool call count |
|
|
42
|
+
| `responseSize` | chars | tool only | Raw response character length. Enforced at `transform:tool_result` — truncates (warning) or replaces with error (block) |
|
|
43
|
+
|
|
44
|
+
### Custom message
|
|
45
|
+
|
|
46
|
+
Each cap supports an optional `message` that overrides the auto-generated enforcement message. Use it to give the agent actionable guidance:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{ "field": "toolCalls", "maximum": 5, "mode": "block", "message": "Search is expensive. Use the index: query__search first" }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Scopes
|
|
53
|
+
|
|
54
|
+
| Scope | Coverage | DB query |
|
|
55
|
+
| --------- | ----------------------------------------------------- | -------------------------------------------- |
|
|
56
|
+
| `task` | Current task in-memory only | None |
|
|
57
|
+
| `session` | Current task + all historical tasks in session | `task_usage` JOIN `tasks` WHERE `session_id` |
|
|
58
|
+
| `agent` | Current task + all historical tasks for this agent | `task_usage` WHERE `agent_name` |
|
|
59
|
+
| `global` | Current task + ALL historical tasks across all agents | `task_usage` all rows |
|
|
60
|
+
|
|
61
|
+
Scope is inherited from the dimension's `scope` field if not set on the individual cap:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"defaults": {
|
|
66
|
+
"scope": "agent",
|
|
67
|
+
"caps": [
|
|
68
|
+
{ "field": "tokens", "maximum": 50000 },
|
|
69
|
+
{ "field": "calls", "maximum": 8 }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Time windows
|
|
76
|
+
|
|
77
|
+
Use ISO8601 durations for rolling window caps:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{ "field": "tokens", "maximum": 200000, "window": "PT24H", "scope": "agent" }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Supported formats: `PT24H`, `PT1H30M`, `P1DT6H`, `PT30M`, etc.
|
|
84
|
+
|
|
85
|
+
When a cap has both `scope` and `window`, the window query provides the historical total (scope-filtered, within the window) — no double-counting.
|
|
86
|
+
|
|
87
|
+
### Tool-level caps
|
|
88
|
+
|
|
89
|
+
Tool caps enforce at `pre:tool_call` (toolCalls) or `transform:tool_result` (responseSize):
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"tool": {
|
|
94
|
+
"default": { "caps": [{ "field": "toolCalls", "maximum": 100 }], "mode": "warning" },
|
|
95
|
+
"overrides": {
|
|
96
|
+
"web_search": {
|
|
97
|
+
"caps": [{ "field": "toolCalls", "maximum": 20 }],
|
|
98
|
+
"mode": "block"
|
|
99
|
+
},
|
|
100
|
+
"filesystem__read_text_file": {
|
|
101
|
+
"caps": [
|
|
102
|
+
{ "field": "responseSize", "maximum": 500, "mode": "warning", "message": "File too large. Use read_text_file with head:100 to preview, search_files for pattern matching, or read_multiple_files to batch-read small files" },
|
|
103
|
+
{ "field": "toolCalls", "maximum": 100, "mode": "warning", "message": "Too many file reads. Use search_files or get_file_info first" }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `toolCalls` field — enforced at `pre:tool_call`
|
|
112
|
+
- `warning` — tool call is blocked, agent receives a diagnostic, task continues
|
|
113
|
+
- `block` — tool call is blocked, task fails with `BUDGET_EXCEEDED`
|
|
114
|
+
- `responseSize` field — enforced at `transform:tool_result` (after the tool runs)
|
|
115
|
+
- `warning` — tool result is **truncated** to `maximum` chars, task continues
|
|
116
|
+
- `block` — tool result is replaced with an error message (task continues, agent sees the error)
|
|
117
|
+
|
|
118
|
+
## Dimensions
|
|
119
|
+
|
|
120
|
+
Caps are merged from three dimensions in this order:
|
|
121
|
+
`defaults ← agent ← provider`
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"defaults": {
|
|
126
|
+
"caps": [{ "field": "calls", "maximum": 8 }]
|
|
127
|
+
},
|
|
128
|
+
"agent": {
|
|
129
|
+
"default": { "caps": [{ "field": "calls", "maximum": 5 }] },
|
|
130
|
+
"overrides": {
|
|
131
|
+
"cheap-agent": {
|
|
132
|
+
"caps": [{ "field": "calls", "maximum": 1, "message": "Cheap agents get 1 call max" }]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"provider": {
|
|
137
|
+
"default": { "caps": [{ "field": "cost", "maximum": 0.1 }] },
|
|
138
|
+
"overrides": {
|
|
139
|
+
"anthropic": { "caps": [{ "field": "cost", "maximum": 0.5 }] }
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
"tool": {
|
|
143
|
+
"default": { "caps": [{ "field": "toolCalls", "maximum": 100 }], "mode": "warning" },
|
|
144
|
+
"overrides": {
|
|
145
|
+
"web_search": {
|
|
146
|
+
"caps": [{ "field": "toolCalls", "maximum": 20, "message": "Consider caching search results" }],
|
|
147
|
+
"mode": "block"
|
|
148
|
+
},
|
|
149
|
+
"filesystem__read_file": {
|
|
150
|
+
"caps": [{ "field": "responseSize", "maximum": 500, "mode": "warning", "message": "Raw file content is too large. Use shell tools instead: head -n 100, wc -l, grep for patterns" }]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Caps are **additive** across dimensions — multiple caps targeting the same field all apply. For example, `defaults` and `agent.overrides` can both cap `calls`; the stricter bound wins by throwing first.
|
|
158
|
+
|
|
159
|
+
## Cost estimation
|
|
160
|
+
|
|
161
|
+
For the `cost` field, set token prices on the defaults dimension:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"defaults": {
|
|
166
|
+
"costPerInputToken": 0.000003,
|
|
167
|
+
"costPerOutputToken": 0.000015,
|
|
168
|
+
"caps": [{ "field": "cost", "maximum": 0.1 }]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Cost = `inputTokens × costPerInputToken + outputTokens × costPerOutputToken`.
|
|
174
|
+
|
|
175
|
+
## Backward compat (flat format)
|
|
176
|
+
|
|
177
|
+
The old flat field format is supported via automatic conversion:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"config": {
|
|
182
|
+
"maxTotalTokens": 50000,
|
|
183
|
+
"maxModelCalls": 8,
|
|
184
|
+
"maxWallClockMs": 120000,
|
|
185
|
+
"scope": "agent"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Converts internally to:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"caps": [
|
|
195
|
+
{ "field": "tokens", "maximum": 50000 },
|
|
196
|
+
{ "field": "calls", "maximum": 8 },
|
|
197
|
+
{ "field": "wallClock", "maximum": 120000 }
|
|
198
|
+
],
|
|
199
|
+
"scope": "agent"
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Custom messages in flat format
|
|
204
|
+
|
|
205
|
+
The `message` key is also passed through in flat format — it applies to all caps in the dimension:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"config": {
|
|
210
|
+
"maxTotalTokens": 50000,
|
|
211
|
+
"maxModelCalls": 8,
|
|
212
|
+
"mode": "warning",
|
|
213
|
+
"message": "Budget limit hit"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Performance
|
|
219
|
+
|
|
220
|
+
Enforcement makes exactly `U + W` DB queries per event where:
|
|
221
|
+
|
|
222
|
+
- `U` = number of unique non-task scopes across all caps (0..3)
|
|
223
|
+
- `W` = number of unique (scope, window) pairs across all caps
|
|
224
|
+
|
|
225
|
+
Zero DB queries when no caps are configured. No per-cap scaling.
|
|
226
|
+
|
|
227
|
+
## Testing
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
nx test agent-plugin-budget
|
|
231
|
+
```
|
package/index.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const p=require("zod");function E(f){const e=/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/,t=f.match(e);if(!t)throw new Error(`invalid ISO 8601 duration: ${f}`);const[,o,s,i,n,a,c]=t;let r=0;return o&&(r+=parseInt(o)*365.25*864e5),s&&(r+=parseInt(s)*30.44*864e5),i&&(r+=parseInt(i)*864e5),n&&(r+=parseInt(n)*36e5),a&&(r+=parseInt(a)*6e4),c&&(r+=parseFloat(c)*1e3),Math.round(r)}const w=["tokens","inputTokens","outputTokens","calls","wallClock","modelMs","cost","toolCalls","responseSize"],b=p.z.object({field:p.z.enum(w),maximum:p.z.number().min(0),window:p.z.string().optional(),scope:p.z.enum(["task","session","agent","global"]).optional(),mode:p.z.enum(["warning","block"]).optional(),message:p.z.string().optional()}),g=p.z.object({caps:p.z.array(b).optional(),mode:p.z.enum(["warning","block"]).optional(),costPerInputToken:p.z.number().min(0).optional(),costPerOutputToken:p.z.number().min(0).optional(),scope:p.z.enum(["task","session","agent","global"]).optional()}),T=p.z.object({defaults:g.optional(),agent:p.z.object({default:g.optional(),overrides:p.z.record(p.z.string(),g.partial()).optional().default({})}).optional(),provider:p.z.object({default:g.optional(),overrides:p.z.record(p.z.string(),g.partial()).optional().default({})}).optional(),tool:p.z.object({default:g.optional(),overrides:p.z.record(p.z.string(),g.partial()).optional().default({})}).optional()}),x=p.z.object({}).passthrough(),_={maxInputTokens:{field:"inputTokens"},maxOutputTokens:{field:"outputTokens"},maxTotalTokens:{field:"tokens"},maxModelCalls:{field:"calls"},maxWallClockMs:{field:"wallClock"},maxModelMs:{field:"modelMs"},maxCostUSD:{field:"cost"},maxTokensPer24h:{field:"tokens",window:"PT24H"},maxCalls:{field:"toolCalls"}};function v(f){const e=[],t={};for(const[o,s]of Object.entries(f)){const i=_[o];if(i&&typeof s=="number"){const n={field:i.field,maximum:s};i.window&&(n.window=i.window),e.push(n)}else(o==="scope"||o==="mode"||o==="costPerInputToken"||o==="costPerOutputToken"||o==="message")&&(t[o]=s)}return e.length>0&&(t.caps=e),g.parse(t)}function O(f){const e=f;if(e.defaults!==void 0||e.agent!==void 0||e.provider!==void 0||e.tool!==void 0){const t=T.parse(f);return{defaults:t.defaults??g.parse({}),agent:t.agent??{overrides:{}},provider:t.provider??{overrides:{}},tool:t.tool??{overrides:{}}}}return{defaults:v(e),agent:{overrides:{}},provider:{overrides:{}},tool:{overrides:{}}}}function S(f,e,t,o){return{isEnforcementError:!0,code:"BUDGET_EXCEEDED",message:o??`${f} limit is ${e}, current value is ${Math.round(t)}`}}function A(f,e,t){return{isToolWarning:!0,toolName:f,callId:e,message:t}}class y{constructor(e,t,o=0,s=0){this.db=e,this.cfg=t,this.costPerInput=o,this.costPerOutput=s,this.name="agent-mcp-budget",this.accumulators=new Map}install(e){e.register("task:start",t=>{try{this.onTaskStart(t)}catch{}}),e.register("pre:model_request",t=>{try{this.onPreModelRequest(t)}catch{}}),e.register("post:model_response",t=>{try{this.onPostModelResponse(t)}catch{}}),e.register("task:completed",t=>{try{this.onTerminal(t.executionContext.taskId)}catch{}}),e.register("task:failed",t=>{try{this.onTerminal(t.executionContext.taskId)}catch{}}),e.register("task:cancelled",t=>{try{this.onTerminal(t.executionContext.taskId)}catch{}}),e.registerEnforcement("pre:model_request",t=>this.enforcePreModel(t)),e.registerEnforcement("pre:tool_call",t=>this.enforcePreTool(t)),e.register("transform:tool_result",t=>{try{this.enforceResponseSize(t)}catch{}})}onTaskStart(e){var n,a;const{taskId:t,sessionId:o,agentName:s}=e.executionContext,i=((a=(n=e.executionContext.agentDefinition)==null?void 0:n.provider)==null?void 0:a.type)??"unknown";this.accumulators.set(t,{taskId:t,sessionId:o??void 0,agentName:s,providerType:i,startedAtMs:Date.now(),inputTokens:0,outputTokens:0,modelCalls:0,totalModelMs:0,toolCalls:new Map})}onPreModelRequest(e){const t=this.accumulators.get(e.executionContext.taskId);t&&(t.modelCallStartMs=Date.now())}onPostModelResponse(e){const t=this.accumulators.get(e.executionContext.taskId);if(!t)return;const o=e.tokenUsage;o&&(t.inputTokens+=o.inputTokens??0,t.outputTokens+=o.outputTokens??0),t.modelCalls+=1,t.modelCallStartMs!==void 0&&(t.totalModelMs+=Date.now()-t.modelCallStartMs,t.modelCallStartMs=void 0)}onTerminal(e){this.accumulators.delete(e)}mergeDim(e){let t={caps:[]};for(const o of e)o&&(t={caps:[...t.caps??[],...o.caps??[]],mode:o.mode??t.mode,costPerInputToken:o.costPerInputToken??t.costPerInputToken,costPerOutputToken:o.costPerOutputToken??t.costPerOutputToken,scope:o.scope??t.scope});return t}resolveCaps(e,t,o){var u,m,d;const s=this.cfg.defaults,i=this.cfg.agent,n=this.cfg.provider,a=this.cfg.tool;if(o){const k=(u=a==null?void 0:a.overrides)==null?void 0:u[o],h=this.mergeDim([s,a==null?void 0:a.default,k]);return{caps:h.caps??[],mode:h.mode,scope:h.scope}}const c=(m=i==null?void 0:i.overrides)==null?void 0:m[e],r=(d=n==null?void 0:n.overrides)==null?void 0:d[t],l=this.mergeDim([s,i==null?void 0:i.default,c,n==null?void 0:n.default,r]);return{caps:l.caps??[],mode:l.mode,scope:l.scope}}queryScopeTotals(e,t,o,s){const i=this.accumulators.get(e),n=i?{inputTokens:i.inputTokens,outputTokens:i.outputTokens,modelCalls:i.modelCalls}:{inputTokens:0,outputTokens:0,modelCalls:0};if(s==="task"||!this.db)return n;try{const a=this.db;let c;if(s==="session"&&t?c=a.prepare(`SELECT
|
|
2
|
+
COALESCE(SUM(tu.input_tokens), 0) AS input,
|
|
3
|
+
COALESCE(SUM(tu.output_tokens), 0) AS output,
|
|
4
|
+
COALESCE(SUM(tu.model_calls), 0) AS calls
|
|
5
|
+
FROM task_usage tu
|
|
6
|
+
JOIN tasks t ON tu.task_id = t.id
|
|
7
|
+
WHERE t.session_id = ? AND tu.task_id != ?`).get(t,e):s==="agent"?c=a.prepare(`SELECT
|
|
8
|
+
COALESCE(SUM(input_tokens), 0) AS input,
|
|
9
|
+
COALESCE(SUM(output_tokens), 0) AS output,
|
|
10
|
+
COALESCE(SUM(model_calls), 0) AS calls
|
|
11
|
+
FROM task_usage
|
|
12
|
+
WHERE agent_name = ? AND task_id != ?`).get(o,e):s==="global"&&(c=a.prepare(`SELECT
|
|
13
|
+
COALESCE(SUM(input_tokens), 0) AS input,
|
|
14
|
+
COALESCE(SUM(output_tokens), 0) AS output,
|
|
15
|
+
COALESCE(SUM(model_calls), 0) AS calls
|
|
16
|
+
FROM task_usage
|
|
17
|
+
WHERE task_id != ?`).get(e)),c)return{inputTokens:(c.input??0)+n.inputTokens,outputTokens:(c.output??0)+n.outputTokens,modelCalls:(c.calls??0)+n.modelCalls}}catch{}return n}queryWindowTokens(e,t,o,s){if(!this.db)return 0;try{const i=this.db,n=new Date(Date.now()-o).toISOString();let a;if(e==="session"){const c=s?" AND tu.task_id != ?":"",r=[t,n];s&&r.push(s),a=i.prepare(`SELECT COALESCE(SUM(tu.input_tokens + tu.output_tokens), 0) AS total
|
|
18
|
+
FROM task_usage tu
|
|
19
|
+
JOIN tasks t ON tu.task_id = t.id
|
|
20
|
+
WHERE t.session_id = ? AND tu.created_at >= ?${c}`).get(...r)}else if(e==="agent"){const c=s?" AND task_id != ?":"",r=[t,n];s&&r.push(s),a=i.prepare(`SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
|
|
21
|
+
FROM task_usage
|
|
22
|
+
WHERE agent_name = ? AND created_at >= ?${c}`).get(...r)}else if(e==="global"){const c=s?" AND task_id != ?":"",r=[n];s&&r.push(s),a=i.prepare(`SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
|
|
23
|
+
FROM task_usage
|
|
24
|
+
WHERE created_at >= ?${c}`).get(...r)}return(a==null?void 0:a.total)??0}catch{return 0}}buildSnapshot(e,t,o,s,i,n){const a={};a.inputTokens=t.inputTokens,a.outputTokens=t.outputTokens,a.calls=t.modelCalls,a.wallClock=Date.now()-t.startedAtMs,a.modelMs=t.totalModelMs,a.cost=t.inputTokens*this.costPerInput+t.outputTokens*this.costPerOutput;const c=new Set,r=new Map;for(const l of e){const u=l.scope??n??"task";if(u!=="task"&&c.add(u),l.window){const m=`${u}:${l.window}`;r.has(m)||r.set(m,{scope:u,windowMs:E(l.window)})}}for(const l of c){const u=this.queryScopeTotals(o,s,i,l);a[`${l}:inputTokens`]=u.inputTokens,a[`${l}:outputTokens`]=u.outputTokens,a[`${l}:calls`]=u.modelCalls}for(const[l,{scope:u,windowMs:m}]of r){let d="";u==="session"?d=s??"":u==="agent"&&(d=i??""),a[l]=this.queryWindowTokens(u,d,m,o)}return a}getSnapshotValue(e,t,o){const s=t.scope??o??"task";let i;t.window?i="":i=s!=="task"?`${s}:`:"";let n;switch(t.field){case"inputTokens":n=e[`${i}inputTokens`]??e.inputTokens;break;case"outputTokens":n=e[`${i}outputTokens`]??e.outputTokens;break;case"tokens":n=(e[`${i}inputTokens`]??e.inputTokens)+(e[`${i}outputTokens`]??e.outputTokens);break;case"calls":n=e[`${i}calls`]??e.calls;break;case"wallClock":n=e.wallClock;break;case"modelMs":n=e.modelMs;break;case"cost":n=e.cost;break;case"toolCalls":n=0;break;default:n=0}return t.window&&(n+=e[`${s}:${t.window}`]??0),n}evaluateCap(e,t,o){const s=this.getSnapshotValue(t,e,o);if(s>=e.maximum)throw S(e.field,e.maximum,s,e.message)}enforcePreModel(e){var u,m;const{taskId:t,sessionId:o,agentName:s}=e.executionContext,i=((m=(u=e.executionContext.agentDefinition)==null?void 0:u.provider)==null?void 0:m.type)??"unknown",n=this.accumulators.get(t);if(!n)return;const{caps:a,scope:c}=this.resolveCaps(s,i),r=a.filter(d=>d.field!=="toolCalls");if(r.length===0)return;const l=this.buildSnapshot(r,n,t,o,s,c);for(const d of r)this.evaluateCap(d,l)}enforcePreTool(e){const{toolName:t,callId:o,executionContext:s}=e,{caps:i,mode:n,scope:a}=this.resolveCaps(s.agentName,"",t),c=this.accumulators.get(s.taskId);if(!c||i.length===0)return;const r=c.toolCalls.get(t)??0,l=this.buildSnapshot(i,c,s.taskId,s.sessionId,s.agentName,a);for(const u of i){const m=u.field==="toolCalls"?r:this.getSnapshotValue(l,u,a);if(m>=u.maximum){const d=u.message??`tool "${t}": ${u.field} limit is ${u.maximum}, current value is ${Math.round(m)}`;throw(u.mode??n??"warning")==="warning"?A(t,o,d):S(`tool:${t}:${u.field}`,u.maximum,m,u.message)}}c.toolCalls.set(t,r+1)}enforceResponseSize(e){const{toolName:t,result:o}=e;if(typeof o!="object"||o===null)return;const{caps:s,mode:i}=this.resolveCaps("","",t),n=s.filter(l=>l.field==="responseSize");if(n.length===0)return;const a=o,c=a.content;if(!Array.isArray(c))return;let r=0;for(const l of c)if(typeof l=="object"&&l!==null){const u=l;u.type==="text"&&(r+=(u.text??"").length)}for(const l of n){if(r<=l.maximum)continue;if((l.mode??i??"warning")==="block")a.content=[{type:"text",text:l.message??`Response size (${r} chars) exceeds limit of ${l.maximum}. Use offset/limit or shell paging tools instead.`}],e.isError=!0;else{let m=l.maximum;const d=[];for(const k of c){if(typeof k!="object"||k===null){d.push(k);continue}const h=k;if(h.type!=="text"){d.push(k);continue}const C=h.text??"";if(C.length<=m)d.push(k),m-=C.length;else{d.push({type:"text",text:C.slice(0,m)});break}}d.push({type:"text",text:`
|
|
25
|
+
|
|
26
|
+
[truncated: response was ${r} chars, limited to ${l.maximum}. ${l.message??"Use offset/limit or shell paging tools for full content."}]`}),a.content=d}break}}}const M=({db:f,config:e})=>{var i,n;const t=O(e),o=((i=t.defaults)==null?void 0:i.costPerInputToken)??0,s=((n=t.defaults)==null?void 0:n.costPerOutputToken)??0;return new y(f,t,o,s)};exports.configSchema=x;exports.createPlugin=M;exports.default=M;exports.pluginConfigSchema=T;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { PluginFactory } from '@adhd/agent-base-types';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const capSchema: z.ZodObject<{
|
|
5
|
+
field: z.ZodEnum<{
|
|
6
|
+
tokens: "tokens";
|
|
7
|
+
inputTokens: "inputTokens";
|
|
8
|
+
outputTokens: "outputTokens";
|
|
9
|
+
calls: "calls";
|
|
10
|
+
wallClock: "wallClock";
|
|
11
|
+
modelMs: "modelMs";
|
|
12
|
+
cost: "cost";
|
|
13
|
+
toolCalls: "toolCalls";
|
|
14
|
+
responseSize: "responseSize";
|
|
15
|
+
}>;
|
|
16
|
+
maximum: z.ZodNumber;
|
|
17
|
+
window: z.ZodOptional<z.ZodString>;
|
|
18
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
19
|
+
task: "task";
|
|
20
|
+
session: "session";
|
|
21
|
+
agent: "agent";
|
|
22
|
+
global: "global";
|
|
23
|
+
}>>;
|
|
24
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
25
|
+
warning: "warning";
|
|
26
|
+
block: "block";
|
|
27
|
+
}>>;
|
|
28
|
+
message: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export type Cap = z.infer<typeof capSchema>;
|
|
31
|
+
export declare const pluginConfigSchema: z.ZodObject<{
|
|
32
|
+
defaults: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
caps: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
34
|
+
field: z.ZodEnum<{
|
|
35
|
+
tokens: "tokens";
|
|
36
|
+
inputTokens: "inputTokens";
|
|
37
|
+
outputTokens: "outputTokens";
|
|
38
|
+
calls: "calls";
|
|
39
|
+
wallClock: "wallClock";
|
|
40
|
+
modelMs: "modelMs";
|
|
41
|
+
cost: "cost";
|
|
42
|
+
toolCalls: "toolCalls";
|
|
43
|
+
responseSize: "responseSize";
|
|
44
|
+
}>;
|
|
45
|
+
maximum: z.ZodNumber;
|
|
46
|
+
window: z.ZodOptional<z.ZodString>;
|
|
47
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
48
|
+
task: "task";
|
|
49
|
+
session: "session";
|
|
50
|
+
agent: "agent";
|
|
51
|
+
global: "global";
|
|
52
|
+
}>>;
|
|
53
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
54
|
+
warning: "warning";
|
|
55
|
+
block: "block";
|
|
56
|
+
}>>;
|
|
57
|
+
message: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strip>>>;
|
|
59
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
warning: "warning";
|
|
61
|
+
block: "block";
|
|
62
|
+
}>>;
|
|
63
|
+
costPerInputToken: z.ZodOptional<z.ZodNumber>;
|
|
64
|
+
costPerOutputToken: z.ZodOptional<z.ZodNumber>;
|
|
65
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
task: "task";
|
|
67
|
+
session: "session";
|
|
68
|
+
agent: "agent";
|
|
69
|
+
global: "global";
|
|
70
|
+
}>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
agent: z.ZodOptional<z.ZodObject<{
|
|
73
|
+
default: z.ZodOptional<z.ZodObject<{
|
|
74
|
+
caps: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
75
|
+
field: z.ZodEnum<{
|
|
76
|
+
tokens: "tokens";
|
|
77
|
+
inputTokens: "inputTokens";
|
|
78
|
+
outputTokens: "outputTokens";
|
|
79
|
+
calls: "calls";
|
|
80
|
+
wallClock: "wallClock";
|
|
81
|
+
modelMs: "modelMs";
|
|
82
|
+
cost: "cost";
|
|
83
|
+
toolCalls: "toolCalls";
|
|
84
|
+
responseSize: "responseSize";
|
|
85
|
+
}>;
|
|
86
|
+
maximum: z.ZodNumber;
|
|
87
|
+
window: z.ZodOptional<z.ZodString>;
|
|
88
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
89
|
+
task: "task";
|
|
90
|
+
session: "session";
|
|
91
|
+
agent: "agent";
|
|
92
|
+
global: "global";
|
|
93
|
+
}>>;
|
|
94
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
95
|
+
warning: "warning";
|
|
96
|
+
block: "block";
|
|
97
|
+
}>>;
|
|
98
|
+
message: z.ZodOptional<z.ZodString>;
|
|
99
|
+
}, z.core.$strip>>>;
|
|
100
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
101
|
+
warning: "warning";
|
|
102
|
+
block: "block";
|
|
103
|
+
}>>;
|
|
104
|
+
costPerInputToken: z.ZodOptional<z.ZodNumber>;
|
|
105
|
+
costPerOutputToken: z.ZodOptional<z.ZodNumber>;
|
|
106
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
107
|
+
task: "task";
|
|
108
|
+
session: "session";
|
|
109
|
+
agent: "agent";
|
|
110
|
+
global: "global";
|
|
111
|
+
}>>;
|
|
112
|
+
}, z.core.$strip>>;
|
|
113
|
+
overrides: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
114
|
+
caps: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
115
|
+
field: z.ZodEnum<{
|
|
116
|
+
tokens: "tokens";
|
|
117
|
+
inputTokens: "inputTokens";
|
|
118
|
+
outputTokens: "outputTokens";
|
|
119
|
+
calls: "calls";
|
|
120
|
+
wallClock: "wallClock";
|
|
121
|
+
modelMs: "modelMs";
|
|
122
|
+
cost: "cost";
|
|
123
|
+
toolCalls: "toolCalls";
|
|
124
|
+
responseSize: "responseSize";
|
|
125
|
+
}>;
|
|
126
|
+
maximum: z.ZodNumber;
|
|
127
|
+
window: z.ZodOptional<z.ZodString>;
|
|
128
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
129
|
+
task: "task";
|
|
130
|
+
session: "session";
|
|
131
|
+
agent: "agent";
|
|
132
|
+
global: "global";
|
|
133
|
+
}>>;
|
|
134
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
135
|
+
warning: "warning";
|
|
136
|
+
block: "block";
|
|
137
|
+
}>>;
|
|
138
|
+
message: z.ZodOptional<z.ZodString>;
|
|
139
|
+
}, z.core.$strip>>>>;
|
|
140
|
+
mode: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
141
|
+
warning: "warning";
|
|
142
|
+
block: "block";
|
|
143
|
+
}>>>;
|
|
144
|
+
costPerInputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
145
|
+
costPerOutputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
146
|
+
scope: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
147
|
+
task: "task";
|
|
148
|
+
session: "session";
|
|
149
|
+
agent: "agent";
|
|
150
|
+
global: "global";
|
|
151
|
+
}>>>;
|
|
152
|
+
}, z.core.$strip>>>>;
|
|
153
|
+
}, z.core.$strip>>;
|
|
154
|
+
provider: z.ZodOptional<z.ZodObject<{
|
|
155
|
+
default: z.ZodOptional<z.ZodObject<{
|
|
156
|
+
caps: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
157
|
+
field: z.ZodEnum<{
|
|
158
|
+
tokens: "tokens";
|
|
159
|
+
inputTokens: "inputTokens";
|
|
160
|
+
outputTokens: "outputTokens";
|
|
161
|
+
calls: "calls";
|
|
162
|
+
wallClock: "wallClock";
|
|
163
|
+
modelMs: "modelMs";
|
|
164
|
+
cost: "cost";
|
|
165
|
+
toolCalls: "toolCalls";
|
|
166
|
+
responseSize: "responseSize";
|
|
167
|
+
}>;
|
|
168
|
+
maximum: z.ZodNumber;
|
|
169
|
+
window: z.ZodOptional<z.ZodString>;
|
|
170
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
171
|
+
task: "task";
|
|
172
|
+
session: "session";
|
|
173
|
+
agent: "agent";
|
|
174
|
+
global: "global";
|
|
175
|
+
}>>;
|
|
176
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
177
|
+
warning: "warning";
|
|
178
|
+
block: "block";
|
|
179
|
+
}>>;
|
|
180
|
+
message: z.ZodOptional<z.ZodString>;
|
|
181
|
+
}, z.core.$strip>>>;
|
|
182
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
183
|
+
warning: "warning";
|
|
184
|
+
block: "block";
|
|
185
|
+
}>>;
|
|
186
|
+
costPerInputToken: z.ZodOptional<z.ZodNumber>;
|
|
187
|
+
costPerOutputToken: z.ZodOptional<z.ZodNumber>;
|
|
188
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
189
|
+
task: "task";
|
|
190
|
+
session: "session";
|
|
191
|
+
agent: "agent";
|
|
192
|
+
global: "global";
|
|
193
|
+
}>>;
|
|
194
|
+
}, z.core.$strip>>;
|
|
195
|
+
overrides: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
196
|
+
caps: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
197
|
+
field: z.ZodEnum<{
|
|
198
|
+
tokens: "tokens";
|
|
199
|
+
inputTokens: "inputTokens";
|
|
200
|
+
outputTokens: "outputTokens";
|
|
201
|
+
calls: "calls";
|
|
202
|
+
wallClock: "wallClock";
|
|
203
|
+
modelMs: "modelMs";
|
|
204
|
+
cost: "cost";
|
|
205
|
+
toolCalls: "toolCalls";
|
|
206
|
+
responseSize: "responseSize";
|
|
207
|
+
}>;
|
|
208
|
+
maximum: z.ZodNumber;
|
|
209
|
+
window: z.ZodOptional<z.ZodString>;
|
|
210
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
211
|
+
task: "task";
|
|
212
|
+
session: "session";
|
|
213
|
+
agent: "agent";
|
|
214
|
+
global: "global";
|
|
215
|
+
}>>;
|
|
216
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
217
|
+
warning: "warning";
|
|
218
|
+
block: "block";
|
|
219
|
+
}>>;
|
|
220
|
+
message: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$strip>>>>;
|
|
222
|
+
mode: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
223
|
+
warning: "warning";
|
|
224
|
+
block: "block";
|
|
225
|
+
}>>>;
|
|
226
|
+
costPerInputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
227
|
+
costPerOutputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
228
|
+
scope: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
229
|
+
task: "task";
|
|
230
|
+
session: "session";
|
|
231
|
+
agent: "agent";
|
|
232
|
+
global: "global";
|
|
233
|
+
}>>>;
|
|
234
|
+
}, z.core.$strip>>>>;
|
|
235
|
+
}, z.core.$strip>>;
|
|
236
|
+
tool: z.ZodOptional<z.ZodObject<{
|
|
237
|
+
default: z.ZodOptional<z.ZodObject<{
|
|
238
|
+
caps: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
239
|
+
field: z.ZodEnum<{
|
|
240
|
+
tokens: "tokens";
|
|
241
|
+
inputTokens: "inputTokens";
|
|
242
|
+
outputTokens: "outputTokens";
|
|
243
|
+
calls: "calls";
|
|
244
|
+
wallClock: "wallClock";
|
|
245
|
+
modelMs: "modelMs";
|
|
246
|
+
cost: "cost";
|
|
247
|
+
toolCalls: "toolCalls";
|
|
248
|
+
responseSize: "responseSize";
|
|
249
|
+
}>;
|
|
250
|
+
maximum: z.ZodNumber;
|
|
251
|
+
window: z.ZodOptional<z.ZodString>;
|
|
252
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
253
|
+
task: "task";
|
|
254
|
+
session: "session";
|
|
255
|
+
agent: "agent";
|
|
256
|
+
global: "global";
|
|
257
|
+
}>>;
|
|
258
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
259
|
+
warning: "warning";
|
|
260
|
+
block: "block";
|
|
261
|
+
}>>;
|
|
262
|
+
message: z.ZodOptional<z.ZodString>;
|
|
263
|
+
}, z.core.$strip>>>;
|
|
264
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
265
|
+
warning: "warning";
|
|
266
|
+
block: "block";
|
|
267
|
+
}>>;
|
|
268
|
+
costPerInputToken: z.ZodOptional<z.ZodNumber>;
|
|
269
|
+
costPerOutputToken: z.ZodOptional<z.ZodNumber>;
|
|
270
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
271
|
+
task: "task";
|
|
272
|
+
session: "session";
|
|
273
|
+
agent: "agent";
|
|
274
|
+
global: "global";
|
|
275
|
+
}>>;
|
|
276
|
+
}, z.core.$strip>>;
|
|
277
|
+
overrides: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
278
|
+
caps: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
279
|
+
field: z.ZodEnum<{
|
|
280
|
+
tokens: "tokens";
|
|
281
|
+
inputTokens: "inputTokens";
|
|
282
|
+
outputTokens: "outputTokens";
|
|
283
|
+
calls: "calls";
|
|
284
|
+
wallClock: "wallClock";
|
|
285
|
+
modelMs: "modelMs";
|
|
286
|
+
cost: "cost";
|
|
287
|
+
toolCalls: "toolCalls";
|
|
288
|
+
responseSize: "responseSize";
|
|
289
|
+
}>;
|
|
290
|
+
maximum: z.ZodNumber;
|
|
291
|
+
window: z.ZodOptional<z.ZodString>;
|
|
292
|
+
scope: z.ZodOptional<z.ZodEnum<{
|
|
293
|
+
task: "task";
|
|
294
|
+
session: "session";
|
|
295
|
+
agent: "agent";
|
|
296
|
+
global: "global";
|
|
297
|
+
}>>;
|
|
298
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
299
|
+
warning: "warning";
|
|
300
|
+
block: "block";
|
|
301
|
+
}>>;
|
|
302
|
+
message: z.ZodOptional<z.ZodString>;
|
|
303
|
+
}, z.core.$strip>>>>;
|
|
304
|
+
mode: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
305
|
+
warning: "warning";
|
|
306
|
+
block: "block";
|
|
307
|
+
}>>>;
|
|
308
|
+
costPerInputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
309
|
+
costPerOutputToken: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
310
|
+
scope: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
|
|
311
|
+
task: "task";
|
|
312
|
+
session: "session";
|
|
313
|
+
agent: "agent";
|
|
314
|
+
global: "global";
|
|
315
|
+
}>>>;
|
|
316
|
+
}, z.core.$strip>>>>;
|
|
317
|
+
}, z.core.$strip>>;
|
|
318
|
+
}, z.core.$strip>;
|
|
319
|
+
export type PluginConfig = z.input<typeof pluginConfigSchema>;
|
|
320
|
+
export declare const configSchema: z.ZodObject<{}, z.core.$loose>;
|
|
321
|
+
declare const createPlugin: PluginFactory;
|
|
322
|
+
export default createPlugin;
|
|
323
|
+
export { createPlugin };
|
package/index.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { z as p } from "zod";
|
|
2
|
+
function S(f) {
|
|
3
|
+
const e = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/, t = f.match(e);
|
|
4
|
+
if (!t)
|
|
5
|
+
throw new Error(`invalid ISO 8601 duration: ${f}`);
|
|
6
|
+
const [, o, s, a, n, i, c] = t;
|
|
7
|
+
let r = 0;
|
|
8
|
+
return o && (r += parseInt(o) * 365.25 * 864e5), s && (r += parseInt(s) * 30.44 * 864e5), a && (r += parseInt(a) * 864e5), n && (r += parseInt(n) * 36e5), i && (r += parseInt(i) * 6e4), c && (r += parseFloat(c) * 1e3), Math.round(r);
|
|
9
|
+
}
|
|
10
|
+
const E = [
|
|
11
|
+
"tokens",
|
|
12
|
+
"inputTokens",
|
|
13
|
+
"outputTokens",
|
|
14
|
+
"calls",
|
|
15
|
+
"wallClock",
|
|
16
|
+
"modelMs",
|
|
17
|
+
"cost",
|
|
18
|
+
"toolCalls",
|
|
19
|
+
"responseSize"
|
|
20
|
+
], M = p.object({
|
|
21
|
+
field: p.enum(E),
|
|
22
|
+
maximum: p.number().min(0),
|
|
23
|
+
window: p.string().optional(),
|
|
24
|
+
scope: p.enum(["task", "session", "agent", "global"]).optional(),
|
|
25
|
+
mode: p.enum(["warning", "block"]).optional(),
|
|
26
|
+
message: p.string().optional()
|
|
27
|
+
}), g = p.object({
|
|
28
|
+
caps: p.array(M).optional(),
|
|
29
|
+
mode: p.enum(["warning", "block"]).optional(),
|
|
30
|
+
costPerInputToken: p.number().min(0).optional(),
|
|
31
|
+
costPerOutputToken: p.number().min(0).optional(),
|
|
32
|
+
scope: p.enum(["task", "session", "agent", "global"]).optional()
|
|
33
|
+
}), w = p.object({
|
|
34
|
+
defaults: g.optional(),
|
|
35
|
+
agent: p.object({
|
|
36
|
+
default: g.optional(),
|
|
37
|
+
overrides: p.record(p.string(), g.partial()).optional().default({})
|
|
38
|
+
}).optional(),
|
|
39
|
+
provider: p.object({
|
|
40
|
+
default: g.optional(),
|
|
41
|
+
overrides: p.record(p.string(), g.partial()).optional().default({})
|
|
42
|
+
}).optional(),
|
|
43
|
+
tool: p.object({
|
|
44
|
+
default: g.optional(),
|
|
45
|
+
overrides: p.record(p.string(), g.partial()).optional().default({})
|
|
46
|
+
}).optional()
|
|
47
|
+
}), y = p.object({}).passthrough(), x = {
|
|
48
|
+
maxInputTokens: { field: "inputTokens" },
|
|
49
|
+
maxOutputTokens: { field: "outputTokens" },
|
|
50
|
+
maxTotalTokens: { field: "tokens" },
|
|
51
|
+
maxModelCalls: { field: "calls" },
|
|
52
|
+
maxWallClockMs: { field: "wallClock" },
|
|
53
|
+
maxModelMs: { field: "modelMs" },
|
|
54
|
+
maxCostUSD: { field: "cost" },
|
|
55
|
+
maxTokensPer24h: { field: "tokens", window: "PT24H" },
|
|
56
|
+
maxCalls: { field: "toolCalls" }
|
|
57
|
+
};
|
|
58
|
+
function b(f) {
|
|
59
|
+
const e = [], t = {};
|
|
60
|
+
for (const [o, s] of Object.entries(f)) {
|
|
61
|
+
const a = x[o];
|
|
62
|
+
if (a && typeof s == "number") {
|
|
63
|
+
const n = { field: a.field, maximum: s };
|
|
64
|
+
a.window && (n.window = a.window), e.push(n);
|
|
65
|
+
} else
|
|
66
|
+
(o === "scope" || o === "mode" || o === "costPerInputToken" || o === "costPerOutputToken" || o === "message") && (t[o] = s);
|
|
67
|
+
}
|
|
68
|
+
return e.length > 0 && (t.caps = e), g.parse(t);
|
|
69
|
+
}
|
|
70
|
+
function _(f) {
|
|
71
|
+
const e = f;
|
|
72
|
+
if (e.defaults !== void 0 || e.agent !== void 0 || e.provider !== void 0 || e.tool !== void 0) {
|
|
73
|
+
const t = w.parse(f);
|
|
74
|
+
return {
|
|
75
|
+
defaults: t.defaults ?? g.parse({}),
|
|
76
|
+
agent: t.agent ?? { overrides: {} },
|
|
77
|
+
provider: t.provider ?? { overrides: {} },
|
|
78
|
+
tool: t.tool ?? { overrides: {} }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
defaults: b(e),
|
|
83
|
+
agent: { overrides: {} },
|
|
84
|
+
provider: { overrides: {} },
|
|
85
|
+
tool: { overrides: {} }
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function T(f, e, t, o) {
|
|
89
|
+
return {
|
|
90
|
+
isEnforcementError: !0,
|
|
91
|
+
code: "BUDGET_EXCEEDED",
|
|
92
|
+
message: o ?? `${f} limit is ${e}, current value is ${Math.round(t)}`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function v(f, e, t) {
|
|
96
|
+
return { isToolWarning: !0, toolName: f, callId: e, message: t };
|
|
97
|
+
}
|
|
98
|
+
class O {
|
|
99
|
+
constructor(e, t, o = 0, s = 0) {
|
|
100
|
+
this.db = e, this.cfg = t, this.costPerInput = o, this.costPerOutput = s, this.name = "agent-mcp-budget", this.accumulators = /* @__PURE__ */ new Map();
|
|
101
|
+
}
|
|
102
|
+
install(e) {
|
|
103
|
+
e.register("task:start", (t) => {
|
|
104
|
+
try {
|
|
105
|
+
this.onTaskStart(t);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}), e.register("pre:model_request", (t) => {
|
|
109
|
+
try {
|
|
110
|
+
this.onPreModelRequest(t);
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}), e.register("post:model_response", (t) => {
|
|
114
|
+
try {
|
|
115
|
+
this.onPostModelResponse(t);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}), e.register("task:completed", (t) => {
|
|
119
|
+
try {
|
|
120
|
+
this.onTerminal(t.executionContext.taskId);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}), e.register("task:failed", (t) => {
|
|
124
|
+
try {
|
|
125
|
+
this.onTerminal(t.executionContext.taskId);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}), e.register("task:cancelled", (t) => {
|
|
129
|
+
try {
|
|
130
|
+
this.onTerminal(t.executionContext.taskId);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}), e.registerEnforcement(
|
|
134
|
+
"pre:model_request",
|
|
135
|
+
(t) => this.enforcePreModel(t)
|
|
136
|
+
), e.registerEnforcement("pre:tool_call", (t) => this.enforcePreTool(t)), e.register("transform:tool_result", (t) => {
|
|
137
|
+
try {
|
|
138
|
+
this.enforceResponseSize(t);
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// ── Observational handlers ────────────────────────────────────────────────
|
|
144
|
+
onTaskStart(e) {
|
|
145
|
+
var n, i;
|
|
146
|
+
const { taskId: t, sessionId: o, agentName: s } = e.executionContext, a = ((i = (n = e.executionContext.agentDefinition) == null ? void 0 : n.provider) == null ? void 0 : i.type) ?? "unknown";
|
|
147
|
+
this.accumulators.set(t, {
|
|
148
|
+
taskId: t,
|
|
149
|
+
sessionId: o ?? void 0,
|
|
150
|
+
agentName: s,
|
|
151
|
+
providerType: a,
|
|
152
|
+
startedAtMs: Date.now(),
|
|
153
|
+
inputTokens: 0,
|
|
154
|
+
outputTokens: 0,
|
|
155
|
+
modelCalls: 0,
|
|
156
|
+
totalModelMs: 0,
|
|
157
|
+
toolCalls: /* @__PURE__ */ new Map()
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
onPreModelRequest(e) {
|
|
161
|
+
const t = this.accumulators.get(e.executionContext.taskId);
|
|
162
|
+
t && (t.modelCallStartMs = Date.now());
|
|
163
|
+
}
|
|
164
|
+
onPostModelResponse(e) {
|
|
165
|
+
const t = this.accumulators.get(e.executionContext.taskId);
|
|
166
|
+
if (!t)
|
|
167
|
+
return;
|
|
168
|
+
const o = e.tokenUsage;
|
|
169
|
+
o && (t.inputTokens += o.inputTokens ?? 0, t.outputTokens += o.outputTokens ?? 0), t.modelCalls += 1, t.modelCallStartMs !== void 0 && (t.totalModelMs += Date.now() - t.modelCallStartMs, t.modelCallStartMs = void 0);
|
|
170
|
+
}
|
|
171
|
+
onTerminal(e) {
|
|
172
|
+
this.accumulators.delete(e);
|
|
173
|
+
}
|
|
174
|
+
// ── Config resolution ─────────────────────────────────────────────────────
|
|
175
|
+
mergeDim(e) {
|
|
176
|
+
let t = { caps: [] };
|
|
177
|
+
for (const o of e)
|
|
178
|
+
o && (t = {
|
|
179
|
+
caps: [...t.caps ?? [], ...o.caps ?? []],
|
|
180
|
+
mode: o.mode ?? t.mode,
|
|
181
|
+
costPerInputToken: o.costPerInputToken ?? t.costPerInputToken,
|
|
182
|
+
costPerOutputToken: o.costPerOutputToken ?? t.costPerOutputToken,
|
|
183
|
+
scope: o.scope ?? t.scope
|
|
184
|
+
});
|
|
185
|
+
return t;
|
|
186
|
+
}
|
|
187
|
+
resolveCaps(e, t, o) {
|
|
188
|
+
var u, m, d;
|
|
189
|
+
const s = this.cfg.defaults, a = this.cfg.agent, n = this.cfg.provider, i = this.cfg.tool;
|
|
190
|
+
if (o) {
|
|
191
|
+
const k = (u = i == null ? void 0 : i.overrides) == null ? void 0 : u[o], h = this.mergeDim([s, i == null ? void 0 : i.default, k]);
|
|
192
|
+
return {
|
|
193
|
+
caps: h.caps ?? [],
|
|
194
|
+
mode: h.mode,
|
|
195
|
+
scope: h.scope
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const c = (m = a == null ? void 0 : a.overrides) == null ? void 0 : m[e], r = (d = n == null ? void 0 : n.overrides) == null ? void 0 : d[t], l = this.mergeDim([
|
|
199
|
+
s,
|
|
200
|
+
a == null ? void 0 : a.default,
|
|
201
|
+
c,
|
|
202
|
+
n == null ? void 0 : n.default,
|
|
203
|
+
r
|
|
204
|
+
]);
|
|
205
|
+
return { caps: l.caps ?? [], mode: l.mode, scope: l.scope };
|
|
206
|
+
}
|
|
207
|
+
// ── Scope-aware DB queries ────────────────────────────────────────────────
|
|
208
|
+
queryScopeTotals(e, t, o, s) {
|
|
209
|
+
const a = this.accumulators.get(e), n = a ? {
|
|
210
|
+
inputTokens: a.inputTokens,
|
|
211
|
+
outputTokens: a.outputTokens,
|
|
212
|
+
modelCalls: a.modelCalls
|
|
213
|
+
} : { inputTokens: 0, outputTokens: 0, modelCalls: 0 };
|
|
214
|
+
if (s === "task" || !this.db)
|
|
215
|
+
return n;
|
|
216
|
+
try {
|
|
217
|
+
const i = this.db;
|
|
218
|
+
let c;
|
|
219
|
+
if (s === "session" && t ? c = i.prepare(
|
|
220
|
+
`SELECT
|
|
221
|
+
COALESCE(SUM(tu.input_tokens), 0) AS input,
|
|
222
|
+
COALESCE(SUM(tu.output_tokens), 0) AS output,
|
|
223
|
+
COALESCE(SUM(tu.model_calls), 0) AS calls
|
|
224
|
+
FROM task_usage tu
|
|
225
|
+
JOIN tasks t ON tu.task_id = t.id
|
|
226
|
+
WHERE t.session_id = ? AND tu.task_id != ?`
|
|
227
|
+
).get(t, e) : s === "agent" ? c = i.prepare(
|
|
228
|
+
`SELECT
|
|
229
|
+
COALESCE(SUM(input_tokens), 0) AS input,
|
|
230
|
+
COALESCE(SUM(output_tokens), 0) AS output,
|
|
231
|
+
COALESCE(SUM(model_calls), 0) AS calls
|
|
232
|
+
FROM task_usage
|
|
233
|
+
WHERE agent_name = ? AND task_id != ?`
|
|
234
|
+
).get(o, e) : s === "global" && (c = i.prepare(
|
|
235
|
+
`SELECT
|
|
236
|
+
COALESCE(SUM(input_tokens), 0) AS input,
|
|
237
|
+
COALESCE(SUM(output_tokens), 0) AS output,
|
|
238
|
+
COALESCE(SUM(model_calls), 0) AS calls
|
|
239
|
+
FROM task_usage
|
|
240
|
+
WHERE task_id != ?`
|
|
241
|
+
).get(e)), c)
|
|
242
|
+
return {
|
|
243
|
+
inputTokens: (c.input ?? 0) + n.inputTokens,
|
|
244
|
+
outputTokens: (c.output ?? 0) + n.outputTokens,
|
|
245
|
+
modelCalls: (c.calls ?? 0) + n.modelCalls
|
|
246
|
+
};
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
return n;
|
|
250
|
+
}
|
|
251
|
+
queryWindowTokens(e, t, o, s) {
|
|
252
|
+
if (!this.db)
|
|
253
|
+
return 0;
|
|
254
|
+
try {
|
|
255
|
+
const a = this.db, n = new Date(Date.now() - o).toISOString();
|
|
256
|
+
let i;
|
|
257
|
+
if (e === "session") {
|
|
258
|
+
const c = s ? " AND tu.task_id != ?" : "", r = [t, n];
|
|
259
|
+
s && r.push(s), i = a.prepare(
|
|
260
|
+
`SELECT COALESCE(SUM(tu.input_tokens + tu.output_tokens), 0) AS total
|
|
261
|
+
FROM task_usage tu
|
|
262
|
+
JOIN tasks t ON tu.task_id = t.id
|
|
263
|
+
WHERE t.session_id = ? AND tu.created_at >= ?${c}`
|
|
264
|
+
).get(...r);
|
|
265
|
+
} else if (e === "agent") {
|
|
266
|
+
const c = s ? " AND task_id != ?" : "", r = [t, n];
|
|
267
|
+
s && r.push(s), i = a.prepare(
|
|
268
|
+
`SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
|
|
269
|
+
FROM task_usage
|
|
270
|
+
WHERE agent_name = ? AND created_at >= ?${c}`
|
|
271
|
+
).get(...r);
|
|
272
|
+
} else if (e === "global") {
|
|
273
|
+
const c = s ? " AND task_id != ?" : "", r = [n];
|
|
274
|
+
s && r.push(s), i = a.prepare(
|
|
275
|
+
`SELECT COALESCE(SUM(input_tokens + output_tokens), 0) AS total
|
|
276
|
+
FROM task_usage
|
|
277
|
+
WHERE created_at >= ?${c}`
|
|
278
|
+
).get(...r);
|
|
279
|
+
}
|
|
280
|
+
return (i == null ? void 0 : i.total) ?? 0;
|
|
281
|
+
} catch {
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// ── Generic cap evaluation ────────────────────────────────────────────────
|
|
286
|
+
// ── Single-shot usage snapshot ──────────────────────────────────────────
|
|
287
|
+
/**
|
|
288
|
+
* Build a complete usage snapshot for the current enforcement event.
|
|
289
|
+
*
|
|
290
|
+
* Makes exactly `U + W` DB queries where:
|
|
291
|
+
* U = number of unique non-task scopes across all caps (0..3)
|
|
292
|
+
* W = number of unique (scope, window) pairs across all caps
|
|
293
|
+
*
|
|
294
|
+
* Independent of cap count, agent count, session count, or history depth.
|
|
295
|
+
*/
|
|
296
|
+
buildSnapshot(e, t, o, s, a, n) {
|
|
297
|
+
const i = {};
|
|
298
|
+
i.inputTokens = t.inputTokens, i.outputTokens = t.outputTokens, i.calls = t.modelCalls, i.wallClock = Date.now() - t.startedAtMs, i.modelMs = t.totalModelMs, i.cost = t.inputTokens * this.costPerInput + t.outputTokens * this.costPerOutput;
|
|
299
|
+
const c = /* @__PURE__ */ new Set(), r = /* @__PURE__ */ new Map();
|
|
300
|
+
for (const l of e) {
|
|
301
|
+
const u = l.scope ?? n ?? "task";
|
|
302
|
+
if (u !== "task" && c.add(u), l.window) {
|
|
303
|
+
const m = `${u}:${l.window}`;
|
|
304
|
+
r.has(m) || r.set(m, {
|
|
305
|
+
scope: u,
|
|
306
|
+
windowMs: S(l.window)
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const l of c) {
|
|
311
|
+
const u = this.queryScopeTotals(o, s, a, l);
|
|
312
|
+
i[`${l}:inputTokens`] = u.inputTokens, i[`${l}:outputTokens`] = u.outputTokens, i[`${l}:calls`] = u.modelCalls;
|
|
313
|
+
}
|
|
314
|
+
for (const [l, { scope: u, windowMs: m }] of r) {
|
|
315
|
+
let d = "";
|
|
316
|
+
u === "session" ? d = s ?? "" : u === "agent" && (d = a ?? ""), i[l] = this.queryWindowTokens(u, d, m, o);
|
|
317
|
+
}
|
|
318
|
+
return i;
|
|
319
|
+
}
|
|
320
|
+
getSnapshotValue(e, t, o) {
|
|
321
|
+
const s = t.scope ?? o ?? "task";
|
|
322
|
+
let a;
|
|
323
|
+
t.window ? a = "" : a = s !== "task" ? `${s}:` : "";
|
|
324
|
+
let n;
|
|
325
|
+
switch (t.field) {
|
|
326
|
+
case "inputTokens":
|
|
327
|
+
n = e[`${a}inputTokens`] ?? e.inputTokens;
|
|
328
|
+
break;
|
|
329
|
+
case "outputTokens":
|
|
330
|
+
n = e[`${a}outputTokens`] ?? e.outputTokens;
|
|
331
|
+
break;
|
|
332
|
+
case "tokens":
|
|
333
|
+
n = (e[`${a}inputTokens`] ?? e.inputTokens) + (e[`${a}outputTokens`] ?? e.outputTokens);
|
|
334
|
+
break;
|
|
335
|
+
case "calls":
|
|
336
|
+
n = e[`${a}calls`] ?? e.calls;
|
|
337
|
+
break;
|
|
338
|
+
case "wallClock":
|
|
339
|
+
n = e.wallClock;
|
|
340
|
+
break;
|
|
341
|
+
case "modelMs":
|
|
342
|
+
n = e.modelMs;
|
|
343
|
+
break;
|
|
344
|
+
case "cost":
|
|
345
|
+
n = e.cost;
|
|
346
|
+
break;
|
|
347
|
+
case "toolCalls":
|
|
348
|
+
n = 0;
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
n = 0;
|
|
352
|
+
}
|
|
353
|
+
return t.window && (n += e[`${s}:${t.window}`] ?? 0), n;
|
|
354
|
+
}
|
|
355
|
+
evaluateCap(e, t, o) {
|
|
356
|
+
const s = this.getSnapshotValue(t, e, o);
|
|
357
|
+
if (s >= e.maximum)
|
|
358
|
+
throw T(e.field, e.maximum, s, e.message);
|
|
359
|
+
}
|
|
360
|
+
// ── Enforcement: pre:model_request ────────────────────────────────────────
|
|
361
|
+
enforcePreModel(e) {
|
|
362
|
+
var u, m;
|
|
363
|
+
const { taskId: t, sessionId: o, agentName: s } = e.executionContext, a = ((m = (u = e.executionContext.agentDefinition) == null ? void 0 : u.provider) == null ? void 0 : m.type) ?? "unknown", n = this.accumulators.get(t);
|
|
364
|
+
if (!n)
|
|
365
|
+
return;
|
|
366
|
+
const { caps: i, scope: c } = this.resolveCaps(s, a), r = i.filter((d) => d.field !== "toolCalls");
|
|
367
|
+
if (r.length === 0)
|
|
368
|
+
return;
|
|
369
|
+
const l = this.buildSnapshot(
|
|
370
|
+
r,
|
|
371
|
+
n,
|
|
372
|
+
t,
|
|
373
|
+
o,
|
|
374
|
+
s,
|
|
375
|
+
c
|
|
376
|
+
);
|
|
377
|
+
for (const d of r)
|
|
378
|
+
this.evaluateCap(d, l);
|
|
379
|
+
}
|
|
380
|
+
// ── Enforcement: pre:tool_call ────────────────────────────────────────────
|
|
381
|
+
enforcePreTool(e) {
|
|
382
|
+
const { toolName: t, callId: o, executionContext: s } = e, {
|
|
383
|
+
caps: a,
|
|
384
|
+
mode: n,
|
|
385
|
+
scope: i
|
|
386
|
+
} = this.resolveCaps(s.agentName, "", t), c = this.accumulators.get(s.taskId);
|
|
387
|
+
if (!c || a.length === 0)
|
|
388
|
+
return;
|
|
389
|
+
const r = c.toolCalls.get(t) ?? 0, l = this.buildSnapshot(
|
|
390
|
+
a,
|
|
391
|
+
c,
|
|
392
|
+
s.taskId,
|
|
393
|
+
s.sessionId,
|
|
394
|
+
s.agentName,
|
|
395
|
+
i
|
|
396
|
+
);
|
|
397
|
+
for (const u of a) {
|
|
398
|
+
const m = u.field === "toolCalls" ? r : this.getSnapshotValue(l, u, i);
|
|
399
|
+
if (m >= u.maximum) {
|
|
400
|
+
const d = u.message ?? `tool "${t}": ${u.field} limit is ${u.maximum}, current value is ${Math.round(m)}`;
|
|
401
|
+
throw (u.mode ?? n ?? "warning") === "warning" ? v(t, o, d) : T(
|
|
402
|
+
`tool:${t}:${u.field}`,
|
|
403
|
+
u.maximum,
|
|
404
|
+
m,
|
|
405
|
+
u.message
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
c.toolCalls.set(t, r + 1);
|
|
410
|
+
}
|
|
411
|
+
// ── Enforcement: transform:tool_result (response size) ────────────────────
|
|
412
|
+
enforceResponseSize(e) {
|
|
413
|
+
const { toolName: t, result: o } = e;
|
|
414
|
+
if (typeof o != "object" || o === null)
|
|
415
|
+
return;
|
|
416
|
+
const { caps: s, mode: a } = this.resolveCaps("", "", t), n = s.filter((l) => l.field === "responseSize");
|
|
417
|
+
if (n.length === 0)
|
|
418
|
+
return;
|
|
419
|
+
const i = o, c = i.content;
|
|
420
|
+
if (!Array.isArray(c))
|
|
421
|
+
return;
|
|
422
|
+
let r = 0;
|
|
423
|
+
for (const l of c)
|
|
424
|
+
if (typeof l == "object" && l !== null) {
|
|
425
|
+
const u = l;
|
|
426
|
+
u.type === "text" && (r += (u.text ?? "").length);
|
|
427
|
+
}
|
|
428
|
+
for (const l of n) {
|
|
429
|
+
if (r <= l.maximum)
|
|
430
|
+
continue;
|
|
431
|
+
if ((l.mode ?? a ?? "warning") === "block")
|
|
432
|
+
i.content = [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: l.message ?? `Response size (${r} chars) exceeds limit of ${l.maximum}. Use offset/limit or shell paging tools instead.`
|
|
436
|
+
}
|
|
437
|
+
], e.isError = !0;
|
|
438
|
+
else {
|
|
439
|
+
let m = l.maximum;
|
|
440
|
+
const d = [];
|
|
441
|
+
for (const k of c) {
|
|
442
|
+
if (typeof k != "object" || k === null) {
|
|
443
|
+
d.push(k);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const h = k;
|
|
447
|
+
if (h.type !== "text") {
|
|
448
|
+
d.push(k);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const C = h.text ?? "";
|
|
452
|
+
if (C.length <= m)
|
|
453
|
+
d.push(k), m -= C.length;
|
|
454
|
+
else {
|
|
455
|
+
d.push({ type: "text", text: C.slice(0, m) });
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
d.push({
|
|
460
|
+
type: "text",
|
|
461
|
+
text: `
|
|
462
|
+
|
|
463
|
+
[truncated: response was ${r} chars, limited to ${l.maximum}. ${l.message ?? "Use offset/limit or shell paging tools for full content."}]`
|
|
464
|
+
}), i.content = d;
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const $ = ({ db: f, config: e }) => {
|
|
471
|
+
var a, n;
|
|
472
|
+
const t = _(e), o = ((a = t.defaults) == null ? void 0 : a.costPerInputToken) ?? 0, s = ((n = t.defaults) == null ? void 0 : n.costPerOutputToken) ?? 0;
|
|
473
|
+
return new O(f, t, o, s);
|
|
474
|
+
};
|
|
475
|
+
export {
|
|
476
|
+
y as configSchema,
|
|
477
|
+
$ as createPlugin,
|
|
478
|
+
$ as default,
|
|
479
|
+
w as pluginConfigSchema
|
|
480
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adhd/agent-plugin-budget",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Budget enforcement plugin for @adhd/agent-mcp — caps token spend, cost, wall-clock time, and model calls per task/session/agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./index.cjs",
|
|
10
|
+
"module": "./index.js",
|
|
11
|
+
"typings": "./index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./index.js",
|
|
15
|
+
"require": "./index.cjs",
|
|
16
|
+
"types": "./index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@adhd/agent-base-types": "^2.1.1"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"zod": "*"
|
|
24
|
+
}
|
|
25
|
+
}
|