@stigg/agent-sdk-js 0.4.0 → 0.5.1

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.
@@ -57,6 +57,20 @@ const report_1 = require("./report");
57
57
  * connection, a dropped socket - which is what tells a refusal apart from an outage.
58
58
  */
59
59
  const errorCode = (err) => err?.errorCode;
60
+ /**
61
+ * The failures that say nothing about the customer - a rate limit, a timeout, an unexpected error -
62
+ * so asking again later may succeed. Every other code Stigg answers with is a final answer about
63
+ * this customer or this configuration - an unknown plan, a key that cannot write, an invalid id -
64
+ * and asking again gets the same one.
65
+ */
66
+ const PASSING_FAILURES = new Set([
67
+ 'RateLimitExceeded',
68
+ 'OperationTimeout',
69
+ 'UnexpectedError',
70
+ 'RequestTimeout',
71
+ // A lock another request holds on the customer - creating one takes it.
72
+ 'ObjectAlreadyBeingUsedByAnotherRequestError',
73
+ ]);
60
74
  const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
61
75
  /**
62
76
  * Drops keys that were never set, so a spread cannot un-set a default.
@@ -148,9 +162,10 @@ class NodeSdkEngine {
148
162
  * mutation, and writes the entitlements it answers with straight into the cache - so the check
149
163
  * that follows reads the new plan locally instead of fetching it.
150
164
  *
151
- * Throws only when Stigg did not answer. Any answer - created, duplicate, or refused - resolves,
152
- * because the caller treats them differently: a refusal will be the same next time, and an
153
- * unreachable server may not.
165
+ * Resolves with a final answer - created, duplicate, or a refusal that will be the same next
166
+ * time. Throws for a passing failure - unreachable, rate limited, timed out - so the customer is
167
+ * asked again later: remembering one as a refusal would deny that customer until the process
168
+ * restarts.
154
169
  */
