palaryn 0.1.0 → 0.3.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/README.md +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { BudgetConfig, BudgetState, CostEstimate } from '../types/budget';
|
|
2
|
+
import { BudgetReport, UsageData } from '../types/tool-result';
|
|
3
|
+
import { BudgetStore, WorkspaceBudgetConfig } from '../storage/interfaces';
|
|
4
|
+
import { ToolCall } from '../types/tool-call';
|
|
5
|
+
|
|
6
|
+
/** Small epsilon for floating-point comparison tolerance in budget checks */
|
|
7
|
+
const BUDGET_EPSILON = 1e-10;
|
|
8
|
+
|
|
9
|
+
// Default cost table for different tool types (used when no config override provided)
|
|
10
|
+
const DEFAULT_COST_TABLE: Record<string, number> = {
|
|
11
|
+
'http.request': 0.001,
|
|
12
|
+
'http.request.write': 0.002,
|
|
13
|
+
'slack.post': 0.003,
|
|
14
|
+
'git.operation': 0.002,
|
|
15
|
+
'db.query': 0.005,
|
|
16
|
+
'browser.navigate': 0.01,
|
|
17
|
+
'default': 0.001,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface CostRecord {
|
|
21
|
+
estimated_cost_usd: number;
|
|
22
|
+
actual_cost_usd?: number;
|
|
23
|
+
usage?: UsageData;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class BudgetManager {
|
|
27
|
+
private config: BudgetConfig;
|
|
28
|
+
private costTable: Record<string, number>;
|
|
29
|
+
private externalStore?: BudgetStore;
|
|
30
|
+
// In-memory stores (fallback when no external store)
|
|
31
|
+
private taskStates: Map<string, BudgetState>;
|
|
32
|
+
private userDailySpend: Map<string, number>; // key: "actor_id:date"
|
|
33
|
+
private userMonthlySpend: Map<string, number>; // key: "actor_id:month"
|
|
34
|
+
private workspaceDailySpend: Map<string, number>; // key: "workspace_id:date"
|
|
35
|
+
private workspaceMonthlySpend: Map<string, number>; // key: "workspace_id:month"
|
|
36
|
+
private callRetryCounts: Map<string, number>; // key: tool_call_id
|
|
37
|
+
private costRecords: Map<string, CostRecord>; // key: tool_call_id
|
|
38
|
+
private reservations: Map<string, { amount: number; taskId: string; workspaceId: string; actorId: string; createdAt: number }>;
|
|
39
|
+
/** Promise-based mutex per budget key to serialize concurrent reservations */
|
|
40
|
+
private budgetLocks: Map<string, Promise<void>>;
|
|
41
|
+
|
|
42
|
+
constructor(config: BudgetConfig, store?: BudgetStore) {
|
|
43
|
+
this.config = {
|
|
44
|
+
task_budget_usd: config.task_budget_usd ?? 2.0,
|
|
45
|
+
user_daily_budget_usd: config.user_daily_budget_usd,
|
|
46
|
+
user_monthly_budget_usd: config.user_monthly_budget_usd,
|
|
47
|
+
workspace_daily_budget_usd: config.workspace_daily_budget_usd,
|
|
48
|
+
workspace_monthly_budget_usd: config.workspace_monthly_budget_usd,
|
|
49
|
+
max_steps_per_task: config.max_steps_per_task ?? 50,
|
|
50
|
+
max_retries_per_call: config.max_retries_per_call ?? 3,
|
|
51
|
+
max_wall_clock_ms: config.max_wall_clock_ms ?? 300000,
|
|
52
|
+
token_pricing: config.token_pricing,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Merge config.cost_table over defaults
|
|
56
|
+
this.costTable = { ...DEFAULT_COST_TABLE, ...(config.cost_table || {}) };
|
|
57
|
+
|
|
58
|
+
this.externalStore = store;
|
|
59
|
+
this.taskStates = new Map();
|
|
60
|
+
this.userDailySpend = new Map();
|
|
61
|
+
this.userMonthlySpend = new Map();
|
|
62
|
+
this.workspaceDailySpend = new Map();
|
|
63
|
+
this.workspaceMonthlySpend = new Map();
|
|
64
|
+
this.callRetryCounts = new Map();
|
|
65
|
+
this.costRecords = new Map();
|
|
66
|
+
this.reservations = new Map();
|
|
67
|
+
this.budgetLocks = new Map();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Populate in-memory maps from the external store.
|
|
72
|
+
* Call once at startup after the store itself has been hydrated.
|
|
73
|
+
*/
|
|
74
|
+
hydrateFromStore(): void {
|
|
75
|
+
if (!this.externalStore) return;
|
|
76
|
+
|
|
77
|
+
if (this.externalStore.getAllTaskStates) {
|
|
78
|
+
for (const [id, state] of this.externalStore.getAllTaskStates()) {
|
|
79
|
+
if (!this.taskStates.has(id)) this.taskStates.set(id, state);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.externalStore.getAllCounters) {
|
|
84
|
+
for (const [key, value] of this.externalStore.getAllCounters()) {
|
|
85
|
+
if (!this.userDailySpend.has(key)) this.userDailySpend.set(key, value);
|
|
86
|
+
if (!this.userMonthlySpend.has(key)) this.userMonthlySpend.set(key, value);
|
|
87
|
+
if (!this.workspaceDailySpend.has(key)) this.workspaceDailySpend.set(key, value);
|
|
88
|
+
if (!this.workspaceMonthlySpend.has(key)) this.workspaceMonthlySpend.set(key, value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.externalStore.getAllRetryCounts) {
|
|
93
|
+
for (const [id, count] of this.externalStore.getAllRetryCounts()) {
|
|
94
|
+
if (!this.callRetryCounts.has(id)) this.callRetryCounts.set(id, count);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Estimate cost for a tool call
|
|
100
|
+
estimateCost(toolCall: ToolCall): CostEstimate {
|
|
101
|
+
const toolName = toolCall.tool.name;
|
|
102
|
+
const capability = toolCall.tool.capability;
|
|
103
|
+
|
|
104
|
+
// First try an exact match with capability suffix for write/delete operations
|
|
105
|
+
let cost: number | undefined;
|
|
106
|
+
|
|
107
|
+
if (capability === 'write' || capability === 'delete' || capability === 'admin') {
|
|
108
|
+
cost = this.costTable[`${toolName}.${capability}`] ?? this.costTable[`${toolName}.write`];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fall back to base tool name, then default
|
|
112
|
+
if (cost === undefined) {
|
|
113
|
+
cost = this.costTable[toolName] ?? this.costTable['default'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Apply a multiplier for destructive or elevated operations
|
|
117
|
+
if (capability === 'delete') {
|
|
118
|
+
cost *= 1.5;
|
|
119
|
+
} else if (capability === 'admin') {
|
|
120
|
+
cost *= 2.0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Respect per-call max_cost_usd constraint as an upper bound on the estimate
|
|
124
|
+
if (toolCall.constraints?.max_cost_usd !== undefined && toolCall.constraints.max_cost_usd > 0) {
|
|
125
|
+
cost = Math.min(cost, toolCall.constraints.max_cost_usd);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
tool_name: toolName,
|
|
130
|
+
capability,
|
|
131
|
+
estimated_cost_usd: cost,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if a tool call is within budget (returns allowed: false with reason if not)
|
|
136
|
+
// Optional overrides allow per-workspace budget configuration.
|
|
137
|
+
check(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): { allowed: boolean; reason?: string; report: BudgetReport } {
|
|
138
|
+
const cfg = overrides ? { ...this.config, ...overrides } : this.config;
|
|
139
|
+
const state = this.getTaskState(toolCall);
|
|
140
|
+
const estimate = this.estimateCost(toolCall);
|
|
141
|
+
const estimatedCost = estimate.estimated_cost_usd;
|
|
142
|
+
|
|
143
|
+
// Check task budget
|
|
144
|
+
const taskBudget = cfg.task_budget_usd!;
|
|
145
|
+
if (state.spent_usd + estimatedCost > taskBudget + BUDGET_EPSILON) {
|
|
146
|
+
return {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: `Task budget exceeded: spent $${state.spent_usd.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${taskBudget.toFixed(4)}`,
|
|
149
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check step limit
|
|
154
|
+
const maxSteps = cfg.max_steps_per_task!;
|
|
155
|
+
if (state.steps + 1 > maxSteps) {
|
|
156
|
+
return {
|
|
157
|
+
allowed: false,
|
|
158
|
+
reason: `Step limit exceeded: ${state.steps} steps taken, max is ${maxSteps}`,
|
|
159
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check wall clock time
|
|
164
|
+
const maxWallClock = cfg.max_wall_clock_ms!;
|
|
165
|
+
const elapsed = Date.now() - new Date(state.started_at).getTime();
|
|
166
|
+
if (elapsed > maxWallClock) {
|
|
167
|
+
return {
|
|
168
|
+
allowed: false,
|
|
169
|
+
reason: `Wall clock time exceeded: ${elapsed}ms elapsed, max is ${maxWallClock}ms`,
|
|
170
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check user daily budget
|
|
175
|
+
const userDailyLimit = cfg.user_daily_budget_usd;
|
|
176
|
+
if (userDailyLimit !== undefined) {
|
|
177
|
+
const dailyKey = `${state.actor_id}:${this.getDateKey()}`;
|
|
178
|
+
const dailySpend = this.userDailySpend.get(dailyKey) ?? 0;
|
|
179
|
+
if (dailySpend + estimatedCost > userDailyLimit + BUDGET_EPSILON) {
|
|
180
|
+
return {
|
|
181
|
+
allowed: false,
|
|
182
|
+
reason: `User daily budget exceeded: spent $${dailySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${userDailyLimit.toFixed(4)}`,
|
|
183
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check user monthly budget
|
|
189
|
+
const userMonthlyLimit = cfg.user_monthly_budget_usd;
|
|
190
|
+
if (userMonthlyLimit !== undefined) {
|
|
191
|
+
const monthlyKey = `${state.actor_id}:${this.getMonthKey()}`;
|
|
192
|
+
const monthlySpend = this.userMonthlySpend.get(monthlyKey) ?? 0;
|
|
193
|
+
if (monthlySpend + estimatedCost > userMonthlyLimit + BUDGET_EPSILON) {
|
|
194
|
+
return {
|
|
195
|
+
allowed: false,
|
|
196
|
+
reason: `User monthly budget exceeded: spent $${monthlySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${userMonthlyLimit.toFixed(4)}`,
|
|
197
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check workspace daily budget
|
|
203
|
+
const wsDailyLimit = cfg.workspace_daily_budget_usd;
|
|
204
|
+
if (wsDailyLimit !== undefined) {
|
|
205
|
+
const dailyKey = `${state.workspace_id}:${this.getDateKey()}`;
|
|
206
|
+
const dailySpend = this.workspaceDailySpend.get(dailyKey) ?? 0;
|
|
207
|
+
if (dailySpend + estimatedCost > wsDailyLimit + BUDGET_EPSILON) {
|
|
208
|
+
return {
|
|
209
|
+
allowed: false,
|
|
210
|
+
reason: `Workspace daily budget exceeded: spent $${dailySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${wsDailyLimit.toFixed(4)}`,
|
|
211
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check workspace monthly budget
|
|
217
|
+
const wsMonthlyLimit = cfg.workspace_monthly_budget_usd;
|
|
218
|
+
if (wsMonthlyLimit !== undefined) {
|
|
219
|
+
const monthlyKey = `${state.workspace_id}:${this.getMonthKey()}`;
|
|
220
|
+
const monthlySpend = this.workspaceMonthlySpend.get(monthlyKey) ?? 0;
|
|
221
|
+
if (monthlySpend + estimatedCost > wsMonthlyLimit + BUDGET_EPSILON) {
|
|
222
|
+
return {
|
|
223
|
+
allowed: false,
|
|
224
|
+
reason: `Workspace monthly budget exceeded: spent $${monthlySpend.toFixed(4)} + estimated $${estimatedCost.toFixed(4)} > limit $${wsMonthlyLimit.toFixed(4)}`,
|
|
225
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
allowed: true,
|
|
232
|
+
report: this.getReport(toolCall, estimatedCost, cfg),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Atomically check budget and reserve the estimated cost.
|
|
238
|
+
* The reservation temporarily reduces available budget so concurrent
|
|
239
|
+
* requests cannot both pass the check.
|
|
240
|
+
* Uses a per-task lock to serialize concurrent reservations for the same budget key.
|
|
241
|
+
*/
|
|
242
|
+
async reserveAndCheck(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): Promise<{ allowed: boolean; reason?: string; report: BudgetReport; reservationKey?: string }> {
|
|
243
|
+
// Serialize concurrent reservations for the same task to prevent interleaving
|
|
244
|
+
const lockKey = toolCall.task_id;
|
|
245
|
+
const existingLock = this.budgetLocks.get(lockKey) ?? Promise.resolve();
|
|
246
|
+
|
|
247
|
+
let releaseLock!: () => void;
|
|
248
|
+
const newLock = new Promise<void>(resolve => { releaseLock = resolve; });
|
|
249
|
+
this.budgetLocks.set(lockKey, existingLock.then(() => newLock));
|
|
250
|
+
|
|
251
|
+
// Wait for prior reservation with timeout to prevent deadlocks
|
|
252
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
253
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
254
|
+
try {
|
|
255
|
+
await Promise.race([
|
|
256
|
+
existingLock,
|
|
257
|
+
new Promise<void>((_, reject) => {
|
|
258
|
+
timeoutHandle = setTimeout(() => reject(new Error('Budget lock timeout')), LOCK_TIMEOUT_MS);
|
|
259
|
+
}),
|
|
260
|
+
]);
|
|
261
|
+
} catch {
|
|
262
|
+
// On timeout, proceed without lock (accept brief race risk vs permanent hang)
|
|
263
|
+
console.warn(`[budget] Lock timeout for task ${lockKey}, proceeding without lock`);
|
|
264
|
+
} finally {
|
|
265
|
+
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
return this._reserveAndCheckSync(toolCall, overrides);
|
|
270
|
+
} finally {
|
|
271
|
+
releaseLock();
|
|
272
|
+
// Clean up lock if no one else is waiting
|
|
273
|
+
if (this.budgetLocks.get(lockKey) === existingLock.then(() => newLock)) {
|
|
274
|
+
this.budgetLocks.delete(lockKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Synchronous check-and-reserve. Must be called while holding the per-task lock.
|
|
281
|
+
*/
|
|
282
|
+
private _reserveAndCheckSync(toolCall: ToolCall, overrides?: WorkspaceBudgetConfig): { allowed: boolean; reason?: string; report: BudgetReport; reservationKey?: string } {
|
|
283
|
+
const result = this.check(toolCall, overrides);
|
|
284
|
+
if (!result.allowed) {
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Reserve the estimated cost
|
|
289
|
+
const estimate = this.estimateCost(toolCall);
|
|
290
|
+
const reservationKey = `reservation:${toolCall.tool_call_id}`;
|
|
291
|
+
|
|
292
|
+
// Temporarily record the estimated cost so concurrent requests see it
|
|
293
|
+
const state = this.getTaskState(toolCall);
|
|
294
|
+
state.spent_usd += estimate.estimated_cost_usd;
|
|
295
|
+
this.taskStates.set(toolCall.task_id, state);
|
|
296
|
+
|
|
297
|
+
// Also reserve against user/workspace counters
|
|
298
|
+
const dateKey = this.getDateKey();
|
|
299
|
+
const monthKey = this.getMonthKey();
|
|
300
|
+
|
|
301
|
+
const userDailyKey = `${state.actor_id}:${dateKey}`;
|
|
302
|
+
this.userDailySpend.set(userDailyKey, (this.userDailySpend.get(userDailyKey) ?? 0) + estimate.estimated_cost_usd);
|
|
303
|
+
|
|
304
|
+
const userMonthlyKey = `${state.actor_id}:${monthKey}`;
|
|
305
|
+
this.userMonthlySpend.set(userMonthlyKey, (this.userMonthlySpend.get(userMonthlyKey) ?? 0) + estimate.estimated_cost_usd);
|
|
306
|
+
|
|
307
|
+
const workspaceDailyKey = `${state.workspace_id}:${dateKey}`;
|
|
308
|
+
this.workspaceDailySpend.set(workspaceDailyKey, (this.workspaceDailySpend.get(workspaceDailyKey) ?? 0) + estimate.estimated_cost_usd);
|
|
309
|
+
|
|
310
|
+
const workspaceMonthlyKey = `${state.workspace_id}:${monthKey}`;
|
|
311
|
+
this.workspaceMonthlySpend.set(workspaceMonthlyKey, (this.workspaceMonthlySpend.get(workspaceMonthlyKey) ?? 0) + estimate.estimated_cost_usd);
|
|
312
|
+
|
|
313
|
+
// Track the reservation so we can commit or release later
|
|
314
|
+
this.reservations.set(reservationKey, {
|
|
315
|
+
amount: estimate.estimated_cost_usd,
|
|
316
|
+
taskId: toolCall.task_id,
|
|
317
|
+
workspaceId: state.workspace_id,
|
|
318
|
+
actorId: state.actor_id,
|
|
319
|
+
createdAt: Date.now(),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return { ...result, reservationKey };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Commit a reservation with the actual cost. Releases the difference
|
|
327
|
+
* between the reserved (estimated) amount and the actual cost.
|
|
328
|
+
*/
|
|
329
|
+
commitReservation(reservationKey: string, actualCost: number): void {
|
|
330
|
+
const reservation = this.reservations.get(reservationKey);
|
|
331
|
+
if (!reservation) return;
|
|
332
|
+
|
|
333
|
+
const diff = reservation.amount - actualCost;
|
|
334
|
+
if (diff !== 0) {
|
|
335
|
+
// Adjust task state: remove reserved amount, add actual
|
|
336
|
+
const state = this.taskStates.get(reservation.taskId);
|
|
337
|
+
if (state) {
|
|
338
|
+
state.spent_usd -= diff;
|
|
339
|
+
this.taskStates.set(reservation.taskId, state);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Adjust user/workspace counters
|
|
343
|
+
const dateKey = this.getDateKey();
|
|
344
|
+
const monthKey = this.getMonthKey();
|
|
345
|
+
|
|
346
|
+
const userDailyKey = `${reservation.actorId}:${dateKey}`;
|
|
347
|
+
const curUD = this.userDailySpend.get(userDailyKey);
|
|
348
|
+
if (curUD !== undefined) this.userDailySpend.set(userDailyKey, curUD - diff);
|
|
349
|
+
|
|
350
|
+
const userMonthlyKey = `${reservation.actorId}:${monthKey}`;
|
|
351
|
+
const curUM = this.userMonthlySpend.get(userMonthlyKey);
|
|
352
|
+
if (curUM !== undefined) this.userMonthlySpend.set(userMonthlyKey, curUM - diff);
|
|
353
|
+
|
|
354
|
+
const workspaceDailyKey = `${reservation.workspaceId}:${dateKey}`;
|
|
355
|
+
const curWD = this.workspaceDailySpend.get(workspaceDailyKey);
|
|
356
|
+
if (curWD !== undefined) this.workspaceDailySpend.set(workspaceDailyKey, curWD - diff);
|
|
357
|
+
|
|
358
|
+
const workspaceMonthlyKey = `${reservation.workspaceId}:${monthKey}`;
|
|
359
|
+
const curWM = this.workspaceMonthlySpend.get(workspaceMonthlyKey);
|
|
360
|
+
if (curWM !== undefined) this.workspaceMonthlySpend.set(workspaceMonthlyKey, curWM - diff);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.reservations.delete(reservationKey);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Release a reservation entirely (e.g. on error before execution completes).
|
|
368
|
+
* Removes the reserved amount from all counters.
|
|
369
|
+
*/
|
|
370
|
+
releaseReservation(reservationKey: string): void {
|
|
371
|
+
const reservation = this.reservations.get(reservationKey);
|
|
372
|
+
if (!reservation) return;
|
|
373
|
+
|
|
374
|
+
// Remove the reserved amount from task state
|
|
375
|
+
const state = this.taskStates.get(reservation.taskId);
|
|
376
|
+
if (state) {
|
|
377
|
+
state.spent_usd -= reservation.amount;
|
|
378
|
+
state.steps = Math.max(0, state.steps - 0); // steps not incremented during reserve
|
|
379
|
+
this.taskStates.set(reservation.taskId, state);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Remove from user/workspace counters
|
|
383
|
+
const dateKey = this.getDateKey();
|
|
384
|
+
const monthKey = this.getMonthKey();
|
|
385
|
+
|
|
386
|
+
const userDailyKey = `${reservation.actorId}:${dateKey}`;
|
|
387
|
+
const curUD = this.userDailySpend.get(userDailyKey);
|
|
388
|
+
if (curUD !== undefined) this.userDailySpend.set(userDailyKey, curUD - reservation.amount);
|
|
389
|
+
|
|
390
|
+
const userMonthlyKey = `${reservation.actorId}:${monthKey}`;
|
|
391
|
+
const curUM = this.userMonthlySpend.get(userMonthlyKey);
|
|
392
|
+
if (curUM !== undefined) this.userMonthlySpend.set(userMonthlyKey, curUM - reservation.amount);
|
|
393
|
+
|
|
394
|
+
const workspaceDailyKey = `${reservation.workspaceId}:${dateKey}`;
|
|
395
|
+
const curWD = this.workspaceDailySpend.get(workspaceDailyKey);
|
|
396
|
+
if (curWD !== undefined) this.workspaceDailySpend.set(workspaceDailyKey, curWD - reservation.amount);
|
|
397
|
+
|
|
398
|
+
const workspaceMonthlyKey = `${reservation.workspaceId}:${monthKey}`;
|
|
399
|
+
const curWM = this.workspaceMonthlySpend.get(workspaceMonthlyKey);
|
|
400
|
+
if (curWM !== undefined) this.workspaceMonthlySpend.set(workspaceMonthlyKey, curWM - reservation.amount);
|
|
401
|
+
|
|
402
|
+
this.reservations.delete(reservationKey);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Record a completed tool call's cost.
|
|
407
|
+
* When skipCostIncrement is true (e.g. because a reservation already accounts
|
|
408
|
+
* for the cost), only step counting and cost record storage are performed.
|
|
409
|
+
*/
|
|
410
|
+
record(toolCall: ToolCall, costOrRecord: number | CostRecord, skipCostIncrement = false): void {
|
|
411
|
+
const actualCostUsd = typeof costOrRecord === 'number' ? costOrRecord : (costOrRecord.actual_cost_usd ?? costOrRecord.estimated_cost_usd);
|
|
412
|
+
|
|
413
|
+
// Store cost record for later retrieval
|
|
414
|
+
if (typeof costOrRecord !== 'number') {
|
|
415
|
+
this.costRecords.set(toolCall.tool_call_id, costOrRecord);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const state = this.getTaskState(toolCall);
|
|
419
|
+
|
|
420
|
+
if (!skipCostIncrement) {
|
|
421
|
+
// Update task state cost
|
|
422
|
+
state.spent_usd += actualCostUsd;
|
|
423
|
+
}
|
|
424
|
+
// Always increment steps
|
|
425
|
+
state.steps += 1;
|
|
426
|
+
this.taskStates.set(toolCall.task_id, state);
|
|
427
|
+
|
|
428
|
+
// Persist to external store if available
|
|
429
|
+
if (this.externalStore) {
|
|
430
|
+
this.externalStore.setTaskState(toolCall.task_id, state);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!skipCostIncrement) {
|
|
434
|
+
// Update user daily spend
|
|
435
|
+
const dateKey = this.getDateKey();
|
|
436
|
+
const monthKey = this.getMonthKey();
|
|
437
|
+
|
|
438
|
+
const userDailyKey = `${state.actor_id}:${dateKey}`;
|
|
439
|
+
const currentUserDaily = this.userDailySpend.get(userDailyKey) ?? 0;
|
|
440
|
+
this.userDailySpend.set(userDailyKey, currentUserDaily + actualCostUsd);
|
|
441
|
+
|
|
442
|
+
// Update user monthly spend
|
|
443
|
+
const userMonthlyKey = `${state.actor_id}:${monthKey}`;
|
|
444
|
+
const currentUserMonthly = this.userMonthlySpend.get(userMonthlyKey) ?? 0;
|
|
445
|
+
this.userMonthlySpend.set(userMonthlyKey, currentUserMonthly + actualCostUsd);
|
|
446
|
+
|
|
447
|
+
// Update workspace daily spend
|
|
448
|
+
const workspaceDailyKey = `${state.workspace_id}:${dateKey}`;
|
|
449
|
+
const currentWorkspaceDaily = this.workspaceDailySpend.get(workspaceDailyKey) ?? 0;
|
|
450
|
+
this.workspaceDailySpend.set(workspaceDailyKey, currentWorkspaceDaily + actualCostUsd);
|
|
451
|
+
|
|
452
|
+
// Update workspace monthly spend
|
|
453
|
+
const workspaceMonthlyKey = `${state.workspace_id}:${monthKey}`;
|
|
454
|
+
const currentWorkspaceMonthly = this.workspaceMonthlySpend.get(workspaceMonthlyKey) ?? 0;
|
|
455
|
+
this.workspaceMonthlySpend.set(workspaceMonthlyKey, currentWorkspaceMonthly + actualCostUsd);
|
|
456
|
+
|
|
457
|
+
// Persist counter updates to external store
|
|
458
|
+
if (this.externalStore) {
|
|
459
|
+
this.externalStore.incrementCounter(userDailyKey, actualCostUsd);
|
|
460
|
+
this.externalStore.incrementCounter(userMonthlyKey, actualCostUsd);
|
|
461
|
+
this.externalStore.incrementCounter(workspaceDailyKey, actualCostUsd);
|
|
462
|
+
this.externalStore.incrementCounter(workspaceMonthlyKey, actualCostUsd);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Prune stale counter keys (from prior days/months) to prevent unbounded growth
|
|
466
|
+
this.pruneStaleCounters(dateKey, monthKey);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Remove counter entries whose date/month suffix doesn't match today */
|
|
471
|
+
private pruneStaleCounters(currentDateKey: string, currentMonthKey: string): void {
|
|
472
|
+
// Only prune periodically (when maps get large)
|
|
473
|
+
if (this.userDailySpend.size <= 100) return;
|
|
474
|
+
|
|
475
|
+
for (const key of this.userDailySpend.keys()) {
|
|
476
|
+
if (!key.endsWith(`:${currentDateKey}`)) {
|
|
477
|
+
this.userDailySpend.delete(key);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const key of this.workspaceDailySpend.keys()) {
|
|
481
|
+
if (!key.endsWith(`:${currentDateKey}`)) {
|
|
482
|
+
this.workspaceDailySpend.delete(key);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (const key of this.userMonthlySpend.keys()) {
|
|
486
|
+
if (!key.endsWith(`:${currentMonthKey}`)) {
|
|
487
|
+
this.userMonthlySpend.delete(key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const key of this.workspaceMonthlySpend.keys()) {
|
|
491
|
+
if (!key.endsWith(`:${currentMonthKey}`)) {
|
|
492
|
+
this.workspaceMonthlySpend.delete(key);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Record a retry for a tool call
|
|
498
|
+
recordRetry(toolCallId: string): { allowed: boolean; count: number } {
|
|
499
|
+
const currentCount = this.callRetryCounts.get(toolCallId) ?? 0;
|
|
500
|
+
const newCount = currentCount + 1;
|
|
501
|
+
this.callRetryCounts.set(toolCallId, newCount);
|
|
502
|
+
|
|
503
|
+
if (this.externalStore) {
|
|
504
|
+
this.externalStore.incrementRetryCount(toolCallId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const maxRetries = this.config.max_retries_per_call!;
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
allowed: newCount <= maxRetries,
|
|
511
|
+
count: newCount,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Get budget report for a tool call
|
|
516
|
+
getReport(toolCall: ToolCall, estimatedCost: number, effectiveConfig?: BudgetConfig): BudgetReport {
|
|
517
|
+
const cfg = effectiveConfig ?? this.config;
|
|
518
|
+
const state = this.getTaskState(toolCall);
|
|
519
|
+
return {
|
|
520
|
+
estimated_cost_usd: estimatedCost,
|
|
521
|
+
spent_cost_usd_task: state.spent_usd,
|
|
522
|
+
remaining_cost_usd_task: Math.max(0, (cfg.task_budget_usd ?? 2.0) - state.spent_usd),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Get budget report with actual cost and usage data
|
|
527
|
+
getReportWithActual(toolCall: ToolCall, estimatedCost: number, actualCostUsd?: number, usage?: UsageData): BudgetReport {
|
|
528
|
+
const base = this.getReport(toolCall, estimatedCost);
|
|
529
|
+
return {
|
|
530
|
+
...base,
|
|
531
|
+
actual_cost_usd: actualCostUsd,
|
|
532
|
+
usage,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Get the active cost table (for introspection)
|
|
537
|
+
getCostTable(): Record<string, number> {
|
|
538
|
+
return { ...this.costTable };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Get the cost record for a specific tool call
|
|
542
|
+
getCostRecord(toolCallId: string): CostRecord | undefined {
|
|
543
|
+
return this.costRecords.get(toolCallId);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Get or create task state
|
|
547
|
+
private getTaskState(toolCall: ToolCall): BudgetState {
|
|
548
|
+
const existing = this.taskStates.get(toolCall.task_id);
|
|
549
|
+
if (existing) {
|
|
550
|
+
return existing;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const newState: BudgetState = {
|
|
554
|
+
task_id: toolCall.task_id,
|
|
555
|
+
workspace_id: toolCall.workspace_id,
|
|
556
|
+
actor_id: toolCall.actor.id,
|
|
557
|
+
spent_usd: 0,
|
|
558
|
+
steps: 0,
|
|
559
|
+
started_at: toolCall.timestamp ?? new Date().toISOString(),
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
this.taskStates.set(toolCall.task_id, newState);
|
|
563
|
+
return newState;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Helper to get date key (YYYY-MM-DD)
|
|
567
|
+
private getDateKey(): string {
|
|
568
|
+
return new Date().toISOString().split('T')[0];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Helper to get month key (YYYY-MM)
|
|
572
|
+
private getMonthKey(): string {
|
|
573
|
+
return new Date().toISOString().slice(0, 7);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Reset task state (for testing)
|
|
577
|
+
async flush(): Promise<void> {
|
|
578
|
+
if (this.externalStore?.flush) await this.externalStore.flush();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
reset(): void {
|
|
582
|
+
this.taskStates.clear();
|
|
583
|
+
this.userDailySpend.clear();
|
|
584
|
+
this.userMonthlySpend.clear();
|
|
585
|
+
this.workspaceDailySpend.clear();
|
|
586
|
+
this.workspaceMonthlySpend.clear();
|
|
587
|
+
this.callRetryCounts.clear();
|
|
588
|
+
this.costRecords.clear();
|
|
589
|
+
this.reservations.clear();
|
|
590
|
+
this.budgetLocks.clear();
|
|
591
|
+
this.externalStore?.reset();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Get spending data for a specific actor */
|
|
595
|
+
getActorSpending(actorId: string, days?: number): {
|
|
596
|
+
daily_spend: number;
|
|
597
|
+
monthly_spend: number;
|
|
598
|
+
task_count: number;
|
|
599
|
+
total_spend: number;
|
|
600
|
+
} {
|
|
601
|
+
const dateKey = this.getDateKey();
|
|
602
|
+
const monthKey = this.getMonthKey();
|
|
603
|
+
|
|
604
|
+
// Daily spend for today
|
|
605
|
+
const dailyKey = `${actorId}:${dateKey}`;
|
|
606
|
+
const dailySpend = this.userDailySpend.get(dailyKey) ?? 0;
|
|
607
|
+
|
|
608
|
+
// Monthly spend for current month
|
|
609
|
+
const monthlyKey = `${actorId}:${monthKey}`;
|
|
610
|
+
const monthlySpend = this.userMonthlySpend.get(monthlyKey) ?? 0;
|
|
611
|
+
|
|
612
|
+
// Count tasks where this actor is involved
|
|
613
|
+
let taskCount = 0;
|
|
614
|
+
let totalSpend = 0;
|
|
615
|
+
for (const state of this.taskStates.values()) {
|
|
616
|
+
if (state.actor_id === actorId) {
|
|
617
|
+
taskCount++;
|
|
618
|
+
totalSpend += state.spent_usd;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
daily_spend: dailySpend,
|
|
624
|
+
monthly_spend: monthlySpend,
|
|
625
|
+
task_count: taskCount,
|
|
626
|
+
total_spend: totalSpend,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Get current config
|
|
631
|
+
getConfig(): BudgetConfig {
|
|
632
|
+
return { ...this.config };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** Return aggregate spending across all tracked entities */
|
|
636
|
+
getSpendingSummary(): {
|
|
637
|
+
task_total: number;
|
|
638
|
+
user_daily_total: number;
|
|
639
|
+
user_monthly_total: number;
|
|
640
|
+
workspace_daily_total: number;
|
|
641
|
+
workspace_monthly_total: number;
|
|
642
|
+
} {
|
|
643
|
+
let taskTotal = 0;
|
|
644
|
+
for (const state of this.taskStates.values()) {
|
|
645
|
+
taskTotal += state.spent_usd;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const dateKey = this.getDateKey();
|
|
649
|
+
const monthKey = this.getMonthKey();
|
|
650
|
+
|
|
651
|
+
let userDailyTotal = 0;
|
|
652
|
+
for (const [key, val] of this.userDailySpend) {
|
|
653
|
+
if (key.endsWith(`:${dateKey}`)) userDailyTotal += val;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let userMonthlyTotal = 0;
|
|
657
|
+
for (const [key, val] of this.userMonthlySpend) {
|
|
658
|
+
if (key.endsWith(`:${monthKey}`)) userMonthlyTotal += val;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let workspaceDailyTotal = 0;
|
|
662
|
+
for (const [key, val] of this.workspaceDailySpend) {
|
|
663
|
+
if (key.endsWith(`:${dateKey}`)) workspaceDailyTotal += val;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
let workspaceMonthlyTotal = 0;
|
|
667
|
+
for (const [key, val] of this.workspaceMonthlySpend) {
|
|
668
|
+
if (key.endsWith(`:${monthKey}`)) workspaceMonthlyTotal += val;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
task_total: taskTotal,
|
|
673
|
+
user_daily_total: userDailyTotal,
|
|
674
|
+
user_monthly_total: userMonthlyTotal,
|
|
675
|
+
workspace_daily_total: workspaceDailyTotal,
|
|
676
|
+
workspace_monthly_total: workspaceMonthlyTotal,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|