@stigg/agent-sdk-js 0.3.2 → 0.5.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.
- package/README.md +25 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +2 -2
- package/dist/bindings/orchestrator/core/agent-middleware.d.ts +81 -0
- package/dist/bindings/orchestrator/core/agent-middleware.js +231 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.d.ts +50 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.js +151 -0
- package/dist/core/context.d.ts +27 -4
- package/dist/core/context.js +26 -5
- package/dist/core/session.d.ts +27 -0
- package/dist/core/session.js +74 -21
- package/dist/core/telemetry.d.ts +8 -0
- package/dist/core/telemetry.js +97 -0
- package/dist/engine/composite-engine.d.ts +7 -1
- package/dist/engine/composite-engine.js +9 -0
- package/dist/engine/create-engine.js +9 -2
- package/dist/engine/engine.d.ts +28 -0
- package/dist/engine/node-sdk-engine.d.ts +16 -1
- package/dist/engine/node-sdk-engine.js +38 -0
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +28 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/integrations/langchain.d.ts +14 -0
- package/dist/integrations/langchain.js +21 -0
- package/dist/stigg-agent-sdk.d.ts +43 -5
- package/dist/stigg-agent-sdk.js +106 -3
- package/dist/types.d.ts +97 -0
- package/package.json +37 -1
package/dist/errors.js
CHANGED
|
@@ -78,15 +78,37 @@ class GovernanceNotEnabledError extends Error {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
exports.GovernanceNotEnabledError = GovernanceNotEnabledError;
|
|
81
|
-
/**
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
10
|
-
*
|
|
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"
|
|
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"
|
|
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
|
})[];
|
|
@@ -159,6 +159,18 @@ export declare class StiggAgentSDK {
|
|
|
159
159
|
* rather than once each. Only the memoized path is shared - see `register`.
|
|
160
160
|
*/
|
|
161
161
|
private readonly registering;
|
|
162
|
+
/**
|
|
163
|
+
* Customers this process has provisioned, by id - settled or still in flight. Shared by every
|
|
164
|
+
* client, so a customer seen by ten wrapped clients is provisioned once.
|
|
165
|
+
*
|
|
166
|
+
* A refusal is remembered like a success: an unknown plan or a key that cannot write will be
|
|
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.
|
|
169
|
+
*
|
|
170
|
+
* Capped at {@link KNOWN_CUSTOMERS_CAP}, oldest first. Forgetting a customer costs one more
|
|
171
|
+
* provisioning call, which Stigg answers as a duplicate - so the cap bounds memory, not safety.
|
|
172
|
+
*/
|
|
173
|
+
private readonly customers;
|
|
162
174
|
private started?;
|
|
163
175
|
/**
|
|
164
176
|
* When the attempt currently in `started` rejected, or undefined while it is pending or has
|
|
@@ -230,6 +242,20 @@ export declare class StiggAgentSDK {
|
|
|
230
242
|
* see {@link GovernanceSession.warmUp} for which cache that is and when there is one at all.
|
|
231
243
|
*/
|
|
232
244
|
wrap<T extends object, O extends ClientOptions>(client: T, options?: O): WrappedClient<T, O['stiggContext'], PathsOf<O['recipe'], T>>;
|
|
245
|
+
/**
|
|
246
|
+
* An agent graph's governance middleware - the orchestration counterpart to `wrap`.
|
|
247
|
+
*
|
|
248
|
+
* There is no single client to gate in a graph: it is several agents, each spending against its
|
|
249
|
+
* own entitlement. So the graph is configured once - the shared context, and each agent's own
|
|
250
|
+
* under `agents` - and every step it takes is checked and billed as the agent that took it. A
|
|
251
|
+
* refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
|
|
252
|
+
* run is the graph's to decide.
|
|
253
|
+
*
|
|
254
|
+
* What comes back is framework-neutral: the start and end of each model call and tool. A
|
|
255
|
+
* framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
|
|
256
|
+
* `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
|
|
257
|
+
*/
|
|
258
|
+
middleware(options?: MiddlewareOptions): GovernedAgentMiddleware;
|
|
233
259
|
/**
|
|
234
260
|
* Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
|
|
235
261
|
* already has: silently replacing one of its own properties would be a worse surprise.
|
|
@@ -254,6 +280,18 @@ export declare class StiggAgentSDK {
|
|
|
254
280
|
*/
|
|
255
281
|
private get contextDefaults();
|
|
256
282
|
private newSession;
|
|
283
|
+
/**
|
|
284
|
+
* Creates the customer on `planId` the first time this process sees them, if Stigg does not
|
|
285
|
+
* already know them. Keyed by customer alone: once one exists, which plan a later call names
|
|
286
|
+
* is irrelevant - an existing customer is never moved.
|
|
287
|
+
*/
|
|
288
|
+
private provisionCustomer;
|
|
289
|
+
/**
|
|
290
|
+
* The server picks the first subscription, and the plan asked for is only a request: a
|
|
291
|
+
* predefined free or trial plan can win, or no subscription may be created at all. Logging
|
|
292
|
+
* success regardless would hide a customer who landed somewhere nobody chose.
|
|
293
|
+
*/
|
|
294
|
+
private describeProvisioned;
|
|
257
295
|
/** The session the no-client paths share, built on first use so an unused SDK builds none. */
|
|
258
296
|
private standalone;
|
|
259
297
|
/**
|
package/dist/stigg-agent-sdk.js
CHANGED
|
@@ -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.
|
|
47
|
-
*
|
|
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
|
|
@@ -120,6 +121,11 @@ const DEFAULT_WALLET = { currencyId: 'credits' };
|
|
|
120
121
|
* for why remembering it at all is the point.
|
|
121
122
|
*/
|
|
122
123
|
const RETRY_START_AFTER_MS = 2_000;
|
|
124
|
+
/**
|
|
125
|
+
* How many customers one process remembers having provisioned. A server serving an open-ended set
|
|
126
|
+
* of tenants would otherwise grow this for as long as it runs.
|
|
127
|
+
*/
|
|
128
|
+
const KNOWN_CUSTOMERS_CAP = 10_000;
|
|
123
129
|
/** The key shorthand and the options object, folded into the one object the config takes. */
|
|
124
130
|
function toOptions(first, second) {
|
|
125
131
|
return typeof first === 'string' ? { ...second, apiKey: first } : { ...first, ...second };
|
|
@@ -184,6 +190,18 @@ class StiggAgentSDK {
|
|
|
184
190
|
* rather than once each. Only the memoized path is shared - see `register`.
|
|
185
191
|
*/
|
|
186
192
|
registering = new Map();
|
|
193
|
+
/**
|
|
194
|
+
* Customers this process has provisioned, by id - settled or still in flight. Shared by every
|
|
195
|
+
* client, so a customer seen by ten wrapped clients is provisioned once.
|
|
196
|
+
*
|
|
197
|
+
* A refusal is remembered like a success: an unknown plan or a key that cannot write will be
|
|
198
|
+
* refused the same way next time, and retrying it would spend a rate-limited call on every call.
|
|
199
|
+
* Only an unreachable Stigg is forgotten, so the customer's next call tries again.
|
|
200
|
+
*
|
|
201
|
+
* Capped at {@link KNOWN_CUSTOMERS_CAP}, oldest first. Forgetting a customer costs one more
|
|
202
|
+
* provisioning call, which Stigg answers as a duplicate - so the cap bounds memory, not safety.
|
|
203
|
+
*/
|
|
204
|
+
customers = new Map();
|
|
187
205
|
started;
|
|
188
206
|
/**
|
|
189
207
|
* When the attempt currently in `started` rejected, or undefined while it is pending or has
|
|
@@ -381,6 +399,43 @@ class StiggAgentSDK {
|
|
|
381
399
|
void session.warmUp(context);
|
|
382
400
|
return governed;
|
|
383
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* An agent graph's governance middleware - the orchestration counterpart to `wrap`.
|
|
404
|
+
*
|
|
405
|
+
* There is no single client to gate in a graph: it is several agents, each spending against its
|
|
406
|
+
* own entitlement. So the graph is configured once - the shared context, and each agent's own
|
|
407
|
+
* under `agents` - and every step it takes is checked and billed as the agent that took it. A
|
|
408
|
+
* refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
|
|
409
|
+
* run is the graph's to decide.
|
|
410
|
+
*
|
|
411
|
+
* What comes back is framework-neutral: the start and end of each model call and tool. A
|
|
412
|
+
* framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
|
|
413
|
+
* `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
|
|
414
|
+
*/
|
|
415
|
+
middleware(options = {}) {
|
|
416
|
+
// Started, not awaited, and caught - for the reasons `wrap` gives.
|
|
417
|
+
void this.ensureStarted().catch((err) => this.reportStartupFailure(err));
|
|
418
|
+
const shared = (0, context_1.resolveContext)(options.stiggContext ?? {}, (0, node_crypto_1.randomUUID)(), {
|
|
419
|
+
requireCustomer: false,
|
|
420
|
+
defaults: this.contextDefaults,
|
|
421
|
+
});
|
|
422
|
+
const { shared: sharedAgent, agents } = (0, agent_middleware_1.resolveAgents)(shared, options);
|
|
423
|
+
// Spans for the observability tool already tracing the graph - out of the box, for orchestrators only.
|
|
424
|
+
const session = this.newSession(options.hooks, { tracing: true });
|
|
425
|
+
const middleware = (0, agent_middleware_1.createAgentMiddleware)(session, {
|
|
426
|
+
shared: sharedAgent,
|
|
427
|
+
agents,
|
|
428
|
+
onUnlisted: (agent) => this.config.logger.warn(`[stigg] agent "${agent}" is not listed in the middleware's \`agents\`, so its calls are ` +
|
|
429
|
+
'governed by the shared `stiggContext` alone - at the customer level unless that context ' +
|
|
430
|
+
'names an agent. List it to govern it as itself.'),
|
|
431
|
+
missingCustomer: options.missingCustomer ??
|
|
432
|
+
'Name one per run, or give the middleware a default in `stiggContext: { customerId }`.',
|
|
433
|
+
});
|
|
434
|
+
this.attachGovernance(middleware, session, shared);
|
|
435
|
+
if (shared.customerId)
|
|
436
|
+
void session.warmUp(shared);
|
|
437
|
+
return middleware;
|
|
438
|
+
}
|
|
384
439
|
/**
|
|
385
440
|
* Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
|
|
386
441
|
* already has: silently replacing one of its own properties would be a worse surprise.
|
|
@@ -440,9 +495,11 @@ class StiggAgentSDK {
|
|
|
440
495
|
get contextDefaults() {
|
|
441
496
|
return { checkErrorAction: this.config.checkErrorAction, ingestMode: this.config.ingest };
|
|
442
497
|
}
|
|
443
|
-
newSession(hooks) {
|
|
498
|
+
newSession(hooks, { tracing = false } = {}) {
|
|
444
499
|
const session = new session_1.GovernanceSession({
|
|
500
|
+
tracing,
|
|
445
501
|
engine: () => this.ensureStarted().then((state) => state.engine),
|
|
502
|
+
provisionCustomer: (customerId, planId) => this.provisionCustomer(customerId, planId),
|
|
446
503
|
logger: this.config.logger,
|
|
447
504
|
onError: this.config.onError,
|
|
448
505
|
hooks: hooks ?? {},
|
|
@@ -450,6 +507,52 @@ class StiggAgentSDK {
|
|
|
450
507
|
this.sessions.add(session);
|
|
451
508
|
return session;
|
|
452
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* Creates the customer on `planId` the first time this process sees them, if Stigg does not
|
|
512
|
+
* already know them. Keyed by customer alone: once one exists, which plan a later call names
|
|
513
|
+
* is irrelevant - an existing customer is never moved.
|
|
514
|
+
*/
|
|
515
|
+
provisionCustomer(customerId, planId) {
|
|
516
|
+
const known = this.customers.get(customerId);
|
|
517
|
+
if (known)
|
|
518
|
+
return known;
|
|
519
|
+
const pending = this.ensureStarted().then(async ({ engine }) => {
|
|
520
|
+
const outcome = await engine.provisionCustomer?.(customerId, planId);
|
|
521
|
+
if (outcome?.status === 'created')
|
|
522
|
+
this.describeProvisioned(customerId, planId, outcome);
|
|
523
|
+
// Said once, here, rather than by every call that follows: the memo keeps the refusal, so
|
|
524
|
+
// later calls go straight to the check, which answers for a customer that does not exist.
|
|
525
|
+
if (outcome?.status === 'rejected') {
|
|
526
|
+
const error = new errors_1.EngineError('provisionCustomer', `Stigg refused to provision customer ${customerId} on plan ${planId}, and will not be ` +
|
|
527
|
+
`asked again by this process: ${outcome.reason}`);
|
|
528
|
+
(0, hooks_1.safely)(() => this.config.onError(error, { operation: 'provisionCustomer', customerId }), (reportErr) => this.config.logger.warn(`[stigg] onError threw while reporting ${error.message}: ${String(reportErr)}`));
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
this.customers.set(customerId, pending);
|
|
532
|
+
if (this.customers.size > KNOWN_CUSTOMERS_CAP) {
|
|
533
|
+
this.customers.delete(this.customers.keys().next().value);
|
|
534
|
+
}
|
|
535
|
+
// Only its own entry: one evicted by the cap may have been replaced by a newer attempt for the
|
|
536
|
+
// same customer by the time it fails, and deleting that one would re-ask a settled customer.
|
|
537
|
+
pending.catch(() => {
|
|
538
|
+
if (this.customers.get(customerId) === pending)
|
|
539
|
+
this.customers.delete(customerId);
|
|
540
|
+
});
|
|
541
|
+
return pending;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The server picks the first subscription, and the plan asked for is only a request: a
|
|
545
|
+
* predefined free or trial plan can win, or no subscription may be created at all. Logging
|
|
546
|
+
* success regardless would hide a customer who landed somewhere nobody chose.
|
|
547
|
+
*/
|
|
548
|
+
describeProvisioned(customerId, planId, outcome) {
|
|
549
|
+
if (outcome.decision === 'REQUESTED_PLAN' && outcome.planId === planId) {
|
|
550
|
+
this.config.logger.info(`[stigg] customer ${customerId} provisioned on plan ${planId}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.config.logger.warn(`[stigg] customer ${customerId} was created, but not on plan ${planId}: Stigg chose ` +
|
|
554
|
+
`${outcome.decision}, and the customer is on ${outcome.planId ?? 'no plan'}.`);
|
|
555
|
+
}
|
|
453
556
|
/** The session the no-client paths share, built on first use so an unused SDK builds none. */
|
|
454
557
|
standalone() {
|
|
455
558
|
this.loose ??= this.newSession();
|
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.
|
|
3
|
+
"version": "0.5.0",
|
|
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
|
}
|