@axlsdk/axl 0.19.1 → 0.20.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
@@ -168,7 +168,16 @@ const calculator = tool({
168
168
  });
169
169
  ```
170
170
 
171
- `toModelOutput` is an opt-in, synchronous mapper for successful agent-invoked local tools. Strings are sent verbatim; JSON-compatible values are strictly validated and serialized once. The full post-hook result stays on `tool_call_end.data.result` for host rendering (subject to `trace.redact`). `sensitive: true` and thrown handler/hook/mock failures skip projection, and projection errors fail closed with `ToolModelOutputError`—there is no raw fallback. Direct `tool.run()`/`_execute()`, MCP tools, and handoffs are unchanged. See the [API reference](../../docs/api-reference.md#model-facing-tool-output) for the exact type, validation, mock, and delivery contract.
171
+ `toModelOutput` is an opt-in, synchronous mapper for successful agent-invoked local tools. Strings are sent verbatim; JSON-compatible values are strictly validated and serialized once. The full post-hook result stays on the succeeded `tool_call_end.data.outcome.result` for host rendering (subject to `trace.redact`). `sensitive: true` and thrown handler/hook/mock failures skip projection, and projection errors close the call as `failed` / `projection` with no raw fallback. Direct `tool.run()`/`_execute()`, MCP tools, and handoffs are unchanged. See the [API reference](../../docs/api-reference.md#model-facing-tool-output) for the exact type, validation, mock, and delivery contract.
172
+
173
+ A normally returned value succeeds even when it contains an `error` property,
174
+ and `hooks.after` runs after every normal handler return. Throw `ToolFailure`
175
+ when a known failure has an author-declared message that is safe for model
176
+ recovery; ordinary throws abort the ask without provider tool content. Live
177
+ events use schema v2: pre-start rejection is `tool_call_rejected`, while every
178
+ accepted call closes with a `succeeded`, `failed`, `denied`, or `cancelled`
179
+ outcome. See the
180
+ [migration guide](../../docs/migration/stream-first-observation.md).
172
181
 
173
182
  Tool handlers receive a second parameter `ctx: WorkflowContext` (a child context), enabling the "agent-as-tool" composition pattern:
174
183
 
@@ -401,6 +410,11 @@ const wf = workflow({
401
410
 
402
411
  Subscribe before the first `ctx.ask()` — the streaming code path inside `ctx.ask()` only activates when an observer is present at the time the ask starts. The bus auto-terminates on `workflow_end` / `error` (and on signal abort, for ad-hoc `runtime.createContext({ signal })` flows). Configure the iterator-queue cap and overflow policy via `runtime.execute(..., { events: { maxQueued, onOverflow } })` — defaults (`maxQueued: 10_000`, `onOverflow: 'drop-oldest-non-terminal'`) are a default-on safety net against slow consumers. See [`docs/observability.md`](../../docs/observability.md#observation-paths) for the full Observation paths comparison, [`docs/api-reference.md`](../../docs/api-reference.md#ctxevents) for the type table, and [`docs/migration/stream-first-observation.md`](../../docs/migration/stream-first-observation.md) for upgrade notes.
403
412
 
413
+ The former `onToken`, `onToolCall`, and `onAgentStart` options on
414
+ `runtime.createContext()` are removed. Use `ctx.events`, `runtime.stream()`, or
415
+ the runtime trace listener according to observation scope; untyped callers that
416
+ still pass a removed key receive a targeted migration error.
417
+
404
418
  ### Context Primitives
405
419
 
406
420
  All available on `ctx` inside workflow handlers. See the [API Reference](../../docs/api-reference.md) for complete option types, valid values, and defaults.
@@ -452,11 +466,10 @@ const decision = await ctx.awaitHuman({
452
466
  prompt: 'Approve this action?',
453
467
  });
454
468
 
455
- // Durable checkpoint — on first run, executes and saves the result under
456
- // the given name. On replay after a restart, returns the saved result
457
- // without re-executing, preventing duplicate side effects (double API
458
- // calls, double charges, etc.). The name is a stable identifier and must
459
- // match across runs for replay to work.
469
+ // Scoped checkpoint — on first evaluation, executes and saves the result
470
+ // under the current execution ID. A later evaluation in that same explicit
471
+ // execution scope can replay it. Axl does not automatically restart or
472
+ // resume workflows across processes.
460
473
  const checkpointed = await ctx.checkpoint('expensive-op', async () => expensiveOperation());
461
474
  ```
