@stigg/agent-sdk-js 0.5.1 β 0.6.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 +48 -0
- package/dist/bindings/orchestrator/core/agent-middleware.d.ts +9 -5
- package/dist/bindings/orchestrator/core/agent-middleware.js +29 -12
- package/dist/bindings/orchestrator/langchain/callback-handler.d.ts +22 -10
- package/dist/bindings/orchestrator/langchain/callback-handler.js +89 -20
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +31 -3
- package/dist/engine/engine.d.ts +2 -0
- package/dist/engine/rest-governance-engine.js +14 -8
- package/dist/integrations/langchain.d.ts +1 -1
- package/dist/integrations/langchain.js +2 -1
- package/dist/stigg-agent-sdk.js +14 -1
- package/dist/types.d.ts +23 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -73,6 +73,54 @@ try {
|
|
|
73
73
|
the shortest path from nothing to working metering is a currency and a `customerId` β no catalog
|
|
74
74
|
modelling first.
|
|
75
75
|
|
|
76
|
+
## πΈοΈ Governing an agent graph
|
|
77
|
+
|
|
78
|
+
A wrapper governs one client. A LangChain or LangGraph graph runs many agents and tools, so it
|
|
79
|
+
gets one handler instead, passed on `invoke` β LangChain hands it to every model and tool call
|
|
80
|
+
beneath, sub-agents included. Each agent is checked and reports its usage as itself. It imports
|
|
81
|
+
`@langchain/core`, an optional peer, so it uses the copy your application already has.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { StiggAgentSDK } from '@stigg/agent-sdk-js';
|
|
85
|
+
import { StiggLangChainCallbackHandler } from '@stigg/agent-sdk-js/langchain';
|
|
86
|
+
|
|
87
|
+
const stigg = new StiggAgentSDK();
|
|
88
|
+
const credits = { currencyId: 'credits' };
|
|
89
|
+
|
|
90
|
+
// Once per customer: each agent an entity with its own budget - what `agentId` below names.
|
|
91
|
+
for (const agentId of ['agent-researcher', 'agent-writer']) {
|
|
92
|
+
await stigg.register({ customerId: 'customer-acme', agentId, entityTypeId: 'agent', budget: { entitlement: credits, limit: 100 } });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stiggHandler = new StiggLangChainCallbackHandler({
|
|
96
|
+
stigg,
|
|
97
|
+
stiggContext: { attributionKey: 'agentId', entitlement: credits }, // the whole graph
|
|
98
|
+
agents: {
|
|
99
|
+
// by the name LangChain knows the agent or node by
|
|
100
|
+
researcher: { stiggContext: { agentId: 'agent-researcher' }, tools: { web_search: credits } },
|
|
101
|
+
writer: { stiggContext: { agentId: 'agent-writer' } },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await graph.invoke(input, {
|
|
106
|
+
callbacks: [stiggHandler],
|
|
107
|
+
metadata: { stiggContext: { customerId: 'customer-acme' } }, // per run
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Both take the same `stiggContext`, merged the same way β only where it attaches differs:
|
|
112
|
+
|
|
113
|
+
| | Wrapper | Agent graph |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| Attaches to | one vendor client | the graph's `invoke` |
|
|
116
|
+
| The whole thing | `wrap(client, { stiggContext })` | `new StiggLangChainCallbackHandler({ stiggContext })` |
|
|
117
|
+
| Per agent | a wrapped client per agent | `agents[name].stiggContext` |
|
|
118
|
+
| Per request | `create({ ..., stiggContext })` | `invoke(input, { metadata: { stiggContext } })` |
|
|
119
|
+
| Tools | not seen | `tools`, checked and metered per call |
|
|
120
|
+
| A refusal | the call throws | the step throws, and the graph decides β a refused sub-agent reads to its lead as a failed tool call |
|
|
121
|
+
|
|
122
|
+
The [LangChain and LangGraph guide](docs/langchain.md) has every configuration.
|
|
123
|
+
|
|
76
124
|
## π§ How it works
|
|
77
125
|
|
|
78
126
|
Three things happen around a call you already make:
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { type ResolvedContext } from '../../../core/context';
|
|
1
|
+
import { type ResolvedContext, type StiggContext } from '../../../core/context';
|
|
2
2
|
import type { GovernanceSession } from '../../../core/session';
|
|
3
3
|
import { type Entitlement, type Measurement, type MiddlewareOptions, type MiddlewareTools, type ToolUsageMapper } from '../../../types';
|
|
4
4
|
/** Who a step is for, as the framework stated it on that step. */
|
|
5
5
|
export type StepAttribution = {
|
|
6
6
|
/** The agent that took the step, by the framework's own name for it. */
|
|
7
7
|
agent?: string;
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The `stiggContext`s the run stated on the way down to this step, outermost first - the run's,
|
|
10
|
+
* then any a node set for its own calls. Each is applied over the agent's context the way `wrap`
|
|
11
|
+
* applies a call's: the customer, dimensions, and anything else a call can override.
|
|
12
|
+
*/
|
|
13
|
+
stiggContexts?: readonly StiggContext[];
|
|
10
14
|
};
|
|
11
15
|
/** A tool call the model issued in its reply: the id the tool step will refer back to, and what it named. */
|
|
12
16
|
export type IssuedToolCall = {
|
|
@@ -34,14 +38,14 @@ export type AgentMiddleware = {
|
|
|
34
38
|
/**
|
|
35
39
|
* After a model call: its measurement is ingested, or the missing one reported. Never throws.
|
|
36
40
|
* `issued` is the tool calls the reply asked for, so a tool step that arrives with only its call
|
|
37
|
-
* id is named, and
|
|
41
|
+
* id is named, and reported for the agent that called it.
|
|
38
42
|
*/
|
|
39
43
|
endModel(runId: string, measurement: Measurement | undefined, issued?: readonly IssuedToolCall[]): Promise<void>;
|
|
40
44
|
/** Before a tool runs: checked when the tool is a paid one, raising when refused. */
|
|
41
45
|
startTool(runId: string, tool: ToolStep, attribution: StepAttribution): Promise<void>;
|
|
42
46
|
/** After a tool succeeded: what its `usage` makes of the call, or one unit. Never throws. */
|
|
43
47
|
endTool(runId: string, output?: unknown, name?: string): Promise<void>;
|
|
44
|
-
/** A step that failed: forgotten, and
|
|
48
|
+
/** A step that failed: forgotten, and reports nothing - it did not deliver what it charges for. */
|
|
45
49
|
abandon(runId: string): void;
|
|
46
50
|
};
|
|
47
51
|
/** How one tool is governed, its shorthand expanded. */
|
|
@@ -11,7 +11,7 @@ exports.createAgentMiddleware = createAgentMiddleware;
|
|
|
11
11
|
* knows one vendor; here the middleware governs a graph's steps and a framework binding (the
|
|
12
12
|
* LangChain callback handler, or a harness of your own) knows how its framework announces them.
|
|
13
13
|
*
|
|
14
|
-
* Every step is checked and
|
|
14
|
+
* Every step is checked and reported as the agent that took it, so one agent running dry refuses
|
|
15
15
|
* that agent's steps and nothing else. A refusal is the SDK's own `AccessDeniedError`, thrown from
|
|
16
16
|
* inside the step - the frameworks worth binding to offer hooks that observe a step, not ones that
|
|
17
17
|
* replace its result - and what it means for the run is the graph's to decide. Nothing else throws
|
|
@@ -19,7 +19,7 @@ exports.createAgentMiddleware = createAgentMiddleware;
|
|
|
19
19
|
* `checkErrorAction`.
|
|
20
20
|
*
|
|
21
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
|
|
22
|
+
* carries nothing else: what the start resolved is what the end reports.
|
|
23
23
|
*/
|
|
24
24
|
const errors_1 = require("../../../errors");
|
|
25
25
|
const context_1 = require("../../../core/context");
|
|
@@ -69,7 +69,7 @@ function resolveAgents(shared, { tools, agents }) {
|
|
|
69
69
|
// into the first. The shared context is refused the same way, in `resolveContext`.
|
|
70
70
|
if (agent.stiggContext?.idempotencyKey !== undefined) {
|
|
71
71
|
throw new Error(`stigg.middleware's agents.${name}.stiggContext cannot set \`idempotencyKey\` - one key shared ` +
|
|
72
|
-
'by every step that agent takes would
|
|
72
|
+
'by every step that agent takes would report the first and discard the rest. Each step is keyed by its own id.');
|
|
73
73
|
}
|
|
74
74
|
resolved.set(name, {
|
|
75
75
|
context: agent.stiggContext ? (0, context_1.overrideContext)(shared, agent.stiggContext) : shared,
|
|
@@ -125,7 +125,7 @@ function createAgentMiddleware(session, config) {
|
|
|
125
125
|
const warnedUnlisted = new Set();
|
|
126
126
|
/**
|
|
127
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
|
|
128
|
+
* agent - resolves to the shared context, so nothing runs unmetered for having been left out.
|
|
129
129
|
*/
|
|
130
130
|
const agentFor = (agent) => {
|
|
131
131
|
const own = agent === undefined ? undefined : config.agents.get(agent);
|
|
@@ -140,9 +140,21 @@ function createAgentMiddleware(session, config) {
|
|
|
140
140
|
}
|
|
141
141
|
return own ?? config.shared;
|
|
142
142
|
};
|
|
143
|
-
/** The context for one step. The customer is required: an empty one would
|
|
144
|
-
const contextFor = (base,
|
|
145
|
-
const
|
|
143
|
+
/** The context for one step. The customer is required: an empty one would report usage for nobody. */
|
|
144
|
+
const contextFor = (base, attribution) => {
|
|
145
|
+
const { agent, stiggContexts = [] } = attribution;
|
|
146
|
+
// A harness from 0.5 still naming the customer here would otherwise be told it named none.
|
|
147
|
+
if ('customerId' in attribution) {
|
|
148
|
+
throw new Error('A step names its customer in `stiggContexts: [{ customerId }]` - `StepAttribution.customerId` was ' +
|
|
149
|
+
'removed in 0.6.');
|
|
150
|
+
}
|
|
151
|
+
// A run spans many steps, so a key of its own would dedupe them all into the first - refused
|
|
152
|
+
// as the shared and the agents' contexts refuse it. Each step is keyed by its own id.
|
|
153
|
+
if (stiggContexts.some((layer) => layer?.idempotencyKey !== undefined)) {
|
|
154
|
+
throw new Error("A run's stiggContext cannot set `idempotencyKey` - one key shared by every step beneath it would " +
|
|
155
|
+
'report the first and discard the rest. Each step is keyed by its own id.');
|
|
156
|
+
}
|
|
157
|
+
const context = stiggContexts.reduce((resolved, override) => (0, context_1.overrideContext)(resolved, override), base);
|
|
146
158
|
if (!context.customerId)
|
|
147
159
|
throw (0, context_1.noCustomer)(config.missingCustomer);
|
|
148
160
|
return agent === undefined ? context : { ...context, agent };
|
|
@@ -155,7 +167,7 @@ function createAgentMiddleware(session, config) {
|
|
|
155
167
|
if (!decision.hasAccess)
|
|
156
168
|
throw new errors_1.AccessDeniedError(decision);
|
|
157
169
|
};
|
|
158
|
-
/** A step in flight. One dropped to make room is never
|
|
170
|
+
/** A step in flight. One dropped to make room is never reported, so that is said, not silent. */
|
|
159
171
|
const rememberStep = (runId, step) => {
|
|
160
172
|
const dropped = remember(steps, runId, step, STEPS_LIMIT);
|
|
161
173
|
if (dropped)
|
|
@@ -175,11 +187,16 @@ function createAgentMiddleware(session, config) {
|
|
|
175
187
|
for (const call of issued)
|
|
176
188
|
remember(issuedCalls, call.id, { name: call.name, agent }, ISSUED_CALLS_LIMIT);
|
|
177
189
|
};
|
|
178
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* A tool step's context: priced by the tool's own entitlement, when it has one, and labelled with
|
|
192
|
+
* the tool. The tool goes last - a run's `stiggContext` naming another entitlement re-prices its
|
|
193
|
+
* model calls, not the tools it set a price on.
|
|
194
|
+
*/
|
|
179
195
|
const toolContext = (agent, tool, name, attribution) => {
|
|
180
196
|
const eventName = { name: `tool.${name}` };
|
|
181
|
-
const
|
|
182
|
-
|
|
197
|
+
const step = contextFor(agent.context, attribution);
|
|
198
|
+
const priced = (0, context_1.overrideContext)(step, tool.entitlement ? { entitlement: tool.entitlement, eventName } : { eventName });
|
|
199
|
+
return { ...priced, agent: step.agent, tool: name };
|
|
183
200
|
};
|
|
184
201
|
/** Reports what a tool step that ended used: its `usage`, or one unit. A bad `usage` is a metering failure. */
|
|
185
202
|
const ingestTool = async ({ context, idempotencyKey, tool }, output) => {
|
|
@@ -256,7 +273,7 @@ function createAgentMiddleware(session, config) {
|
|
|
256
273
|
},
|
|
257
274
|
async startTool(runId, { name, callId, kind, input }, attribution) {
|
|
258
275
|
// 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,
|
|
276
|
+
// or after the call succeeded, is the same call, owned by the same agent, reported under one key.
|
|
260
277
|
const issued = callId === undefined ? undefined : issuedCalls.get(callId);
|
|
261
278
|
const named = name ?? issued?.name;
|
|
262
279
|
const toolName = named ?? kind;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* The LangChain and LangGraph binding: a callback handler over an agent graph's middleware.
|
|
3
3
|
*
|
|
4
4
|
* const stiggHandler = new StiggLangChainCallbackHandler({ stigg, stiggContext, agents });
|
|
5
|
-
* await agent.invoke(input, { callbacks: [stiggHandler], metadata: {
|
|
5
|
+
* await agent.invoke(input, { callbacks: [stiggHandler], metadata: { stiggContext: { customerId } } });
|
|
6
6
|
*
|
|
7
|
-
* Everything here is LangChain's protocol - which hook a step starts on, where the
|
|
8
|
-
* the usage are, which agent a step belongs to. Whether a step may run and what it is worth is the
|
|
7
|
+
* Everything here is LangChain's protocol - which hook a step starts on, where the run's
|
|
8
|
+
* `stiggContext` and the usage are, which agent a step belongs to. Whether a step may run and what it is worth is the
|
|
9
9
|
* middleware's. LangChain hands a callback to every run beneath the `invoke`, sub-agents and
|
|
10
10
|
* LangGraph nodes included, so one handler governs the whole graph.
|
|
11
11
|
*
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `createReactAgent` by its `name`, a hand-built graph's node by its own. That is LangChain's run
|
|
14
14
|
* tree, not metadata - `lc_agent_name` in metadata is inherited by an unnamed sub-agent, carried
|
|
15
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
|
|
16
|
+
* metadata, so it would charge the wrong agent or let a client pick whose budget pays.
|
|
17
17
|
*
|
|
18
18
|
* It blocks by default. LangChain swallows a handler's error unless `raiseError` is set, and runs
|
|
19
19
|
* an async handler in the background unless `awaitHandlers` is set too, so both are set - through
|
|
@@ -28,10 +28,14 @@ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
|
28
28
|
import type { StiggAgentSDK } from '../../../stigg-agent-sdk';
|
|
29
29
|
import type { GovernedAgentMiddleware, MiddlewareOptions } from '../../../types';
|
|
30
30
|
/**
|
|
31
|
-
* The metadata key a
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* The metadata key a run states its `stiggContext` under, on `invoke(input, { metadata })` - the
|
|
32
|
+
* same override `wrap` takes per call, applied over the handler's and the agent's. On `invoke` it
|
|
33
|
+
* names only the customer and `correlationId` (`RUN_FIELDS`); a node's own config may state more.
|
|
34
|
+
* On LangGraph Platform the client sends `metadata`, so set the customer in your auth handler,
|
|
35
|
+
* never from the request.
|
|
34
36
|
*/
|
|
37
|
+
export declare const CONTEXT_METADATA_KEY = "stiggContext";
|
|
38
|
+
/** @deprecated The customer alone, from before a run took a whole `stiggContext`. Read as `stiggContext.customerId`. */
|
|
35
39
|
export declare const CUSTOMER_METADATA_KEY = "stiggCustomerId";
|
|
36
40
|
export type StiggLangChainCallbackOptions = MiddlewareOptions & {
|
|
37
41
|
/** The SDK this handler governs through - one per environment, shared with everything else. */
|
|
@@ -64,9 +68,17 @@ export declare class StiggLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
64
68
|
handleToolEnd(output: unknown, runId: string): Promise<void>;
|
|
65
69
|
handleToolError(_err: unknown, runId: string): void;
|
|
66
70
|
/**
|
|
67
|
-
* Who a step is for: the configured agent above it in the run tree, and the
|
|
68
|
-
*
|
|
69
|
-
* never a configured agent's name, which the tree alone can
|
|
71
|
+
* Who a step is for: the configured agent above it in the run tree, and the `stiggContext`s the
|
|
72
|
+
* run stated on the way down. With no agent above it, a name from metadata is passed on only as a
|
|
73
|
+
* label for the "not listed" warning - never a configured agent's name, which the tree alone can
|
|
74
|
+
* confer.
|
|
70
75
|
*/
|
|
71
76
|
private attributionOf;
|
|
77
|
+
/**
|
|
78
|
+
* The `stiggContext` a run's metadata states, with the customer the deprecated key names. That key
|
|
79
|
+
* is what a server's auth handler was told to set, so it is the one trusted: a `stiggContext`
|
|
80
|
+
* naming another customer beside it is refused rather than charged to someone else. A run with no
|
|
81
|
+
* parent - `invoke`'s own metadata - states only `RUN_FIELDS`, with the deprecated key or without.
|
|
82
|
+
*/
|
|
83
|
+
private stiggContextOf;
|
|
72
84
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StiggLangChainCallbackHandler = exports.CUSTOMER_METADATA_KEY = void 0;
|
|
3
|
+
exports.StiggLangChainCallbackHandler = exports.CUSTOMER_METADATA_KEY = exports.CONTEXT_METADATA_KEY = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The LangChain and LangGraph binding: a callback handler over an agent graph's middleware.
|
|
6
6
|
*
|
|
7
7
|
* const stiggHandler = new StiggLangChainCallbackHandler({ stigg, stiggContext, agents });
|
|
8
|
-
* await agent.invoke(input, { callbacks: [stiggHandler], metadata: {
|
|
8
|
+
* await agent.invoke(input, { callbacks: [stiggHandler], metadata: { stiggContext: { customerId } } });
|
|
9
9
|
*
|
|
10
|
-
* Everything here is LangChain's protocol - which hook a step starts on, where the
|
|
11
|
-
* the usage are, which agent a step belongs to. Whether a step may run and what it is worth is the
|
|
10
|
+
* Everything here is LangChain's protocol - which hook a step starts on, where the run's
|
|
11
|
+
* `stiggContext` and the usage are, which agent a step belongs to. Whether a step may run and what it is worth is the
|
|
12
12
|
* middleware's. LangChain hands a callback to every run beneath the `invoke`, sub-agents and
|
|
13
13
|
* LangGraph nodes included, so one handler governs the whole graph.
|
|
14
14
|
*
|
|
@@ -16,7 +16,7 @@ exports.StiggLangChainCallbackHandler = exports.CUSTOMER_METADATA_KEY = void 0;
|
|
|
16
16
|
* `createReactAgent` by its `name`, a hand-built graph's node by its own. That is LangChain's run
|
|
17
17
|
* tree, not metadata - `lc_agent_name` in metadata is inherited by an unnamed sub-agent, carried
|
|
18
18
|
* into a sub-agent by a tool that passes its own config, and settable by whoever sends the run's
|
|
19
|
-
* metadata, so it would
|
|
19
|
+
* metadata, so it would charge the wrong agent or let a client pick whose budget pays.
|
|
20
20
|
*
|
|
21
21
|
* It blocks by default. LangChain swallows a handler's error unless `raiseError` is set, and runs
|
|
22
22
|
* an async handler in the background unless `awaitHandlers` is set too, so both are set - through
|
|
@@ -31,13 +31,24 @@ const base_1 = require("@langchain/core/callbacks/base");
|
|
|
31
31
|
const hooks_1 = require("../../../core/hooks");
|
|
32
32
|
const token_usage_1 = require("../../shared/token-usage");
|
|
33
33
|
/**
|
|
34
|
-
* The metadata key a
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* The metadata key a run states its `stiggContext` under, on `invoke(input, { metadata })` - the
|
|
35
|
+
* same override `wrap` takes per call, applied over the handler's and the agent's. On `invoke` it
|
|
36
|
+
* names only the customer and `correlationId` (`RUN_FIELDS`); a node's own config may state more.
|
|
37
|
+
* On LangGraph Platform the client sends `metadata`, so set the customer in your auth handler,
|
|
38
|
+
* never from the request.
|
|
37
39
|
*/
|
|
40
|
+
exports.CONTEXT_METADATA_KEY = 'stiggContext';
|
|
41
|
+
/**
|
|
42
|
+
* What the `stiggContext` of a run with no parent - `invoke`'s own metadata - may state. On LangGraph
|
|
43
|
+
* Platform the request's client writes that metadata, so it names who the run is for and nothing that
|
|
44
|
+
* prices it or says which agent it is. The rest belongs on the handler, under `agents`, or in a
|
|
45
|
+
* node's own config.
|
|
46
|
+
*/
|
|
47
|
+
const RUN_FIELDS = new Set(['customerId', 'correlationId']);
|
|
48
|
+
/** @deprecated The customer alone, from before a run took a whole `stiggContext`. Read as `stiggContext.customerId`. */
|
|
38
49
|
exports.CUSTOMER_METADATA_KEY = 'stiggCustomerId';
|
|
39
50
|
/**
|
|
40
|
-
* What `createAgent({ name })` and LangGraph stamp in metadata. Never what a step is
|
|
51
|
+
* What `createAgent({ name })` and LangGraph stamp in metadata. Never what a step is reported as -
|
|
41
52
|
* see the top of this file - only the label an unlisted agent is reported under.
|
|
42
53
|
*/
|
|
43
54
|
const AGENT_METADATA_KEY = 'lc_agent_name';
|
|
@@ -47,7 +58,7 @@ const OPEN_RUNS_LIMIT = 10_000;
|
|
|
47
58
|
/** The counters LangChain's `usage_metadata` states at the top level, reported under its own names. */
|
|
48
59
|
const LANGCHAIN_METRICS = ['input_tokens', 'output_tokens', 'total_tokens'];
|
|
49
60
|
/** Where a caller puts the customer, for the refusal a step with none raises. */
|
|
50
|
-
const MISSING_CUSTOMER = `Name it on every run as \`invoke(input, { metadata: { ${exports.
|
|
61
|
+
const MISSING_CUSTOMER = `Name it on every run as \`invoke(input, { metadata: { ${exports.CONTEXT_METADATA_KEY}: { customerId } } })\`, ` +
|
|
51
62
|
'or give the handler a default in its `stiggContext: { customerId }`.';
|
|
52
63
|
/**
|
|
53
64
|
* The runs LangChain has open, each with its name and parent, so a step can be traced up to the
|
|
@@ -70,8 +81,12 @@ class RunTree {
|
|
|
70
81
|
this.rootAgent = rootAgent;
|
|
71
82
|
this.warn = warn;
|
|
72
83
|
}
|
|
73
|
-
start(runId, parent, name) {
|
|
74
|
-
this.open.set(runId, {
|
|
84
|
+
start(runId, parent, name, stiggContext) {
|
|
85
|
+
this.open.set(runId, {
|
|
86
|
+
name: parent === undefined && this.rootAgent ? this.rootAgent : name,
|
|
87
|
+
parent,
|
|
88
|
+
stiggContext,
|
|
89
|
+
});
|
|
75
90
|
if (this.open.size <= OPEN_RUNS_LIMIT)
|
|
76
91
|
return;
|
|
77
92
|
// A forgotten run breaks the walk for the steps beneath it: they fall back to the shared
|
|
@@ -105,6 +120,22 @@ class RunTree {
|
|
|
105
120
|
isAgent(name) {
|
|
106
121
|
return this.agents.has(name);
|
|
107
122
|
}
|
|
123
|
+
isOpen(runId) {
|
|
124
|
+
return this.open.has(runId);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The `stiggContext`s stated from the top of the tree down to a run, outermost first. LangChain
|
|
128
|
+
* merges `metadata` one key deep, so a node that sets its own `stiggContext` replaces the run's
|
|
129
|
+
* rather than adding to it - walking the tree puts the run's back underneath. A context a run
|
|
130
|
+
* merely inherited is the same object as its parent's, and is applied once.
|
|
131
|
+
*/
|
|
132
|
+
stiggContextsAt(runId) {
|
|
133
|
+
const run = runId === undefined ? undefined : this.open.get(runId);
|
|
134
|
+
if (!run)
|
|
135
|
+
return [];
|
|
136
|
+
const above = this.stiggContextsAt(run.parent);
|
|
137
|
+
return run.stiggContext && run.stiggContext !== above.at(-1) ? [...above, run.stiggContext] : above;
|
|
138
|
+
}
|
|
108
139
|
}
|
|
109
140
|
class StiggLangChainCallbackHandler extends base_1.BaseCallbackHandler {
|
|
110
141
|
name = 'StiggLangChainCallbackHandler';
|
|
@@ -138,8 +169,8 @@ class StiggLangChainCallbackHandler extends base_1.BaseCallbackHandler {
|
|
|
138
169
|
// True of `@langchain/core` 1.2; the attribution tests run against the real manager, so a release
|
|
139
170
|
// that changes either side fails them.
|
|
140
171
|
handleChainStart(...args) {
|
|
141
|
-
const [, , runId, parentRunId, , , , runName] = args;
|
|
142
|
-
this.runs.start(runId, parentRunId, runName);
|
|
172
|
+
const [, , runId, parentRunId, , metadata, , runName] = args;
|
|
173
|
+
this.runs.start(runId, parentRunId, runName, this.stiggContextOf(metadata, parentRunId));
|
|
143
174
|
}
|
|
144
175
|
handleChainEnd(_outputs, runId) {
|
|
145
176
|
this.runs.end(runId);
|
|
@@ -171,7 +202,7 @@ class StiggLangChainCallbackHandler extends base_1.BaseCallbackHandler {
|
|
|
171
202
|
const serialized = tool;
|
|
172
203
|
const name = runName ?? serialized?.name;
|
|
173
204
|
// A tool is a run too: a sub-agent it starts belongs below it in the tree.
|
|
174
|
-
this.runs.start(runId, parentRunId, name);
|
|
205
|
+
this.runs.start(runId, parentRunId, name, this.stiggContextOf(metadata, parentRunId));
|
|
175
206
|
return this.middleware.startTool(runId, { name, callId: toolCallId, kind: serialized?.id?.at(-1), input: decoded(input) }, this.attributionOf(parentRunId, metadata));
|
|
176
207
|
}
|
|
177
208
|
handleToolEnd(output, runId) {
|
|
@@ -186,14 +217,14 @@ class StiggLangChainCallbackHandler extends base_1.BaseCallbackHandler {
|
|
|
186
217
|
this.middleware.abandon(runId);
|
|
187
218
|
}
|
|
188
219
|
/**
|
|
189
|
-
* Who a step is for: the configured agent above it in the run tree, and the
|
|
190
|
-
*
|
|
191
|
-
* never a configured agent's name, which the tree alone can
|
|
220
|
+
* Who a step is for: the configured agent above it in the run tree, and the `stiggContext`s the
|
|
221
|
+
* run stated on the way down. With no agent above it, a name from metadata is passed on only as a
|
|
222
|
+
* label for the "not listed" warning - never a configured agent's name, which the tree alone can
|
|
223
|
+
* confer.
|
|
192
224
|
*/
|
|
193
225
|
attributionOf(parentRunId, metadata) {
|
|
194
226
|
const label = metadata?.[AGENT_METADATA_KEY] ?? metadata?.[NODE_METADATA_KEY];
|
|
195
227
|
const unlisted = typeof label === 'string' && !this.runs.isAgent(label) ? label : undefined;
|
|
196
|
-
const customerId = metadata?.[exports.CUSTOMER_METADATA_KEY];
|
|
197
228
|
const agent = this.runs.agentAt(parentRunId);
|
|
198
229
|
// Metadata naming a configured agent the tree does not find is a misconfiguration - a subgraph
|
|
199
230
|
// node named apart from its agent, a top-level agent with no `rootAgent` - or a caller's attempt.
|
|
@@ -202,11 +233,49 @@ class StiggLangChainCallbackHandler extends base_1.BaseCallbackHandler {
|
|
|
202
233
|
'governed by the shared context. Name the node or the agent after it, or set `rootAgent` if it is ' +
|
|
203
234
|
'the top-level agent.');
|
|
204
235
|
}
|
|
236
|
+
const above = this.runs.stiggContextsAt(parentRunId);
|
|
237
|
+
const own = this.stiggContextOf(metadata, parentRunId);
|
|
238
|
+
// A step's own `stiggContext` replaced the run's in its metadata; only the tree puts the run's
|
|
239
|
+
// back. A parent this handler never saw start - the handler given to a model rather than to
|
|
240
|
+
// `invoke` - leaves the step with its own context alone, and the run's customer gone.
|
|
241
|
+
if (own && own.customerId === undefined && parentRunId !== undefined && !this.runs.isOpen(parentRunId)) {
|
|
242
|
+
this.runs.warnOnce("[stigg] a step's own stiggContext names no customer, and the run above it was never seen by this " +
|
|
243
|
+
"handler - so the run's stiggContext is lost. Pass the handler at `invoke` (`callbacks: [handler]`), " +
|
|
244
|
+
'not to a model or tool.');
|
|
245
|
+
}
|
|
205
246
|
return {
|
|
206
247
|
agent: agent ?? unlisted,
|
|
207
|
-
|
|
248
|
+
stiggContexts: own && own !== above.at(-1) ? [...above, own] : above,
|
|
208
249
|
};
|
|
209
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The `stiggContext` a run's metadata states, with the customer the deprecated key names. That key
|
|
253
|
+
* is what a server's auth handler was told to set, so it is the one trusted: a `stiggContext`
|
|
254
|
+
* naming another customer beside it is refused rather than charged to someone else. A run with no
|
|
255
|
+
* parent - `invoke`'s own metadata - states only `RUN_FIELDS`, with the deprecated key or without.
|
|
256
|
+
*/
|
|
257
|
+
stiggContextOf(metadata, parentRunId) {
|
|
258
|
+
const stated = metadata?.[exports.CONTEXT_METADATA_KEY];
|
|
259
|
+
const customerId = metadata?.[exports.CUSTOMER_METADATA_KEY];
|
|
260
|
+
if (stated !== undefined && (typeof stated !== 'object' || stated === null || Array.isArray(stated))) {
|
|
261
|
+
throw new Error(`metadata.${exports.CONTEXT_METADATA_KEY} must be a \`stiggContext\` object, not ${String(stated)}.`);
|
|
262
|
+
}
|
|
263
|
+
const unlisted = parentRunId === undefined && stated ? Object.keys(stated).filter((key) => !RUN_FIELDS.has(key)) : [];
|
|
264
|
+
if (unlisted.length) {
|
|
265
|
+
throw new Error(`metadata.${exports.CONTEXT_METADATA_KEY} on a run's \`invoke\` names only the customer and \`correlationId\`, not ` +
|
|
266
|
+
`${unlisted.join(', ')} - a client can write that metadata. Set the rest on the handler, under \`agents\`, ` +
|
|
267
|
+
"or in a node's own config.");
|
|
268
|
+
}
|
|
269
|
+
if (typeof customerId !== 'string' || !customerId)
|
|
270
|
+
return stated;
|
|
271
|
+
this.runs.warnOnce(`[stigg] metadata.${exports.CUSTOMER_METADATA_KEY} is deprecated - pass \`metadata: { ${exports.CONTEXT_METADATA_KEY}: { customerId } }\` instead.`);
|
|
272
|
+
const context = stated;
|
|
273
|
+
if (context?.customerId !== undefined && context.customerId !== customerId) {
|
|
274
|
+
throw new Error(`metadata.${exports.CONTEXT_METADATA_KEY}.customerId (${String(context.customerId)}) differs from metadata.${exports.CUSTOMER_METADATA_KEY} ` +
|
|
275
|
+
`(${customerId}) - name the customer once, in metadata.${exports.CONTEXT_METADATA_KEY}.`);
|
|
276
|
+
}
|
|
277
|
+
return { ...context, customerId };
|
|
278
|
+
}
|
|
210
279
|
}
|
|
211
280
|
exports.StiggLangChainCallbackHandler = StiggLangChainCallbackHandler;
|
|
212
281
|
/** LangChain's `ToolMessage`, told by shape - the class is the application's copy, not ours. */
|
package/dist/core/context.d.ts
CHANGED
|
@@ -144,6 +144,11 @@ export declare const CONTEXT_DEFAULTS: {
|
|
|
144
144
|
readonly checkErrorAction: "allow";
|
|
145
145
|
readonly ingestMode: "async";
|
|
146
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* A plan id as given, trimmed - or a refusal when it was given blank. Absent stays absent: no
|
|
149
|
+
* plan is a legal choice, while an empty one is a typo that would provision nobody onto nothing.
|
|
150
|
+
*/
|
|
151
|
+
export declare function planId(value: unknown, field?: string): string | undefined;
|
|
147
152
|
/**
|
|
148
153
|
* The one field a call cannot proceed without, refused the same way from every binding. Only where
|
|
149
154
|
* to put the customer differs, so the remedy is the caller's to word.
|
package/dist/core/context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.addMetrics = exports.isEnforced = exports.noCustomer = exports.CONTEXT_DEFAULTS = void 0;
|
|
4
|
+
exports.planId = planId;
|
|
4
5
|
exports.resolveContext = resolveContext;
|
|
5
6
|
exports.overrideContext = overrideContext;
|
|
6
7
|
/**
|
|
@@ -62,12 +63,14 @@ function agentDimension(context) {
|
|
|
62
63
|
* A plan id as given, trimmed - or a refusal when it was given blank. Absent stays absent: no
|
|
63
64
|
* plan is a legal choice, while an empty one is a typo that would provision nobody onto nothing.
|
|
64
65
|
*/
|
|
65
|
-
function planId(value) {
|
|
66
|
+
function planId(value, field = 'stiggContext.defaultPlanId') {
|
|
66
67
|
if (value === undefined)
|
|
67
68
|
return undefined;
|
|
68
|
-
|
|
69
|
+
if (typeof value !== 'string')
|
|
70
|
+
throw new Error(`${field} must be a plan id string, not ${typeof value}.`);
|
|
71
|
+
const named = value.trim();
|
|
69
72
|
if (!named) {
|
|
70
|
-
throw new Error(
|
|
73
|
+
throw new Error(`${field} is empty - leave the field out to provision no plan, or name the ` +
|
|
71
74
|
'plan new customers should start on.');
|
|
72
75
|
}
|
|
73
76
|
return named;
|
|
@@ -178,7 +181,32 @@ function assertKnownContext(context, where) {
|
|
|
178
181
|
throw new Error(`${where} has no field \`${key}\` - this SDK would ignore it, and the call would be ` +
|
|
179
182
|
`governed by whatever remained instead. Valid fields: ${Object.keys(CONTEXT_KEYS).join(', ')}.`);
|
|
180
183
|
}
|
|
184
|
+
// The shapes a JavaScript caller - or a run's metadata - can get wrong without a compiler to say
|
|
185
|
+
// so: each would otherwise surface later as a bare TypeError, or as a call that looks unmetered.
|
|
186
|
+
for (const key of STRING_FIELDS) {
|
|
187
|
+
const value = context[key];
|
|
188
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
189
|
+
throw new Error(`${where}.${key} must be a string, not ${typeof value}.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (context.usageMapper !== undefined && typeof context.usageMapper !== 'function') {
|
|
193
|
+
throw new Error(`${where}.usageMapper must be a function, not ${typeof context.usageMapper}.`);
|
|
194
|
+
}
|
|
195
|
+
const { dimensions } = context;
|
|
196
|
+
if (dimensions !== undefined &&
|
|
197
|
+
(typeof dimensions !== 'object' || dimensions === null || Array.isArray(dimensions))) {
|
|
198
|
+
throw new Error(`${where}.dimensions must be an object of dimension values.`);
|
|
199
|
+
}
|
|
200
|
+
// Values are matched against entity ids, which are strings: a number would match nothing, and
|
|
201
|
+
// the call would look attributed while governing nothing. An absent one is left to mean absent.
|
|
202
|
+
for (const [key, value] of Object.entries(dimensions ?? {})) {
|
|
203
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
204
|
+
throw new Error(`${where}.dimensions.${key} must be a string, not ${typeof value}.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
181
207
|
}
|
|
208
|
+
/** The fields that are strings or absent. The customer and the plan say so in messages of their own. */
|
|
209
|
+
const STRING_FIELDS = ['agentId', 'attributionKey', 'idempotencyKey', 'correlationId'];
|
|
182
210
|
/**
|
|
183
211
|
* One call's override, applied over what `wrap` already resolved.
|
|
184
212
|
*
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -126,6 +126,8 @@ export type EntityPlacement = {
|
|
|
126
126
|
*/
|
|
127
127
|
usageLimitOnCreate?: number;
|
|
128
128
|
cadence?: string;
|
|
129
|
+
/** The entities the budget is scoped to - its own assignment beside the agent's whole one. */
|
|
130
|
+
scopeEntityIds?: string[];
|
|
129
131
|
};
|
|
130
132
|
export type EntityInput = {
|
|
131
133
|
id: string;
|
|
@@ -87,6 +87,8 @@ class RestGovernanceEngine {
|
|
|
87
87
|
...capability,
|
|
88
88
|
// Tri-state: omitted leaves the parent alone, null makes it a root, an id re-homes it.
|
|
89
89
|
...(placement.parentId === undefined ? {} : { parentId: placement.parentId }),
|
|
90
|
+
// Part of the assignment's identity: a scoped budget is its own row beside the whole one.
|
|
91
|
+
...(placement.scopeEntityIds ? { scopeEntityIds: placement.scopeEntityIds } : {}),
|
|
90
92
|
...budget,
|
|
91
93
|
},
|
|
92
94
|
],
|
|
@@ -107,14 +109,18 @@ class RestGovernanceEngine {
|
|
|
107
109
|
return { usageLimit: placement.usageLimit, ...cadence };
|
|
108
110
|
if (placement.usageLimitOnCreate === undefined)
|
|
109
111
|
return cadence;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
// An assignment is the `(entity, capability, scope)` triple, so "exists" means one with this
|
|
113
|
+
// scope - the agent's whole budget existing says nothing about its per-model one.
|
|
114
|
+
// Encoded whole, not joined: an id may hold a comma, and ['a,b'] is not ['a', 'b'].
|
|
115
|
+
const scopeOf = (ids) => JSON.stringify([...(ids ?? [])].sort());
|
|
116
|
+
const scope = scopeOf(placement.scopeEntityIds);
|
|
117
|
+
// Every page: the one with this scope can sit past the first, and missing it would overwrite
|
|
118
|
+
// an admin's limit with the seed.
|
|
119
|
+
for await (const row of this.client.v1Beta.customers.assignments.list(customerId, { entityId, ...capability })) {
|
|
120
|
+
// Already assigned: leave the limit and its window exactly as they are.
|
|
121
|
+
if (scopeOf(row.scopeEntityIds) === scope)
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
118
124
|
return { usageLimit: placement.usageLimitOnCreate, ...cadence };
|
|
119
125
|
}
|
|
120
126
|
async check(customerId, { entitlement, requestedUsage, dimensions }) {
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
* import { StiggAgentSDK } from "@stigg/agent-sdk-js";
|
|
10
10
|
* import { StiggLangChainCallbackHandler, AccessDeniedError } from "@stigg/agent-sdk-js/langchain";
|
|
11
11
|
*/
|
|
12
|
-
export { StiggLangChainCallbackHandler, CUSTOMER_METADATA_KEY, } from '../bindings/orchestrator/langchain/callback-handler';
|
|
12
|
+
export { StiggLangChainCallbackHandler, CONTEXT_METADATA_KEY, CUSTOMER_METADATA_KEY, } from '../bindings/orchestrator/langchain/callback-handler';
|
|
13
13
|
export type { StiggLangChainCallbackOptions } from '../bindings/orchestrator/langchain/callback-handler';
|
|
14
14
|
export { AccessDeniedError, isAccessDenied } from '../errors';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isAccessDenied = exports.AccessDeniedError = exports.CUSTOMER_METADATA_KEY = exports.StiggLangChainCallbackHandler = void 0;
|
|
3
|
+
exports.isAccessDenied = exports.AccessDeniedError = exports.CUSTOMER_METADATA_KEY = exports.CONTEXT_METADATA_KEY = exports.StiggLangChainCallbackHandler = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* `@stigg/agent-sdk-js/langchain` - Stigg governance for LangChain and LangGraph.
|
|
6
6
|
*
|
|
@@ -14,6 +14,7 @@ exports.isAccessDenied = exports.AccessDeniedError = exports.CUSTOMER_METADATA_K
|
|
|
14
14
|
*/
|
|
15
15
|
var callback_handler_1 = require("../bindings/orchestrator/langchain/callback-handler");
|
|
16
16
|
Object.defineProperty(exports, "StiggLangChainCallbackHandler", { enumerable: true, get: function () { return callback_handler_1.StiggLangChainCallbackHandler; } });
|
|
17
|
+
Object.defineProperty(exports, "CONTEXT_METADATA_KEY", { enumerable: true, get: function () { return callback_handler_1.CONTEXT_METADATA_KEY; } });
|
|
17
18
|
Object.defineProperty(exports, "CUSTOMER_METADATA_KEY", { enumerable: true, get: function () { return callback_handler_1.CUSTOMER_METADATA_KEY; } });
|
|
18
19
|
// What a refusal is, re-exported so a catch block needs one import: `e instanceof AccessDeniedError`.
|
|
19
20
|
var errors_1 = require("../errors");
|
package/dist/stigg-agent-sdk.js
CHANGED
|
@@ -136,7 +136,7 @@ function toOptions(first, second) {
|
|
|
136
136
|
function toPlacement(input) {
|
|
137
137
|
if (!input.budget)
|
|
138
138
|
return undefined;
|
|
139
|
-
const { entitlement, cadence, parentId } = input.budget;
|
|
139
|
+
const { entitlement, cadence, parentId, scope } = input.budget;
|
|
140
140
|
// A placement writes an assignment against the entitlement, so an empty one persists a budget
|
|
141
141
|
// attached to nothing rather than merely routing badly.
|
|
142
142
|
const invalid = (0, types_1.entitlementError)(entitlement);
|
|
@@ -153,12 +153,19 @@ function toPlacement(input) {
|
|
|
153
153
|
"`budget.apply: 'on-create'` writes a cap the first time and never again, so there is " +
|
|
154
154
|
'nothing to write without `budget.limit`.');
|
|
155
155
|
}
|
|
156
|
+
// Each entry names an entity, and a blank one names none: the budget would be scoped to an
|
|
157
|
+
// entity no request resolves, and apply to nothing.
|
|
158
|
+
if (scope !== undefined && (!Array.isArray(scope) || scope.some((id) => typeof id !== 'string' || !id.trim()))) {
|
|
159
|
+
throw new Error(`Agent ${input.agentId} has a budget.scope that is not a list of entity ids: ${JSON.stringify(scope)}. ` +
|
|
160
|
+
'Leave it out, or pass [], for the whole budget.');
|
|
161
|
+
}
|
|
156
162
|
return {
|
|
157
163
|
parentId,
|
|
158
164
|
entitlement,
|
|
159
165
|
cadence,
|
|
160
166
|
usageLimit: onCreate ? undefined : input.budget.limit,
|
|
161
167
|
usageLimitOnCreate: onCreate ? (input.budget.limit ?? undefined) : undefined,
|
|
168
|
+
scopeEntityIds: scope?.length ? scope : undefined,
|
|
162
169
|
};
|
|
163
170
|
}
|
|
164
171
|
class StiggAgentSDK {
|
|
@@ -270,6 +277,8 @@ class StiggAgentSDK {
|
|
|
270
277
|
if (typeof agentId !== 'string' || !agentId.trim()) {
|
|
271
278
|
throw new Error('register needs an agentId - an empty one provisions an entity nobody can name.');
|
|
272
279
|
}
|
|
280
|
+
// Validated by presence, not truthiness: a blank plan is a typo, not a request for none.
|
|
281
|
+
const defaultPlanId = (0, context_1.planId)(input.defaultPlanId, 'register defaultPlanId');
|
|
273
282
|
const placement = toPlacement(input);
|
|
274
283
|
const key = `${customerId}:${agentId}`;
|
|
275
284
|
// Memoized on the description, not just the pair: a second call describing the agent
|
|
@@ -292,6 +301,10 @@ class StiggAgentSDK {
|
|
|
292
301
|
// built: `run()` already validates in this order, and a malformed call that reports itself as
|
|
293
302
|
// a probe timeout sends the reader looking at the network for a typo they can see.
|
|
294
303
|
await this.ensureStarted();
|
|
304
|
+
// The customer before the agent: an entity is created under a customer, so one Stigg has never
|
|
305
|
+
// seen would be refused. The same once-per-customer provisioning every check does.
|
|
306
|
+
if (defaultPlanId)
|
|
307
|
+
await this.provisionCustomer(customerId, defaultPlanId);
|
|
295
308
|
if (!this.engine.capabilities.provisioning) {
|
|
296
309
|
// Only a call that asked for something governance-only is an error. A budget or a parent
|
|
297
310
|
// cannot be honoured, and carrying on would leave an agent that looks governed with a
|
package/dist/types.d.ts
CHANGED
|
@@ -359,8 +359,9 @@ export type ClientContext = Omit<StiggContext, 'idempotencyKey'> & {
|
|
|
359
359
|
* agent and nothing else.
|
|
360
360
|
*
|
|
361
361
|
* The customer is the one thing usually left out, because that changes per request: it rides each
|
|
362
|
-
* run instead,
|
|
363
|
-
*
|
|
362
|
+
* run instead, in the run's own `stiggContext` - the per-call override `wrap` takes - wherever the
|
|
363
|
+
* binding says (on LangChain, `metadata.stiggContext`), and wins over a default here. Set it
|
|
364
|
+
* server-side, from the authenticated user, never from the request.
|
|
364
365
|
*/
|
|
365
366
|
export type MiddlewareOptions = {
|
|
366
367
|
/**
|
|
@@ -498,6 +499,18 @@ export type AgentBudget = {
|
|
|
498
499
|
cadence?: string;
|
|
499
500
|
/** Places the agent under a parent entity. Tri-state: omit to leave it, null to make it a root. */
|
|
500
501
|
parentId?: string | null;
|
|
502
|
+
/**
|
|
503
|
+
* Narrows the budget to requests whose resolved entities include every one of these - a budget
|
|
504
|
+
* per value of a dimension, such as one per model: register each model as an entity of a type
|
|
505
|
+
* whose attribution key is the dimension, then scope one budget to each. Each scope is its own
|
|
506
|
+
* budget on the agent, so register once per scope. Omitted or empty, the budget is the agent's
|
|
507
|
+
* whole one.
|
|
508
|
+
*
|
|
509
|
+
* A scoped budget governs only the requests it names: one naming no model, or a model with no
|
|
510
|
+
* budget, is limited by the agent's whole budget and its ancestors' alone - with neither, by
|
|
511
|
+
* nothing. Register the whole budget too.
|
|
512
|
+
*/
|
|
513
|
+
scope?: string[];
|
|
501
514
|
} & ({
|
|
502
515
|
/** The limit, written on every call. `null` clears whatever is assigned. */
|
|
503
516
|
limit?: number | null;
|
|
@@ -519,7 +532,14 @@ export type AgentScope = {
|
|
|
519
532
|
customerId: string;
|
|
520
533
|
};
|
|
521
534
|
/** `register` - the standalone call, which has no run to take the scope from. */
|
|
522
|
-
export type RegisterInput = AgentOptions & AgentScope
|
|
535
|
+
export type RegisterInput = AgentOptions & AgentScope & {
|
|
536
|
+
/**
|
|
537
|
+
* The plan to provision the customer onto first, when Stigg does not know them yet - the same
|
|
538
|
+
* provisioning a check with `stiggContext.defaultPlanId` does, once per customer per process.
|
|
539
|
+
* Without it, registering under a customer Stigg has never seen fails: an entity needs one.
|
|
540
|
+
*/
|
|
541
|
+
defaultPlanId?: string;
|
|
542
|
+
};
|
|
523
543
|
/** `deregister` - archiving the entity. No entitlement: an entity is not archived per-entitlement. */
|
|
524
544
|
export type DeregisterInput = Pick<AgentScope, 'customerId'> & Pick<AgentOptions, 'agentId'>;
|
|
525
545
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigg/agent-sdk-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"example-client-typesafe": "tsx examples/client/typesafe.ts",
|
|
50
50
|
"example-two-agents": "tsx examples/client/two-agents.ts",
|
|
51
51
|
"example-orchestrator-langchain": "tsx examples/orchestrator/langchain.ts",
|
|
52
|
-
"example-orchestrator-langgraph": "tsx examples/orchestrator/langgraph.ts",
|
|
52
|
+
"example-orchestrator-langgraph-handoff": "tsx examples/orchestrator/langgraph-handoff.ts",
|
|
53
|
+
"example-orchestrator-langgraph-model-fallback": "tsx examples/orchestrator/langgraph-model-fallback.ts",
|
|
53
54
|
"example-gateway": "tsx examples/gateway/openrouter.ts",
|
|
54
55
|
"example-gateway-openrouter": "tsx examples/gateway/openrouter.ts",
|
|
55
56
|
"example-gateway-litellm": "tsx examples/gateway/litellm.ts",
|