@axlsdk/axl 0.25.2 → 0.26.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 CHANGED
@@ -509,22 +509,28 @@ const checkpointed = await ctx.checkpoint('expensive-op', async () => expensiveO
509
509
 
510
510
  ### OpenTelemetry Observability
511
511
 
512
- Automatic span emission for every `ctx.*` primitive with cost-per-span attribution. Install `@opentelemetry/api` as an optional peer dependency.
512
+ Automatic spans for `ctx.*` primitives and every model call Axl sends. Install
513
+ `@opentelemetry/api` as an optional peer dependency.
513
514
 
514
515
  ```typescript
515
516
  import { defineConfig, AxlRuntime } from '@axlsdk/axl';
517
+ import { context, trace } from '@opentelemetry/api';
518
+ import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
516
519
  import {
517
520
  BasicTracerProvider,
518
521
  SimpleSpanProcessor,
519
522
  } from '@opentelemetry/sdk-trace-base';
520
523
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
521
524
 
522
- const tracerProvider = new BasicTracerProvider();
523
- tracerProvider.addSpanProcessor(
524
- new SimpleSpanProcessor(
525
- new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }),
526
- ),
527
- );
525
+ const tracerProvider = new BasicTracerProvider({
526
+ spanProcessors: [
527
+ new SimpleSpanProcessor(
528
+ new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' }),
529
+ ),
530
+ ],
531
+ });
532
+ context.setGlobalContextManager(new AsyncLocalStorageContextManager());
533
+ trace.setGlobalTracerProvider(tracerProvider);
528
534
 
529
535
  const config = defineConfig({
530
536
  telemetry: {
@@ -536,11 +542,29 @@ const config = defineConfig({
536
542
 
537
543
  const runtime = new AxlRuntime(config);
538
544
  await runtime.initializeTelemetry();
545
+
546
+ // After the application stops accepting work:
547
+ await runtime.shutdown();
548
+ await tracerProvider.shutdown();
549
+ context.disable();
550
+ trace.disable();
539
551
  ```
540
552
 
541
- **Span model:** `axl.workflow.execute` > `axl.agent.ask` > `axl.tool.call`. Also: `axl.ctx.spawn`, `axl.ctx.race`, `axl.ctx.vote`, `axl.ctx.budget`, `axl.ctx.awaitHuman`. Each span includes relevant attributes (cost, duration, token counts, etc.).
553
+ **Span model:** `axl.workflow.execute` > `axl.agent.ask` > `chat {model}`
554
+ for model calls, with `axl.tool.call` under an agent when tools run. Also:
555
+ `axl.ctx.spawn`, `axl.ctx.race`, `axl.ctx.vote`, `axl.ctx.budget`,
556
+ `axl.ctx.awaitHuman`. Model-call spans use standard `gen_ai.*` attributes.
557
+ Prompt and response content is off by default; see
558
+ [content capture](../../docs/observability.md#genai-content-capture).
542
559
 
543
- When disabled (default), `NoopSpanManager` provides zero overhead.
560
+ When telemetry is disabled (default), `NoopSpanManager` adds no overhead. Axl
561
+ never shuts down your tracer provider; do that after `runtime.shutdown()`.
562
+
563
+ For this Node setup, install the optional OTel packages used above:
564
+
565
+ ```bash
566
+ npm install @opentelemetry/api @opentelemetry/context-async-hooks @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http
567
+ ```
544
568
 
545
569
  ```typescript
546
570
  import { createSpanManager, NoopSpanManager } from '@axlsdk/axl';
@@ -595,6 +619,9 @@ eval history row that owns them is saved, deleted with it, and reclaimed by a
595
619
  startup pass plus a periodic sweep. See
596
620
  [observability.md](../../docs/observability.md#captured-requests-opt-in) and
597
621
  [integration.md](../../docs/integration.md#diagnostic-artifact-storage).
622
+ `foldJournal()` turns journal lines into one validated record per call, and
623
+ `redactModelCallRecord()` returns a redacted copy. See
624
+ [What a record contains](../../docs/observability.md#what-a-record-contains).
598
625
 
599
626
  Compatible loaded ESM/CJS copies share accounting, admission, and capture in
600
627
  one JavaScript realm. Cross-copy work emits one warning identifying both copies;
@@ -12,11 +12,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  var OTelSpanManager = class _OTelSpanManager {
13
13
  tracer;
14
14
  otelApi;
15
- tracerProvider;
16
- constructor(otelApi, tracer, tracerProvider) {
15
+ constructor(otelApi, tracer) {
17
16
  this.otelApi = otelApi;
18
17
  this.tracer = tracer;
19
- this.tracerProvider = tracerProvider;
20
18
  }
21
19
  static async create(config) {
22
20
  let otelApi;
@@ -30,28 +28,14 @@ var OTelSpanManager = class _OTelSpanManager {
30
28
  const tracerProvider = config.tracerProvider ?? otelApi.trace.getTracerProvider();
31
29
  const serviceName = config.serviceName ?? "axl";
32
30
  const tracer = tracerProvider.getTracer(serviceName);
33
- return new _OTelSpanManager(otelApi, tracer, tracerProvider);
31
+ return new _OTelSpanManager(otelApi, tracer);
34
32
  }
35
33
  async withSpanAsync(name, attributes, fn) {
36
- const otelApi = this.otelApi;
37
34
  return this.tracer.startActiveSpan(name, { attributes }, async (otelSpan) => {
38
35
  let statusWasSet = false;
39
- const handle = {
40
- setAttribute(key, value) {
41
- otelSpan.setAttribute(key, value);
42
- },
43
- addEvent(eventName, attrs) {
44
- otelSpan.addEvent(eventName, attrs);
45
- },
46
- setStatus(code, message) {
47
- statusWasSet = true;
48
- const statusCode = code === "ok" ? otelApi.SpanStatusCode.OK : code === "error" ? otelApi.SpanStatusCode.ERROR : otelApi.SpanStatusCode.UNSET;
49
- otelSpan.setStatus({ code: statusCode, message });
50
- },
51
- end() {
52
- otelSpan.end();
53
- }
54
- };
36
+ const handle = this.handle(otelSpan, () => {
37
+ statusWasSet = true;
38
+ });
55
39
  try {
56
40
  const result = await fn(handle);
57
41
  if (!statusWasSet) handle.setStatus("ok");
@@ -64,16 +48,47 @@ var OTelSpanManager = class _OTelSpanManager {
64
48
  }
65
49
  });
66
50
  }
51
+ /** Start a CLIENT inference span under the current OTel context. The caller ends it. */
52
+ startSpan(name, attributes, options) {
53
+ const span = this.tracer.startSpan(name, {
54
+ attributes,
55
+ ...options?.kind === "client" ? { kind: this.otelApi.SpanKind.CLIENT } : {},
56
+ ...options?.startTime !== void 0 ? { startTime: options.startTime } : {}
57
+ });
58
+ return this.handle(span);
59
+ }
60
+ handle(span, onStatus) {
61
+ const otelApi = this.otelApi;
62
+ let ended = false;
63
+ return {
64
+ setAttribute(key, value) {
65
+ if (!ended) span.setAttribute(key, value);
66
+ },
67
+ addEvent(name, attributes) {
68
+ if (!ended) span.addEvent(name, attributes);
69
+ },
70
+ setStatus(code, message) {
71
+ if (ended) return;
72
+ onStatus?.();
73
+ const statusCode = code === "ok" ? otelApi.SpanStatusCode.OK : code === "error" ? otelApi.SpanStatusCode.ERROR : otelApi.SpanStatusCode.UNSET;
74
+ span.setStatus({ code: statusCode, message });
75
+ },
76
+ end() {
77
+ if (ended) return;
78
+ ended = true;
79
+ span.end();
80
+ }
81
+ };
82
+ }
67
83
  addEventToActiveSpan(name, attributes) {
68
84
  const activeSpan = this.otelApi.trace.getActiveSpan?.();
69
85
  if (activeSpan) {
70
86
  activeSpan.addEvent(name, attributes);
71
87
  }
72
88
  }
89
+ // Axl uses an application-owned provider, whether supplied or global.
90
+ // Its lifecycle, including flush and shutdown, belongs to the application.
73
91
  async shutdown() {
74
- if (this.tracerProvider && typeof this.tracerProvider.shutdown === "function") {
75
- await this.tracerProvider.shutdown();
76
- }
77
92
  }
78
93
  };
79
94
 
@@ -81,4 +96,4 @@ export {
81
96
  __require,
82
97
  OTelSpanManager
83
98
  };
84
- //# sourceMappingURL=chunk-PJROX3XU.js.map
99
+ //# sourceMappingURL=chunk-SBQL47BB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/telemetry/span-manager.ts"],"sourcesContent":["import type {\n SpanAttributeValue,\n SpanAttributes,\n SpanHandle,\n SpanManager,\n TelemetryConfig,\n} from './types.js';\n\n/** Minimal interface for an OTel span. */\ninterface OTelSpan {\n setAttribute(key: string, value: SpanAttributeValue): void;\n addEvent(name: string, attributes?: SpanAttributes): void;\n setStatus(status: { code: unknown; message?: string }): void;\n end(): void;\n}\n\n/** Minimal interface for an OTel tracer. */\ninterface OTelTracer {\n startSpan(\n name: string,\n options: { attributes: SpanAttributes; kind?: number; startTime?: number },\n ): OTelSpan;\n startActiveSpan<T>(\n name: string,\n options: { attributes: SpanAttributes },\n fn: (span: OTelSpan) => Promise<T>,\n ): Promise<T>;\n}\n\n/** Minimal interface for an OTel tracer provider. */\ninterface OTelTracerProvider {\n getTracer(name: string): OTelTracer;\n}\n\n/** Minimal interface for the @opentelemetry/api module. */\ninterface OTelApi {\n SpanKind: { CLIENT: number };\n SpanStatusCode: { UNSET: unknown; OK: unknown; ERROR: unknown };\n trace: {\n getTracerProvider(): OTelTracerProvider;\n getActiveSpan?(): OTelSpan | undefined;\n };\n}\n\n/**\n * OpenTelemetry-backed SpanManager.\n * Dynamically imports @opentelemetry/api to avoid hard dependency.\n */\nexport class OTelSpanManager implements SpanManager {\n private tracer: OTelTracer;\n private otelApi: OTelApi;\n\n private constructor(otelApi: OTelApi, tracer: OTelTracer) {\n this.otelApi = otelApi;\n this.tracer = tracer;\n }\n\n static async create(config: TelemetryConfig): Promise<OTelSpanManager> {\n let otelApi: OTelApi;\n try {\n otelApi = (await import('@opentelemetry/api')) as unknown as OTelApi;\n } catch {\n throw new Error(\n '@opentelemetry/api is required for telemetry. Install it with: npm install @opentelemetry/api',\n );\n }\n\n const tracerProvider =\n (config.tracerProvider as OTelTracerProvider | undefined) ??\n otelApi.trace.getTracerProvider();\n const serviceName = config.serviceName ?? 'axl';\n const tracer = tracerProvider.getTracer(serviceName);\n\n return new OTelSpanManager(otelApi, tracer);\n }\n\n async withSpanAsync<T>(\n name: string,\n attributes: SpanAttributes,\n fn: (span: SpanHandle) => Promise<T>,\n ): Promise<T> {\n return this.tracer.startActiveSpan(name, { attributes }, async (otelSpan: OTelSpan) => {\n let statusWasSet = false;\n const handle = this.handle(otelSpan, () => {\n statusWasSet = true;\n });\n\n try {\n const result = await fn(handle);\n if (!statusWasSet) handle.setStatus('ok');\n return result;\n } catch (err) {\n // Error text can contain provider, prompt, or sensitive tool data.\n // Keep exported status structural; host diagnostics remain in Axl's\n // own error and redacted event channels.\n handle.setStatus('error');\n throw err;\n } finally {\n handle.end();\n }\n });\n }\n\n /** Start a CLIENT inference span under the current OTel context. The caller ends it. */\n startSpan(\n name: string,\n attributes: SpanAttributes,\n options?: { kind?: 'client'; startTime?: number },\n ): SpanHandle {\n const span = this.tracer.startSpan(name, {\n attributes,\n ...(options?.kind === 'client' ? { kind: this.otelApi.SpanKind.CLIENT } : {}),\n ...(options?.startTime !== undefined ? { startTime: options.startTime } : {}),\n });\n return this.handle(span);\n }\n\n private handle(span: OTelSpan, onStatus?: () => void): SpanHandle {\n const otelApi = this.otelApi;\n let ended = false;\n return {\n setAttribute(key, value) {\n if (!ended) span.setAttribute(key, value);\n },\n addEvent(name, attributes) {\n if (!ended) span.addEvent(name, attributes);\n },\n setStatus(code, message) {\n if (ended) return;\n onStatus?.();\n const statusCode =\n code === 'ok'\n ? otelApi.SpanStatusCode.OK\n : code === 'error'\n ? otelApi.SpanStatusCode.ERROR\n : otelApi.SpanStatusCode.UNSET;\n span.setStatus({ code: statusCode, message });\n },\n end() {\n if (ended) return;\n ended = true;\n span.end();\n },\n };\n }\n\n addEventToActiveSpan(name: string, attributes?: SpanAttributes): void {\n const activeSpan = this.otelApi.trace.getActiveSpan?.();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n }\n\n // Axl uses an application-owned provider, whether supplied or global.\n // Its lifecycle, including flush and shutdown, belongs to the application.\n async shutdown(): Promise<void> {}\n}\n"],"mappings":";;;;;;;;;;;AAgDO,IAAM,kBAAN,MAAM,iBAAuC;AAAA,EAC1C;AAAA,EACA;AAAA,EAEA,YAAY,SAAkB,QAAoB;AACxD,SAAK,UAAU;AACf,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,aAAa,OAAO,QAAmD;AACrE,QAAI;AACJ,QAAI;AACF,gBAAW,MAAM,OAAO,oBAAoB;AAAA,IAC9C,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,iBACH,OAAO,kBACR,QAAQ,MAAM,kBAAkB;AAClC,UAAM,cAAc,OAAO,eAAe;AAC1C,UAAM,SAAS,eAAe,UAAU,WAAW;AAEnD,WAAO,IAAI,iBAAgB,SAAS,MAAM;AAAA,EAC5C;AAAA,EAEA,MAAM,cACJ,MACA,YACA,IACY;AACZ,WAAO,KAAK,OAAO,gBAAgB,MAAM,EAAE,WAAW,GAAG,OAAO,aAAuB;AACrF,UAAI,eAAe;AACnB,YAAM,SAAS,KAAK,OAAO,UAAU,MAAM;AACzC,uBAAe;AAAA,MACjB,CAAC;AAED,UAAI;AACF,cAAM,SAAS,MAAM,GAAG,MAAM;AAC9B,YAAI,CAAC,aAAc,QAAO,UAAU,IAAI;AACxC,eAAO;AAAA,MACT,SAAS,KAAK;AAIZ,eAAO,UAAU,OAAO;AACxB,cAAM;AAAA,MACR,UAAE;AACA,eAAO,IAAI;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,UACE,MACA,YACA,SACY;AACZ,UAAM,OAAO,KAAK,OAAO,UAAU,MAAM;AAAA,MACvC;AAAA,MACA,GAAI,SAAS,SAAS,WAAW,EAAE,MAAM,KAAK,QAAQ,SAAS,OAAO,IAAI,CAAC;AAAA,MAC3E,GAAI,SAAS,cAAc,SAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,IAC7E,CAAC;AACD,WAAO,KAAK,OAAO,IAAI;AAAA,EACzB;AAAA,EAEQ,OAAO,MAAgB,UAAmC;AAChE,UAAM,UAAU,KAAK;AACrB,QAAI,QAAQ;AACZ,WAAO;AAAA,MACL,aAAa,KAAK,OAAO;AACvB,YAAI,CAAC,MAAO,MAAK,aAAa,KAAK,KAAK;AAAA,MAC1C;AAAA,MACA,SAAS,MAAM,YAAY;AACzB,YAAI,CAAC,MAAO,MAAK,SAAS,MAAM,UAAU;AAAA,MAC5C;AAAA,MACA,UAAU,MAAM,SAAS;AACvB,YAAI,MAAO;AACX,mBAAW;AACX,cAAM,aACJ,SAAS,OACL,QAAQ,eAAe,KACvB,SAAS,UACP,QAAQ,eAAe,QACvB,QAAQ,eAAe;AAC/B,aAAK,UAAU,EAAE,MAAM,YAAY,QAAQ,CAAC;AAAA,MAC9C;AAAA,MACA,MAAM;AACJ,YAAI,MAAO;AACX,gBAAQ;AACR,aAAK,IAAI;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EAEA,qBAAqB,MAAc,YAAmC;AACpE,UAAM,aAAa,KAAK,QAAQ,MAAM,gBAAgB;AACtD,QAAI,YAAY;AACd,iBAAW,SAAS,MAAM,UAAU;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA,EAIA,MAAM,WAA0B;AAAA,EAAC;AACnC;","names":[]}