@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,170 @@
1
+ /**
2
+ * The vendor response shapes these converters read.
3
+ *
4
+ * Typed structurally rather than against each vendor's SDK, so the same handful of functions cover
5
+ * a direct client, a gateway response and an orchestrator callback - and adding a provider whose
6
+ * response happens to match costs nothing.
7
+ */
8
+ /**
9
+ * A flat `usage` object whose cache counters sit BESIDE the input count rather than inside it.
10
+ *
11
+ * Named for the shape, not for a vendor, because more than one reports it: Anthropic's Messages
12
+ * API and TypeSafe's System One, which carries only the first two. The distinction from the two
13
+ * shapes below is the whole reason this is a separate reader - they spell the same two counters
14
+ * the same way but count their cached tokens *within* the input total, so reading one as the
15
+ * other double-counts in one direction or overstates in the other.
16
+ */
17
+ export type FlatTokenUsage = {
18
+ input_tokens?: number | null;
19
+ output_tokens?: number | null;
20
+ cache_read_input_tokens?: number | null;
21
+ cache_creation_input_tokens?: number | null;
22
+ };
23
+ /** OpenAI-compatible `usage` - also what OpenRouter and most gateways return. */
24
+ export type OpenAILikeUsage = {
25
+ prompt_tokens?: number | null;
26
+ completion_tokens?: number | null;
27
+ /** The vendor's own total. `prompt_tokens + completion_tokens`; cached tokens are inside the first. */
28
+ total_tokens?: number | null;
29
+ prompt_tokens_details?: {
30
+ cached_tokens?: number | null;
31
+ cache_write_tokens?: number | null;
32
+ } | null;
33
+ };
34
+ /**
35
+ * Databricks model serving `usage` - OpenAI's three counters, with Anthropic's spelling for cache.
36
+ *
37
+ * The mixed vocabulary is Databricks' own and is reported verbatim rather than normalised to one
38
+ * side or the other. The first three come back from every serving endpoint; the last three are
39
+ * Foundation Model API extensions and are absent on the external-model route, where the body is
40
+ * whatever the upstream provider's counters normalise to.
41
+ */
42
+ export type DatabricksLikeUsage = {
43
+ prompt_tokens?: number | null;
44
+ completion_tokens?: number | null;
45
+ /** Databricks states its own total, so it is read rather than reassembled from the parts. */
46
+ total_tokens?: number | null;
47
+ /** Reasoning models only. */
48
+ reasoning_tokens?: number | null;
49
+ cache_read_input_tokens?: number | null;
50
+ cache_creation_input_tokens?: number | null;
51
+ };
52
+ /**
53
+ * The same block as above, as `@databricks/sdk-modelservingquery` hands it over.
54
+ *
55
+ * Databricks' own `ExternalModelUsageElement`. The **wire** is the snake_case one above - the
56
+ * generated client's zod schema parses `prompt_tokens` and transforms it to `promptTokens` on the
57
+ * way in, exactly as Mistral's generated client does to `UsageInfo`. So this is a second spelling
58
+ * of one vendor's counters rather than a second set of counters, and it is read through
59
+ * `DATABRICKS_QUERY_METRIC_NAME_MAP` so both spellings report under the wire's names.
60
+ *
61
+ * Three, not six: the cache pair and `reasoning_tokens` have no field on this type at all, so a
62
+ * call made through the first-party client reports the three it can and nothing more. Absent, not
63
+ * zero - and the reason the native surface's metric list is a subset of the recipe's.
64
+ *
65
+ * Written out rather than aliased to {@link MistralLikeUsage}, which it currently matches
66
+ * field-for-field: these are two unrelated vendor contracts that happen to agree today, and
67
+ * collapsing them would make a change on either side look like a change to both.
68
+ */
69
+ export type DatabricksQueryLikeUsage = {
70
+ promptTokens?: number | null;
71
+ completionTokens?: number | null;
72
+ /** Databricks states its own total here too, so it is read rather than reassembled. */
73
+ totalTokens?: number | null;
74
+ };
75
+ /** OpenAI's Responses API `usage` - the same counts as above under different names. */
76
+ export type ResponsesLikeUsage = {
77
+ input_tokens?: number | null;
78
+ output_tokens?: number | null;
79
+ /** The vendor's own total, under the same name the Chat Completions shape uses. */
80
+ total_tokens?: number | null;
81
+ input_tokens_details?: {
82
+ cached_tokens?: number | null;
83
+ cache_write_tokens?: number | null;
84
+ } | null;
85
+ };
86
+ /**
87
+ * Bedrock's Converse `usage`. Flat, camelCased, and **disjoint**.
88
+ *
89
+ * AWS is explicit that `inputTokens` counts only the tokens that were neither read from nor
90
+ * written to the cache - "total input tokens = inputTokens + cacheReadInputTokens +
91
+ * cacheWriteInputTokens" - so this shape behaves like Anthropic's and not like OpenAI's, where the
92
+ * cached count nests inside the prompt count. Subtracting here would undercount the call.
93
+ */
94
+ export type BedrockLikeUsage = {
95
+ inputTokens?: number | null;
96
+ outputTokens?: number | null;
97
+ totalTokens?: number | null;
98
+ cacheReadInputTokens?: number | null;
99
+ cacheWriteInputTokens?: number | null;
100
+ };
101
+ /** LangChain's `usage_metadata`, as carried on an AIMessage. */
102
+ export type LangChainLikeUsage = {
103
+ input_tokens?: number | null;
104
+ output_tokens?: number | null;
105
+ input_token_details?: {
106
+ cache_read?: number | null;
107
+ cache_creation?: number | null;
108
+ } | null;
109
+ };
110
+ /**
111
+ * Gemini's `usageMetadata`, which counts in its own names and overlaps in two directions at once.
112
+ *
113
+ * Both overlaps are quoted from the vendor's own field docs, because each one is a doubled or
114
+ * halved bill if read the other way round:
115
+ *
116
+ * - `promptTokenCount` - "when `cached_content` is set, this **also includes** the number of
117
+ * tokens in the cached content", so `cachedContentTokenCount` is counted *inside* it, the way
118
+ * OpenAI nests `cached_tokens` inside `prompt_tokens`.
119
+ * - `totalTokenCount` - "the sum of `prompt_token_count`, `candidates_token_count`,
120
+ * `tool_use_prompt_token_count`, and `thoughts_token_count`", so the last two sit *beside* the
121
+ * first two rather than within them. Thinking tokens in particular are billed as output and
122
+ * are **not** inside `candidatesTokenCount`: a reader that ignores them undercounts the output
123
+ * of every reasoning model this vendor sells.
124
+ */
125
+ export type GeminiLikeUsage = {
126
+ /**
127
+ * The vendor's own total: `promptTokenCount + candidatesTokenCount + toolUsePromptTokenCount +
128
+ * thoughtsTokenCount`, with the cached count already inside the first. Read, never recomputed.
129
+ */
130
+ totalTokenCount?: number | null;
131
+ promptTokenCount?: number | null;
132
+ candidatesTokenCount?: number | null;
133
+ cachedContentTokenCount?: number | null;
134
+ thoughtsTokenCount?: number | null;
135
+ toolUsePromptTokenCount?: number | null;
136
+ };
137
+ /**
138
+ * Mistral's `UsageInfo`, which counts the OpenAI three under camelCase names.
139
+ *
140
+ * The *wire* is snake_case - `prompt_tokens` and the rest - and the generated SDK renames it on
141
+ * the way in, so what a recipe reads here and what it reports are deliberately different strings.
142
+ * See `MISTRAL_METRIC_NAME_MAP` in `token-usage.ts` for the mapping and why it runs in that direction.
143
+ *
144
+ * Every counter is optional because the SDK declares them so: the inbound schema defaults each to
145
+ * zero, which means an absent counter and a genuine zero arrive looking identical. Read them as
146
+ * possibly-missing rather than trusting the declared `number`.
147
+ */
148
+ export type MistralLikeUsage = {
149
+ promptTokens?: number | null;
150
+ completionTokens?: number | null;
151
+ /** The vendor's own total. Stated, so it is read rather than recomputed from the two above. */
152
+ totalTokens?: number | null;
153
+ };
154
+ /**
155
+ * Ollama's counters, which sit **flat on the response root** rather than inside a `usage` object.
156
+ *
157
+ * Every field is optional here even though `ollama`'s own `ChatResponse` declares
158
+ * `prompt_eval_count: number` and `eval_count: number` as required. The server marks the whole
159
+ * `Metrics` struct `omitempty`, so an intermediate stream chunk carries none of them at all and
160
+ * the declared type is simply wrong about it. Trusting it substitutes zeroes for counters that
161
+ * were never sent, which bills a real call at nothing.
162
+ *
163
+ * `prompt_eval_cached_count` exists on the wire and is deliberately **not** read: the SDK does not
164
+ * declare it, and whether it sits inside `prompt_eval_count` or beside it is undocumented. A
165
+ * counter whose overlap direction is unknown cannot be reported without risking a double count.
166
+ */
167
+ export type OllamaLikeUsage = {
168
+ prompt_eval_count?: number | null;
169
+ eval_count?: number | null;
170
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A price per metric, as the mapper a run reports through.
3
+ *
4
+ * Writing one by hand is a reduce over a record and three details that are easy to miss: Stigg
5
+ * rejects fractions, a turn that measured nothing must report nothing rather than a zero, and a
6
+ * counter the response omitted is absent rather than zero. This gets all three right once.
7
+ */
8
+ import type { MetricsMapper } from '../../types';
9
+ import type { ClientRecipe } from '../client/core/types';
10
+ export type WeightedMetricsOptions = {
11
+ /**
12
+ * How many weighted units make one of `target`. Defaults to 1, so a weight reads as the price
13
+ * of one unit of that metric: `output_tokens: 0.5` is half a credit per output token.
14
+ *
15
+ * Set it when the weights are easier to write per thousand than per one - `per: 1000` with
16
+ * `output_tokens: 5` prices five credits per thousand output tokens.
17
+ */
18
+ per?: number;
19
+ };
20
+ /**
21
+ * Prices what a vendor reported into one metric of your own.
22
+ *
23
+ * The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
24
+ * is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
25
+ *
26
+ * ```ts
27
+ * // Half a credit per output token, a whole one per input token, cache reads free.
28
+ * metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
29
+ * input_tokens: 1,
30
+ * output_tokens: 0.5,
31
+ * })
32
+ * ```
33
+ *
34
+ * Prices what the vendor actually said, under its own names. For one vocabulary across several
35
+ * vendors, `normalizedMetrics` reports the four neutral counters instead.
36
+ */
37
+ export declare function weightedMetrics<M extends string>(recipe: ClientRecipe<M>, target: string, weights: Partial<Record<M, number>>, options?: WeightedMetricsOptions): MetricsMapper;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.weightedMetrics = weightedMetrics;
4
+ /**
5
+ * Prices what a vendor reported into one metric of your own.
6
+ *
7
+ * The weights are keyed to the recipe's own metric names, so a typo does not compile. Naming one
8
+ * is optional: a metric with no weight is free, and a table that prices nothing meters nothing.
9
+ *
10
+ * ```ts
11
+ * // Half a credit per output token, a whole one per input token, cache reads free.
12
+ * metrics: weightedMetrics(anthropicRecipe, 'data_credit', {
13
+ * input_tokens: 1,
14
+ * output_tokens: 0.5,
15
+ * })
16
+ * ```
17
+ *
18
+ * Prices what the vendor actually said, under its own names. For one vocabulary across several
19
+ * vendors, `normalizedMetrics` reports the four neutral counters instead.
20
+ */
21
+ function weightedMetrics(recipe, target, weights, options = {}) {
22
+ const per = options.per && options.per > 0 ? options.per : 1;
23
+ // The one thing this cannot report its way out of: an empty target is a feature id of `''`,
24
+ // which nothing downstream rejects - the value is validated, the key is not - so the turn
25
+ // reports to a feature that does not exist and the budget never moves.
26
+ if (!target.trim()) {
27
+ throw new Error('weightedMetrics needs a target to price into - an empty one reports to no feature.');
28
+ }
29
+ // Deduplicated, because the reducer walks the declaration: a recipe listing a name twice would
30
+ // charge for it twice, which is the one direction a meter must not be wrong in.
31
+ const priced = [...new Set(recipe.metrics)];
32
+ return ({ metrics }) => {
33
+ if (!metrics)
34
+ return {};
35
+ // An unpriced metric is free rather than an error, so a table names only what it charges for.
36
+ const weighted = priced.reduce((sum, name) => sum + (metrics[name] ?? 0) * (weights[name] ?? 0), 0);
37
+ // Nothing measured is not the same as nothing owed: report no metric at all, which the run
38
+ // counts as the no-op ingest it is rather than as a report of zero.
39
+ if (weighted <= 0)
40
+ return {};
41
+ // Reported as it came out of the weights. A credit currency takes fractions, so a turn worth
42
+ // a fifth of a unit costs a fifth - rounding here would be this SDK's arithmetic on someone
43
+ // else's price. A feature is the exception, since `POST /api/v1/usage` takes integers: choose
44
+ // `per` so the weights divide, or round in a mapper of your own.
45
+ return { [target]: weighted / per };
46
+ };
47
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Client configuration - everything that is true of one Stigg *environment*, and nothing else.
3
+ *
4
+ * A Stigg API key is environment-bound, so the key identifies the environment: one instance per
5
+ * environment, like the Node server SDK. That draws the line for what belongs here. Anything that
6
+ * varies within an environment - which customer, which agent, what it bills against, who wants to
7
+ * watch it happen - is context for the call that needs it, not configuration for the process.
8
+ *
9
+ * The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
10
+ * a readable message instead of as a 404 three calls later. The collaborator half (logger,
11
+ * onError) is injected as-is - those are substitution seams, not data to validate.
12
+ */
13
+ import { z } from 'zod';
14
+ import { type Logger } from './logger';
15
+ import type { CacheOptions, NodeSdkClient } from './engine/node-sdk-engine';
16
+ import type { CheckErrorAction, IngestMode } from './types';
17
+ import type { EngineError } from './errors';
18
+ declare const dataSchema: z.ZodObject<{
19
+ apiKey: z.ZodString;
20
+ baseUrl: z.ZodOptional<z.ZodString>;
21
+ checkErrorAction: z.ZodDefault<z.ZodEnum<{
22
+ deny: "deny";
23
+ allow: "allow";
24
+ }>>;
25
+ ingest: z.ZodDefault<z.ZodEnum<{
26
+ sync: "sync";
27
+ async: "async";
28
+ }>>;
29
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
30
+ }, z.core.$strip>;
31
+ export type { CacheOptions, NodeSdkClient };
32
+ /** Reported to `onError` alongside the failure, so a swallowed ingest is still diagnosable. */
33
+ export type ErrorContext = {
34
+ operation: string;
35
+ runId?: string;
36
+ idempotencyKey?: string;
37
+ customerId?: string;
38
+ };
39
+ /**
40
+ * Everything the SDK can be told, in one object.
41
+ *
42
+ * All of it optional and all of it with a working default, including where to send traffic: the
43
+ * key falls back to `STIGG_SERVER_API_KEY` and the URL to the hosted API, so the smallest working
44
+ * program is `new StiggAgentSDK()`.
45
+ *
46
+ * One object rather than positional arguments so that each answer has exactly one place to go -
47
+ * a URL given in two places needs a runtime check to refuse both, and an optional argument in
48
+ * front of a required one forces `undefined` placeholders at the call site.
49
+ */
50
+ export type StiggAgentSDKOptions = {
51
+ /**
52
+ * The server key, which names the environment. Falls back to `STIGG_SERVER_API_KEY`, the way
53
+ * every vendor SDK does - **including when `baseUrl` is set**, since a URL says where traffic
54
+ * goes and nothing about who is sending it.
55
+ *
56
+ * So a shell with that variable exported sends it to whatever `baseUrl` names, a local mock
57
+ * included. To run keyless on such a machine, unset the variable for the process rather than
58
+ * relying on `baseUrl` to suppress it: a deployment that genuinely needs no key is the case
59
+ * where nothing is configured anywhere, and that is the one the fallback below serves.
60
+ */
61
+ apiKey?: string;
62
+ /**
63
+ * Where to send traffic, when it is not the hosted API. Pass it alongside `apiKey` for a
64
+ * self-hosted deployment that still wants a key, or on its own for one that needs none.
65
+ */
66
+ baseUrl?: string;
67
+ /**
68
+ * How checks are cached. See {@link CacheOptions} - it is the Node server SDK's own cache
69
+ * configuration, passed through, because that is where caching actually happens.
70
+ */
71
+ cache?: CacheOptions;
72
+ /**
73
+ * A Node server SDK client you already have, used instead of building one.
74
+ *
75
+ * Pass it when the application uses `@stigg/node-server-sdk` directly. It is the only way to
76
+ * settle ownership: `Stigg.initialize` keeps one global instance per process and hands it back
77
+ * when the API key matches, so without this the SDK may be handed your client, believe it built
78
+ * it, and close it out from under you on `stigg.close()` - taking its cache and socket along.
79
+ * A client passed here is borrowed and never closed.
80
+ *
81
+ * It replaces only the cached half. The governance REST client is still built from the API key,
82
+ * because the Node SDK has no governance surface. Mutually exclusive with
83
+ * {@link StiggAgentSDKOptions.cache}, which configures a client this SDK is no longer building.
84
+ */
85
+ client?: NodeSdkClient;
86
+ /**
87
+ * @deprecated Ignored. There is no local decision cache to put a TTL on any more - the Node
88
+ * server SDK's entitlement cache serves these checks and keeps itself fresh, so there is no
89
+ * window to size. Configure caching with {@link StiggAgentSDKOptions.cache}; for a hard limit
90
+ * with no cached state at all, there is nothing to turn off on the attributed governance path.
91
+ */
92
+ cacheTtlMs?: number;
93
+ /**
94
+ * What happens to a call when the check itself fails - Stigg unreachable, a timeout, a 5xx.
95
+ *
96
+ * Defaults to `'allow'`: a soft limit, so an outage does not take the application down with it.
97
+ * Pass `'deny'` to mirror governance's own fail-closed behaviour. Overridable per call through
98
+ * `stiggContext`, and by `wrap`'s own context for every call through one client.
99
+ */
100
+ checkErrorAction?: CheckErrorAction;
101
+ /**
102
+ * Which of Stigg's ingest APIs a run reports through. Defaults to `'async'`: the event API, which
103
+ * costs a fraction of the latency and takes a far higher rate. Overridable per run, and see
104
+ * {@link IngestMode} for what the two trade against each other.
105
+ */
106
+ ingest?: IngestMode;
107
+ timeoutMs?: number;
108
+ /**
109
+ * Where the SDK narrates what it did. Defaults to `'console'`, because the things it says are
110
+ * the ones you need before anything is configured - which transport resolved, that a run is
111
+ * gating nothing, that what was reported names no entitlement the budget converts.
112
+ *
113
+ * Pass `'none'` to silence it, or your own logger to route it.
114
+ */
115
+ logger?: Logger | 'console' | 'none';
116
+ /**
117
+ * Every instrumentation failure, as structure rather than as a sentence.
118
+ *
119
+ * The one observability hook that is environment-scoped, because it is the only one that fires
120
+ * with no run to belong to: engine resolution, a bare `register()`, `close()`. Watching a
121
+ * decision or an ingest is per-run and lives on `run.start(..., { hooks })`.
122
+ *
123
+ * It is also the only place metering failures surface at all - they are swallowed so they
124
+ * cannot break a run - so it defaults to a warning through `logger` rather than to silence.
125
+ * Replace it to route an `EngineError` and its {@link ErrorContext} into your own telemetry.
126
+ */
127
+ onError?: (error: EngineError, context: ErrorContext) => void;
128
+ };
129
+ export type ResolvedConfig = Omit<Required<z.infer<typeof dataSchema>>, 'baseUrl'> & {
130
+ baseUrl?: string;
131
+ cache?: CacheOptions;
132
+ client?: NodeSdkClient;
133
+ logger: Logger;
134
+ onError: (error: EngineError, context: ErrorContext) => void;
135
+ };
136
+ export declare function resolveConfig(options?: StiggAgentSDKOptions): ResolvedConfig;
137
+ /** The slice of config the engine factory needs. */
138
+ export type EngineConfig = {
139
+ apiKey: string;
140
+ baseUrl?: string;
141
+ timeoutMs: number;
142
+ cache?: CacheOptions;
143
+ client?: NodeSdkClient;
144
+ logger: Logger;
145
+ };
package/dist/config.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConfig = resolveConfig;
4
+ /**
5
+ * Client configuration - everything that is true of one Stigg *environment*, and nothing else.
6
+ *
7
+ * A Stigg API key is environment-bound, so the key identifies the environment: one instance per
8
+ * environment, like the Node server SDK. That draws the line for what belongs here. Anything that
9
+ * varies within an environment - which customer, which agent, what it bills against, who wants to
10
+ * watch it happen - is context for the call that needs it, not configuration for the process.
11
+ *
12
+ * The data half (ids, urls, numbers, enums) goes through zod so a typo fails at construction with
13
+ * a readable message instead of as a 404 three calls later. The collaborator half (logger,
14
+ * onError) is injected as-is - those are substitution seams, not data to validate.
15
+ */
16
+ const zod_1 = require("zod");
17
+ const context_1 = require("./core/context");
18
+ const logger_1 = require("./logger");
19
+ /** The key sent when the target is a URL that needs none. Never reaches a real environment. */
20
+ const KEYLESS = 'offline-mock';
21
+ const dataSchema = zod_1.z.object({
22
+ apiKey: zod_1.z.string().min(1),
23
+ baseUrl: zod_1.z.string().url().optional(),
24
+ // Both default through the context's own table rather than restating it: the two used to
25
+ // disagree, and the one written here was the one nothing read.
26
+ checkErrorAction: zod_1.z.enum(['deny', 'allow']).default(context_1.CONTEXT_DEFAULTS.checkErrorAction),
27
+ ingest: zod_1.z.enum(['sync', 'async']).default(context_1.CONTEXT_DEFAULTS.ingestMode),
28
+ timeoutMs: zod_1.z.number().int().positive().default(3_000),
29
+ });
30
+ const resolveLogger = (logger) => {
31
+ if (logger === 'console' || logger === undefined)
32
+ return logger_1.consoleLogger;
33
+ if (logger === 'none')
34
+ return logger_1.noopLogger;
35
+ return logger;
36
+ };
37
+ /**
38
+ * The key to send, which is the environment's identity.
39
+ *
40
+ * The environment variable is tried before the keyless fallback, and that order is the whole of
41
+ * it. A `baseUrl` says *where* to send traffic and nothing about who is sending it, so the two
42
+ * are independent: a self-hosted deployment names its URL in options and keeps its key in
43
+ * `STIGG_SERVER_API_KEY`, and substituting a sentinel there would 401 with nothing in the config
44
+ * to say why.
45
+ *
46
+ * {@link KEYLESS} is the last resort, not the rule: it stands in for a deployment that needs no
47
+ * key at all - a local stand-in, or the built-in mock - and exists only because the schema below
48
+ * demands a non-empty string. Nothing authenticates with it.
49
+ *
50
+ * The failure for a missing key is raised here rather than left to zod, because "set
51
+ * STIGG_SERVER_API_KEY" is the actionable half and the schema only knows the field was too short.
52
+ */
53
+ function resolveApiKey({ apiKey, baseUrl }) {
54
+ const named = apiKey?.trim();
55
+ if (named)
56
+ return named;
57
+ const fromEnv = process.env.STIGG_SERVER_API_KEY?.trim();
58
+ if (fromEnv)
59
+ return fromEnv;
60
+ // No key anywhere. A URL to send to is the one case where that is a configuration rather than
61
+ // an omission.
62
+ if (baseUrl)
63
+ return KEYLESS;
64
+ throw new Error('No Stigg API key - pass `apiKey`, or set STIGG_SERVER_API_KEY. To run without one, pass ' +
65
+ '`baseUrl` instead: a local stand-in, a self-hosted deployment, or the built-in mock.');
66
+ }
67
+ function resolveConfig(options = {}) {
68
+ const parsed = dataSchema.parse({
69
+ apiKey: resolveApiKey(options),
70
+ baseUrl: options.baseUrl,
71
+ checkErrorAction: options.checkErrorAction,
72
+ ingest: options.ingest,
73
+ timeoutMs: options.timeoutMs,
74
+ });
75
+ // Refused rather than silently preferred, like the two-URL case above: `cache` configures a
76
+ // client, and a client that was handed in is already configured. Whichever the SDK picked, the
77
+ // other would be settings the caller believes are in force and are not.
78
+ if (options.client && options.cache) {
79
+ throw new Error('This SDK was given both a `client` and `cache` options. The cache options configure a ' +
80
+ 'client at construction, and the one you passed is already built - so they would be ' +
81
+ 'ignored. Pass one: `client` for a client you own, or `cache` to have the SDK build one.');
82
+ }
83
+ const logger = resolveLogger(options.logger);
84
+ // Said rather than ignored in silence: someone who set this was sizing an overshoot window, and
85
+ // the answer to that question is now different rather than smaller.
86
+ if (options.cacheTtlMs !== undefined) {
87
+ logger.warn('[stigg] cacheTtlMs is ignored - this SDK no longer caches decisions. Checks are served by ' +
88
+ "the Node server SDK's entitlement cache, which invalidates on updates and writes reported " +
89
+ 'usage back, so there is no TTL to size. Configure it with `cache` instead.');
90
+ }
91
+ return {
92
+ ...parsed,
93
+ cache: options.cache,
94
+ client: options.client,
95
+ logger,
96
+ // Never silent, never fatal. Replace it to route into your own telemetry.
97
+ onError: options.onError ?? ((error, context) => logger.warn(`[stigg] ${context.operation} failed: ${error.message}`)),
98
+ };
99
+ }
@@ -0,0 +1,160 @@
1
+ import type { CheckErrorAction, Entitlement, IngestMode, Metrics, MetricsMapper } from '../types';
2
+ import type { EventIngest } from '../engine/engine';
3
+ /**
4
+ * What a call is governed by. Every field is optional here because each one may have been given
5
+ * at `wrap` instead - `customerId` is the only one that has to come from somewhere, and the
6
+ * wrapped client's types say which calls still owe it.
7
+ */
8
+ export type StiggContext = {
9
+ /** The tenant. Every governance call is scoped to one customer. */
10
+ customerId?: string;
11
+ /**
12
+ * The agent this call spends as. Travels as a dimension under
13
+ * {@link StiggContext.attributionKey}, which is the entire mechanism by which an agent is
14
+ * attributed - so naming one without a key is refused rather than guessed at.
15
+ *
16
+ * Omitted, the call resolves no entity and is checked against the customer's own budget.
17
+ */
18
+ agentId?: string;
19
+ /**
20
+ * The key the agent's entity type declares, under which `agentId` is sent on every call.
21
+ * Required whenever `agentId` is given, and not defaulted: a guessed key resolves no entity, so
22
+ * the spend lands on the customer looking exactly like a call that was never governed.
23
+ *
24
+ * `register` takes an `entityTypeId` as well, because that call creates the entity. Attributing
25
+ * a call does not need to know what type it was created as - only what key to send it under.
26
+ */
27
+ attributionKey?: string;
28
+ /**
29
+ * Extra attribution, sent verbatim on the check and the report. Every key must be an attribution
30
+ * key on one of the entity types involved, or it matches nothing and the usage rolls up to the
31
+ * customer instead.
32
+ *
33
+ * Overriding this **merges** per key over the wrap's own, so a per-call `{ tenantId }` does not
34
+ * silently drop a client-wide `{ region }`.
35
+ */
36
+ dimensions?: Record<string, string>;
37
+ /**
38
+ * What this call **gates on** - a credit currency or a metered feature, never both. **Omit it
39
+ * and the call gates nothing**: no check leaves the process, nothing can be refused, and
40
+ * metering is unchanged. The presence of this field is the enforcement switch, because a call
41
+ * with nothing to check against cannot be enforced, and a separate flag would only be a second
42
+ * place for the two to disagree.
43
+ */
44
+ entitlement?: Entitlement;
45
+ /**
46
+ * What a check resolves to when the engine could not answer it. Defaults to `'allow'` - a soft
47
+ * limit, so an outage does not take the application down with it.
48
+ *
49
+ * Worth knowing what that trades: reporting fails in the same outage and is swallowed, so the
50
+ * spend allowed through is neither capped nor recorded afterwards. `'deny'` mirrors governance,
51
+ * which is fail-closed server-side.
52
+ */
53
+ checkErrorAction?: CheckErrorAction;
54
+ /**
55
+ * What to report for each completed call. Defaults to the vendor's own metric names and values
56
+ * exactly as they came back, so what lands in Stigg reconciles against that vendor's invoice.
57
+ *
58
+ * The name on the left is a Stigg feature or credit id, and metering the one the call checks -
59
+ * {@link StiggContext.entitlement} - is what makes the budget move.
60
+ */
61
+ usageMapper?: MetricsMapper;
62
+ /** Which of Stigg's ingest APIs this call reports through. Defaults to `'async'`. */
63
+ ingestMode?: IngestMode;
64
+ /**
65
+ * The event name to report under in `'async'` mode. Defaults to the vendor call that produced
66
+ * the usage - `anthropic.messages.create` - which is what a Stigg meter filters on.
67
+ */
68
+ eventName?: EventIngest;
69
+ /**
70
+ * The key that deduplicates this report. Defaults to the vendor's own request id when it has
71
+ * one - stable across the vendor's retries, which a local counter cannot be - and to a
72
+ * generated key otherwise.
73
+ */
74
+ idempotencyKey?: string;
75
+ /**
76
+ * Correlates work that continues in another process, as `x-stigg-run-id`. Defaults to the id
77
+ * the wrap generated, so a gateway sees one value per client unless a call says otherwise.
78
+ */
79
+ correlationId?: string;
80
+ };
81
+ /**
82
+ * A context with everything decided and everything derivable already derived.
83
+ *
84
+ * Nothing here is optional that the call path would otherwise have to default, and nothing is
85
+ * computed that a call could be asked to compute twice.
86
+ */
87
+ export type ResolvedContext = {
88
+ /**
89
+ * Empty only for a client wrapped without one, whose calls each name their own. Every path that
90
+ * reaches the engine resolves it first, so nothing downstream has to re-check it.
91
+ */
92
+ customerId: string;
93
+ entitlement?: Entitlement;
94
+ /** The agent folded in, so the check and the report send one object and neither re-derives it. */
95
+ dimensions: Record<string, string>;
96
+ checkErrorAction: CheckErrorAction;
97
+ ingestMode: IngestMode;
98
+ eventName?: EventIngest;
99
+ usageMapper: MetricsMapper;
100
+ idempotencyKey?: string;
101
+ correlationId: string;
102
+ /** The entitlement's own id, resolved once - undefined when this context gates on nothing. */
103
+ checkedId?: string;
104
+ /**
105
+ * Kept so a per-call `agentId` can inherit it.
106
+ *
107
+ * The key belongs to the entity type, which is a property of the client, while the agent varies
108
+ * per call - so naming it once at `wrap` and then only the agent per call is the shape a
109
+ * multi-agent server wants. The folded dimension alone would not do: an override needs the key
110
+ * itself to build its own.
111
+ */
112
+ attributionKey?: string;
113
+ };
114
+ /**
115
+ * What nobody told the SDK, and it will not guess.
116
+ *
117
+ * Exported because the constructor's own schema defaults to these: two copies of "what happens
118
+ * when nothing says" is how a configured `deny` came to be parsed, stored and then ignored in
119
+ * favour of an `allow` written somewhere else.
120
+ *
121
+ * `allow` is the soft limit - an unreachable Stigg does not take the application down with it.
122
+ */
123
+ export declare const CONTEXT_DEFAULTS: {
124
+ readonly checkErrorAction: "allow";
125
+ readonly ingestMode: "async";
126
+ };
127
+ /**
128
+ * Validates and derives, once. Everything expensive about a context happens here rather than on
129
+ * the call path - see the note at the top of this file for why that split exists.
130
+ */
131
+ /** The one field a call cannot proceed without, said the same way wherever it is missing. */
132
+ export declare const noCustomer: () => Error;
133
+ /**
134
+ * The client-wide policy a context falls back to before the SDK's own defaults.
135
+ *
136
+ * The chain is per-call, then `wrap`, then the constructor, then {@link CONTEXT_DEFAULTS}. The
137
+ * constructor link is this parameter: without it a configured policy is inert, because every
138
+ * context would land on the defaults instead.
139
+ */
140
+ export type ContextDefaults = {
141
+ checkErrorAction?: CheckErrorAction;
142
+ ingestMode?: IngestMode;
143
+ };
144
+ export declare function resolveContext(context: StiggContext, correlationId: string, { requireCustomer, defaults }?: {
145
+ requireCustomer?: boolean;
146
+ defaults?: ContextDefaults;
147
+ }): ResolvedContext;
148
+ /**
149
+ * One call's override, applied over what `wrap` already resolved.
150
+ *
151
+ * Only reached when a call actually carried a `stiggContext`; a call that did not reuses the
152
+ * resolved object as-is. The ordering below is the one the caller asked for: the base identity
153
+ * first, because a changed customer or agent changes which budget is consulted, and the metering
154
+ * fields last, because nothing before the response depends on them.
155
+ */
156
+ export declare function overrideContext(base: ResolvedContext, override: StiggContext): ResolvedContext;
157
+ /** Whether a context gates at all. The enforcement switch, read on every call. */
158
+ export declare const isEnforced: (context: ResolvedContext) => boolean;
159
+ /** Reported metrics, accumulated across a client's calls. */
160
+ export declare const addMetrics: (into: Metrics, add: Metrics) => void;