155
170
  async provisionCustomer(customerId, planId) {
156
171
  try {
@@ -165,7 +180,7 @@ class NodeSdkEngine {
165
180
  const code = errorCode(err);
166
181
  if (code === 'DuplicatedEntityNotAllowed')
167
182
  return { status: 'existing' };
168
- if (code && code !== 'RequestTimeout') {
183
+ if (code && !PASSING_FAILURES.has(code)) {
169
184
  return { status: 'rejected', code, reason: err instanceof Error ? err.message : String(err) };
170
185
  }
171
186
  throw (0, errors_1.asEngineError)('provisionCustomer', err);
package/dist/errors.d.ts CHANGED
@@ -50,7 +50,14 @@ export declare class EngineError extends Error {
50
50
  export declare class GovernanceNotEnabledError extends Error {
51
51
  constructor(detail?: string);
52
52
  }
53
- /** Returns the decision when `err` is a governance denial - the one-liner for a binding catch. */
53
+ /**
54
+ * Returns the decision when `err` is a governance denial - the one-liner for a binding catch.
55
+ *
56
+ * Frameworks rarely let an error through untouched. Vendor SDKs wrap callback errors, LangChain
57
+ * wraps a model call's error once per middleware, and LangGraph reports two branches that failed
58
+ * in one step as an `AggregateError` - so the denial is looked for down the whole `cause` chain and
59
+ * inside every aggregated error, not only at the top.
60
+ */
54
61
  export declare function isAccessDenied(err: unknown): AccessDecision | undefined;
55
62
  export declare const isEngineError: (err: unknown) => err is EngineError;
56
63
  export declare const isGovernanceNotEnabled: (err: unknown) => err is GovernanceNotEnabledError;
package/dist/errors.js CHANGED
@@ -78,15 +78,37 @@ class GovernanceNotEnabledError extends Error {
78
78
  }
79
79
  }
80
80
  exports.GovernanceNotEnabledError = GovernanceNotEnabledError;
81
- /** Returns the decision when `err` is a governance denial - the one-liner for a binding catch. */
81
+ /**
82
+ * How far down a wrapped error is searched. Each LangChain middleware that wraps a model call adds
83
+ * one `MiddlewareError` around a denial, and a sub-agent's run nests its own inside its parent's,
84
+ * so a handful of levels is ordinary. The bound is only there so a cyclic `cause` cannot spin.
85
+ */
86
+ const MAX_CAUSE_DEPTH = 16;
87
+ /**
88
+ * Returns the decision when `err` is a governance denial - the one-liner for a binding catch.
89
+ *
90
+ * Frameworks rarely let an error through untouched. Vendor SDKs wrap callback errors, LangChain
91
+ * wraps a model call's error once per middleware, and LangGraph reports two branches that failed
92
+ * in one step as an `AggregateError` - so the denial is looked for down the whole `cause` chain and
93
+ * inside every aggregated error, not only at the top.
94
+ */
82
95
  function isAccessDenied(err) {
96
+ return findDenial(err, 0);
97
+ }
98
+ function findDenial(err, depth) {
83
99
  if (isKind(err, 'denied'))
84
100
  return err.decision;
85
- // Vendor SDKs wrap callback errors, so the denial can arrive one level down.
86
- const cause = err?.cause;
87
- if (cause && isKind(cause, 'denied'))
88
- return cause.decision;
89
- return undefined;
101
+ if (typeof err !== 'object' || err === null || depth >= MAX_CAUSE_DEPTH)
102
+ return undefined;
103
+ const { cause, errors } = err;
104
+ if (Array.isArray(errors)) {
105
+ for (const inner of errors) {
106
+ const decision = findDenial(inner, depth + 1);
107
+ if (decision)
108
+ return decision;
109
+ }
110
+ }
111
+ return cause === undefined ? undefined : findDenial(cause, depth + 1);
90
112
  }
91
113
  const isEngineError = (err) => isKind(err, 'engine');
92
114
  exports.isEngineError = isEngineError;
package/dist/index.d.ts CHANGED
@@ -19,11 +19,12 @@ export { describeDecision, remedyMessage } from './decision-text';
19
19
  export { GOVERNANCE_HEADERS, parseGovernanceHeaders } from './headers';
20
20
  export type { GovernanceHeaders, ParsedGovernanceHeaders } from './headers';
21
21
  export { consoleLogger, noopLogger } from './logger';
22
+ export { TRACER_NAME as STIGG_TRACER_NAME } from './core/telemetry';
22
23
  export type { Logger } from './logger';
23
24
  export { EngineError, AccessDeniedError, GovernanceNotEnabledError, isEngineError, isAccessDenied, isGovernanceNotEnabled, looksLikeGovernanceNotEnabled, } from './errors';
24
25
  export type { EngineCapabilities, EngineChainNode, EngineCheckParams, EngineCheckResult, EngineReportParams, EngineReportResult, EngineResolution, EntityInput, EventIngest, EntityPlacement, GovernanceEngine, } from './engine/engine';
25
26
  export { unitsPerTokens, vendorMetrics, emptyTokenUsage, entitlementId, entitlementKind, entitlementRef, toMeasurement, } from './types';
26
- export type { AgentBudget, AgentOptions, AgentScope, ClientGovernance, CheckErrorAction, ClientOptions, DecisionSource, DeregisterInput, Entitlement, AccessDecision, Remedy, WrappedClient, IngestContext, IngestMode, IngestResult, Measurement, RegisterInput, GatedSurface, ChatOf, CallContext, TokenUsage, Metrics, MetricsMapper, } from './types';
27
+ export type { AgentBudget, MiddlewareOptions, MiddlewareAgentOptions, MiddlewareTools, ToolGovernance, ToolUsageMapper, ToolCallMeasurement, GovernedAgentMiddleware, AgentOptions, AgentScope, ClientGovernance, CheckErrorAction, ClientOptions, DecisionSource, DeregisterInput, Entitlement, AccessDecision, Remedy, WrappedClient, IngestContext, IngestMode, IngestResult, Measurement, RegisterInput, GatedSurface, ChatOf, CallContext, TokenUsage, Metrics, MetricsMapper, } from './types';
27
28
  export { wrapClient } from './bindings/client/core/wrap-client';
28
29
  export { createClientRegistry, hasMethod } from './bindings/client/core/registry';
29
30
  export type { ClientRecipe, ClientRegistry, MethodRecipe, MetricsFor, MetricsOf, MeteredResponse, } from './bindings/client/core/types';
@@ -60,4 +61,5 @@ export type { TypeSafeMetric, TypeSafeMetrics } from './bindings/client/provider
60
61
  export { weightedMetrics } from './bindings/shared/weighted-metrics';
61
62
  export type { WeightedMetricsOptions } from './bindings/shared/weighted-metrics';
62
63
  export type { ProxyIngestInput } from './bindings/proxy';
64
+ export type { AgentMiddleware, IssuedToolCall, StepAttribution, ToolStep, } from './bindings/orchestrator/core/agent-middleware';
63
65
  export { normalizedMetrics, normalizeUsage } from './bindings/shared/token-usage';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeUsage = exports.normalizedMetrics = exports.weightedMetrics = exports.estimateFromState = exports.typesafeRecipe = exports.cloudflareRecipe = exports.bedrockRecipe = exports.databricksExternalRecipe = exports.databricksRecipe = exports.ollamaRecipe = exports.mistralRecipe = exports.groqRecipe = exports.litellmRecipe = exports.openrouterRecipe = exports.estimateFromMaxOutputTokens = exports.geminiChatRecipe = exports.geminiRecipe = exports.openAIRecipe = exports.estimateFromMaxTokens = exports.anthropicRecipe = exports.hasMethod = exports.createClientRegistry = exports.wrapClient = exports.toMeasurement = exports.entitlementRef = exports.entitlementKind = exports.entitlementId = exports.emptyTokenUsage = exports.vendorMetrics = exports.unitsPerTokens = exports.looksLikeGovernanceNotEnabled = exports.isGovernanceNotEnabled = exports.isAccessDenied = exports.isEngineError = exports.GovernanceNotEnabledError = exports.AccessDeniedError = exports.EngineError = exports.noopLogger = exports.consoleLogger = exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = exports.remedyMessage = exports.describeDecision = exports.flattenRaw = exports.RAW_PREFIX = exports.StiggAgentSDK = void 0;
3
+ exports.normalizeUsage = exports.normalizedMetrics = exports.weightedMetrics = exports.estimateFromState = exports.typesafeRecipe = exports.cloudflareRecipe = exports.bedrockRecipe = exports.databricksExternalRecipe = exports.databricksRecipe = exports.ollamaRecipe = exports.mistralRecipe = exports.groqRecipe = exports.litellmRecipe = exports.openrouterRecipe = exports.estimateFromMaxOutputTokens = exports.geminiChatRecipe = exports.geminiRecipe = exports.openAIRecipe = exports.estimateFromMaxTokens = exports.anthropicRecipe = exports.hasMethod = exports.createClientRegistry = exports.wrapClient = exports.toMeasurement = exports.entitlementRef = exports.entitlementKind = exports.entitlementId = exports.emptyTokenUsage = exports.vendorMetrics = exports.unitsPerTokens = exports.looksLikeGovernanceNotEnabled = exports.isGovernanceNotEnabled = exports.isAccessDenied = exports.isEngineError = exports.GovernanceNotEnabledError = exports.AccessDeniedError = exports.EngineError = exports.STIGG_TRACER_NAME = exports.noopLogger = exports.consoleLogger = exports.parseGovernanceHeaders = exports.GOVERNANCE_HEADERS = exports.remedyMessage = exports.describeDecision = exports.flattenRaw = exports.RAW_PREFIX = exports.StiggAgentSDK = void 0;
4
4
  /**
5
5
  * Public surface.
6
6
  *
@@ -28,6 +28,10 @@ Object.defineProperty(exports, "parseGovernanceHeaders", { enumerable: true, get
28
28
  var logger_1 = require("./logger");
29
29
  Object.defineProperty(exports, "consoleLogger", { enumerable: true, get: function () { return logger_1.consoleLogger; } });
30
30
  Object.defineProperty(exports, "noopLogger", { enumerable: true, get: function () { return logger_1.noopLogger; } });
31
+ // The OpenTelemetry instrumentation scope of every `stigg.check` / `stigg.ingest` span, for a span
32
+ // filter to match on - Langfuse's `shouldExportSpan`, for one, exports only scopes it knows.
33
+ var telemetry_1 = require("./core/telemetry");
34
+ Object.defineProperty(exports, "STIGG_TRACER_NAME", { enumerable: true, get: function () { return telemetry_1.TRACER_NAME; } });
31
35
  var errors_1 = require("./errors");
32
36
  Object.defineProperty(exports, "EngineError", { enumerable: true, get: function () { return errors_1.EngineError; } });
33
37
  Object.defineProperty(exports, "AccessDeniedError", { enumerable: true, get: function () { return errors_1.AccessDeniedError; } });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `@stigg/agent-sdk-js/langchain` - Stigg governance for LangChain and LangGraph.
3
+ *
4
+ * Its own entry point because it imports `@langchain/core`, which the package's main entry never
5
+ * does: an application that does not run LangChain never loads it. `@langchain/core` is an optional
6
+ * peer dependency, so this uses the copy the application already has - the one whose callback
7
+ * manager will be calling the handler.
8
+ *
9
+ * import { StiggAgentSDK } from "@stigg/agent-sdk-js";
10
+ * import { StiggLangChainCallbackHandler, AccessDeniedError } from "@stigg/agent-sdk-js/langchain";
11
+ */
12
+ export { StiggLangChainCallbackHandler, CUSTOMER_METADATA_KEY, } from '../bindings/orchestrator/langchain/callback-handler';
13
+ export type { StiggLangChainCallbackOptions } from '../bindings/orchestrator/langchain/callback-handler';
14
+ export { AccessDeniedError, isAccessDenied } from '../errors';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAccessDenied = exports.AccessDeniedError = exports.CUSTOMER_METADATA_KEY = exports.StiggLangChainCallbackHandler = void 0;
4
+ /**
5
+ * `@stigg/agent-sdk-js/langchain` - Stigg governance for LangChain and LangGraph.
6
+ *
7
+ * Its own entry point because it imports `@langchain/core`, which the package's main entry never
8
+ * does: an application that does not run LangChain never loads it. `@langchain/core` is an optional
9
+ * peer dependency, so this uses the copy the application already has - the one whose callback
10
+ * manager will be calling the handler.
11
+ *
12
+ * import { StiggAgentSDK } from "@stigg/agent-sdk-js";
13
+ * import { StiggLangChainCallbackHandler, AccessDeniedError } from "@stigg/agent-sdk-js/langchain";
14
+ */
15
+ var callback_handler_1 = require("../bindings/orchestrator/langchain/callback-handler");
16
+ Object.defineProperty(exports, "StiggLangChainCallbackHandler", { enumerable: true, get: function () { return callback_handler_1.StiggLangChainCallbackHandler; } });
17
+ Object.defineProperty(exports, "CUSTOMER_METADATA_KEY", { enumerable: true, get: function () { return callback_handler_1.CUSTOMER_METADATA_KEY; } });
18
+ // What a refusal is, re-exported so a catch block needs one import: `e instanceof AccessDeniedError`.
19
+ var errors_1 = require("../errors");
20
+ Object.defineProperty(exports, "AccessDeniedError", { enumerable: true, get: function () { return errors_1.AccessDeniedError; } });
21
+ Object.defineProperty(exports, "isAccessDenied", { enumerable: true, get: function () { return errors_1.isAccessDenied; } });
@@ -2,12 +2,12 @@ import { type ResolvedConfig, type StiggAgentSDKOptions } from './config';
2
2
  import { type EngineResolution, type GovernanceEngine } from './engine/engine';
3
3
  import type { StiggContext } from './core/context';
4
4
  import type { SessionSummary } from './core/session';
5
- import type { Metrics, ClientOptions, DeregisterInput, AccessDecision, WrappedClient, PathsOf, IngestContext, IngestResult, RegisterInput } from './types';
5
+ import type { Metrics, GovernedAgentMiddleware, MiddlewareOptions, ClientOptions, DeregisterInput, AccessDecision, WrappedClient, PathsOf, IngestContext, IngestResult, RegisterInput } from './types';
6
6
  import type { GovernanceHeaders } from './headers';
7
7
  /**
8
8
  * Recipes are data - method paths and usage extractors - so wiring them here costs nothing at
9
- * runtime and pulls in no vendor package. An orchestration adapter is different: it needs its
10
- * framework loaded, so it stays an explicit subpath import.
9
+ * runtime and pulls in no vendor package. The LangChain handler, which does load its framework,
10
+ * is reached only through the `@stigg/agent-sdk-js/langchain` entry, never from here.
11
11
  */
12
12
  /**
13
13
  * Order matters: the gateways are matched before the plain OpenAI adapter, because every one of
@@ -97,11 +97,11 @@ export declare const DEFAULT_RECIPES: ({
97
97
  totalTokensMetric: "total_tokens";
98
98
  matches: (client: unknown) => boolean;
99
99
  methods: ({
100
- path: ("stream" | "chat" | "complete" | "parse" | "parseStream" | "fim" | "agents")[];
100
+ path: ("agents" | "stream" | "chat" | "complete" | "parse" | "parseStream" | "fim")[];
101
101
  attachStream: (stream: unknown, ingest: (measurement: import("./types").Measurement) => void) => void;
102
102
  extractUsage?: undefined;
103
103
  } | {
104
- path: ("stream" | "chat" | "complete" | "parse" | "parseStream" | "fim" | "agents")[];
104
+ path: ("agents" | "stream" | "chat" | "complete" | "parse" | "parseStream" | "fim")[];
105
105
  extractUsage: (result: import("./bindings/client/providers/mistral/types").MistralCompletionResponse | undefined) => import("./types").Measurement | undefined;
106
106
  attachStream?: undefined;
107
107
  })[];
@@ -165,12 +165,18 @@ export declare class StiggAgentSDK {
165
165
  *
166
166
  * A refusal is remembered like a success: an unknown plan or a key that cannot write will be
167
167
  * refused the same way next time, and retrying it would spend a rate-limited call on every call.
168
- * Only an unreachable Stigg is forgotten, so the customer's next call tries again.
168
+ * A passing failure - unreachable, rate limited - is remembered for
169
+ * {@link PROVISION_RETRY_AFTER_MS}, then forgotten, so the customer is asked again: soon, but not
170
+ * on every call while Stigg is the one asking for less.
169
171
  *
170
172
  * Capped at {@link KNOWN_CUSTOMERS_CAP}, oldest first. Forgetting a customer costs one more
171
173
  * provisioning call, which Stigg answers as a duplicate - so the cap bounds memory, not safety.
172
174
  */
173
175
  private readonly customers;
176
+ /** When a customer whose provisioning failed in passing may be asked again. */
177
+ private readonly provisionRetryAt;
178
+ /** Not readonly, so a test can shorten the wait rather than sleep through it. */
179
+ private provisionRetryAfterMs;
174
180
  private started?;
175
181
  /**
176
182
  * When the attempt currently in `started` rejected, or undefined while it is pending or has
@@ -242,6 +248,20 @@ export declare class StiggAgentSDK {
242
248
  * see {@link GovernanceSession.warmUp} for which cache that is and when there is one at all.
243
249
  */
244
250
  wrap<T extends object, O extends ClientOptions>(client: T, options?: O): WrappedClient<T, O['stiggContext'], PathsOf<O['recipe'], T>>;
251
+ /**
252
+ * An agent graph's governance middleware - the orchestration counterpart to `wrap`.
253
+ *
254
+ * There is no single client to gate in a graph: it is several agents, each spending against its
255
+ * own entitlement. So the graph is configured once - the shared context, and each agent's own
256
+ * under `agents` - and every step it takes is checked and billed as the agent that took it. A
257
+ * refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
258
+ * run is the graph's to decide.
259
+ *
260
+ * What comes back is framework-neutral: the start and end of each model call and tool. A
261
+ * framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
262
+ * `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
263
+ */
264
+ middleware(options?: MiddlewareOptions): GovernedAgentMiddleware;
245
265
  /**
246
266
  * Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
247
267
  * already has: silently replacing one of its own properties would be a worse surprise.
@@ -36,6 +36,7 @@ const openrouter_recipe_1 = require("./bindings/client/gateways/openrouter/openr
36
36
  const typesafe_recipe_1 = require("./bindings/client/providers/typesafe/typesafe-recipe");
37
37
  const registry_1 = require("./bindings/client/core/registry");
38
38
  const wrap_client_1 = require("./bindings/client/core/wrap-client");
39
+ const agent_middleware_1 = require("./bindings/orchestrator/core/agent-middleware");
39
40
  const token_usage_1 = require("./bindings/shared/token-usage");
40
41
  const context_1 = require("./core/context");
41
42
  const session_1 = require("./core/session");
@@ -43,8 +44,8 @@ const hooks_1 = require("./core/hooks");
43
44
  const types_1 = require("./types");
44
45
  /**
45
46
  * Recipes are data - method paths and usage extractors - so wiring them here costs nothing at
46
- * runtime and pulls in no vendor package. An orchestration adapter is different: it needs its
47
- * framework loaded, so it stays an explicit subpath import.
47
+ * runtime and pulls in no vendor package. The LangChain handler, which does load its framework,
48
+ * is reached only through the `@stigg/agent-sdk-js/langchain` entry, never from here.
48
49
  */
49
50
  /**
50
51
  * Order matters: the gateways are matched before the plain OpenAI adapter, because every one of
@@ -125,6 +126,8 @@ const RETRY_START_AFTER_MS = 2_000;
125
126
  * of tenants would otherwise grow this for as long as it runs.
126
127
  */
127
128
  const KNOWN_CUSTOMERS_CAP = 10_000;
129
+ /** How long a customer whose provisioning failed in passing waits before it is asked again. */
130
+ const PROVISION_RETRY_AFTER_MS = 30_000;
128
131
  /** The key shorthand and the options object, folded into the one object the config takes. */
129
132
  function toOptions(first, second) {
130
133
  return typeof first === 'string' ? { ...second, apiKey: first } : { ...first, ...second };
@@ -195,12 +198,18 @@ class StiggAgentSDK {
195
198
  *
196
199
  * A refusal is remembered like a success: an unknown plan or a key that cannot write will be
197
200
  * refused the same way next time, and retrying it would spend a rate-limited call on every call.
198
- * Only an unreachable Stigg is forgotten, so the customer's next call tries again.
201
+ * A passing failure - unreachable, rate limited - is remembered for
202
+ * {@link PROVISION_RETRY_AFTER_MS}, then forgotten, so the customer is asked again: soon, but not
203
+ * on every call while Stigg is the one asking for less.
199
204
  *
200
205
  * Capped at {@link KNOWN_CUSTOMERS_CAP}, oldest first. Forgetting a customer costs one more
201
206
  * provisioning call, which Stigg answers as a duplicate - so the cap bounds memory, not safety.
202
207
  */
203
208
  customers = new Map();
209
+ /** When a customer whose provisioning failed in passing may be asked again. */
210
+ provisionRetryAt = new Map();
211
+ /** Not readonly, so a test can shorten the wait rather than sleep through it. */
212
+ provisionRetryAfterMs = PROVISION_RETRY_AFTER_MS;
204
213
  started;
205
214
  /**
206
215
  * When the attempt currently in `started` rejected, or undefined while it is pending or has
@@ -398,6 +407,47 @@ class StiggAgentSDK {
398
407
  void session.warmUp(context);
399
408
  return governed;
400
409
  }
410
+ /**
411
+ * An agent graph's governance middleware - the orchestration counterpart to `wrap`.
412
+ *
413
+ * There is no single client to gate in a graph: it is several agents, each spending against its
414
+ * own entitlement. So the graph is configured once - the shared context, and each agent's own
415
+ * under `agents` - and every step it takes is checked and billed as the agent that took it. A
416
+ * refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
417
+ * run is the graph's to decide.
418
+ *
419
+ * What comes back is framework-neutral: the start and end of each model call and tool. A
420
+ * framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
421
+ * `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
422
+ */
423
+ middleware(options = {}) {
424
+ // Started, not awaited, and caught - for the reasons `wrap` gives.
425
+ void this.ensureStarted().catch((err) => this.reportStartupFailure(err));
426
+ const shared = (0, context_1.resolveContext)(options.stiggContext ?? {}, (0, node_crypto_1.randomUUID)(), {
427
+ requireCustomer: false,
428
+ defaults: this.contextDefaults,
429
+ });
430
+ const { shared: sharedAgent, agents } = (0, agent_middleware_1.resolveAgents)(shared, options);
431
+ // Spans for the observability tool already tracing the graph - out of the box, for orchestrators only.
432
+ const session = this.newSession(options.hooks, { tracing: true });
433
+ const middleware = (0, agent_middleware_1.createAgentMiddleware)(session, {
434
+ shared: sharedAgent,
435
+ agents,
436
+ onUnlisted: (agent) => this.config.logger.warn(`[stigg] agent "${agent}" is not listed in the middleware's \`agents\`, so its calls are ` +
437
+ 'governed by the shared `stiggContext` alone - at the customer level unless that context ' +
438
+ 'names an agent. List it to govern it as itself.'),
439
+ onUncheckedTool: (tool) => this.config.logger.warn(`[stigg] tool "${tool}" was metered but not checked: it started with no name, so nothing ` +
440
+ "could match it until its result named it. Add `'*'` to `tools` to check tools like it."),
441
+ onUnnamedTool: (kind) => this.config.logger.warn(`[stigg] a tool${kind ? ` (${kind})` : ''} ran with no name, outside a model's tool call, so it ` +
442
+ "could not be checked or priced. If it is a paid tool, add `'*'` to `tools` to govern tools like it."),
443
+ missingCustomer: options.missingCustomer ??
444
+ 'Name one per run, or give the middleware a default in `stiggContext: { customerId }`.',
445
+ });
446
+ this.attachGovernance(middleware, session, shared);
447
+ if (shared.customerId)
448
+ void session.warmUp(shared);
449
+ return middleware;
450
+ }
401
451
  /**
402
452
  * Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
403
453
  * already has: silently replacing one of its own properties would be a worse surprise.
@@ -457,8 +507,9 @@ class StiggAgentSDK {
457
507
  get contextDefaults() {
458
508
  return { checkErrorAction: this.config.checkErrorAction, ingestMode: this.config.ingest };
459
509
  }
460
- newSession(hooks) {
510
+ newSession(hooks, { tracing = false } = {}) {
461
511
  const session = new session_1.GovernanceSession({
512
+ tracing,
462
513
  engine: () => this.ensureStarted().then((state) => state.engine),
463
514
  provisionCustomer: (customerId, planId) => this.provisionCustomer(customerId, planId),
464
515
  logger: this.config.logger,
@@ -475,8 +526,12 @@ class StiggAgentSDK {
475
526
  */
476
527
  provisionCustomer(customerId, planId) {
477
528
  const known = this.customers.get(customerId);
478
- if (known)
529
+ const retryAt = this.provisionRetryAt.get(customerId);
530
+ // A passing failure answers every call until its wait is over - asked again on each one, a
531
+ // rate-limited Stigg would only be asked harder.
532
+ if (known && (retryAt === undefined || Date.now() < retryAt))
479
533
  return known;
534
+ this.provisionRetryAt.delete(customerId);
480
535
  const pending = this.ensureStarted().then(async ({ engine }) => {
481
536
  const outcome = await engine.provisionCustomer?.(customerId, planId);
482
537
  if (outcome?.status === 'created')
@@ -491,13 +546,16 @@ class StiggAgentSDK {
491
546
  });
492
547
  this.customers.set(customerId, pending);
493
548
  if (this.customers.size > KNOWN_CUSTOMERS_CAP) {
494
- this.customers.delete(this.customers.keys().next().value);
549
+ const oldest = this.customers.keys().next().value;
550
+ this.customers.delete(oldest);
551
+ this.provisionRetryAt.delete(oldest);
495
552
  }
496
553
  // Only its own entry: one evicted by the cap may have been replaced by a newer attempt for the
497
- // same customer by the time it fails, and deleting that one would re-ask a settled customer.
554
+ // same customer by the time it fails, and timing that one out would re-ask a settled customer.
498
555
  pending.catch(() => {
499
- if (this.customers.get(customerId) === pending)
500
- this.customers.delete(customerId);
556
+ if (this.customers.get(customerId) === pending) {
557
+ this.provisionRetryAt.set(customerId, Date.now() + this.provisionRetryAfterMs);
558
+ }
501
559
  });
502
560
  return pending;
503
561
  }
package/dist/types.d.ts CHANGED
@@ -13,6 +13,7 @@ import type { StiggContext } from './core/context';
13
13
  import type { SessionSummary, SessionHooks } from './core/session';
14
14
  import type { GovernanceHeaders } from './headers';
15
15
  import type { ClientRecipe } from './bindings/client/core/types';
16
+ import type { AgentMiddleware } from './bindings/orchestrator/core/agent-middleware';
16
17
  import type { ObservedVendor } from './bindings/shared/token-usage';
17
18
  import type { EventIngest } from './engine/engine';
18
19
  /**
@@ -347,6 +348,102 @@ export type ClientOptions = {
347
348
  export type ClientContext = Omit<StiggContext, 'idempotencyKey'> & {
348
349
  idempotencyKey?: never;
349
350
  };
351
+ /**
352
+ * The governance of one agent graph: `stigg.middleware(options)`. Framework-neutral - a framework
353
+ * binding drives it (for LangChain and LangGraph, `StiggLangChainCallbackHandler`, which takes the
354
+ * same options), and so can a customer's own harness.
355
+ *
356
+ * A graph is configured here rather than wired call by call: the shared settings once, and each
357
+ * agent that should be governed as itself under `agents`, by the name its framework knows it by.
358
+ * Every step is checked and billed as the agent that took it, so one agent running dry stops that
359
+ * agent and nothing else.
360
+ *
361
+ * The customer is the one thing usually left out, because that changes per request: it rides each
362
+ * run instead, wherever the binding says (on LangChain, `metadata.stiggCustomerId`), and wins over a
363
+ * default here. Set it server-side, from the authenticated user, never from the request.
364
+ */
365
+ export type MiddlewareOptions = {
366
+ /**
367
+ * The context every agent starts from - what is checked, how usage prices, how it is reported.
368
+ * An agent's own `stiggContext` is laid over it, so this is where the parts they share go once.
369
+ * No `idempotencyKey`, for the reason a client's default has none: every step would share it.
370
+ */
371
+ stiggContext?: ClientContext;
372
+ /**
373
+ * The tools every agent may call that are governed, by tool name - `'*'` for every tool not
374
+ * named. An agent's own `tools` are laid over these. A tool not covered runs ungoverned.
375
+ */
376
+ tools?: MiddlewareTools;
377
+ /**
378
+ * Everything the graph reports back while it works. Each call's `HookContext.agent` says which
379
+ * agent took the step, so one hook can tell them apart.
380
+ */
381
+ hooks?: SessionHooks;
382
+ /**
383
+ * The agents governed as themselves, by the name the framework stamps on their steps - on
384
+ * LangChain, the `name` given to `createAgent`, or the node's name in a hand-built LangGraph graph.
385
+ *
386
+ * An agent left out is not left ungoverned: its steps run under the shared `stiggContext` alone -
387
+ * at the customer level, unless that context names an agent - and the SDK says once that it was
388
+ * not listed. Leave `agents` out entirely and every step is governed that way, which is a
389
+ * one-agent graph or a customer-level one.
390
+ */
391
+ agents?: Record<string, MiddlewareAgentOptions>;
392
+ /**
393
+ * Where this framework's caller puts the customer, for the refusal a step with none raises. Set
394
+ * by a framework binding; a harness of your own can name its own convention here.
395
+ */
396
+ missingCustomer?: string;
397
+ };
398
+ /** One agent's part of a graph's middleware, laid over the shared settings. */
399
+ export type MiddlewareAgentOptions = {
400
+ /** This agent's context over the shared one - typically its `agentId`, and what it gates on. */
401
+ stiggContext?: ClientContext;
402
+ /** This agent's tools over the shared ones - its own paid tools, or `false` to exempt one. */
403
+ tools?: MiddlewareTools;
404
+ };
405
+ /**
406
+ * The governed tools of a graph, by tool name, with `'*'` standing for every tool not named.
407
+ *
408
+ * `false` exempts a tool, `'*'` included. It is what a sub-agent held as a tool needs under `'*'`:
409
+ * that sub-agent's own model calls are already checked and billed, so billing the tool that runs it
410
+ * too would charge the same work twice.
411
+ */
412
+ export type MiddlewareTools = Record<string, Entitlement | ToolGovernance | false>;
413
+ /**
414
+ * How one tool is governed. An entitlement alone is shorthand for `{ entitlement }`.
415
+ *
416
+ * - `entitlement` - checked before the tool runs, and refused like a model call.
417
+ * - `usage` - what a call that succeeded reports, from the tool's own input and output, as a
418
+ * client's `usageMapper` does for a model call. Left out, a call reports one unit against
419
+ * `entitlement`. Given without `entitlement`, the tool is metered and never refused.
420
+ */
421
+ export type ToolGovernance = {
422
+ entitlement: Entitlement;
423
+ usage?: ToolUsageMapper;
424
+ } | {
425
+ entitlement?: undefined;
426
+ usage: ToolUsageMapper;
427
+ };
428
+ /**
429
+ * Turns one tool call into the metrics to report. Never throws into the run: a mapper that throws
430
+ * is a metering failure, reported through `onError` like any other.
431
+ */
432
+ export type ToolUsageMapper = (call: ToolCallMeasurement) => Metrics;
433
+ /** One tool call that succeeded, as a `ToolUsageMapper` sees it. */
434
+ export type ToolCallMeasurement = {
435
+ /** The tool's name. */
436
+ name: string;
437
+ /** What the tool was called with - its arguments, parsed, when the framework passed them as JSON. */
438
+ input: unknown;
439
+ /** What the tool returned - parsed, when the framework passed it on as JSON. */
440
+ output: unknown;
441
+ };
442
+ /** A graph's middleware, carrying the same governance handle a wrapped client does. */
443
+ export type GovernedAgentMiddleware = AgentMiddleware & {
444
+ /** What this graph did, and what is still in flight. */
445
+ readonly stigg: ClientGovernance;
446
+ };
350
447
  /**
351
448
  * `register` - giving an agent an identity, a budget, and a place in the tree.
352
449
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigg/agent-sdk-js",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Stigg agent SDK (JS) - run-lifecycle governance for agentic stacks, with examples per layer",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -17,6 +17,10 @@
17
17
  "types": "./dist/internal.d.ts",
18
18
  "default": "./dist/internal.js"
19
19
  },
20
+ "./langchain": {
21
+ "types": "./dist/integrations/langchain.d.ts",
22
+ "default": "./dist/integrations/langchain.js"
23
+ },
20
24
  "./package.json": "./package.json"
21
25
  }
22
26
  },
@@ -44,6 +48,8 @@
44
48
  "example-client-databricks": "tsx examples/client/databricks.ts",
45
49
  "example-client-typesafe": "tsx examples/client/typesafe.ts",
46
50
  "example-two-agents": "tsx examples/client/two-agents.ts",
51
+ "example-orchestrator-langchain": "tsx examples/orchestrator/langchain.ts",
52
+ "example-orchestrator-langgraph": "tsx examples/orchestrator/langgraph.ts",
47
53
  "example-gateway": "tsx examples/gateway/openrouter.ts",
48
54
  "example-gateway-openrouter": "tsx examples/gateway/openrouter.ts",
49
55
  "example-gateway-litellm": "tsx examples/gateway/litellm.ts",
@@ -69,17 +75,43 @@
69
75
  "@stigg/typescript": "0.1.0-beta.55",
70
76
  "zod": "^4.6.1"
71
77
  },
78
+ "peerDependencies": {
79
+ "@langchain/core": ">=1.0.0 <2",
80
+ "@opentelemetry/api": "^1.9.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "@langchain/core": {
84
+ "optional": true
85
+ },
86
+ "@opentelemetry/api": {
87
+ "optional": true
88
+ }
89
+ },
72
90
  "devDependencies": {
73
91
  "@anthropic-ai/sdk": "^0.124.0",
74
92
  "@aws-sdk/client-bedrock-runtime": "^3.1150.0",
75
93
  "@databricks/sdk-modelservingquery": "0.25.0",
76
94
  "@google/genai": "^2.28.0",
95
+ "@langchain/anthropic": "^1.5.12",
96
+ "@langchain/core": "^1.2.18",
97
+ "@langchain/langgraph": "^1.5.1",
98
+ "@langchain/tavily": "^1.2.0",
99
+ "@langfuse/langchain": "^6.0.0",
100
+ "@langfuse/otel": "^6.0.0",
101
+ "@langfuse/tracing": "^6.0.0",
77
102
  "@mistralai/mistralai": "^2.7.0",
78
103
  "@openrouter/sdk": "^1.4.25",
104
+ "@opentelemetry/api": "^1.9.0",
105
+ "@opentelemetry/core": "^2.12.0",
106
+ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
107
+ "@opentelemetry/otlp-exporter-base": "^0.220.0",
108
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
109
+ "@opentelemetry/sdk-trace-node": "^2.0.1",
79
110
  "@typesafe-ai/sdk": "^0.6.0",
80
111
  "cloudflare": "^7.3.0",
81
112
  "dotenv": "^17.4.2",
82
113
  "groq-sdk": "^1.6.0",
114
+ "langchain": "^1.5.18",
83
115
  "ollama": "^0.6.4",
84
116
  "openai": "^7.13.0",
85
117
  "tsx": "^4.23.13"
@@ -93,6 +125,10 @@
93
125
  "types": "./dist/internal.d.ts",
94
126
  "default": "./dist/internal.js"
95
127
  },
128
+ "./langchain": {
129
+ "types": "./dist/integrations/langchain.d.ts",
130
+ "default": "./dist/integrations/langchain.js"
131
+ },
96
132
  "./package.json": "./package.json"
97
133
  }
98
134
  }