@stigg/agent-sdk-js 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/bindings/client/providers/mistral/mistral-recipe.d.ts +2 -2
- package/dist/bindings/orchestrator/core/agent-middleware.d.ts +81 -0
- package/dist/bindings/orchestrator/core/agent-middleware.js +231 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.d.ts +50 -0
- package/dist/bindings/orchestrator/langchain/callback-handler.js +151 -0
- package/dist/core/context.d.ts +15 -4
- package/dist/core/context.js +7 -5
- package/dist/core/session.d.ts +14 -0
- package/dist/core/session.js +27 -11
- package/dist/core/telemetry.d.ts +8 -0
- package/dist/core/telemetry.js +97 -0
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +28 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/integrations/langchain.d.ts +14 -0
- package/dist/integrations/langchain.js +21 -0
- package/dist/stigg-agent-sdk.d.ts +19 -5
- package/dist/stigg-agent-sdk.js +42 -3
- package/dist/types.d.ts +97 -0
- package/package.json +37 -1
package/dist/core/session.js
CHANGED
|
@@ -29,6 +29,7 @@ const errors_1 = require("../errors");
|
|
|
29
29
|
const headers_1 = require("../headers");
|
|
30
30
|
const dimensions_1 = require("./dimensions");
|
|
31
31
|
const hooks_1 = require("./hooks");
|
|
32
|
+
const telemetry_1 = require("./telemetry");
|
|
32
33
|
const context_1 = require("./context");
|
|
33
34
|
const types_1 = require("../types");
|
|
34
35
|
/** What a context looks like to a hook: the attribution, and nothing a hook could change. */
|
|
@@ -37,6 +38,8 @@ const hookContext = (context) => ({
|
|
|
37
38
|
dimensions: context.dimensions,
|
|
38
39
|
correlationId: context.correlationId,
|
|
39
40
|
entitlementId: context.checkedId,
|
|
41
|
+
...(context.agent !== undefined ? { agent: context.agent } : {}),
|
|
42
|
+
...(context.tool !== undefined ? { tool: context.tool } : {}),
|
|
40
43
|
});
|
|
41
44
|
/** A metric nobody can bill: refused where the mapper that invented it can still be found. */
|
|
42
45
|
const emptyMetricNameError = () => new errors_1.EngineError('measure', 'a metric needs a name - an empty one reports to no feature at all.');
|
|
@@ -111,13 +114,14 @@ class GovernanceSession {
|
|
|
111
114
|
* application routes on, not an exception it recovers from.
|
|
112
115
|
*/
|
|
113
116
|
async check(context, requestedUsage = 1) {
|
|
117
|
+
const startedAt = Date.now();
|
|
114
118
|
this.checks += 1;
|
|
115
119
|
const { entitlement } = context;
|
|
116
120
|
// Asked and answered, with nobody to ask: this context named no entitlement, so there is no
|
|
117
121
|
// budget to consult and no refusal that could come back. Still counted and recorded, because
|
|
118
122
|
// the caller did ask and `lastDecision` is where they read what they got.
|
|
119
123
|
if (!entitlement)
|
|
120
|
-
return this.record((0, decision_1.unenforcedDecision)(), context);
|
|
124
|
+
return this.record((0, decision_1.unenforcedDecision)(), context, startedAt);
|
|
121
125
|
try {
|
|
122
126
|
const engine = await this.deps.engine();
|
|
123
127
|
// Before the check, not after: a customer Stigg has never seen holds no entitlement, so their
|
|
@@ -134,16 +138,16 @@ class GovernanceSession {
|
|
|
134
138
|
// refused - an unknown plan, an archived customer - is refused here too, rather than
|
|
135
139
|
// waved through by an `allow` that exists for outages, not for configuration mistakes.
|
|
136
140
|
if (!reached && result.accessDeniedReason === CUSTOMER_NOT_FOUND) {
|
|
137
|
-
return this.record((0, decision_1.policyDecision)(context.checkErrorAction === 'allow'), context);
|
|
141
|
+
return this.record((0, decision_1.policyDecision)(context.checkErrorAction === 'allow'), context, startedAt);
|
|
138
142
|
}
|
|
139
|
-
return this.record((0, decision_1.toDecision)(result, { customerId: context.customerId, entitlement: context.entitlement }), context);
|
|
143
|
+
return this.record((0, decision_1.toDecision)(result, { customerId: context.customerId, entitlement: context.entitlement }), context, startedAt);
|
|
140
144
|
}
|
|
141
145
|
catch (err) {
|
|
142
146
|
const error = asEngineError('check', err);
|
|
143
147
|
this.reportError(error, 'check', context);
|
|
144
148
|
// Governance is fail-closed server-side; `'deny'` mirrors that, and `'allow'` - the default -
|
|
145
149
|
// is the soft limit that keeps the application up while Stigg is unreachable.
|
|
146
|
-
return this.record((0, decision_1.policyDecision)(context.checkErrorAction === 'allow'), context);
|
|
150
|
+
return this.record((0, decision_1.policyDecision)(context.checkErrorAction === 'allow'), context, startedAt);
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
/** Applies the context's mapper to a token measurement. The path every binding uses. */
|
|
@@ -216,6 +220,7 @@ class GovernanceSession {
|
|
|
216
220
|
send(context, metrics, sendOptions) {
|
|
217
221
|
const { dimensions, idempotencyKey, raw, event } = sendOptions;
|
|
218
222
|
const mode = context.ingestMode;
|
|
223
|
+
const startedAt = Date.now();
|
|
219
224
|
// A broken value is one broken metric, not a broken call: the others measured fine and the
|
|
220
225
|
// spend they describe already happened. Dropping only the bad one reports what is knowable.
|
|
221
226
|
//
|
|
@@ -260,10 +265,10 @@ class GovernanceSession {
|
|
|
260
265
|
// distinguishable from one that never tried, and reported to `onIngest` like every result.
|
|
261
266
|
if (!Object.keys(sendable).length) {
|
|
262
267
|
if (rejected)
|
|
263
|
-
return Promise.resolve(this.failedIngest(context, idempotencyKey, rejected));
|
|
268
|
+
return Promise.resolve(this.failedIngest(context, idempotencyKey, rejected, startedAt));
|
|
264
269
|
this.ingested += 1;
|
|
265
270
|
const noop = { ok: true, reported: {}, idempotencyKey, mode, replayed: false };
|
|
266
|
-
this.
|
|
271
|
+
this.emitIngest(context, noop, startedAt);
|
|
267
272
|
return Promise.resolve(noop);
|
|
268
273
|
}
|
|
269
274
|
const promise = this.deps
|
|
@@ -311,10 +316,10 @@ class GovernanceSession {
|
|
|
311
316
|
this.ingestFailures += 1;
|
|
312
317
|
this.reportError(rejected, rejected.operation, context, idempotencyKey);
|
|
313
318
|
}
|
|
314
|
-
this.
|
|
319
|
+
this.emitIngest(context, landed, startedAt);
|
|
315
320
|
return landed;
|
|
316
321
|
})
|
|
317
|
-
.catch((err) => this.failedIngest(context, idempotencyKey, asEngineError('ingest', err)))
|
|
322
|
+
.catch((err) => this.failedIngest(context, idempotencyKey, asEngineError('ingest', err), startedAt))
|
|
318
323
|
.finally(() => {
|
|
319
324
|
this.pending.delete(promise);
|
|
320
325
|
});
|
|
@@ -403,11 +408,22 @@ class GovernanceSession {
|
|
|
403
408
|
this.deps.logger.warn(`[stigg] reported ${names} but the response carried no credit balance - ${names.includes(',') ? 'these features are' : 'this feature is'} not mapped to ${checked}, so the budget this call checks ` +
|
|
404
409
|
'against will never drain. Map it in Stigg, or report to the currency directly.');
|
|
405
410
|
}
|
|
406
|
-
record(decision, context) {
|
|
411
|
+
record(decision, context, startedAt) {
|
|
407
412
|
this.decision = decision;
|
|
413
|
+
// Telemetry is instrumentation exactly as a hook is: it never reaches the caller.
|
|
414
|
+
if (this.deps.tracing) {
|
|
415
|
+
(0, hooks_1.safely)(() => (0, telemetry_1.traceCheck)(context, decision, startedAt), (err) => this.deps.logger.debug(`[stigg] check span failed: ${String(err)}`));
|
|
416
|
+
}
|
|
408
417
|
this.hook('onDecision', context, () => this.deps.hooks.onDecision?.(decision, hookContext(context)));
|
|
409
418
|
return decision;
|
|
410
419
|
}
|
|
420
|
+
/** Every ingest result, however it ended: to its span, then to the application's hook. */
|
|
421
|
+
emitIngest(context, result, startedAt) {
|
|
422
|
+
if (this.deps.tracing) {
|
|
423
|
+
(0, hooks_1.safely)(() => (0, telemetry_1.traceIngest)(context, result, startedAt), (err) => this.deps.logger.debug(`[stigg] ingest span failed: ${String(err)}`));
|
|
424
|
+
}
|
|
425
|
+
this.hook('onIngest', context, () => this.deps.hooks.onIngest?.(result, hookContext(context)));
|
|
426
|
+
}
|
|
411
427
|
/**
|
|
412
428
|
* Hooks are instrumentation, so a throwing one must not reach the caller: it cannot turn an
|
|
413
429
|
* allow into a denial, flip a delivered ingest to `ok: false`, or reject a promise the contract
|
|
@@ -446,7 +462,7 @@ class GovernanceSession {
|
|
|
446
462
|
customerId: context?.customerId,
|
|
447
463
|
}), (err) => this.deps.logger.warn(`[stigg] onError threw while reporting ${error.message}: ${String(err)}`));
|
|
448
464
|
}
|
|
449
|
-
failedIngest(context, idempotencyKey, error) {
|
|
465
|
+
failedIngest(context, idempotencyKey, error, startedAt = Date.now()) {
|
|
450
466
|
this.ingestFailures += 1;
|
|
451
467
|
// The error knows what failed - reading a vendor's usage is not the same event as the report
|
|
452
468
|
// that would have followed it, and a context that says otherwise makes the two
|
|
@@ -472,7 +488,7 @@ class GovernanceSession {
|
|
|
472
488
|
replayed: error.replayed ?? false,
|
|
473
489
|
error,
|
|
474
490
|
};
|
|
475
|
-
this.
|
|
491
|
+
this.emitIngest(context, failure, startedAt);
|
|
476
492
|
return failure;
|
|
477
493
|
}
|
|
478
494
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccessDecision, IngestResult } from '../types';
|
|
2
|
+
import type { ResolvedContext } from './context';
|
|
3
|
+
/** The instrumentation scope every Stigg span is reported under - what a span filter matches on. */
|
|
4
|
+
export declare const TRACER_NAME = "@stigg/agent-sdk-js";
|
|
5
|
+
/** One check, from when it was asked to when it was answered. */
|
|
6
|
+
export declare function traceCheck(context: ResolvedContext, decision: AccessDecision, startedAt: number): void;
|
|
7
|
+
/** One report, whether it landed, failed, or had nothing to send. */
|
|
8
|
+
export declare function traceIngest(context: ResolvedContext, result: IngestResult, startedAt: number): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TRACER_NAME = void 0;
|
|
4
|
+
exports.traceCheck = traceCheck;
|
|
5
|
+
exports.traceIngest = traceIngest;
|
|
6
|
+
const sdk_identity_1 = require("../sdk-identity");
|
|
7
|
+
/** The instrumentation scope every Stigg span is reported under - what a span filter matches on. */
|
|
8
|
+
exports.TRACER_NAME = '@stigg/agent-sdk-js';
|
|
9
|
+
let api;
|
|
10
|
+
/** The application's copy of the API, loaded once - or `null` when it has none installed. */
|
|
11
|
+
function openTelemetry() {
|
|
12
|
+
if (api === undefined) {
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
15
|
+
api = require('@opentelemetry/api');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
api = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return api;
|
|
22
|
+
}
|
|
23
|
+
/** Asked for per span, so a provider registered after this package was loaded is still found. */
|
|
24
|
+
const tracer = (otel) => otel.trace.getTracer(exports.TRACER_NAME, sdk_identity_1.AGENT_SDK_VERSION);
|
|
25
|
+
/** Who the call was for: the attribution, as the check and the report both carry it. */
|
|
26
|
+
function attribution(context) {
|
|
27
|
+
const attributes = {
|
|
28
|
+
'stigg.customer.id': context.customerId,
|
|
29
|
+
'stigg.correlation_id': context.correlationId,
|
|
30
|
+
};
|
|
31
|
+
if (context.agent !== undefined)
|
|
32
|
+
attributes['gen_ai.agent.name'] = context.agent;
|
|
33
|
+
if (context.tool !== undefined)
|
|
34
|
+
attributes['gen_ai.tool.name'] = context.tool;
|
|
35
|
+
if (context.attributionKey && context.dimensions[context.attributionKey] !== undefined) {
|
|
36
|
+
attributes['stigg.agent.id'] = context.dimensions[context.attributionKey];
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of Object.entries(context.dimensions))
|
|
39
|
+
attributes[`stigg.dimension.${key}`] = value;
|
|
40
|
+
if (context.eventName)
|
|
41
|
+
attributes['stigg.event.name'] = context.eventName.name;
|
|
42
|
+
return attributes;
|
|
43
|
+
}
|
|
44
|
+
/** One check, from when it was asked to when it was answered. */
|
|
45
|
+
function traceCheck(context, decision, startedAt) {
|
|
46
|
+
const otel = openTelemetry();
|
|
47
|
+
if (!otel)
|
|
48
|
+
return;
|
|
49
|
+
const attributes = {
|
|
50
|
+
...attribution(context),
|
|
51
|
+
'stigg.decision.has_access': decision.hasAccess,
|
|
52
|
+
'stigg.decision.source': decision.source,
|
|
53
|
+
};
|
|
54
|
+
if (context.checkedId)
|
|
55
|
+
attributes['stigg.entitlement.id'] = context.checkedId;
|
|
56
|
+
if (decision.reason)
|
|
57
|
+
attributes['stigg.decision.reason'] = decision.reason;
|
|
58
|
+
if (decision.deniedBy)
|
|
59
|
+
attributes['stigg.decision.denied_by'] = decision.deniedBy;
|
|
60
|
+
if (decision.remaining !== undefined)
|
|
61
|
+
attributes['stigg.decision.remaining'] = decision.remaining;
|
|
62
|
+
if (decision.remedy)
|
|
63
|
+
attributes['stigg.decision.remedy'] = decision.remedy;
|
|
64
|
+
if (decision.isFallback)
|
|
65
|
+
attributes['stigg.decision.is_fallback'] = true;
|
|
66
|
+
const span = tracer(otel).startSpan('stigg.check', { startTime: startedAt, attributes });
|
|
67
|
+
if (!decision.hasAccess) {
|
|
68
|
+
span.recordException({ name: 'AccessDeniedError', message: decision.message });
|
|
69
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message: decision.summary });
|
|
70
|
+
}
|
|
71
|
+
span.end();
|
|
72
|
+
}
|
|
73
|
+
/** One report, whether it landed, failed, or had nothing to send. */
|
|
74
|
+
function traceIngest(context, result, startedAt) {
|
|
75
|
+
const otel = openTelemetry();
|
|
76
|
+
if (!otel)
|
|
77
|
+
return;
|
|
78
|
+
const attributes = {
|
|
79
|
+
...attribution(context),
|
|
80
|
+
'stigg.ingest.ok': result.ok,
|
|
81
|
+
'stigg.ingest.mode': result.mode,
|
|
82
|
+
'stigg.ingest.idempotency_key': result.idempotencyKey,
|
|
83
|
+
};
|
|
84
|
+
for (const [metric, value] of Object.entries(result.reported))
|
|
85
|
+
attributes[`stigg.metric.${metric}`] = value;
|
|
86
|
+
if (result.creditsConsumed !== undefined)
|
|
87
|
+
attributes['stigg.ingest.credits_consumed'] = result.creditsConsumed;
|
|
88
|
+
if (result.replayed !== undefined)
|
|
89
|
+
attributes['stigg.ingest.replayed'] = result.replayed;
|
|
90
|
+
const span = tracer(otel).startSpan('stigg.ingest', { startTime: startedAt, attributes });
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
const message = result.error instanceof Error ? result.error.message : String(result.error ?? 'ingest failed');
|
|
93
|
+
span.recordException({ name: 'EngineError', message });
|
|
94
|
+
span.setStatus({ code: otel.SpanStatusCode.ERROR, message });
|
|
95
|
+
}
|
|
96
|
+
span.end();
|
|
97
|
+
}
|
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
|
})[];
|
|
@@ -242,6 +242,20 @@ export declare class StiggAgentSDK {
|
|
|
242
242
|
* see {@link GovernanceSession.warmUp} for which cache that is and when there is one at all.
|
|
243
243
|
*/
|
|
244
244
|
wrap<T extends object, O extends ClientOptions>(client: T, options?: O): WrappedClient<T, O['stiggContext'], PathsOf<O['recipe'], T>>;
|
|
245
|
+
/**
|
|
246
|
+
* An agent graph's governance middleware - the orchestration counterpart to `wrap`.
|
|
247
|
+
*
|
|
248
|
+
* There is no single client to gate in a graph: it is several agents, each spending against its
|
|
249
|
+
* own entitlement. So the graph is configured once - the shared context, and each agent's own
|
|
250
|
+
* under `agents` - and every step it takes is checked and billed as the agent that took it. A
|
|
251
|
+
* refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
|
|
252
|
+
* run is the graph's to decide.
|
|
253
|
+
*
|
|
254
|
+
* What comes back is framework-neutral: the start and end of each model call and tool. A
|
|
255
|
+
* framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
|
|
256
|
+
* `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
|
|
257
|
+
*/
|
|
258
|
+
middleware(options?: MiddlewareOptions): GovernedAgentMiddleware;
|
|
245
259
|
/**
|
|
246
260
|
* Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
|
|
247
261
|
* 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
|
|
@@ -398,6 +399,43 @@ class StiggAgentSDK {
|
|
|
398
399
|
void session.warmUp(context);
|
|
399
400
|
return governed;
|
|
400
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* An agent graph's governance middleware - the orchestration counterpart to `wrap`.
|
|
404
|
+
*
|
|
405
|
+
* There is no single client to gate in a graph: it is several agents, each spending against its
|
|
406
|
+
* own entitlement. So the graph is configured once - the shared context, and each agent's own
|
|
407
|
+
* under `agents` - and every step it takes is checked and billed as the agent that took it. A
|
|
408
|
+
* refusal is thrown as `AccessDeniedError` from inside the refused agent; what that means for the
|
|
409
|
+
* run is the graph's to decide.
|
|
410
|
+
*
|
|
411
|
+
* What comes back is framework-neutral: the start and end of each model call and tool. A
|
|
412
|
+
* framework binding drives it - `new StiggLangChainCallbackHandler({ stigg, ...options })` from
|
|
413
|
+
* `@stigg/agent-sdk-js/langchain` builds one of these - and so can a harness of your own.
|
|
414
|
+
*/
|
|
415
|
+
middleware(options = {}) {
|
|
416
|
+
// Started, not awaited, and caught - for the reasons `wrap` gives.
|
|
417
|
+
void this.ensureStarted().catch((err) => this.reportStartupFailure(err));
|
|
418
|
+
const shared = (0, context_1.resolveContext)(options.stiggContext ?? {}, (0, node_crypto_1.randomUUID)(), {
|
|
419
|
+
requireCustomer: false,
|
|
420
|
+
defaults: this.contextDefaults,
|
|
421
|
+
});
|
|
422
|
+
const { shared: sharedAgent, agents } = (0, agent_middleware_1.resolveAgents)(shared, options);
|
|
423
|
+
// Spans for the observability tool already tracing the graph - out of the box, for orchestrators only.
|
|
424
|
+
const session = this.newSession(options.hooks, { tracing: true });
|
|
425
|
+
const middleware = (0, agent_middleware_1.createAgentMiddleware)(session, {
|
|
426
|
+
shared: sharedAgent,
|
|
427
|
+
agents,
|
|
428
|
+
onUnlisted: (agent) => this.config.logger.warn(`[stigg] agent "${agent}" is not listed in the middleware's \`agents\`, so its calls are ` +
|
|
429
|
+
'governed by the shared `stiggContext` alone - at the customer level unless that context ' +
|
|
430
|
+
'names an agent. List it to govern it as itself.'),
|
|
431
|
+
missingCustomer: options.missingCustomer ??
|
|
432
|
+
'Name one per run, or give the middleware a default in `stiggContext: { customerId }`.',
|
|
433
|
+
});
|
|
434
|
+
this.attachGovernance(middleware, session, shared);
|
|
435
|
+
if (shared.customerId)
|
|
436
|
+
void session.warmUp(shared);
|
|
437
|
+
return middleware;
|
|
438
|
+
}
|
|
401
439
|
/**
|
|
402
440
|
* Hangs the governance handle off the wrapped client. Refuses to overwrite a `stigg` the vendor
|
|
403
441
|
* already has: silently replacing one of its own properties would be a worse surprise.
|
|
@@ -457,8 +495,9 @@ class StiggAgentSDK {
|
|
|
457
495
|
get contextDefaults() {
|
|
458
496
|
return { checkErrorAction: this.config.checkErrorAction, ingestMode: this.config.ingest };
|
|
459
497
|
}
|
|
460
|
-
newSession(hooks) {
|
|
498
|
+
newSession(hooks, { tracing = false } = {}) {
|
|
461
499
|
const session = new session_1.GovernanceSession({
|
|
500
|
+
tracing,
|
|
462
501
|
engine: () => this.ensureStarted().then((state) => state.engine),
|
|
463
502
|
provisionCustomer: (customerId, planId) => this.provisionCustomer(customerId, planId),
|
|
464
503
|
logger: this.config.logger,
|
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
|
*
|