@axlsdk/axl 0.19.0 → 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
@@ -162,9 +162,23 @@ const calculator = tool({
162
162
  // handler also accepts (input, ctx) for nested agent invocations — see below
163
163
  retry: { attempts: 3, backoff: 'exponential' },
164
164
  sensitive: false,
165
+ // Keep the complete handler result for the host while sending only this
166
+ // allowlisted projection back to the model:
167
+ toModelOutput: (result) => ({ answer: result.result }),
165
168
  });
166
169
  ```
167
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 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).
181
+
168
182
  Tool handlers receive a second parameter `ctx: WorkflowContext` (a child context), enabling the "agent-as-tool" composition pattern:
169
183
 
170
184
  ```typescript
@@ -396,6 +410,11 @@ const wf = workflow({
396
410
 
397
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.
398
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
+
399
418
  ### Context Primitives
400
419
 
401
420
  All available on `ctx` inside workflow handlers. See the [API Reference](../../docs/api-reference.md) for complete option types, valid values, and defaults.
@@ -447,11 +466,10 @@ const decision = await ctx.awaitHuman({
447
466
  prompt: 'Approve this action?',
448
467
  });
449
468
 
450
- // Durable checkpoint — on first run, executes and saves the result under
451
- // the given name. On replay after a restart, returns the saved result
452
- // without re-executing, preventing duplicate side effects (double API
453
- // calls, double charges, etc.). The name is a stable identifier and must
454
- // 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.
455
473
  const checkpointed = await ctx.checkpoint('expensive-op', async () => expensiveOperation());
456
474
  ```
457
475
 
@@ -605,7 +623,7 @@ const result = await ctx.ask(extractAgent, 'Extract user from this text', {
605
623
 
606
624
  ### State Stores
607
625
 
608
- 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.
609
627
 
610
628
  **Memory** (default) — in-process, no persistence. Use for development and stateless workflows.
611
629
 
@@ -684,7 +702,7 @@ app.listen(3000);
684
702
 
685
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).
686
704
 
687
- 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.
688
706
 
689
707
  See [docs/migration/state-store-durability.md](../../docs/migration/state-store-durability.md) for the full design.
690
708
 
@@ -699,11 +717,15 @@ runtime.on('execution_deleted', (e) => {
699
717
  // e: { executionId, wasActive, hadPendingDecision, removed }
700
718
  auditLog.write({ event: 'execution.deleted', ...e });
701
719
  });
720
+
721
+ runtime.on('decision_cleanup_failed', (e) => {
722
+ opsAlerts.write({ event: 'approval.cleanup_failed', ...e });
723
+ });
702
724
  ```
703
725
 
704
- 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`.
705
727
 
706
- `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.
707
729
 
708
730
  ### Session Options
709
731
 
@@ -743,7 +765,6 @@ import {
743
765
  BudgetExceededError, // Budget limit exceeded
744
766
  GuardrailError, // Guardrail blocked input or output
745
767
  ValidationError, // Post-schema business rule validation failed after retries
746
- ToolDenied, // Agent tried to call unauthorized tool
747
768
  } from '@axlsdk/axl';
748
769
  ```
749
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":[]}