@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.
- package/README.md +2 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +2 -2
- package/dist/bindings/orchestrator/core/agent-middleware.d.ts +88 -0
- package/dist/bindings/orchestrator/core/agent-middleware.js +299 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.d.ts +72 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.js +277 -0
- package/dist/core/context.d.ts +15 -4
- package/dist/core/context.js +7 -5
- package/dist/core/session.d.ts +17 -3
- package/dist/core/session.js +30 -14
- package/dist/core/telemetry.d.ts +8 -0
- package/dist/core/telemetry.js +97 -0
- package/dist/engine/node-sdk-engine.d.ts +4 -3
- package/dist/engine/node-sdk-engine.js +19 -4
- 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 +26 -6
- package/dist/stigg-agent-sdk.js +67 -9
- package/dist/types.d.ts +97 -0
- package/package.json +37 -1
|
@@ -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
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
})[];
|
|
@@ -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
|
-
*
|
|
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.
|
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
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
}
|