@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.
Files changed (66) hide show
  1. package/dist/ai-sdk.cjs +4 -5
  2. package/dist/ai-sdk.d.cts +7 -4
  3. package/dist/ai-sdk.d.mts +7 -4
  4. package/dist/ai-sdk.mjs +1 -2
  5. package/dist/{events-JiVPYrct.mjs → decision-BezSD_YC.mjs} +327 -20
  6. package/dist/{events-CRQj3VtP.cjs → decision-dWGhBh0P.cjs} +401 -28
  7. package/dist/errors-BQRk9eiZ.d.cts +19 -0
  8. package/dist/errors-C9rxnWbX.d.mts +19 -0
  9. package/dist/errors-CeSXQx0v.mjs +23 -0
  10. package/dist/errors-DUBBzRLP.cjs +28 -0
  11. package/dist/event-log-store-CNT_7F0V.cjs +452 -0
  12. package/dist/event-log-store-CriMgX1D.d.mts +144 -0
  13. package/dist/event-log-store-D7pWtIhb.mjs +411 -0
  14. package/dist/event-log-store-Ruq18mGp.d.cts +144 -0
  15. package/dist/index.cjs +1050 -705
  16. package/dist/index.d.cts +538 -565
  17. package/dist/index.d.mts +538 -565
  18. package/dist/index.mjs +950 -644
  19. package/dist/machines.cjs +752 -0
  20. package/dist/machines.d.cts +372 -0
  21. package/dist/machines.d.mts +372 -0
  22. package/dist/machines.mjs +741 -0
  23. package/dist/otel.cjs +268 -0
  24. package/dist/otel.d.cts +67 -0
  25. package/dist/otel.d.mts +67 -0
  26. package/dist/otel.mjs +267 -0
  27. package/dist/run-agent-C3mFDGTf.d.mts +1111 -0
  28. package/dist/run-agent-DnvtcnTZ.d.cts +1111 -0
  29. package/dist/setup-agent-DAZZSjDS.mjs +1711 -0
  30. package/dist/setup-agent-DP95MFrI.cjs +1836 -0
  31. package/dist/sqlite.cjs +135 -0
  32. package/dist/sqlite.d.cts +57 -0
  33. package/dist/sqlite.d.mts +57 -0
  34. package/dist/sqlite.mjs +133 -0
  35. package/dist/{text-logic-CaKqgX4Y.d.mts → text-logic-BDxwQNsD.d.cts} +155 -72
  36. package/dist/{text-logic-Ckhr2kKC.d.cts → text-logic-TkKPw8Aq.d.mts} +155 -72
  37. package/dist/{types-qm00QF91.d.mts → types-QbEfCVny.d.cts} +1 -1
  38. package/dist/{types-C9QiMjre.d.cts → types-_FXoFBGO.d.mts} +1 -1
  39. package/package.json +47 -39
  40. package/readme.md +49 -12
  41. package/schemas/agent-workflow.json +40 -21
  42. package/skills/generate-machine/SKILL.md +267 -0
  43. package/dist/adapter.cjs +0 -15
  44. package/dist/adapter.d.cts +0 -4
  45. package/dist/adapter.d.mts +0 -4
  46. package/dist/adapter.mjs +0 -2
  47. package/dist/decision-C3k4ve51.mjs +0 -227
  48. package/dist/decision-D8wJrM8W.cjs +0 -286
  49. package/dist/openai-compat.cjs +0 -309
  50. package/dist/openai-compat.d.cts +0 -59
  51. package/dist/openai-compat.d.mts +0 -59
  52. package/dist/openai-compat.mjs +0 -308
  53. package/dist/steps-BALp1eZo.d.mts +0 -198
  54. package/dist/steps-CVe54GPP.cjs +0 -420
  55. package/dist/steps-CkyyyuHd.mjs +0 -379
  56. package/dist/steps-MjnQI4aB.d.cts +0 -198
  57. package/dist/steps.cjs +0 -12
  58. package/dist/steps.d.cts +0 -3
  59. package/dist/steps.d.mts +0 -3
  60. package/dist/steps.mjs +0 -3
  61. package/dist/utils-BYqT_Dyv.d.cts +0 -108
  62. package/dist/utils-Do5wIJrh.d.mts +0 -108
  63. package/dist/zod.cjs +0 -31
  64. package/dist/zod.d.cts +0 -30
  65. package/dist/zod.d.mts +0 -30
  66. 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;
@@ -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 };
@@ -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 };