@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 CHANGED
@@ -121,6 +121,8 @@ rather than handing back a client that quietly meters nothing.
121
121
  | Give an agent its own budget | `stigg.register({ customerId, agentId, budget })` |
122
122
  | Start new customers on a plan | `stiggContext: { defaultPlanId }`, beside `customerId` |
123
123
  | Meter a gateway you operate | `client.stigg.headers()` out, `stigg.ingestFromHeaders(...)` back |
124
+ | Govern a LangChain / LangGraph agent graph ([guide](docs/langchain.md)) | `new StiggLangChainCallbackHandler({ stigg, agents, tools })` from `@stigg/agent-sdk-js/langchain` |
125
+ | Govern a graph from your own harness | `stigg.middleware({ stiggContext, agents })` |
124
126
 
125
127
  One wrapped client serves every customer and every agent: the context is an argument, given once at
126
128
  `wrap` as a default and overridable inside any single call.
@@ -7,11 +7,11 @@ export declare const mistralRecipe: {
7
7
  totalTokensMetric: "total_tokens";
8
8
  matches: (client: unknown) => boolean;
9
9
  methods: ({
10
- path: ("stream" | "chat" | "complete" | "parse" | "parseStream" | "fim" | "agents")[];
10
+ path: ("agents" | "stream" | "chat" | "complete" | "parse" | "parseStream" | "fim")[];
11
11
  attachStream: (stream: unknown, ingest: (measurement: Measurement) => void) => void;
12
12
  extractUsage?: undefined;
13
13
  } | {
14
- path: ("stream" | "chat" | "complete" | "parse" | "parseStream" | "fim" | "agents")[];
14
+ path: ("agents" | "stream" | "chat" | "complete" | "parse" | "parseStream" | "fim")[];
15
15
  extractUsage: (result: MistralCompletionResponse | undefined) => Measurement | undefined;
16
16
  attachStream?: undefined;
17
17
  })[];
@@ -0,0 +1,88 @@
1
+ import { type ResolvedContext } from '../../../core/context';
2
+ import type { GovernanceSession } from '../../../core/session';
3
+ import { type Entitlement, type Measurement, type MiddlewareOptions, type MiddlewareTools, type ToolUsageMapper } from '../../../types';
4
+ /** Who a step is for, as the framework stated it on that step. */
5
+ export type StepAttribution = {
6
+ /** The agent that took the step, by the framework's own name for it. */
7
+ agent?: string;
8
+ /** The customer the run is for, when the run named one. */
9
+ customerId?: string;
10
+ };
11
+ /** A tool call the model issued in its reply: the id the tool step will refer back to, and what it named. */
12
+ export type IssuedToolCall = {
13
+ id: string;
14
+ name: string;
15
+ };
16
+ /** A tool about to run, as the framework announced it. */
17
+ export type ToolStep = {
18
+ /** The tool's name, when the framework passes one - some do not, for some tools. */
19
+ name?: string;
20
+ /** The id of the model's tool call this step runs, when the framework passes it. */
21
+ callId?: string;
22
+ /**
23
+ * What the tool is when it has no name and no model called it - its class, for a packaged tool
24
+ * a node invokes directly. Only `'*'` can match it, so the catch-all still covers the call.
25
+ */
26
+ kind?: string;
27
+ /** What the tool was called with, for a `usage` mapper. */
28
+ input?: unknown;
29
+ };
30
+ /** What a framework binding drives: the start and end of every step a graph takes. */
31
+ export type AgentMiddleware = {
32
+ /** Before a model call: checked, and raises `AccessDeniedError` when refused. */
33
+ startModel(runId: string, attribution: StepAttribution, eventName: string): Promise<void>;
34
+ /**
35
+ * After a model call: its measurement is ingested, or the missing one reported. Never throws.
36
+ * `issued` is the tool calls the reply asked for, so a tool step that arrives with only its call
37
+ * id is named, and billed to the agent that called it.
38
+ */
39
+ endModel(runId: string, measurement: Measurement | undefined, issued?: readonly IssuedToolCall[]): Promise<void>;
40
+ /** Before a tool runs: checked when the tool is a paid one, raising when refused. */
41
+ startTool(runId: string, tool: ToolStep, attribution: StepAttribution): Promise<void>;
42
+ /** After a tool succeeded: what its `usage` makes of the call, or one unit. Never throws. */
43
+ endTool(runId: string, output?: unknown, name?: string): Promise<void>;
44
+ /** A step that failed: forgotten, and billed nothing - it did not deliver what it charges for. */
45
+ abandon(runId: string): void;
46
+ };
47
+ /** How one tool is governed, its shorthand expanded. */
48
+ export type ResolvedTool = {
49
+ entitlement?: Entitlement;
50
+ usage?: ToolUsageMapper;
51
+ };
52
+ /** A graph's tools by name, `'*'` for the rest - `false` for one left ungoverned. */
53
+ export type ResolvedTools = Readonly<Record<string, ResolvedTool | false>>;
54
+ /** One agent's part of the graph, already resolved over the shared context. */
55
+ export type ResolvedAgent = {
56
+ context: ResolvedContext;
57
+ tools: ResolvedTools;
58
+ };
59
+ /** The key that stands for every tool a configuration does not name. */
60
+ export declare const ALL_TOOLS = "*";
61
+ /** A configuration's tools, each validated and expanded once, when the graph is configured. */
62
+ export declare function resolveTools(tools: MiddlewareTools | undefined, where: string): ResolvedTools;
63
+ /**
64
+ * The graph's agents, each resolved once over the shared context and tools, so a step does no
65
+ * more than look its agent up. An agent's tools are laid over the shared ones, name by name.
66
+ */
67
+ export declare function resolveAgents(shared: ResolvedContext, { tools, agents }: Pick<MiddlewareOptions, 'tools' | 'agents'>): {
68
+ shared: ResolvedAgent;
69
+ agents: Map<string, ResolvedAgent>;
70
+ };
71
+ export type AgentMiddlewareConfig = {
72
+ /** The shared context and tools - what a step from an agent not listed resolves to. */
73
+ shared: ResolvedAgent;
74
+ /** Each listed agent's own, by name. */
75
+ agents: ReadonlyMap<string, ResolvedAgent>;
76
+ /** Told once per agent that was not listed, so the caller can say so. */
77
+ onUnlisted(agent: string): void;
78
+ /** Told once per tool whose usage was reported but not checked, because its name came only with its result. */
79
+ onUncheckedTool(tool: string): void;
80
+ /**
81
+ * Told once per kind of tool that ran with no name at all - called directly, so nothing named it
82
+ * and it could not be priced. Possibly a free tool, so a warning rather than a metering failure.
83
+ */
84
+ onUnnamedTool(kind: string | undefined): void;
85
+ /** Where the caller puts the customer - quoted when a step has none. */
86
+ missingCustomer: string;
87
+ };
88
+ export declare function createAgentMiddleware(session: GovernanceSession, config: AgentMiddlewareConfig): AgentMiddleware;
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_TOOLS = void 0;
4
+ exports.resolveTools = resolveTools;
5
+ exports.resolveAgents = resolveAgents;
6
+ exports.createAgentMiddleware = createAgentMiddleware;
7
+ /**
8
+ * The orchestration binding's core: an agent graph's middleware, with no framework in it.
9
+ *
10
+ * It splits the way the client binding does: `wrapClient` gates and meters a call and a recipe
11
+ * knows one vendor; here the middleware governs a graph's steps and a framework binding (the
12
+ * LangChain callback handler, or a harness of your own) knows how its framework announces them.
13
+ *
14
+ * Every step is checked and billed as the agent that took it, so one agent running dry refuses
15
+ * that agent's steps and nothing else. A refusal is the SDK's own `AccessDeniedError`, thrown from
16
+ * inside the step - the frameworks worth binding to offer hooks that observe a step, not ones that
17
+ * replace its result - and what it means for the run is the graph's to decide. Nothing else throws
18
+ * into the run: ingest never does, and a check that could not be made resolves through
19
+ * `checkErrorAction`.
20
+ *
21
+ * Steps are keyed by the run id the framework gives each one, because the end of a step usually
22
+ * carries nothing else: what the start resolved is what the end bills.
23
+ */
24
+ const errors_1 = require("../../../errors");
25
+ const context_1 = require("../../../core/context");
26
+ const hooks_1 = require("../../../core/hooks");
27
+ const types_1 = require("../../../types");
28
+ /** The key that stands for every tool a configuration does not name. */
29
+ exports.ALL_TOOLS = '*';
30
+ /** One tool's entry, validated and expanded. A mistake here is said at startup, not on the first call. */
31
+ function resolveTool(name, tool, where) {
32
+ if (tool === false)
33
+ return false;
34
+ const at = `${where}.${name === exports.ALL_TOOLS ? `'${exports.ALL_TOOLS}'` : name}`;
35
+ if (!tool || typeof tool !== 'object') {
36
+ throw new Error(`${at} must be an entitlement, { entitlement, usage }, or false.`);
37
+ }
38
+ const shorthand = 'currencyId' in tool || 'featureId' in tool;
39
+ if (shorthand && ('usage' in tool || 'entitlement' in tool)) {
40
+ throw new Error(`${at} mixes an entitlement's own fields with \`usage\` or \`entitlement\`, so one of them would be ` +
41
+ 'ignored. Write it as { entitlement: { currencyId }, usage }.');
42
+ }
43
+ const resolved = (shorthand ? { entitlement: tool } : tool);
44
+ if (resolved.usage !== undefined && typeof resolved.usage !== 'function') {
45
+ throw new Error(`${at}.usage must be a function from the tool call to the metrics it reports.`);
46
+ }
47
+ if (resolved.entitlement === undefined && !resolved.usage) {
48
+ throw new Error(`${at} names neither an entitlement to check nor a usage to report, so it would do nothing. ` +
49
+ 'Give it one of them, or false to leave the tool ungoverned.');
50
+ }
51
+ const invalid = resolved.entitlement && (0, types_1.entitlementError)(resolved.entitlement);
52
+ if (invalid)
53
+ throw invalid;
54
+ return resolved;
55
+ }
56
+ /** A configuration's tools, each validated and expanded once, when the graph is configured. */
57
+ function resolveTools(tools, where) {
58
+ return Object.fromEntries(Object.entries(tools ?? {}).map(([name, tool]) => [name, resolveTool(name, tool, where)]));
59
+ }
60
+ /**
61
+ * The graph's agents, each resolved once over the shared context and tools, so a step does no
62
+ * more than look its agent up. An agent's tools are laid over the shared ones, name by name.
63
+ */
64
+ function resolveAgents(shared, { tools, agents }) {
65
+ const sharedTools = resolveTools(tools, 'stigg.middleware tools');
66
+ const resolved = new Map();
67
+ for (const [name, agent] of Object.entries(agents ?? {})) {
68
+ // An agent's context is a default for every step it takes, so one key would dedupe them all
69
+ // into the first. The shared context is refused the same way, in `resolveContext`.
70
+ if (agent.stiggContext?.idempotencyKey !== undefined) {
71
+ throw new Error(`stigg.middleware's agents.${name}.stiggContext cannot set \`idempotencyKey\` - one key shared ` +
72
+ 'by every step that agent takes would bill the first and discard the rest. Each step is keyed by its own id.');
73
+ }
74
+ resolved.set(name, {
75
+ context: agent.stiggContext ? (0, context_1.overrideContext)(shared, agent.stiggContext) : shared,
76
+ tools: { ...sharedTools, ...resolveTools(agent.tools, `stigg.middleware agents.${name}.tools`) },
77
+ });
78
+ }
79
+ return { shared: { context: shared, tools: sharedTools }, agents: resolved };
80
+ }
81
+ /**
82
+ * The tool's own entry, else the catch-all - only the catch-all for a tool with no name, so a
83
+ * class name never matches an entry written for a tool's name. An exempted tool, or one not
84
+ * covered, is ungoverned. Own keys only: a tool named `toString` must not find `Object.prototype`'s.
85
+ */
86
+ function toolFor(tools, name) {
87
+ if (name !== undefined && Object.hasOwn(tools, name))
88
+ return tools[name] || undefined;
89
+ if (Object.hasOwn(tools, exports.ALL_TOOLS))
90
+ return tools[exports.ALL_TOOLS] || undefined;
91
+ return undefined;
92
+ }
93
+ /**
94
+ * How many issued calls are remembered, and how many steps can be in flight. An issued call is
95
+ * kept so a re-run finds its owner, and a step whose end never comes would otherwise stay for the
96
+ * life of the process, so past these the oldest go first.
97
+ */
98
+ const ISSUED_CALLS_LIMIT = 1_000;
99
+ const STEPS_LIMIT = 10_000;
100
+ /**
101
+ * What deduplicates a tool step's report: the call id the model issued, plus what it reports
102
+ * against - one tool call is one report, and two entitlements on one call must not collide.
103
+ */
104
+ function toolKey(callId, context, name) {
105
+ return callId ? `${callId}:${context.checkedId ?? name}` : undefined;
106
+ }
107
+ /** Adds to a map that drops its oldest entry past `limit`, and hands back what it dropped. */
108
+ function remember(map, key, value, limit) {
109
+ map.set(key, value);
110
+ if (map.size <= limit)
111
+ return undefined;
112
+ const [oldest, dropped] = map.entries().next().value;
113
+ map.delete(oldest);
114
+ return dropped;
115
+ }
116
+ function createAgentMiddleware(session, config) {
117
+ /** Steps that started and have not ended, by run id. */
118
+ const steps = new Map();
119
+ /** The tool calls models issued, by call id - kept for re-runs, until the cap drops them. */
120
+ const issuedCalls = new Map();
121
+ /** Tools that started without a name, by run id - until they end. */
122
+ const unnamedTools = new Map();
123
+ const warnedUnchecked = new Set();
124
+ const warnedUnnamed = new Set();
125
+ const warnedUnlisted = new Set();
126
+ /**
127
+ * The agent a step belongs to. One the configuration does not list - or a step attributed to no
128
+ * agent - resolves to the shared context, so nothing runs unbilled for having been left out.
129
+ */
130
+ const agentFor = (agent) => {
131
+ const own = agent === undefined ? undefined : config.agents.get(agent);
132
+ // Capped: the names come from the caller's own runs, so an unbounded set is theirs to grow.
133
+ if (!own &&
134
+ agent !== undefined &&
135
+ config.agents.size > 0 &&
136
+ !warnedUnlisted.has(agent) &&
137
+ warnedUnlisted.size < 100) {
138
+ warnedUnlisted.add(agent);
139
+ warn(() => config.onUnlisted(agent));
140
+ }
141
+ return own ?? config.shared;
142
+ };
143
+ /** The context for one step. The customer is required: an empty one would be billed to nobody. */
144
+ const contextFor = (base, { agent, customerId }) => {
145
+ const context = customerId ? (0, context_1.overrideContext)(base, { customerId }) : base;
146
+ if (!context.customerId)
147
+ throw (0, context_1.noCustomer)(config.missingCustomer);
148
+ return agent === undefined ? context : { ...context, agent };
149
+ };
150
+ /** The gate, raising rather than returning - the hooks it runs in have no return path. */
151
+ const check = async (context) => {
152
+ if (!(0, context_1.isEnforced)(context))
153
+ return;
154
+ const decision = await session.check(context);
155
+ if (!decision.hasAccess)
156
+ throw new errors_1.AccessDeniedError(decision);
157
+ };
158
+ /** A step in flight. One dropped to make room is never billed, so that is said, not silent. */
159
+ const rememberStep = (runId, step) => {
160
+ const dropped = remember(steps, runId, step, STEPS_LIMIT);
161
+ if (dropped)
162
+ reportDropped(dropped.context, 'a step');
163
+ };
164
+ /** An unnamed tool held until its result names it - and said, if it is dropped to make room. */
165
+ const rememberUnnamed = (runId, tool) => {
166
+ const dropped = remember(unnamedTools, runId, tool, STEPS_LIMIT);
167
+ if (dropped)
168
+ reportDropped(dropped.context, 'a tool with no name');
169
+ };
170
+ const reportDropped = (context, what) => session.failedToMeasure(context, new Error(`${what} was still in flight after ${STEPS_LIMIT} newer ones started, so it was dropped unreported`));
171
+ /** The application's warnings are its own code: one that throws must not fail a step. */
172
+ const warn = (say) => (0, hooks_1.safely)(say, () => undefined);
173
+ /** The tool calls a reply asked for, kept until each tool has run. */
174
+ const rememberIssuedCalls = (issued, agent) => {
175
+ for (const call of issued)
176
+ remember(issuedCalls, call.id, { name: call.name, agent }, ISSUED_CALLS_LIMIT);
177
+ };
178
+ /** A tool step's context: priced by the tool's own entitlement, when it has one, and labelled with the tool. */
179
+ const toolContext = (agent, tool, name, attribution) => {
180
+ const eventName = { name: `tool.${name}` };
181
+ const priced = (0, context_1.overrideContext)(agent.context, tool.entitlement ? { entitlement: tool.entitlement, eventName } : { eventName });
182
+ return { ...contextFor(priced, attribution), tool: name };
183
+ };
184
+ /** Reports what a tool step that ended used: its `usage`, or one unit. A bad `usage` is a metering failure. */
185
+ const ingestTool = async ({ context, idempotencyKey, tool }, output) => {
186
+ const options = { idempotencyKey };
187
+ if (!tool?.usage) {
188
+ await session.ingest(context, 1, options);
189
+ return;
190
+ }
191
+ try {
192
+ const metrics = tool.usage({ name: tool.name, input: tool.input, output });
193
+ if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) {
194
+ throw new Error(`the usage of tool ${tool.name} returned ${String(metrics)}, not metrics by feature id`);
195
+ }
196
+ await session.ingestMetrics(context, metrics, options);
197
+ }
198
+ catch (err) {
199
+ session.failedToMeasure(context, err);
200
+ }
201
+ };
202
+ /**
203
+ * The step an unnamed tool would have started, now that its result names it - or nothing to
204
+ * report, when it has no name or the name is not governed. It ran unchecked, which is said once
205
+ * per tool: only `'*'` can gate a tool whose name is not known when it starts.
206
+ */
207
+ const stepFromResult = (runId, name) => {
208
+ const unnamed = unnamedTools.get(runId);
209
+ if (!unnamed)
210
+ return undefined;
211
+ if (name === undefined) {
212
+ // Called directly - no tool call, no ToolMessage - so nothing ever named it. It may well be a
213
+ // free tool, so this is said once per kind of tool rather than raised as a metering failure.
214
+ const kind = unnamed.kind ?? '(unknown)';
215
+ if (!warnedUnnamed.has(kind) && warnedUnnamed.size < 100) {
216
+ warnedUnnamed.add(kind);
217
+ warn(() => config.onUnnamedTool(unnamed.kind));
218
+ }
219
+ return undefined;
220
+ }
221
+ const agent = agentFor(unnamed.owner.agent);
222
+ const tool = toolFor(agent.tools, name);
223
+ if (!tool)
224
+ return undefined;
225
+ if (tool.entitlement && !warnedUnchecked.has(name)) {
226
+ warnedUnchecked.add(name);
227
+ warn(() => config.onUncheckedTool(name));
228
+ }
229
+ const context = toolContext(agent, tool, name, unnamed.owner);
230
+ return {
231
+ context,
232
+ tool: { name, input: unnamed.input, usage: tool.usage, callId: unnamed.callId },
233
+ idempotencyKey: toolKey(unnamed.callId, context, name),
234
+ };
235
+ };
236
+ return {
237
+ async startModel(runId, attribution, eventName) {
238
+ const resolved = contextFor(agentFor(attribution.agent).context, attribution);
239
+ const context = resolved.eventName ? resolved : { ...resolved, eventName: { name: eventName } };
240
+ await check(context);
241
+ rememberStep(runId, { context });
242
+ },
243
+ // Awaited by the binding, so the agent's next check sees this spend. Safe to wait on: ingest
244
+ // never throws.
245
+ async endModel(runId, measurement, issued = []) {
246
+ const step = steps.get(runId);
247
+ rememberIssuedCalls(issued, step?.context.agent);
248
+ if (!step)
249
+ return;
250
+ steps.delete(runId);
251
+ if (!measurement) {
252
+ session.failedToMeasure(step.context, new Error('the model call ended without usage, so nothing was metered'));
253
+ return;
254
+ }
255
+ await session.ingestMeasurement(step.context, measurement);
256
+ },
257
+ async startTool(runId, { name, callId, kind, input }, attribution) {
258
+ // Never taken, only looked up - the cap drops old ones: a tools node that runs again, before
259
+ // or after the call succeeded, is the same call, owned by the same agent, billed under one key.
260
+ const issued = callId === undefined ? undefined : issuedCalls.get(callId);
261
+ const named = name ?? issued?.name;
262
+ const toolName = named ?? kind;
263
+ // Billed to the agent whose model asked for the call: a tool can run in a node of its own.
264
+ const owner = { ...attribution, agent: issued?.agent ?? attribution.agent };
265
+ const agent = agentFor(owner.agent);
266
+ const tool = toolName ? toolFor(agent.tools, named) : undefined;
267
+ if (!toolName || !tool) {
268
+ // Nothing to check it against yet - but its result will name it, and a priced tool must
269
+ // not run free because its name was not known when it started.
270
+ // Only when a name could match - and with its context resolved now, so a run with no
271
+ // customer is refused before the tool runs, as a named tool is.
272
+ if (named === undefined && Object.values(agent.tools).some(Boolean)) {
273
+ rememberUnnamed(runId, { owner, context: contextFor(agent.context, owner), input, callId, kind });
274
+ }
275
+ return;
276
+ }
277
+ const context = toolContext(agent, tool, toolName, owner);
278
+ if (tool.entitlement)
279
+ await check(context);
280
+ rememberStep(runId, {
281
+ context,
282
+ tool: { name: toolName, input, usage: tool.usage, callId },
283
+ idempotencyKey: toolKey(callId, context, toolName),
284
+ });
285
+ },
286
+ async endTool(runId, output, name) {
287
+ // Started governed, or started unnamed and governed by the name its result carries.
288
+ const step = steps.get(runId) ?? stepFromResult(runId, name);
289
+ steps.delete(runId);
290
+ unnamedTools.delete(runId);
291
+ if (step)
292
+ await ingestTool(step, output);
293
+ },
294
+ abandon(runId) {
295
+ unnamedTools.delete(runId);
296
+ steps.delete(runId);
297
+ },
298
+ };
299
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The LangChain and LangGraph binding: a callback handler over an agent graph's middleware.
3
+ *
4
+ * const stiggHandler = new StiggLangChainCallbackHandler({ stigg, stiggContext, agents });
5
+ * await agent.invoke(input, { callbacks: [stiggHandler], metadata: { stiggCustomerId } });
6
+ *
7
+ * Everything here is LangChain's protocol - which hook a step starts on, where the customer and
8
+ * the usage are, which agent a step belongs to. Whether a step may run and what it is worth is the
9
+ * middleware's. LangChain hands a callback to every run beneath the `invoke`, sub-agents and
10
+ * LangGraph nodes included, so one handler governs the whole graph.
11
+ *
12
+ * A step belongs to the nearest run above it named for a configured agent: a `createAgent` or
13
+ * `createReactAgent` by its `name`, a hand-built graph's node by its own. That is LangChain's run
14
+ * tree, not metadata - `lc_agent_name` in metadata is inherited by an unnamed sub-agent, carried
15
+ * into a sub-agent by a tool that passes its own config, and settable by whoever sends the run's
16
+ * metadata, so it would bill the wrong agent or let a client pick whose budget pays.
17
+ *
18
+ * It blocks by default. LangChain swallows a handler's error unless `raiseError` is set, and runs
19
+ * an async handler in the background unless `awaitHandlers` is set too, so both are set - through
20
+ * the constructor, because `awaitHandlers` is derived inside `super()`. And LangChain copies a
21
+ * handler (`new this.constructor(this)`) on its way into a nested run, so a copy shares the
22
+ * original's middleware and run tree rather than billing a sub-agent to a fresh session.
23
+ *
24
+ * The one file in `src/` that imports LangChain, reached only through the
25
+ * `@stigg/agent-sdk-js/langchain` entry.
26
+ */
27
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
28
+ import type { StiggAgentSDK } from '../../../stigg-agent-sdk';
29
+ import type { GovernedAgentMiddleware, MiddlewareOptions } from '../../../types';
30
+ /**
31
+ * The metadata key a caller names the customer under, on `invoke(input, { metadata })`. It wins
32
+ * over the handler's default, so set it server-side - on LangGraph Platform the client sends
33
+ * `metadata`, so set it in your auth handler, never from the request.
34
+ */
35
+ export declare const CUSTOMER_METADATA_KEY = "stiggCustomerId";
36
+ export type StiggLangChainCallbackOptions = MiddlewareOptions & {
37
+ /** The SDK this handler governs through - one per environment, shared with everything else. */
38
+ stigg: StiggAgentSDK;
39
+ /**
40
+ * The agent the top-level run is - one of `agents`. Set it when the graph's entry point is itself
41
+ * an agent: the caller names the top-level run (`invoke(input, { runName })`), so without this its
42
+ * name would let them pick whose budget pays, or step outside any agent's.
43
+ */
44
+ rootAgent?: string;
45
+ };
46
+ export declare class StiggLangChainCallbackHandler extends BaseCallbackHandler {
47
+ name: string;
48
+ /**
49
+ * The graph's middleware, which every hook below drives. Protected, for a subclass that traces:
50
+ * the handler itself exposes nothing more, because what it did is flushed through the SDK it was
51
+ * given (`stigg.flush()`), and reported through the `hooks` it was configured with.
52
+ */
53
+ protected readonly middleware: GovernedAgentMiddleware;
54
+ private readonly runs;
55
+ constructor(options: StiggLangChainCallbackOptions | StiggLangChainCallbackHandler);
56
+ handleChainStart(...args: Parameters<NonNullable<BaseCallbackHandler['handleChainStart']>>): void;
57
+ handleChainEnd(_outputs: unknown, runId: string): void;
58
+ handleChainError(_err: unknown, runId: string): void;
59
+ handleChatModelStart(_llm: unknown, _messages: unknown, runId: string, parentRunId?: string, _extraParams?: unknown, _tags?: string[], metadata?: Record<string, unknown>): Promise<void>;
60
+ handleLLMStart(_llm: unknown, _prompts: unknown, runId: string, parentRunId?: string, _extraParams?: unknown, _tags?: string[], metadata?: Record<string, unknown>): Promise<void>;
61
+ handleLLMEnd(output: unknown, runId: string): Promise<void>;
62
+ handleLLMError(_err: unknown, runId: string): void;
63
+ handleToolStart(tool: unknown, input: string, runId: string, parentRunId?: string, _tags?: string[], metadata?: Record<string, unknown>, runName?: string, toolCallId?: string): Promise<void>;
64
+ handleToolEnd(output: unknown, runId: string): Promise<void>;
65
+ handleToolError(_err: unknown, runId: string): void;
66
+ /**
67
+ * Who a step is for: the configured agent above it in the run tree, and the run's customer. With
68
+ * none above it, a name from metadata is passed on only as a label for the "not listed" warning -
69
+ * never a configured agent's name, which the tree alone can confer.
70
+ */
71
+ private attributionOf;
72
+ }