@stigg/agent-sdk-js 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +1411 -2
  2. package/dist/bindings/client/core/registry.d.ts +12 -0
  3. package/dist/bindings/client/core/registry.js +22 -0
  4. package/dist/bindings/client/core/types.d.ts +126 -0
  5. package/dist/bindings/client/core/types.js +13 -0
  6. package/dist/bindings/client/core/wrap-client.d.ts +6 -0
  7. package/dist/bindings/client/core/wrap-client.js +261 -0
  8. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.d.ts +17 -0
  9. package/dist/bindings/client/gateways/bedrock/bedrock-recipe.js +134 -0
  10. package/dist/bindings/client/gateways/bedrock/types.d.ts +13 -0
  11. package/dist/bindings/client/gateways/bedrock/types.js +2 -0
  12. package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.d.ts +10 -0
  13. package/dist/bindings/client/gateways/cloudflare/cloudflare-recipe.js +254 -0
  14. package/dist/bindings/client/gateways/cloudflare/types.d.ts +35 -0
  15. package/dist/bindings/client/gateways/cloudflare/types.js +2 -0
  16. package/dist/bindings/client/gateways/databricks/databricks-recipe.d.ts +32 -0
  17. package/dist/bindings/client/gateways/databricks/databricks-recipe.js +324 -0
  18. package/dist/bindings/client/gateways/databricks/types.d.ts +30 -0
  19. package/dist/bindings/client/gateways/databricks/types.js +2 -0
  20. package/dist/bindings/client/gateways/litellm/litellm-recipe.d.ts +17 -0
  21. package/dist/bindings/client/gateways/litellm/litellm-recipe.js +158 -0
  22. package/dist/bindings/client/gateways/litellm/types.d.ts +30 -0
  23. package/dist/bindings/client/gateways/litellm/types.js +2 -0
  24. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.d.ts +20 -0
  25. package/dist/bindings/client/gateways/openrouter/openrouter-recipe.js +233 -0
  26. package/dist/bindings/client/gateways/openrouter/sdk-usage.d.ts +37 -0
  27. package/dist/bindings/client/gateways/openrouter/sdk-usage.js +63 -0
  28. package/dist/bindings/client/gateways/openrouter/types.d.ts +78 -0
  29. package/dist/bindings/client/gateways/openrouter/types.js +2 -0
  30. package/dist/bindings/client/providers/anthropic/anthropic-recipe.d.ts +34 -0
  31. package/dist/bindings/client/providers/anthropic/anthropic-recipe.js +169 -0
  32. package/dist/bindings/client/providers/gemini/gemini-recipe.d.ts +54 -0
  33. package/dist/bindings/client/providers/gemini/gemini-recipe.js +243 -0
  34. package/dist/bindings/client/providers/groq/groq-recipe.d.ts +20 -0
  35. package/dist/bindings/client/providers/groq/groq-recipe.js +84 -0
  36. package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +22 -0
  37. package/dist/bindings/client/providers/mistral/mistral-recipe.js +114 -0
  38. package/dist/bindings/client/providers/mistral/types.d.ts +38 -0
  39. package/dist/bindings/client/providers/mistral/types.js +2 -0
  40. package/dist/bindings/client/providers/ollama/ollama-recipe.d.ts +31 -0
  41. package/dist/bindings/client/providers/ollama/ollama-recipe.js +107 -0
  42. package/dist/bindings/client/providers/ollama/types.d.ts +24 -0
  43. package/dist/bindings/client/providers/ollama/types.js +2 -0
  44. package/dist/bindings/client/providers/openai/openai-recipe.d.ts +71 -0
  45. package/dist/bindings/client/providers/openai/openai-recipe.js +122 -0
  46. package/dist/bindings/client/providers/typesafe/typesafe-recipe.d.ts +44 -0
  47. package/dist/bindings/client/providers/typesafe/typesafe-recipe.js +83 -0
  48. package/dist/bindings/proxy/headers.d.ts +6 -0
  49. package/dist/bindings/proxy/headers.js +12 -0
  50. package/dist/bindings/proxy/index.d.ts +21 -0
  51. package/dist/bindings/proxy/index.js +18 -0
  52. package/dist/bindings/proxy/types.d.ts +8 -0
  53. package/dist/bindings/proxy/types.js +2 -0
  54. package/dist/bindings/shared/base-url.d.ts +44 -0
  55. package/dist/bindings/shared/base-url.js +90 -0
  56. package/dist/bindings/shared/estimate.d.ts +27 -0
  57. package/dist/bindings/shared/estimate.js +38 -0
  58. package/dist/bindings/shared/meter-stream.d.ts +18 -0
  59. package/dist/bindings/shared/meter-stream.js +151 -0
  60. package/dist/bindings/shared/token-usage.d.ts +345 -0
  61. package/dist/bindings/shared/token-usage.js +735 -0
  62. package/dist/bindings/shared/types.d.ts +170 -0
  63. package/dist/bindings/shared/types.js +2 -0
  64. package/dist/bindings/shared/weighted-metrics.d.ts +37 -0
  65. package/dist/bindings/shared/weighted-metrics.js +47 -0
  66. package/dist/config.d.ts +145 -0
  67. package/dist/config.js +99 -0
  68. package/dist/core/context.d.ts +160 -0
  69. package/dist/core/context.js +221 -0
  70. package/dist/core/dimensions.d.ts +22 -0
  71. package/dist/core/dimensions.js +47 -0
  72. package/dist/core/hooks.d.ts +21 -0
  73. package/dist/core/hooks.js +47 -0
  74. package/dist/core/session.d.ts +190 -0
  75. package/dist/core/session.js +448 -0
  76. package/dist/decision-text.d.ts +15 -0
  77. package/dist/decision-text.js +54 -0
  78. package/dist/engine/composite-engine.d.ts +72 -0
  79. package/dist/engine/composite-engine.js +135 -0
  80. package/dist/engine/create-engine.d.ts +16 -0
  81. package/dist/engine/create-engine.js +93 -0
  82. package/dist/engine/decision.d.ts +42 -0
  83. package/dist/engine/decision.js +135 -0
  84. package/dist/engine/engine.d.ts +166 -0
  85. package/dist/engine/engine.js +11 -0
  86. package/dist/engine/node-sdk-engine.d.ts +125 -0
  87. package/dist/engine/node-sdk-engine.js +262 -0
  88. package/dist/engine/report.d.ts +106 -0
  89. package/dist/engine/report.js +188 -0
  90. package/dist/engine/rest-governance-engine.d.ts +47 -0
  91. package/dist/engine/rest-governance-engine.js +220 -0
  92. package/dist/errors.d.ts +86 -0
  93. package/dist/errors.js +161 -0
  94. package/dist/headers.d.ts +40 -0
  95. package/dist/headers.js +82 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +124 -0
  98. package/dist/internal.d.ts +33 -0
  99. package/dist/internal.js +91 -0
  100. package/dist/logger.d.ts +15 -0
  101. package/dist/logger.js +20 -0
  102. package/dist/sdk-identity.d.ts +36 -0
  103. package/dist/sdk-identity.js +43 -0
  104. package/dist/stigg-agent-sdk.d.ts +300 -0
  105. package/dist/stigg-agent-sdk.js +669 -0
  106. package/dist/types.d.ts +676 -0
  107. package/dist/types.js +147 -0
  108. package/package.json +94 -3
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The engine half that runs on Stigg's own Node server SDK: cached entitlement reads and usage
3
+ * ingest. This is the only file in the package that imports it at runtime.
4
+ *
5
+ * It knows nothing about entities or chains - the GraphQL surface has no governance on it. What
6
+ * it brings is the cache, which is the reason the composite keeps ingest (and customer-level
7
+ * checks) here rather than sending everything to REST - and the reason nothing above this file
8
+ * caches anything. That cache writes reported usage back from the server's own response,
9
+ * invalidates on realtime updates, resets usage periods at read time, falls back when Stigg is
10
+ * unreachable, and optionally lives in Redis so sibling processes share one view.
11
+ *
12
+ * All of that is configured through {@link CacheOptions}, which is the Node SDK's own
13
+ * configuration passed straight through rather than re-described here.
14
+ */
15
+ import Stigg, { type ClientConfiguration } from '@stigg/node-server-sdk';
16
+ import type { EngineCapabilities, EngineCheckParams, EngineCheckResult, EngineReportParams, EngineReportResult, GovernanceEngine } from './engine';
17
+ /**
18
+ * Caching - all of it, and none of it implemented here.
19
+ *
20
+ * This SDK holds no cache. Checks are served by the Node server SDK's entitlement cache, which
21
+ * already does the hard parts: it writes the server's own measurement responses back into cached
22
+ * usage, invalidates on realtime updates, applies usage-period resets at read time, and can be
23
+ * backed by Redis so sibling processes share one view instead of each holding their own.
24
+ *
25
+ * So this is a passthrough rather than a parallel design - the field names are the Node SDK's own,
26
+ * and anything it learns to do lands here without a change. The one thing that does *not* cache is
27
+ * an attributed governance check: it resolves an entity, which only the governance API can do, so
28
+ * it is a live call every time. That is the stricter answer, and it is where per-agent budgets are
29
+ * enforced.
30
+ */
31
+ export type CacheOptions = Pick<ClientConfiguration, 'redis' | 'memoryCacheMaxSizeBytes' | 'entitlementsFallback' | 'creditEntitlementsFallback' | 'realtimeUpdatesEnabled' | 'enableEdge' | 'offline'>;
32
+ /**
33
+ * The Node server SDK client itself, for an application that already has one.
34
+ *
35
+ * Re-exported from `config.ts` as the public name, so the vendor client stays behind this file.
36
+ */
37
+ export type NodeSdkClient = Stigg;
38
+ export type NodeSdkEngineOptions = {
39
+ apiKey: string;
40
+ baseUrl?: string;
41
+ timeoutMs?: number;
42
+ /**
43
+ * Everything about caching, passed through to the Node SDK verbatim. Anything set here wins
44
+ * over the localhost-aware defaults below - including `enableEdge` and `realtimeUpdatesEnabled`,
45
+ * which are cache configuration in all but name: the socket is what keeps a cached entitlement
46
+ * from going stale.
47
+ */
48
+ cache?: CacheOptions;
49
+ /**
50
+ * An existing client to use instead of building one. Borrowed, never closed.
51
+ *
52
+ * This is the only way to be *sure* who owns the client, which matters because
53
+ * `Stigg.initialize` keeps one global instance per process and hands it back when the API key
54
+ * matches. Closing a client the application is still using would take its cache and its socket
55
+ * with it, and nothing from out here can tell a client that was just built from one that was
56
+ * handed back: the instance exposes neither its configuration nor its identity.
57
+ */
58
+ client?: NodeSdkClient;
59
+ };
60
+ export declare class NodeSdkEngine implements GovernanceEngine {
61
+ readonly capabilities: EngineCapabilities;
62
+ /**
63
+ * Whether this engine built the client, and may therefore close it.
64
+ *
65
+ * False for a client that was handed in: it belongs to whoever built it, and its lifecycle is
66
+ * theirs. See `close`.
67
+ */
68
+ readonly ownsClient: boolean;
69
+ private readonly client;
70
+ constructor(options: NodeSdkEngineOptions);
71
+ /**
72
+ * Waits for the cache to be usable.
73
+ *
74
+ * Not cosmetic: until the runtime reports itself initialized, every read is treated as a miss
75
+ * and refetched, so a client that starts serving immediately pays a round trip per check for as
76
+ * long as initialization takes. Awaited once at startup instead.
77
+ */
78
+ ready(): Promise<void>;
79
+ /**
80
+ * Loads this customer's entitlements into the cache, so the checks that follow are local.
81
+ *
82
+ * The whole map, not one decision: a later check against a different entitlement is warm too,
83
+ * and each is evaluated against the amount it actually asks for rather than against whatever
84
+ * amount happened to warm it.
85
+ */
86
+ preload(customerId: string): Promise<void>;
87
+ upsertEntity(): Promise<void>;
88
+ archiveEntity(): Promise<void>;
89
+ /**
90
+ * The two kinds of entitlement are two different endpoints with two different answers, so each
91
+ * gets its own method and this one only picks. Narrowing on `currencyId` rather than through
92
+ * `entitlementKind` is what lets each branch read its own id without an assertion: a function
93
+ * call cannot narrow a union.
94
+ */
95
+ check(customerId: string, { entitlement, requestedUsage }: EngineCheckParams): Promise<EngineCheckResult>;
96
+ /**
97
+ * `getCreditEntitlement` takes no `requestedUsage` - `CreditEntitlementOptions` has no such
98
+ * field - so unlike the metered path, nobody at Stigg is asked whether this particular amount is
99
+ * affordable. Dropping the comparison is not an option: a check for five units against a balance
100
+ * of two would come back granted and the run would overspend. So the SDK answers it, and says so
101
+ * in `locallyDerived` rather than letting a verdict it reached by subtraction read like one the
102
+ * server handed down.
103
+ */
104
+ private checkCredit;
105
+ /** The metered path, where Stigg is asked about this amount and answers for it. */
106
+ private checkFeature;
107
+ /** The four calls this client makes; `reportMetrics` decides which of them each metric is. */
108
+ report(customerId: string, params: EngineReportParams): Promise<EngineReportResult>;
109
+ /** The feature path: bulk, because a turn reports several metrics and this client has one call. */
110
+ private reportUsage;
111
+ /**
112
+ * The credit path: the currency is drawn from the customer's wallet with no feature mapping,
113
+ * so none of the feature-side setup (a metered feature, a credit rate, an entitling
114
+ * subscription) is involved. Stigg forwards the consumption to governance itself, so per-entity
115
+ * budgets still decrement.
116
+ *
117
+ * `replayed` is always false here: the response carries the wallet's running balance but not
118
+ * this call's own deduction, so a replayed idempotency key is indistinguishable from a fresh
119
+ * one. The server still deduplicates on the key - only our ability to report it is lost.
120
+ */
121
+ private consumeCredits;
122
+ /** The raw-events path: one event for the turn, matched by name, and no balance in the answer. */
123
+ private reportAsEvent;
124
+ close(): Promise<void>;
125
+ }
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NodeSdkEngine = void 0;
37
+ /**
38
+ * The engine half that runs on Stigg's own Node server SDK: cached entitlement reads and usage
39
+ * ingest. This is the only file in the package that imports it at runtime.
40
+ *
41
+ * It knows nothing about entities or chains - the GraphQL surface has no governance on it. What
42
+ * it brings is the cache, which is the reason the composite keeps ingest (and customer-level
43
+ * checks) here rather than sending everything to REST - and the reason nothing above this file
44
+ * caches anything. That cache writes reported usage back from the server's own response,
45
+ * invalidates on realtime updates, resets usage periods at read time, falls back when Stigg is
46
+ * unreachable, and optionally lives in Redis so sibling processes share one view.
47
+ *
48
+ * All of that is configured through {@link CacheOptions}, which is the Node SDK's own
49
+ * configuration passed straight through rather than re-described here.
50
+ */
51
+ const node_server_sdk_1 = __importStar(require("@stigg/node-server-sdk"));
52
+ const errors_1 = require("../errors");
53
+ const sdk_identity_1 = require("../sdk-identity");
54
+ const report_1 = require("./report");
55
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
56
+ /**
57
+ * Drops keys that were never set, so a spread cannot un-set a default.
58
+ *
59
+ * The Node SDK merges its configuration with lodash, which skips an `undefined` source value and
60
+ * keeps its own default - so `{ enableEdge: false, ...cache }` with an unset `cache.enableEdge`
61
+ * would land back on `true` and point a local run at an edge service that was never started.
62
+ */
63
+ const defined = (options) => Object.fromEntries(Object.entries(options ?? {}).filter(([, value]) => value !== undefined));
64
+ function isLocalhost(baseUrl) {
65
+ if (!baseUrl)
66
+ return false;
67
+ try {
68
+ return LOCAL_HOSTS.has(new URL(baseUrl).hostname);
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ class NodeSdkEngine {
75
+ capabilities = {
76
+ // The SDK serves reads from its own cache, so two processes can each approve the same budget.
77
+ strictCheck: false,
78
+ chains: false,
79
+ provisioning: false,
80
+ };
81
+ /**
82
+ * Whether this engine built the client, and may therefore close it.
83
+ *
84
+ * False for a client that was handed in: it belongs to whoever built it, and its lifecycle is
85
+ * theirs. See `close`.
86
+ */
87
+ ownsClient;
88
+ client;
89
+ constructor(options) {
90
+ if (options.client) {
91
+ this.client = options.client;
92
+ this.ownsClient = false;
93
+ return;
94
+ }
95
+ this.ownsClient = true;
96
+ const local = isLocalhost(options.baseUrl);
97
+ this.client = node_server_sdk_1.default.initialize({
98
+ apiKey: options.apiKey,
99
+ // So this traffic is attributable to the agent SDK rather than reading as a plain
100
+ // node-server-sdk client. Only applies to a client built here: `Stigg.initialize` returns
101
+ // its existing global instance when the API key matches, and a client handed in through
102
+ // `options.client` keeps whatever name the application gave it.
103
+ clientName: sdk_identity_1.AGENT_SDK_CLIENT_NAME,
104
+ clientVersion: sdk_identity_1.AGENT_SDK_VERSION,
105
+ ...(options.baseUrl ? { baseUri: options.baseUrl } : {}),
106
+ ...(options.timeoutMs ? { entitlementsTimeout: options.timeoutMs } : {}),
107
+ // The edge cache and the realtime socket are separate services from the API, and a Stigg
108
+ // running on localhost usually serves neither - without this a local run fails on
109
+ // ECONNREFUSED against services that were never started.
110
+ enableEdge: !local,
111
+ realtimeUpdatesEnabled: !local,
112
+ // Last, so an explicit choice wins over the defaults above.
113
+ ...defined(options.cache),
114
+ });
115
+ }
116
+ /**
117
+ * Waits for the cache to be usable.
118
+ *
119
+ * Not cosmetic: until the runtime reports itself initialized, every read is treated as a miss
120
+ * and refetched, so a client that starts serving immediately pays a round trip per check for as
121
+ * long as initialization takes. Awaited once at startup instead.
122
+ */
123
+ async ready() {
124
+ await this.client.waitForInitialization();
125
+ }
126
+ /**
127
+ * Loads this customer's entitlements into the cache, so the checks that follow are local.
128
+ *
129
+ * The whole map, not one decision: a later check against a different entitlement is warm too,
130
+ * and each is evaluated against the amount it actually asks for rather than against whatever
131
+ * amount happened to warm it.
132
+ */
133
+ async preload(customerId) {
134
+ await this.client.reloadEntitlements(customerId);
135
+ }
136
+ async upsertEntity() {
137
+ throw new errors_1.GovernanceNotEnabledError('The Node server SDK has no governance surface; entities need the REST client.');
138
+ }
139
+ async archiveEntity() {
140
+ throw new errors_1.GovernanceNotEnabledError('The Node server SDK has no governance surface; entities need the REST client.');
141
+ }
142
+ /**
143
+ * The two kinds of entitlement are two different endpoints with two different answers, so each
144
+ * gets its own method and this one only picks. Narrowing on `currencyId` rather than through
145
+ * `entitlementKind` is what lets each branch read its own id without an assertion: a function
146
+ * call cannot narrow a union.
147
+ */
148
+ async check(customerId, { entitlement, requestedUsage }) {
149
+ try {
150
+ return entitlement.currencyId !== undefined
151
+ ? await this.checkCredit(customerId, entitlement.currencyId, requestedUsage)
152
+ : await this.checkFeature(customerId, entitlement.featureId, requestedUsage);
153
+ }
154
+ catch (err) {
155
+ throw (0, errors_1.asEngineError)('check', err);
156
+ }
157
+ }
158
+ /**
159
+ * `getCreditEntitlement` takes no `requestedUsage` - `CreditEntitlementOptions` has no such
160
+ * field - so unlike the metered path, nobody at Stigg is asked whether this particular amount is
161
+ * affordable. Dropping the comparison is not an option: a check for five units against a balance
162
+ * of two would come back granted and the run would overspend. So the SDK answers it, and says so
163
+ * in `locallyDerived` rather than letting a verdict it reached by subtraction read like one the
164
+ * server handed down.
165
+ */
166
+ async checkCredit(customerId, currencyId, requestedUsage) {
167
+ const state = await this.client.getCreditEntitlement({ customerId, currencyId });
168
+ const affordable = state.remaining >= requestedUsage;
169
+ const denial = state.hasAccess ? node_server_sdk_1.AccessDeniedReason.InsufficientCredits : state.accessDeniedReason;
170
+ return {
171
+ isGranted: state.hasAccess && affordable,
172
+ accessDeniedReason: affordable && state.hasAccess ? null : (denial ?? null),
173
+ locallyDerived: state.hasAccess,
174
+ usageLimit: state.usageLimit,
175
+ currentUsage: state.currentUsage,
176
+ isFallback: state.isFallback,
177
+ source: 'node-sdk',
178
+ };
179
+ }
180
+ /** The metered path, where Stigg is asked about this amount and answers for it. */
181
+ async checkFeature(customerId, featureId, requestedUsage) {
182
+ const state = await this.client.getMeteredEntitlement({ customerId, featureId, options: { requestedUsage } });
183
+ return {
184
+ isGranted: state.hasAccess,
185
+ accessDeniedReason: state.accessDeniedReason ?? null,
186
+ usageLimit: state.isUnlimited ? null : (state.usageLimit ?? null),
187
+ currentUsage: state.currentUsage,
188
+ isFallback: state.isFallback,
189
+ source: 'node-sdk',
190
+ };
191
+ }
192
+ /** The four calls this client makes; `reportMetrics` decides which of them each metric is. */
193
+ async report(customerId, params) {
194
+ return (0, report_1.reportMetrics)(params, {
195
+ credits: (featureId, value) => this.consumeCredits(customerId, params, featureId, value),
196
+ creditsAsync: (featureId, value) => this.consumeCredits(customerId, params, featureId, value, 'async'),
197
+ event: (metrics, dimensions) => this.reportAsEvent(customerId, params, metrics, dimensions),
198
+ usage: (metrics) => this.reportUsage(customerId, params, metrics),
199
+ });
200
+ }
201
+ /** The feature path: bulk, because a turn reports several metrics and this client has one call. */
202
+ async reportUsage(customerId, params, metrics) {
203
+ const acks = await this.client.reportUsageBulk({ usages: (0, report_1.usageEntries)(customerId, params, metrics) });
204
+ return (0, report_1.usageResult)(acks, metrics, 'node-sdk');
205
+ }
206
+ /**
207
+ * The credit path: the currency is drawn from the customer's wallet with no feature mapping,
208
+ * so none of the feature-side setup (a metered feature, a credit rate, an entitling
209
+ * subscription) is involved. Stigg forwards the consumption to governance itself, so per-entity
210
+ * budgets still decrement.
211
+ *
212
+ * `replayed` is always false here: the response carries the wallet's running balance but not
213
+ * this call's own deduction, so a replayed idempotency key is indistinguishable from a fresh
214
+ * one. The server still deduplicates on the key - only our ability to report it is lost.
215
+ */
216
+ async consumeCredits(customerId, params, featureId, value, mode = 'sync') {
217
+ // The wallet's own id. A metric only reaches this path when it names the currency the run
218
+ // checks, so the metric name and the currency are the same string.
219
+ const consumption = (0, report_1.creditConsumption)(customerId, params, featureId, value);
220
+ try {
221
+ // The async form takes the same consumption and reconciles it on Stigg's side, so the
222
+ // amount is still ours to state - which is why a run reporting to its own currency keeps
223
+ // an exact local budget in either mode. Only the balance is lost.
224
+ const ack = mode === 'async' ? undefined : await this.client.consumeCredits(consumption);
225
+ if (mode === 'async')
226
+ await this.client.consumeCreditsAsync({ consumptions: [consumption] });
227
+ return (0, report_1.creditResult)(featureId, value, ack?.credit?.currentUsage, mode, 'node-sdk');
228
+ }
229
+ catch (err) {
230
+ throw (0, errors_1.asEngineError)('ingest', err);
231
+ }
232
+ }
233
+ /** The raw-events path: one event for the turn, matched by name, and no balance in the answer. */
234
+ async reportAsEvent(customerId, params, metrics, dimensions) {
235
+ const event = params.event;
236
+ if (!event)
237
+ throw (0, report_1.missingEventError)();
238
+ try {
239
+ await this.client.reportEvent({
240
+ customerId,
241
+ eventName: event.name,
242
+ // The turn's own key: one event, so no per-feature suffix to keep them apart.
243
+ idempotencyKey: params.idempotencyKey,
244
+ dimensions,
245
+ });
246
+ // No balance comes back, and aggregation lags by seconds - so `replayed` stays unknown.
247
+ return { reported: metrics, mode: 'async', source: 'node-sdk' };
248
+ }
249
+ catch (err) {
250
+ throw (0, errors_1.asEngineError)('ingest', err);
251
+ }
252
+ }
253
+ async close() {
254
+ // The SDK keeps a background cache and sockets alive; without this a CLI never exits. But
255
+ // only for a client this engine built: `close` nulls the Node SDK's global instance and tears
256
+ // down the runtime, so closing a borrowed one would take the application's cache and socket
257
+ // with it.
258
+ if (this.ownsClient)
259
+ await this.client.close();
260
+ }
261
+ }
262
+ exports.NodeSdkEngine = NodeSdkEngine;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The shape of an ingest, which both transports share even though none of their calls do.
3
+ *
4
+ * Stigg has two ingest APIs and they are not interchangeable. The usage report takes a fixed DTO -
5
+ * a `featureId` and a whole-number `value` per metric - and aggregates on arrival, so the next
6
+ * check sees the spend. The event report takes the call as one event and lets Stigg's own meters
7
+ * turn it into usage seconds later, for a fraction of the latency and a far higher rate limit.
8
+ * Which one a run wants is a product decision - immediacy against throughput - so the caller
9
+ * chooses it and this file only carries the choice out.
10
+ *
11
+ * What that costs each metric:
12
+ *
13
+ * - **`'sync'`** sends one usage report per metric, batched into the one bulk call both clients
14
+ * have, because a chat turn otherwise costs four round trips.
15
+ * - **`'async'`** sends **one event for the whole turn**, every metric on its own dimension. One
16
+ * event can feed any number of meters - each picks its number with `aggregation.field` - so
17
+ * fanning out per metric would buy nothing and cost a request each.
18
+ *
19
+ * A metric named after the run's own credit currency is neither: it draws the wallet directly,
20
+ * because a usage report takes only a `featureId` and there is no event path from a raw event to
21
+ * a currency. It follows the mode only in how it waits - `consumeCredits` under `'sync'`, which
22
+ * answers with a balance, and `consumeCreditsAsync` under `'async'`, which does not.
23
+ *
24
+ * That orchestration lives here, and with it the parts of an ingest that are the same whichever
25
+ * client makes it: the consumption payload, the per-metric idempotency key, and how a bulk ack is
26
+ * read. Each engine still passes its own calls, so a vendor method is only ever named by the file
27
+ * that owns that client - but the split between wallet and features, and what a turn is taken to
28
+ * have cost, cannot come to mean one thing over REST and another over the SDK.
29
+ */
30
+ import { EngineError } from '../errors';
31
+ import type { IngestMode, Metrics } from '../types';
32
+ import type { EngineOpSource, EngineReportParams, EngineReportResult } from './engine';
33
+ /** What one transport can do with an ingest, as the calls it already knows how to make. */
34
+ export type ReportCalls = {
35
+ /** Draw the wallet and wait for the balance, for a metric that names the run's currency. */
36
+ credits: (featureId: string, value: number) => Promise<EngineReportResult>;
37
+ /** The same draw, reconciled on Stigg's side. No balance comes back. */
38
+ creditsAsync: (featureId: string, value: number) => Promise<EngineReportResult>;
39
+ /** One bulk usage report, one entry per metric. */
40
+ usage: (metrics: Metrics) => Promise<EngineReportResult>;
41
+ /** One event for the whole turn, carrying every metric as a dimension. */
42
+ event: (metrics: Metrics, dimensions: Record<string, string | number | boolean>) => Promise<EngineReportResult>;
43
+ };
44
+ /** A draw on the wallet. `consume` and `consumeAsync` take it unchanged, on both transports. */
45
+ export declare const creditConsumption: (customerId: string, params: EngineReportParams, currencyId: string, amount: number) => {
46
+ customerId: string;
47
+ currencyId: string;
48
+ amount: number;
49
+ idempotencyKey: string;
50
+ dimensions: Record<string, string>;
51
+ };
52
+ /** One bulk usage report, one entry per metric. REST adds its own `updateBehavior` on top. */
53
+ export declare const usageEntries: (customerId: string, params: EngineReportParams, metrics: Metrics) => {
54
+ customerId: string;
55
+ featureId: string;
56
+ value: number;
57
+ dimensions: Record<string, string>;
58
+ idempotencyKey: string;
59
+ }[];
60
+ /**
61
+ * What a wallet draw amounts to, read the same way whichever client made it.
62
+ *
63
+ * Only the call and where the balance sits on its response differ between the transports; this is
64
+ * everything after that. `creditsConsumed` is the metric's own value because a draw is spent at
65
+ * par - no feature mapping, no rate - and `replayed` is `false` under `'sync'` rather than
66
+ * unknown: the response carries the wallet's running balance but not this call's own deduction,
67
+ * so a replayed key is indistinguishable from a fresh one. Under `'async'` nothing comes back at
68
+ * all, so it stays undefined.
69
+ */
70
+ export declare function creditResult(currencyId: string, amount: number, currentUsage: number | null | undefined, mode: IngestMode, source: EngineOpSource): EngineReportResult;
71
+ /** As much of a bulk ack as either client returns: the balance, and what each entry cost. */
72
+ export type UsageAck = {
73
+ currentUsage?: number | null;
74
+ credit?: {
75
+ currentUsage?: number | null;
76
+ consumed?: number | null;
77
+ } | null;
78
+ };
79
+ /** What a bulk usage report amounts to, read the same way whichever client answered. */
80
+ export declare function usageResult(acks: readonly UsageAck[], metrics: Metrics, source: EngineOpSource): EngineReportResult;
81
+ export declare function reportMetrics(params: EngineReportParams, calls: ReportCalls): Promise<EngineReportResult>;
82
+ /**
83
+ * What one event carries, and in which order.
84
+ *
85
+ * Three namespaces flatten into one map, so the order is the whole design. Attribution is applied
86
+ * last and wins outright: a gateway passes vendor fields through, and a vendor field that happened
87
+ * to be called `agentId` would otherwise re-attribute the bill to an entity that never ran. The
88
+ * raw block sits under its own prefix beneath both, where it can be metered deliberately but
89
+ * cannot displace anything.
90
+ */
91
+ export declare function eventDimensions(params: EngineReportParams, metrics: Metrics): Record<string, string | number | boolean>;
92
+ /**
93
+ * The one thing the caller has to fix themselves, so it reads the same whichever client found it.
94
+ *
95
+ * An in-process run derives the name from the vendor call it wrapped, so this is reachable only
96
+ * out of process, where there is no recipe to derive it from.
97
+ */
98
+ export declare function missingEventError(): EngineError;
99
+ /**
100
+ * A sync report to a feature only a meter can fill.
101
+ *
102
+ * The rejection used to be a routing signal - the engines retried it as an event and remembered.
103
+ * That made the transport a property of the catalog, discovered one rejection at a time, and
104
+ * silently moved a run onto an eventually-consistent path it never asked for. Now it says so.
105
+ */
106
+ export declare function meterMismatchError(metrics: Metrics): EngineError;