462
475
 
@@ -610,7 +623,7 @@ const result = await ctx.ask(extractAgent, 'Extract user from this text', {
610
623
 
611
624
  ### State Stores
612
625
 
613
- Three built-in implementations. All persist the same data: workflow execution checkpoints, `awaitHuman` decisions, session history, memory entries, execution history, eval history, and the execution state needed for suspend/resume.
626
+ Three built-in implementations persist workflow checkpoints, pending `awaitHuman` requests, session history, memory entries, execution history, and eval history. Pending approval requests remain visible after process loss, but their workflow continuation is in-process only; use an application-owned durable command/idempotency protocol for cross-process approval. Cancellation compensation is serialized with public resolution. If compensation fails, the request stays visible and the runtime emits `decision_cleanup_failed` for operator retry or total deletion.
614
627
 
615
628
  **Memory** (default) — in-process, no persistence. Use for development and stateless workflows.
616
629
 
@@ -689,7 +702,7 @@ app.listen(3000);
689
702
 
690
703
  Excluded events (never flushed): `token`, `partial_object`, `string_delta` — reconstructable from `agent_call_end.data.response`. Scope: `runtime.execute()` and `runtime.stream()` only — `createContext()` flows are deliberately excluded (no terminal finalize path).
691
704
 
692
- Synthesized recovered executions carry `status: 'failed'`, `error: 'process terminated (recovered from streaming buffer)'`, and `workflow: '__axl/recovered'` when no `workflow_start` was captured. Events bounded by `state.maxEventsPerExecution`. SQLite does not implement streaming methods — the runtime warns and falls back to terminal mode.
705
+ Synthesized recovered executions carry `status: 'failed'`, `error: 'process terminated (recovered from streaming buffer)'`, and `observation: { complete: false, reason: 'process_interrupted' }`; `workflow` is `__axl/recovered` when no `workflow_start` was captured. Events are bounded by `state.maxEventsPerExecution`. SQLite does not implement streaming methods — the runtime warns and falls back to terminal mode.
693
706
 
694
707
  See [docs/migration/state-store-durability.md](../../docs/migration/state-store-durability.md) for the full design.
695
708
 
@@ -704,11 +717,15 @@ runtime.on('execution_deleted', (e) => {
704
717
  // e: { executionId, wasActive, hadPendingDecision, removed }
705
718
  auditLog.write({ event: 'execution.deleted', ...e });
706
719
  });
720
+
721
+ runtime.on('decision_cleanup_failed', (e) => {
722
+ opsAlerts.write({ event: 'approval.cleanup_failed', ...e });
723
+ });
707
724
  ```
708
725
 
709
- If the execution is still running, the workflow is aborted (and a paused `ctx.awaitHuman()` correctly wakes with `AbortError` — fixed in 0.17.7). The resurrection guard ensures the workflow's eventual `workflow_end` doesn't re-create the row.
726
+ If the execution is still running, the workflow is aborted (and a paused `ctx.awaitHuman()` correctly wakes with `AbortError` — fixed in 0.17.7). The resurrection guard ensures the workflow's eventual `workflow_end` doesn't re-create the row. Deletion waits for any in-process approval resolution or cancellation compensation before the store sweep. Custom stores must make `resolveDecision` idempotent and implement the complete execution-scoped sweep in `deleteExecution`.
710
727
 
711
- `ExecutionInfo.metadata` round-trips from `ExecuteOptions.metadata` (`userId`, `tenantId`, etc.) — queryable via `runtime.getExecutions().filter(...)`. Internal control-plane keys (`sessionHistory`, `sessionId`, `resumeMode`) are stripped before persistence; they remain available via `ctx.metadata` for dynamic selectors.
728
+ `ExecutionInfo.metadata` round-trips from `ExecuteOptions.metadata` (`userId`, `tenantId`, etc.) — queryable via `runtime.getExecutions().filter(...)`. Internal session control-plane keys (`sessionHistory`, `sessionId`) are stripped before persistence; they remain available via `ctx.metadata` for dynamic selectors.
712
729
 
713
730
  ### Session Options
714
731
 
@@ -748,7 +765,6 @@ import {
748
765
  BudgetExceededError, // Budget limit exceeded
749
766
  GuardrailError, // Guardrail blocked input or output
750
767
  ValidationError, // Post-schema business rule validation failed after retries
751
- ToolDenied, // Agent tried to call unauthorized tool
752
768
  } from '@axlsdk/axl';
753
769
  ```
754
770
 
@@ -32,6 +32,7 @@ var OTelSpanManager = class _OTelSpanManager {
32
32
  async withSpanAsync(name, attributes, fn) {
33
33
  const otelApi = this.otelApi;
34
34
  return this.tracer.startActiveSpan(name, { attributes }, async (otelSpan) => {
35
+ let statusWasSet = false;
35
36
  const handle = {
36
37
  setAttribute(key, value) {
37
38
  otelSpan.setAttribute(key, value);
@@ -40,7 +41,8 @@ var OTelSpanManager = class _OTelSpanManager {
40
41
  otelSpan.addEvent(eventName, attrs);
41
42
  },
42
43
  setStatus(code, message) {
43
- const statusCode = code === "ok" ? otelApi.SpanStatusCode.OK : otelApi.SpanStatusCode.ERROR;
44
+ statusWasSet = true;
45
+ const statusCode = code === "ok" ? otelApi.SpanStatusCode.OK : code === "error" ? otelApi.SpanStatusCode.ERROR : otelApi.SpanStatusCode.UNSET;
44
46
  otelSpan.setStatus({ code: statusCode, message });
45
47
  },
46
48
  end() {
@@ -49,10 +51,10 @@ var OTelSpanManager = class _OTelSpanManager {
49
51
  };
50
52
  try {
51
53
  const result = await fn(handle);
52
- handle.setStatus("ok");
54
+ if (!statusWasSet) handle.setStatus("ok");
53
55
  return result;
54
56
  } catch (err) {
55
- handle.setStatus("error", err instanceof Error ? err.message : String(err));
57
+ handle.setStatus("error");
56
58
  throw err;
57
59
  } finally {
58
60
  handle.end();
@@ -76,4 +78,4 @@ export {
76
78
  __require,
77
79
  OTelSpanManager
78
80
  };
79
- //# sourceMappingURL=chunk-JBLQKU6X.js.map
81
+ //# sourceMappingURL=chunk-NVPA4RY6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/telemetry/span-manager.ts"],"sourcesContent":["import type { SpanHandle, SpanManager, TelemetryConfig } from './types.js';\n\n/** Minimal interface for an OTel span. */\ninterface OTelSpan {\n setAttribute(key: string, value: string | number | boolean): void;\n addEvent(name: string, attributes?: Record<string, string | number | boolean>): void;\n setStatus(status: { code: unknown; message?: string }): void;\n end(): void;\n}\n\n/** Minimal interface for an OTel tracer. */\ninterface OTelTracer {\n startActiveSpan<T>(\n name: string,\n options: { attributes: Record<string, string | number | boolean> },\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 shutdown?(): Promise<void>;\n}\n\n/** Minimal interface for the @opentelemetry/api module. */\ninterface OTelApi {\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 private tracerProvider: OTelTracerProvider;\n\n private constructor(otelApi: OTelApi, tracer: OTelTracer, tracerProvider: OTelTracerProvider) {\n this.otelApi = otelApi;\n this.tracer = tracer;\n this.tracerProvider = tracerProvider;\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, tracerProvider);\n }\n\n async withSpanAsync<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: (span: SpanHandle) => Promise<T>,\n ): Promise<T> {\n const otelApi = this.otelApi;\n\n return this.tracer.startActiveSpan(name, { attributes }, async (otelSpan: OTelSpan) => {\n let statusWasSet = false;\n const handle: SpanHandle = {\n setAttribute(key: string, value: string | number | boolean) {\n otelSpan.setAttribute(key, value);\n },\n addEvent(eventName: string, attrs?: Record<string, string | number | boolean>) {\n otelSpan.addEvent(eventName, attrs);\n },\n setStatus(code: 'unset' | 'ok' | 'error', message?: string) {\n statusWasSet = true;\n const statusCode =\n code === 'ok'\n ? otelApi.SpanStatusCode.OK\n : code === 'error'\n ? otelApi.SpanStatusCode.ERROR\n : otelApi.SpanStatusCode.UNSET;\n otelSpan.setStatus({ code: statusCode, message });\n },\n end() {\n otelSpan.end();\n },\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 addEventToActiveSpan(name: string, attributes?: Record<string, string | number | boolean>): void {\n const activeSpan = this.otelApi.trace.getActiveSpan?.();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.tracerProvider && typeof this.tracerProvider.shutdown === 'function') {\n await this.tracerProvider.shutdown();\n }\n }\n}\n"],"mappings":";;;;;;;;AAsCO,IAAM,kBAAN,MAAM,iBAAuC;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EAEA,YAAY,SAAkB,QAAoB,gBAAoC;AAC5F,SAAK,UAAU;AACf,SAAK,SAAS;AACd,SAAK,iBAAiB;AAAA,EACxB;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,QAAQ,cAAc;AAAA,EAC5D;AAAA,EAEA,MAAM,cACJ,MACA,YACA,IACY;AACZ,UAAM,UAAU,KAAK;AAErB,WAAO,KAAK,OAAO,gBAAgB,MAAM,EAAE,WAAW,GAAG,OAAO,aAAuB;AACrF,UAAI,eAAe;AACnB,YAAM,SAAqB;AAAA,QACzB,aAAa,KAAa,OAAkC;AAC1D,mBAAS,aAAa,KAAK,KAAK;AAAA,QAClC;AAAA,QACA,SAAS,WAAmB,OAAmD;AAC7E,mBAAS,SAAS,WAAW,KAAK;AAAA,QACpC;AAAA,QACA,UAAU,MAAgC,SAAkB;AAC1D,yBAAe;AACf,gBAAM,aACJ,SAAS,OACL,QAAQ,eAAe,KACvB,SAAS,UACP,QAAQ,eAAe,QACvB,QAAQ,eAAe;AAC/B,mBAAS,UAAU,EAAE,MAAM,YAAY,QAAQ,CAAC;AAAA,QAClD;AAAA,QACA,MAAM;AACJ,mBAAS,IAAI;AAAA,QACf;AAAA,MACF;AAEA,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,EAEA,qBAAqB,MAAc,YAA8D;AAC/F,UAAM,aAAa,KAAK,QAAQ,MAAM,gBAAgB;AACtD,QAAI,YAAY;AACd,iBAAW,SAAS,MAAM,UAAU;AAAA,IACtC;AAAA,EACF;AAAA,EAEA,MAAM,WAA0B;AAC9B,QAAI,KAAK,kBAAkB,OAAO,KAAK,eAAe,aAAa,YAAY;AAC7E,YAAM,KAAK,eAAe,SAAS;AAAA,IACrC;AAAA,EACF;AACF;","names":[]}