@statelyai/agent 2.0.0-alpha.11 → 2.0.0-alpha.13
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/dist/ai-sdk.cjs +4 -5
- package/dist/ai-sdk.d.cts +7 -4
- package/dist/ai-sdk.d.mts +7 -4
- package/dist/ai-sdk.mjs +1 -2
- package/dist/{events-JiVPYrct.mjs → decision-BezSD_YC.mjs} +327 -20
- package/dist/{events-CRQj3VtP.cjs → decision-dWGhBh0P.cjs} +401 -28
- package/dist/errors-BQRk9eiZ.d.cts +19 -0
- package/dist/errors-C9rxnWbX.d.mts +19 -0
- package/dist/errors-CeSXQx0v.mjs +23 -0
- package/dist/errors-DUBBzRLP.cjs +28 -0
- package/dist/event-log-store-CNT_7F0V.cjs +452 -0
- package/dist/event-log-store-CriMgX1D.d.mts +144 -0
- package/dist/event-log-store-D7pWtIhb.mjs +411 -0
- package/dist/event-log-store-Ruq18mGp.d.cts +144 -0
- package/dist/index.cjs +1050 -705
- package/dist/index.d.cts +538 -565
- package/dist/index.d.mts +538 -565
- package/dist/index.mjs +950 -644
- package/dist/machines.cjs +752 -0
- package/dist/machines.d.cts +372 -0
- package/dist/machines.d.mts +372 -0
- package/dist/machines.mjs +741 -0
- package/dist/otel.cjs +268 -0
- package/dist/otel.d.cts +67 -0
- package/dist/otel.d.mts +67 -0
- package/dist/otel.mjs +267 -0
- package/dist/run-agent-C3mFDGTf.d.mts +1111 -0
- package/dist/run-agent-DnvtcnTZ.d.cts +1111 -0
- package/dist/setup-agent-DAZZSjDS.mjs +1711 -0
- package/dist/setup-agent-DP95MFrI.cjs +1836 -0
- package/dist/sqlite.cjs +135 -0
- package/dist/sqlite.d.cts +57 -0
- package/dist/sqlite.d.mts +57 -0
- package/dist/sqlite.mjs +133 -0
- package/dist/{text-logic-CaKqgX4Y.d.mts → text-logic-BDxwQNsD.d.cts} +155 -72
- package/dist/{text-logic-Ckhr2kKC.d.cts → text-logic-TkKPw8Aq.d.mts} +155 -72
- package/dist/{types-qm00QF91.d.mts → types-QbEfCVny.d.cts} +1 -1
- package/dist/{types-C9QiMjre.d.cts → types-_FXoFBGO.d.mts} +1 -1
- package/package.json +47 -39
- package/readme.md +49 -12
- package/schemas/agent-workflow.json +40 -21
- package/skills/generate-machine/SKILL.md +267 -0
- package/dist/adapter.cjs +0 -15
- package/dist/adapter.d.cts +0 -4
- package/dist/adapter.d.mts +0 -4
- package/dist/adapter.mjs +0 -2
- package/dist/decision-C3k4ve51.mjs +0 -227
- package/dist/decision-D8wJrM8W.cjs +0 -286
- package/dist/openai-compat.cjs +0 -309
- package/dist/openai-compat.d.cts +0 -59
- package/dist/openai-compat.d.mts +0 -59
- package/dist/openai-compat.mjs +0 -308
- package/dist/steps-BALp1eZo.d.mts +0 -198
- package/dist/steps-CVe54GPP.cjs +0 -420
- package/dist/steps-CkyyyuHd.mjs +0 -379
- package/dist/steps-MjnQI4aB.d.cts +0 -198
- package/dist/steps.cjs +0 -12
- package/dist/steps.d.cts +0 -3
- package/dist/steps.d.mts +0 -3
- package/dist/steps.mjs +0 -3
- package/dist/utils-BYqT_Dyv.d.cts +0 -108
- package/dist/utils-Do5wIJrh.d.mts +0 -108
- package/dist/zod.cjs +0 -31
- package/dist/zod.d.cts +0 -30
- package/dist/zod.d.mts +0 -30
- package/dist/zod.mjs +0 -30
package/dist/otel.cjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _opentelemetry_api = require("@opentelemetry/api");
|
|
3
|
+
//#region src/otel/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* OpenTelemetry bridge — maps the versioned `AgentTraceEvent` stream onto
|
|
6
|
+
* GenAI-semconv spans.
|
|
7
|
+
*
|
|
8
|
+
* `createOtelTraceHandler({ tracer })` returns a plain `onTrace` handler, so
|
|
9
|
+
* every OTLP-ingesting backend (Braintrust, Langfuse, LangSmith, Honeycomb,
|
|
10
|
+
* Datadog, Grafana Tempo) is an endpoint + headers away. The bridge is the
|
|
11
|
+
* MAPPING only: it ships no exporter and owns no SDK lifecycle. You bring the
|
|
12
|
+
* `Tracer` from your own OpenTelemetry setup.
|
|
13
|
+
*
|
|
14
|
+
* `@opentelemetry/api` (^1) is an optional peer dependency — installed only by
|
|
15
|
+
* apps that import this subpath.
|
|
16
|
+
*/
|
|
17
|
+
/** Instrumentation scope name used when the handler resolves its own tracer. */
|
|
18
|
+
const TRACER_NAME = "@statelyai/agent";
|
|
19
|
+
/** `AgentCallUsage` → `gen_ai.usage.*`. `totalTokens` has no semconv key. */
|
|
20
|
+
const USAGE_ATTRIBUTES = {
|
|
21
|
+
inputTokens: "gen_ai.usage.input_tokens",
|
|
22
|
+
outputTokens: "gen_ai.usage.output_tokens",
|
|
23
|
+
reasoningTokens: "gen_ai.usage.reasoning.output_tokens",
|
|
24
|
+
cachedInputTokens: "gen_ai.usage.cache_read.input_tokens",
|
|
25
|
+
totalTokens: "agent.usage.total_tokens"
|
|
26
|
+
};
|
|
27
|
+
function usageAttributes(usage) {
|
|
28
|
+
const attributes = {};
|
|
29
|
+
if (!usage) return attributes;
|
|
30
|
+
for (const [field, key] of Object.entries(USAGE_ATTRIBUTES)) {
|
|
31
|
+
const value = usage[field];
|
|
32
|
+
if (typeof value === "number") attributes[key] = value;
|
|
33
|
+
}
|
|
34
|
+
return attributes;
|
|
35
|
+
}
|
|
36
|
+
/** The model ref a request targets: text/plan carry it on `input`, a decision inline. */
|
|
37
|
+
function requestModel(request) {
|
|
38
|
+
const model = request.kind === "decision" ? request.model : request.input?.model;
|
|
39
|
+
return typeof model === "string" ? model : void 0;
|
|
40
|
+
}
|
|
41
|
+
/** The `src` a text/plan invoke was declared with; a decision has none. */
|
|
42
|
+
function requestSrc(request) {
|
|
43
|
+
return "src" in request && typeof request.src === "string" ? request.src : void 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `gen_ai.operation.name` for a request. A plan request drives the machine
|
|
47
|
+
* through a sequence of events, which semconv names `plan`; text and decision
|
|
48
|
+
* requests are both single chat completions.
|
|
49
|
+
*/
|
|
50
|
+
function operationName(request) {
|
|
51
|
+
return request.kind === "plan" ? "plan" : "chat";
|
|
52
|
+
}
|
|
53
|
+
/** Best-effort JSON, never throws — content capture must not break a run. */
|
|
54
|
+
function toJson(value) {
|
|
55
|
+
try {
|
|
56
|
+
const json = JSON.stringify(value);
|
|
57
|
+
return typeof json === "string" ? json : void 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function errorType(error) {
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
const code = error.code;
|
|
65
|
+
return typeof code === "string" ? code : error.name;
|
|
66
|
+
}
|
|
67
|
+
return typeof error;
|
|
68
|
+
}
|
|
69
|
+
/** `recordException` wants an `Exception`; trace errors are `unknown`. */
|
|
70
|
+
function toException(error) {
|
|
71
|
+
return error instanceof Error ? error : {
|
|
72
|
+
name: "Error",
|
|
73
|
+
message: String(error)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds an `onTrace` handler that maps the trace stream onto OpenTelemetry
|
|
78
|
+
* GenAI spans: one `invoke_agent` span per run, one child span per model call,
|
|
79
|
+
* transitions and emissions as span events.
|
|
80
|
+
*
|
|
81
|
+
* Works on both paths. On the controlled path (`runAgent`) the run span opens
|
|
82
|
+
* on `run.start` and closes on `run.end`. On the uncontrolled path
|
|
83
|
+
* (`provideExecutors` + `traceTransitions`) there is no run boundary, so the
|
|
84
|
+
* run span opens lazily on the first event of a `runId` and stays open until
|
|
85
|
+
* you call `dispose()` — always dispose when the actor stops.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import { trace } from '@opentelemetry/api';
|
|
90
|
+
* import { createOtelTraceHandler } from '@statelyai/agent/otel';
|
|
91
|
+
*
|
|
92
|
+
* const onTrace = createOtelTraceHandler({ tracer: trace.getTracer('my-app') });
|
|
93
|
+
* try {
|
|
94
|
+
* await runAgent(machine, { input, executors, onTrace });
|
|
95
|
+
* } finally {
|
|
96
|
+
* onTrace.dispose();
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
function createOtelTraceHandler(options) {
|
|
101
|
+
const tracer = options.tracer ?? options.tracerProvider?.getTracer(TRACER_NAME);
|
|
102
|
+
if (!tracer) throw new TypeError("createOtelTraceHandler: pass a `tracer` (or a `tracerProvider` to take one from).");
|
|
103
|
+
const baseAttributes = options.attributes ?? {};
|
|
104
|
+
const runs = /* @__PURE__ */ new Map();
|
|
105
|
+
const requests = /* @__PURE__ */ new Map();
|
|
106
|
+
/**
|
|
107
|
+
* The run span for an event, created on first sight. Lazy rather than
|
|
108
|
+
* `run.start`-only so the uncontrolled path (no run boundary) and any
|
|
109
|
+
* out-of-order delivery still produce a well-formed tree.
|
|
110
|
+
*/
|
|
111
|
+
const runFor = (event) => {
|
|
112
|
+
const existing = runs.get(event.runId);
|
|
113
|
+
if (existing) return existing;
|
|
114
|
+
const agentName = options.agentName ?? event.machineId;
|
|
115
|
+
const span = tracer.startSpan(agentName ? `invoke_agent ${agentName}` : "invoke_agent", {
|
|
116
|
+
kind: _opentelemetry_api.SpanKind.INTERNAL,
|
|
117
|
+
attributes: {
|
|
118
|
+
...baseAttributes,
|
|
119
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
120
|
+
"gen_ai.agent.name": agentName,
|
|
121
|
+
"gen_ai.agent.version": event.machineVersion,
|
|
122
|
+
"agent.run_id": event.runId,
|
|
123
|
+
"agent.machine_id": event.machineId,
|
|
124
|
+
"agent.machine_version": event.machineVersion,
|
|
125
|
+
"agent.trace_schema_version": event.schemaVersion
|
|
126
|
+
}
|
|
127
|
+
}, _opentelemetry_api.context.active());
|
|
128
|
+
const state = {
|
|
129
|
+
span,
|
|
130
|
+
context: _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span)
|
|
131
|
+
};
|
|
132
|
+
runs.set(event.runId, state);
|
|
133
|
+
return state;
|
|
134
|
+
};
|
|
135
|
+
/** Span events all carry `seq`, so a consumer can re-order them downstream. */
|
|
136
|
+
const addRunEvent = (event, name, attributes) => {
|
|
137
|
+
runFor(event).span.addEvent(name, {
|
|
138
|
+
"agent.seq": event.seq,
|
|
139
|
+
...attributes
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const endRequest = (id, finish) => {
|
|
143
|
+
const state = requests.get(id);
|
|
144
|
+
if (!state) return;
|
|
145
|
+
requests.delete(id);
|
|
146
|
+
if (state.chunks > 0) state.span.setAttribute("agent.stream_chunks", state.chunks);
|
|
147
|
+
finish(state.span);
|
|
148
|
+
state.span.end();
|
|
149
|
+
};
|
|
150
|
+
const handler = (event) => {
|
|
151
|
+
switch (event.type) {
|
|
152
|
+
case "run.start": {
|
|
153
|
+
const { span } = runFor(event);
|
|
154
|
+
if (options.captureContent && event.input !== void 0) {
|
|
155
|
+
const input = toJson(event.input);
|
|
156
|
+
if (input !== void 0) span.setAttribute("agent.input", input);
|
|
157
|
+
}
|
|
158
|
+
if (event.snapshot) span.setAttribute("agent.resumed", true);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case "request.start": {
|
|
162
|
+
const request = event.request;
|
|
163
|
+
endRequest(request.id, () => {});
|
|
164
|
+
const model = requestModel(request);
|
|
165
|
+
const operation = operationName(request);
|
|
166
|
+
const attributes = {
|
|
167
|
+
...baseAttributes,
|
|
168
|
+
"gen_ai.operation.name": operation,
|
|
169
|
+
"agent.run_id": event.runId,
|
|
170
|
+
"agent.request_id": request.id,
|
|
171
|
+
"agent.request_kind": request.kind
|
|
172
|
+
};
|
|
173
|
+
if (model !== void 0) attributes["gen_ai.request.model"] = model;
|
|
174
|
+
if (options.providerName !== void 0) attributes["gen_ai.provider.name"] = options.providerName;
|
|
175
|
+
const src = requestSrc(request);
|
|
176
|
+
if (src !== void 0) attributes["agent.request_src"] = src;
|
|
177
|
+
if (request.kind === "decision" && request.attempts.length > 0) attributes["agent.request_attempt"] = request.attempts.length;
|
|
178
|
+
if (options.captureContent) {
|
|
179
|
+
const input = request.kind === "decision" ? request : request.input;
|
|
180
|
+
const system = input.system;
|
|
181
|
+
if (typeof system === "string") attributes["gen_ai.system_instructions"] = system;
|
|
182
|
+
const json = toJson(input.messages ?? input.prompt);
|
|
183
|
+
if (json !== void 0) attributes["gen_ai.input.messages"] = json;
|
|
184
|
+
}
|
|
185
|
+
const name = operation === "plan" ? `plan ${options.agentName ?? event.machineId}` : `chat ${model ?? request.kind}`;
|
|
186
|
+
const span = tracer.startSpan(name, {
|
|
187
|
+
kind: operation === "plan" ? _opentelemetry_api.SpanKind.INTERNAL : _opentelemetry_api.SpanKind.CLIENT,
|
|
188
|
+
attributes
|
|
189
|
+
}, runFor(event).context);
|
|
190
|
+
requests.set(request.id, {
|
|
191
|
+
span,
|
|
192
|
+
chunks: 0
|
|
193
|
+
});
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "stream.chunk": {
|
|
197
|
+
const state = requests.get(event.request.id);
|
|
198
|
+
if (state) state.chunks += 1;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "request.end":
|
|
202
|
+
endRequest(event.request.id, (span) => {
|
|
203
|
+
span.setAttributes(usageAttributes(event.usage));
|
|
204
|
+
span.setAttribute("agent.output_length", toJson(event.output ?? "")?.length ?? 0);
|
|
205
|
+
if (options.captureContent) {
|
|
206
|
+
const output = toJson(event.output);
|
|
207
|
+
if (output !== void 0) span.setAttribute("gen_ai.output.messages", output);
|
|
208
|
+
}
|
|
209
|
+
span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
|
|
210
|
+
});
|
|
211
|
+
break;
|
|
212
|
+
case "request.error":
|
|
213
|
+
endRequest(event.request.id, (span) => {
|
|
214
|
+
span.recordException(toException(event.error));
|
|
215
|
+
span.setAttribute("error.type", errorType(event.error));
|
|
216
|
+
span.setStatus({
|
|
217
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
218
|
+
message: event.error instanceof Error ? event.error.message : void 0
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
case "machine.transition": {
|
|
223
|
+
const attributes = {
|
|
224
|
+
"agent.event_type": event.event.type,
|
|
225
|
+
"agent.state": toJson(event.snapshot.value) ?? ""
|
|
226
|
+
};
|
|
227
|
+
if (event.eventId !== void 0) attributes["agent.event_id"] = event.eventId;
|
|
228
|
+
addRunEvent(event, "agent.transition", attributes);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
case "emit":
|
|
232
|
+
addRunEvent(event, "agent.emit", { "agent.event_type": event.event.type });
|
|
233
|
+
break;
|
|
234
|
+
case "usage.dropped": {
|
|
235
|
+
const dropped = event.event;
|
|
236
|
+
addRunEvent(event, "agent.usage.dropped", {
|
|
237
|
+
"agent.drop_reason": event.reason,
|
|
238
|
+
...usageAttributes(dropped.usage)
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "run.end": {
|
|
243
|
+
const { span } = runFor(event);
|
|
244
|
+
runs.delete(event.runId);
|
|
245
|
+
span.setAttribute("agent.status", event.status);
|
|
246
|
+
if (event.status === "error") {
|
|
247
|
+
span.recordException(toException(event.error));
|
|
248
|
+
span.setAttribute("error.type", errorType(event.error));
|
|
249
|
+
span.setAttribute("agent.error_cause", event.cause);
|
|
250
|
+
span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
|
|
251
|
+
} else span.setStatus({ code: _opentelemetry_api.SpanStatusCode.OK });
|
|
252
|
+
span.end();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
handler.dispose = () => {
|
|
258
|
+
for (const [id] of requests) endRequest(id, (span) => span.setAttribute("agent.unfinished", true));
|
|
259
|
+
for (const { span } of runs.values()) {
|
|
260
|
+
span.setAttribute("agent.unfinished", true);
|
|
261
|
+
span.end();
|
|
262
|
+
}
|
|
263
|
+
runs.clear();
|
|
264
|
+
};
|
|
265
|
+
return handler;
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
exports.createOtelTraceHandler = createOtelTraceHandler;
|
package/dist/otel.d.cts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { c as AgentTraceEvent } from "./run-agent-DnvtcnTZ.cjs";
|
|
2
|
+
import { Attributes, Tracer, TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
//#region src/otel/index.d.ts
|
|
5
|
+
/** Options for {@link createOtelTraceHandler}. */
|
|
6
|
+
interface OtelTraceHandlerOptions {
|
|
7
|
+
/** The tracer to record with. Pass this OR {@link tracerProvider}. */
|
|
8
|
+
tracer?: Tracer;
|
|
9
|
+
/** A provider to take a `@statelyai/agent`-scoped tracer from. */
|
|
10
|
+
tracerProvider?: TracerProvider;
|
|
11
|
+
/**
|
|
12
|
+
* `gen_ai.provider.name` for model-call spans (`openai`, `anthropic`,
|
|
13
|
+
* `aws.bedrock`, …). Semconv requires it on inference spans, but the trace
|
|
14
|
+
* stream only carries a model ref, so the bridge cannot infer it — set it
|
|
15
|
+
* when your run targets one provider, leave it off otherwise.
|
|
16
|
+
*/
|
|
17
|
+
providerName?: string;
|
|
18
|
+
/** `gen_ai.agent.name`, and the run span's name. Defaults to the machine's `id`. */
|
|
19
|
+
agentName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Record prompts and outputs on spans (`gen_ai.input.messages`,
|
|
22
|
+
* `gen_ai.output.messages`, `gen_ai.system_instructions`), JSON-stringified.
|
|
23
|
+
* Off by default: semconv marks message content Opt-In, and bodies are large
|
|
24
|
+
* and frequently sensitive. Sizes (`agent.output_length`) are always recorded.
|
|
25
|
+
*/
|
|
26
|
+
captureContent?: boolean;
|
|
27
|
+
/** Extra attributes set on every span the bridge creates. */
|
|
28
|
+
attributes?: Attributes;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An `onTrace` handler that writes OpenTelemetry spans, plus `dispose()` to
|
|
32
|
+
* close any span still open (a run that never emitted `run.end`).
|
|
33
|
+
*/
|
|
34
|
+
type OtelTraceHandler = ((event: AgentTraceEvent) => void) & {
|
|
35
|
+
/**
|
|
36
|
+
* Ends every span the handler still holds open and drops its state. Idempotent.
|
|
37
|
+
* Required on the uncontrolled path, where no `run.end` ever arrives.
|
|
38
|
+
*/
|
|
39
|
+
dispose(): void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Builds an `onTrace` handler that maps the trace stream onto OpenTelemetry
|
|
43
|
+
* GenAI spans: one `invoke_agent` span per run, one child span per model call,
|
|
44
|
+
* transitions and emissions as span events.
|
|
45
|
+
*
|
|
46
|
+
* Works on both paths. On the controlled path (`runAgent`) the run span opens
|
|
47
|
+
* on `run.start` and closes on `run.end`. On the uncontrolled path
|
|
48
|
+
* (`provideExecutors` + `traceTransitions`) there is no run boundary, so the
|
|
49
|
+
* run span opens lazily on the first event of a `runId` and stays open until
|
|
50
|
+
* you call `dispose()` — always dispose when the actor stops.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { trace } from '@opentelemetry/api';
|
|
55
|
+
* import { createOtelTraceHandler } from '@statelyai/agent/otel';
|
|
56
|
+
*
|
|
57
|
+
* const onTrace = createOtelTraceHandler({ tracer: trace.getTracer('my-app') });
|
|
58
|
+
* try {
|
|
59
|
+
* await runAgent(machine, { input, executors, onTrace });
|
|
60
|
+
* } finally {
|
|
61
|
+
* onTrace.dispose();
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function createOtelTraceHandler(options: OtelTraceHandlerOptions): OtelTraceHandler;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { OtelTraceHandler, OtelTraceHandlerOptions, createOtelTraceHandler };
|
package/dist/otel.d.mts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { c as AgentTraceEvent } from "./run-agent-C3mFDGTf.mjs";
|
|
2
|
+
import { Attributes, Tracer, TracerProvider } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
//#region src/otel/index.d.ts
|
|
5
|
+
/** Options for {@link createOtelTraceHandler}. */
|
|
6
|
+
interface OtelTraceHandlerOptions {
|
|
7
|
+
/** The tracer to record with. Pass this OR {@link tracerProvider}. */
|
|
8
|
+
tracer?: Tracer;
|
|
9
|
+
/** A provider to take a `@statelyai/agent`-scoped tracer from. */
|
|
10
|
+
tracerProvider?: TracerProvider;
|
|
11
|
+
/**
|
|
12
|
+
* `gen_ai.provider.name` for model-call spans (`openai`, `anthropic`,
|
|
13
|
+
* `aws.bedrock`, …). Semconv requires it on inference spans, but the trace
|
|
14
|
+
* stream only carries a model ref, so the bridge cannot infer it — set it
|
|
15
|
+
* when your run targets one provider, leave it off otherwise.
|
|
16
|
+
*/
|
|
17
|
+
providerName?: string;
|
|
18
|
+
/** `gen_ai.agent.name`, and the run span's name. Defaults to the machine's `id`. */
|
|
19
|
+
agentName?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Record prompts and outputs on spans (`gen_ai.input.messages`,
|
|
22
|
+
* `gen_ai.output.messages`, `gen_ai.system_instructions`), JSON-stringified.
|
|
23
|
+
* Off by default: semconv marks message content Opt-In, and bodies are large
|
|
24
|
+
* and frequently sensitive. Sizes (`agent.output_length`) are always recorded.
|
|
25
|
+
*/
|
|
26
|
+
captureContent?: boolean;
|
|
27
|
+
/** Extra attributes set on every span the bridge creates. */
|
|
28
|
+
attributes?: Attributes;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An `onTrace` handler that writes OpenTelemetry spans, plus `dispose()` to
|
|
32
|
+
* close any span still open (a run that never emitted `run.end`).
|
|
33
|
+
*/
|
|
34
|
+
type OtelTraceHandler = ((event: AgentTraceEvent) => void) & {
|
|
35
|
+
/**
|
|
36
|
+
* Ends every span the handler still holds open and drops its state. Idempotent.
|
|
37
|
+
* Required on the uncontrolled path, where no `run.end` ever arrives.
|
|
38
|
+
*/
|
|
39
|
+
dispose(): void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Builds an `onTrace` handler that maps the trace stream onto OpenTelemetry
|
|
43
|
+
* GenAI spans: one `invoke_agent` span per run, one child span per model call,
|
|
44
|
+
* transitions and emissions as span events.
|
|
45
|
+
*
|
|
46
|
+
* Works on both paths. On the controlled path (`runAgent`) the run span opens
|
|
47
|
+
* on `run.start` and closes on `run.end`. On the uncontrolled path
|
|
48
|
+
* (`provideExecutors` + `traceTransitions`) there is no run boundary, so the
|
|
49
|
+
* run span opens lazily on the first event of a `runId` and stays open until
|
|
50
|
+
* you call `dispose()` — always dispose when the actor stops.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { trace } from '@opentelemetry/api';
|
|
55
|
+
* import { createOtelTraceHandler } from '@statelyai/agent/otel';
|
|
56
|
+
*
|
|
57
|
+
* const onTrace = createOtelTraceHandler({ tracer: trace.getTracer('my-app') });
|
|
58
|
+
* try {
|
|
59
|
+
* await runAgent(machine, { input, executors, onTrace });
|
|
60
|
+
* } finally {
|
|
61
|
+
* onTrace.dispose();
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function createOtelTraceHandler(options: OtelTraceHandlerOptions): OtelTraceHandler;
|
|
66
|
+
//#endregion
|
|
67
|
+
export { OtelTraceHandler, OtelTraceHandlerOptions, createOtelTraceHandler };
|
package/dist/otel.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
|
|
2
|
+
//#region src/otel/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* OpenTelemetry bridge — maps the versioned `AgentTraceEvent` stream onto
|
|
5
|
+
* GenAI-semconv spans.
|
|
6
|
+
*
|
|
7
|
+
* `createOtelTraceHandler({ tracer })` returns a plain `onTrace` handler, so
|
|
8
|
+
* every OTLP-ingesting backend (Braintrust, Langfuse, LangSmith, Honeycomb,
|
|
9
|
+
* Datadog, Grafana Tempo) is an endpoint + headers away. The bridge is the
|
|
10
|
+
* MAPPING only: it ships no exporter and owns no SDK lifecycle. You bring the
|
|
11
|
+
* `Tracer` from your own OpenTelemetry setup.
|
|
12
|
+
*
|
|
13
|
+
* `@opentelemetry/api` (^1) is an optional peer dependency — installed only by
|
|
14
|
+
* apps that import this subpath.
|
|
15
|
+
*/
|
|
16
|
+
/** Instrumentation scope name used when the handler resolves its own tracer. */
|
|
17
|
+
const TRACER_NAME = "@statelyai/agent";
|
|
18
|
+
/** `AgentCallUsage` → `gen_ai.usage.*`. `totalTokens` has no semconv key. */
|
|
19
|
+
const USAGE_ATTRIBUTES = {
|
|
20
|
+
inputTokens: "gen_ai.usage.input_tokens",
|
|
21
|
+
outputTokens: "gen_ai.usage.output_tokens",
|
|
22
|
+
reasoningTokens: "gen_ai.usage.reasoning.output_tokens",
|
|
23
|
+
cachedInputTokens: "gen_ai.usage.cache_read.input_tokens",
|
|
24
|
+
totalTokens: "agent.usage.total_tokens"
|
|
25
|
+
};
|
|
26
|
+
function usageAttributes(usage) {
|
|
27
|
+
const attributes = {};
|
|
28
|
+
if (!usage) return attributes;
|
|
29
|
+
for (const [field, key] of Object.entries(USAGE_ATTRIBUTES)) {
|
|
30
|
+
const value = usage[field];
|
|
31
|
+
if (typeof value === "number") attributes[key] = value;
|
|
32
|
+
}
|
|
33
|
+
return attributes;
|
|
34
|
+
}
|
|
35
|
+
/** The model ref a request targets: text/plan carry it on `input`, a decision inline. */
|
|
36
|
+
function requestModel(request) {
|
|
37
|
+
const model = request.kind === "decision" ? request.model : request.input?.model;
|
|
38
|
+
return typeof model === "string" ? model : void 0;
|
|
39
|
+
}
|
|
40
|
+
/** The `src` a text/plan invoke was declared with; a decision has none. */
|
|
41
|
+
function requestSrc(request) {
|
|
42
|
+
return "src" in request && typeof request.src === "string" ? request.src : void 0;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `gen_ai.operation.name` for a request. A plan request drives the machine
|
|
46
|
+
* through a sequence of events, which semconv names `plan`; text and decision
|
|
47
|
+
* requests are both single chat completions.
|
|
48
|
+
*/
|
|
49
|
+
function operationName(request) {
|
|
50
|
+
return request.kind === "plan" ? "plan" : "chat";
|
|
51
|
+
}
|
|
52
|
+
/** Best-effort JSON, never throws — content capture must not break a run. */
|
|
53
|
+
function toJson(value) {
|
|
54
|
+
try {
|
|
55
|
+
const json = JSON.stringify(value);
|
|
56
|
+
return typeof json === "string" ? json : void 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function errorType(error) {
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
const code = error.code;
|
|
64
|
+
return typeof code === "string" ? code : error.name;
|
|
65
|
+
}
|
|
66
|
+
return typeof error;
|
|
67
|
+
}
|
|
68
|
+
/** `recordException` wants an `Exception`; trace errors are `unknown`. */
|
|
69
|
+
function toException(error) {
|
|
70
|
+
return error instanceof Error ? error : {
|
|
71
|
+
name: "Error",
|
|
72
|
+
message: String(error)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds an `onTrace` handler that maps the trace stream onto OpenTelemetry
|
|
77
|
+
* GenAI spans: one `invoke_agent` span per run, one child span per model call,
|
|
78
|
+
* transitions and emissions as span events.
|
|
79
|
+
*
|
|
80
|
+
* Works on both paths. On the controlled path (`runAgent`) the run span opens
|
|
81
|
+
* on `run.start` and closes on `run.end`. On the uncontrolled path
|
|
82
|
+
* (`provideExecutors` + `traceTransitions`) there is no run boundary, so the
|
|
83
|
+
* run span opens lazily on the first event of a `runId` and stays open until
|
|
84
|
+
* you call `dispose()` — always dispose when the actor stops.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* import { trace } from '@opentelemetry/api';
|
|
89
|
+
* import { createOtelTraceHandler } from '@statelyai/agent/otel';
|
|
90
|
+
*
|
|
91
|
+
* const onTrace = createOtelTraceHandler({ tracer: trace.getTracer('my-app') });
|
|
92
|
+
* try {
|
|
93
|
+
* await runAgent(machine, { input, executors, onTrace });
|
|
94
|
+
* } finally {
|
|
95
|
+
* onTrace.dispose();
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
function createOtelTraceHandler(options) {
|
|
100
|
+
const tracer = options.tracer ?? options.tracerProvider?.getTracer(TRACER_NAME);
|
|
101
|
+
if (!tracer) throw new TypeError("createOtelTraceHandler: pass a `tracer` (or a `tracerProvider` to take one from).");
|
|
102
|
+
const baseAttributes = options.attributes ?? {};
|
|
103
|
+
const runs = /* @__PURE__ */ new Map();
|
|
104
|
+
const requests = /* @__PURE__ */ new Map();
|
|
105
|
+
/**
|
|
106
|
+
* The run span for an event, created on first sight. Lazy rather than
|
|
107
|
+
* `run.start`-only so the uncontrolled path (no run boundary) and any
|
|
108
|
+
* out-of-order delivery still produce a well-formed tree.
|
|
109
|
+
*/
|
|
110
|
+
const runFor = (event) => {
|
|
111
|
+
const existing = runs.get(event.runId);
|
|
112
|
+
if (existing) return existing;
|
|
113
|
+
const agentName = options.agentName ?? event.machineId;
|
|
114
|
+
const span = tracer.startSpan(agentName ? `invoke_agent ${agentName}` : "invoke_agent", {
|
|
115
|
+
kind: SpanKind.INTERNAL,
|
|
116
|
+
attributes: {
|
|
117
|
+
...baseAttributes,
|
|
118
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
119
|
+
"gen_ai.agent.name": agentName,
|
|
120
|
+
"gen_ai.agent.version": event.machineVersion,
|
|
121
|
+
"agent.run_id": event.runId,
|
|
122
|
+
"agent.machine_id": event.machineId,
|
|
123
|
+
"agent.machine_version": event.machineVersion,
|
|
124
|
+
"agent.trace_schema_version": event.schemaVersion
|
|
125
|
+
}
|
|
126
|
+
}, context.active());
|
|
127
|
+
const state = {
|
|
128
|
+
span,
|
|
129
|
+
context: trace.setSpan(context.active(), span)
|
|
130
|
+
};
|
|
131
|
+
runs.set(event.runId, state);
|
|
132
|
+
return state;
|
|
133
|
+
};
|
|
134
|
+
/** Span events all carry `seq`, so a consumer can re-order them downstream. */
|
|
135
|
+
const addRunEvent = (event, name, attributes) => {
|
|
136
|
+
runFor(event).span.addEvent(name, {
|
|
137
|
+
"agent.seq": event.seq,
|
|
138
|
+
...attributes
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const endRequest = (id, finish) => {
|
|
142
|
+
const state = requests.get(id);
|
|
143
|
+
if (!state) return;
|
|
144
|
+
requests.delete(id);
|
|
145
|
+
if (state.chunks > 0) state.span.setAttribute("agent.stream_chunks", state.chunks);
|
|
146
|
+
finish(state.span);
|
|
147
|
+
state.span.end();
|
|
148
|
+
};
|
|
149
|
+
const handler = (event) => {
|
|
150
|
+
switch (event.type) {
|
|
151
|
+
case "run.start": {
|
|
152
|
+
const { span } = runFor(event);
|
|
153
|
+
if (options.captureContent && event.input !== void 0) {
|
|
154
|
+
const input = toJson(event.input);
|
|
155
|
+
if (input !== void 0) span.setAttribute("agent.input", input);
|
|
156
|
+
}
|
|
157
|
+
if (event.snapshot) span.setAttribute("agent.resumed", true);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "request.start": {
|
|
161
|
+
const request = event.request;
|
|
162
|
+
endRequest(request.id, () => {});
|
|
163
|
+
const model = requestModel(request);
|
|
164
|
+
const operation = operationName(request);
|
|
165
|
+
const attributes = {
|
|
166
|
+
...baseAttributes,
|
|
167
|
+
"gen_ai.operation.name": operation,
|
|
168
|
+
"agent.run_id": event.runId,
|
|
169
|
+
"agent.request_id": request.id,
|
|
170
|
+
"agent.request_kind": request.kind
|
|
171
|
+
};
|
|
172
|
+
if (model !== void 0) attributes["gen_ai.request.model"] = model;
|
|
173
|
+
if (options.providerName !== void 0) attributes["gen_ai.provider.name"] = options.providerName;
|
|
174
|
+
const src = requestSrc(request);
|
|
175
|
+
if (src !== void 0) attributes["agent.request_src"] = src;
|
|
176
|
+
if (request.kind === "decision" && request.attempts.length > 0) attributes["agent.request_attempt"] = request.attempts.length;
|
|
177
|
+
if (options.captureContent) {
|
|
178
|
+
const input = request.kind === "decision" ? request : request.input;
|
|
179
|
+
const system = input.system;
|
|
180
|
+
if (typeof system === "string") attributes["gen_ai.system_instructions"] = system;
|
|
181
|
+
const json = toJson(input.messages ?? input.prompt);
|
|
182
|
+
if (json !== void 0) attributes["gen_ai.input.messages"] = json;
|
|
183
|
+
}
|
|
184
|
+
const name = operation === "plan" ? `plan ${options.agentName ?? event.machineId}` : `chat ${model ?? request.kind}`;
|
|
185
|
+
const span = tracer.startSpan(name, {
|
|
186
|
+
kind: operation === "plan" ? SpanKind.INTERNAL : SpanKind.CLIENT,
|
|
187
|
+
attributes
|
|
188
|
+
}, runFor(event).context);
|
|
189
|
+
requests.set(request.id, {
|
|
190
|
+
span,
|
|
191
|
+
chunks: 0
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "stream.chunk": {
|
|
196
|
+
const state = requests.get(event.request.id);
|
|
197
|
+
if (state) state.chunks += 1;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "request.end":
|
|
201
|
+
endRequest(event.request.id, (span) => {
|
|
202
|
+
span.setAttributes(usageAttributes(event.usage));
|
|
203
|
+
span.setAttribute("agent.output_length", toJson(event.output ?? "")?.length ?? 0);
|
|
204
|
+
if (options.captureContent) {
|
|
205
|
+
const output = toJson(event.output);
|
|
206
|
+
if (output !== void 0) span.setAttribute("gen_ai.output.messages", output);
|
|
207
|
+
}
|
|
208
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
case "request.error":
|
|
212
|
+
endRequest(event.request.id, (span) => {
|
|
213
|
+
span.recordException(toException(event.error));
|
|
214
|
+
span.setAttribute("error.type", errorType(event.error));
|
|
215
|
+
span.setStatus({
|
|
216
|
+
code: SpanStatusCode.ERROR,
|
|
217
|
+
message: event.error instanceof Error ? event.error.message : void 0
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
case "machine.transition": {
|
|
222
|
+
const attributes = {
|
|
223
|
+
"agent.event_type": event.event.type,
|
|
224
|
+
"agent.state": toJson(event.snapshot.value) ?? ""
|
|
225
|
+
};
|
|
226
|
+
if (event.eventId !== void 0) attributes["agent.event_id"] = event.eventId;
|
|
227
|
+
addRunEvent(event, "agent.transition", attributes);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "emit":
|
|
231
|
+
addRunEvent(event, "agent.emit", { "agent.event_type": event.event.type });
|
|
232
|
+
break;
|
|
233
|
+
case "usage.dropped": {
|
|
234
|
+
const dropped = event.event;
|
|
235
|
+
addRunEvent(event, "agent.usage.dropped", {
|
|
236
|
+
"agent.drop_reason": event.reason,
|
|
237
|
+
...usageAttributes(dropped.usage)
|
|
238
|
+
});
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "run.end": {
|
|
242
|
+
const { span } = runFor(event);
|
|
243
|
+
runs.delete(event.runId);
|
|
244
|
+
span.setAttribute("agent.status", event.status);
|
|
245
|
+
if (event.status === "error") {
|
|
246
|
+
span.recordException(toException(event.error));
|
|
247
|
+
span.setAttribute("error.type", errorType(event.error));
|
|
248
|
+
span.setAttribute("agent.error_cause", event.cause);
|
|
249
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
250
|
+
} else span.setStatus({ code: SpanStatusCode.OK });
|
|
251
|
+
span.end();
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
handler.dispose = () => {
|
|
257
|
+
for (const [id] of requests) endRequest(id, (span) => span.setAttribute("agent.unfinished", true));
|
|
258
|
+
for (const { span } of runs.values()) {
|
|
259
|
+
span.setAttribute("agent.unfinished", true);
|
|
260
|
+
span.end();
|
|
261
|
+
}
|
|
262
|
+
runs.clear();
|
|
263
|
+
};
|
|
264
|
+
return handler;
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { createOtelTraceHandler };
|