@axlsdk/axl 0.17.2 → 0.17.4

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/index.cjs CHANGED
@@ -113,11 +113,13 @@ __export(index_exports, {
113
113
  AXL_EVENT_TYPES: () => AXL_EVENT_TYPES,
114
114
  AnthropicProvider: () => AnthropicProvider,
115
115
  AxlError: () => AxlError,
116
+ AxlEventBus: () => AxlEventBus,
116
117
  AxlRuntime: () => AxlRuntime,
117
118
  AxlStream: () => AxlStream,
118
119
  BudgetExceededError: () => BudgetExceededError,
119
120
  CONFIG_CANDIDATES: () => CONFIG_CANDIDATES,
120
121
  COST_BEARING_LEAF_TYPES: () => COST_BEARING_LEAF_TYPES,
122
+ EventStreamOverflowError: () => EventStreamOverflowError,
121
123
  GeminiProvider: () => GeminiProvider,
122
124
  GuardrailError: () => GuardrailError,
123
125
  InMemoryVectorStore: () => InMemoryVectorStore,
@@ -2523,6 +2525,598 @@ function isRootLevel(event) {
2523
2525
  return (d ?? 0) === 0;
2524
2526
  }
2525
2527
 
2528
+ // src/event-stream.ts
2529
+ var import_node_events = require("events");
2530
+
2531
+ // src/types.ts
2532
+ var AXL_EVENT_TYPES = [
2533
+ // Workflow lifecycle
2534
+ "workflow_start",
2535
+ "workflow_end",
2536
+ // Ask boundary
2537
+ "ask_start",
2538
+ "ask_end",
2539
+ // Agent turn lifecycle
2540
+ "agent_call_start",
2541
+ "agent_call_end",
2542
+ // Content delivery (stream-only)
2543
+ "token",
2544
+ // Tool invocation lifecycle
2545
+ "tool_call_start",
2546
+ "tool_call_end",
2547
+ // Single-point tool events
2548
+ "tool_approval",
2549
+ "tool_denied",
2550
+ // Delegation
2551
+ "delegate",
2552
+ // Handoff (spans two asks; not AskScoped).
2553
+ // `handoff_start` emits before the target ask begins; always fired.
2554
+ // `handoff_return` emits when control returns to source; roundtrip only
2555
+ // (oneway handoffs have no return trip — the target's `ask_end` IS the
2556
+ // end of the chain).
2557
+ "handoff_start",
2558
+ "handoff_return",
2559
+ // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
2560
+ "pipeline",
2561
+ // Progressive structured output — added in PR 2; reserved here.
2562
+ "partial_object",
2563
+ // Verification
2564
+ "verify",
2565
+ // Observability
2566
+ "log",
2567
+ "memory_remember",
2568
+ "memory_recall",
2569
+ "memory_forget",
2570
+ // Durable execution checkpoints (`ctx.checkpoint`)
2571
+ "checkpoint_save",
2572
+ "checkpoint_replay",
2573
+ // Human-in-the-loop (`ctx.awaitHuman`)
2574
+ "await_human",
2575
+ "await_human_resolved",
2576
+ // Legacy gate events — emitted by current code; collapsed into `pipeline`
2577
+ // in PR 2 (spec/16-streaming-wire-reliability §4.2).
2578
+ "guardrail",
2579
+ "schema_check",
2580
+ "validate",
2581
+ // Terminal workflow markers
2582
+ "done",
2583
+ "error"
2584
+ ];
2585
+
2586
+ // src/event-stream.ts
2587
+ var STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
2588
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
2589
+ "done",
2590
+ "error",
2591
+ "workflow_end"
2592
+ ]);
2593
+ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
2594
+ "ask_start",
2595
+ "ask_end",
2596
+ "agent_call_start",
2597
+ "agent_call_end",
2598
+ "tool_call_start",
2599
+ "tool_call_end",
2600
+ "tool_approval",
2601
+ "tool_denied",
2602
+ "handoff_start",
2603
+ "handoff_return",
2604
+ "delegate",
2605
+ "pipeline",
2606
+ "verify",
2607
+ "workflow_start",
2608
+ "workflow_end",
2609
+ "checkpoint_save",
2610
+ "checkpoint_replay",
2611
+ "await_human",
2612
+ "await_human_resolved"
2613
+ ]);
2614
+ var EventStreamOverflowError = class extends Error {
2615
+ maxQueued;
2616
+ eventType;
2617
+ constructor(maxQueued, eventType) {
2618
+ super(
2619
+ `AxlEventBus queue exceeded maxQueued=${maxQueued} (event type: ${eventType}). Consumer is too slow or the producer is unbounded. Configure \`maxQueued\`/\`onOverflow\` on the runtime, or set maxQueued: Infinity to disable.`
2620
+ );
2621
+ this.name = "EventStreamOverflowError";
2622
+ this.maxQueued = maxQueued;
2623
+ this.eventType = eventType;
2624
+ }
2625
+ };
2626
+ var DEFAULT_MAX_QUEUED = 1e4;
2627
+ var DEFAULT_OVERFLOW = "drop-oldest-non-terminal";
2628
+ var AxlEventBus = class {
2629
+ bus = new import_node_events.EventEmitter();
2630
+ eventQueue = [];
2631
+ waiters = [];
2632
+ finished = false;
2633
+ maxQueued;
2634
+ onOverflow;
2635
+ overflowWarned = false;
2636
+ /** Callbacks fired exactly once when `_finish()` is called. Used by
2637
+ * curated views (e.g., `partialObjects`) that are listener-based and
2638
+ * need a termination signal independent of any synthetic `done` event. */
2639
+ finishCallbacks = [];
2640
+ /**
2641
+ * Latest coalesced partial-object snapshot per `askId`, maintained on
2642
+ * every `_push` regardless of consumer state. Seeds the
2643
+ * `partialObjects` view's `pending` map at subscription time so a
2644
+ * late subscriber recovers the most recent state per ask even if
2645
+ * earlier `partial_object` events were already delivered to a prior
2646
+ * iterator's waiter (and thus removed from `eventQueue`).
2647
+ *
2648
+ * The pre-fix snapshot walked `eventQueue` directly — that misses
2649
+ * events already drained by past consumers. The map walks past
2650
+ * `_push` calls instead, which is the actual definition of "what's
2651
+ * the latest partial per ask". `pipeline(status: 'failed')` clears
2652
+ * the entry for the affected askId so a discarded attempt can't
2653
+ * surface to a future subscriber.
2654
+ *
2655
+ * Memory bound: O(active asks holding a pending partial). Cleared
2656
+ * on `pipeline(failed)` (per ask) and otherwise grows with the
2657
+ * number of distinct askIds that emitted at least one partial.
2658
+ */
2659
+ latestPartialByAsk = /* @__PURE__ */ new Map();
2660
+ /**
2661
+ * Hook fired when an iterator obtained from `[Symbol.asyncIterator]`
2662
+ * is `await using`-disposed. Set via the second constructor argument.
2663
+ * Used by `AxlStream` to call `Readable.destroy()` on its owning
2664
+ * stream so disposing the iterator cascades to the Readable contract.
2665
+ * Set once at construction; never reassigned.
2666
+ *
2667
+ * Replaces the previous `AxlStreamEventBus extends AxlEventBus`
2668
+ * subclass — composition over inheritance for what is essentially a
2669
+ * single function-pointer hook.
2670
+ *
2671
+ * @internal
2672
+ */
2673
+ onIteratorDispose;
2674
+ /**
2675
+ * @param options Public stream options (queue cap, overflow policy).
2676
+ * @param onIteratorDispose @internal Invoked from `_disposeIterator`
2677
+ * when an iterator obtained from this bus is `await using`-disposed.
2678
+ * Used by `AxlStream` to forward to `Readable.destroy()`.
2679
+ */
2680
+ constructor(options, onIteratorDispose) {
2681
+ const requestedMax = options?.maxQueued ?? DEFAULT_MAX_QUEUED;
2682
+ if (typeof requestedMax !== "number" || Number.isNaN(requestedMax) || requestedMax !== Infinity && requestedMax < 1) {
2683
+ throw new Error(
2684
+ `EventStreamOptions.maxQueued must be >= 1 or Infinity (got ${String(requestedMax)}). To disable the cap, pass Infinity.`
2685
+ );
2686
+ }
2687
+ this.maxQueued = requestedMax;
2688
+ this.onOverflow = options?.onOverflow ?? DEFAULT_OVERFLOW;
2689
+ this.onIteratorDispose = onIteratorDispose;
2690
+ this.bus.on("error", () => {
2691
+ });
2692
+ }
2693
+ unknownEventWarned = false;
2694
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2695
+ on(event, handler) {
2696
+ if (STREAM_EVENTS.has(event)) {
2697
+ this.bus.on(event, handler);
2698
+ } else {
2699
+ this.warnUnknownEventName("on", event);
2700
+ }
2701
+ return this;
2702
+ }
2703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2704
+ off(event, handler) {
2705
+ if (STREAM_EVENTS.has(event)) {
2706
+ this.bus.off(event, handler);
2707
+ } else {
2708
+ this.warnUnknownEventName("off", event);
2709
+ }
2710
+ return this;
2711
+ }
2712
+ /** One-shot warn on subscribe/unsubscribe for an event name outside
2713
+ * `AXL_EVENT_TYPES`. Pre-fix this branch silently dropped, which
2714
+ * meant a typo like `'agent_call_ended'` produced no signal. */
2715
+ warnUnknownEventName(method, event) {
2716
+ if (this.unknownEventWarned) return;
2717
+ this.unknownEventWarned = true;
2718
+ console.warn(
2719
+ `[axl] AxlEventBus.${method}('${event}', ...) \u2014 '${event}' is not an AxlEventType; subscription dropped. Valid names: ${AXL_EVENT_TYPES.join(", ")}. This warning fires once per bus instance.`
2720
+ );
2721
+ }
2722
+ // No explicit return-type annotation: structural inference yields a type
2723
+ // assignable to Readable's `() => AsyncIterator<any, undefined, any>` when
2724
+ // AxlStream delegates here, while preserving the richer iterator-with-dispose
2725
+ // shape for direct AxlEventBus consumers.
2726
+ [Symbol.asyncIterator]() {
2727
+ const self = this;
2728
+ let myResolver = null;
2729
+ return {
2730
+ next: () => {
2731
+ if (self.eventQueue.length > 0) {
2732
+ return Promise.resolve({ value: self.eventQueue.shift(), done: false });
2733
+ }
2734
+ if (self.finished && self.eventQueue.length === 0) {
2735
+ return Promise.resolve({ value: void 0, done: true });
2736
+ }
2737
+ return new Promise((resolve2) => {
2738
+ const wrapped = (value) => {
2739
+ myResolver = null;
2740
+ resolve2(value);
2741
+ };
2742
+ myResolver = wrapped;
2743
+ self.waiters.push(wrapped);
2744
+ });
2745
+ },
2746
+ return: () => {
2747
+ if (myResolver) {
2748
+ const idx = self.waiters.indexOf(myResolver);
2749
+ if (idx >= 0) self.waiters.splice(idx, 1);
2750
+ const r = myResolver;
2751
+ myResolver = null;
2752
+ r({ value: void 0, done: true });
2753
+ }
2754
+ return Promise.resolve({ value: void 0, done: true });
2755
+ },
2756
+ [Symbol.asyncIterator]() {
2757
+ return this;
2758
+ },
2759
+ [Symbol.asyncDispose]: () => self._disposeIterator()
2760
+ };
2761
+ }
2762
+ /** Hook for iterator disposal. Calls the constructor-supplied
2763
+ * `onIteratorDispose` callback if any. The default behavior is a
2764
+ * no-op when no callback was passed — a plain bus has no
2765
+ * Node-stream resource to release. */
2766
+ _disposeIterator() {
2767
+ this.onIteratorDispose?.();
2768
+ return Promise.resolve();
2769
+ }
2770
+ /** Iterator over root-only `token` chunks. Skips nested-ask tokens
2771
+ * (consumers wanting nested can iterate the full bus and filter on
2772
+ * `event.depth >= 1`). */
2773
+ get text() {
2774
+ const self = this;
2775
+ return {
2776
+ [Symbol.asyncIterator]() {
2777
+ const iter = self[Symbol.asyncIterator]();
2778
+ return {
2779
+ async next() {
2780
+ while (true) {
2781
+ const { value, done } = await iter.next();
2782
+ if (done) return { value: void 0, done: true };
2783
+ if (value.type === "token" && isRootLevel(value)) {
2784
+ return { value: value.data, done: false };
2785
+ }
2786
+ }
2787
+ },
2788
+ // Propagate `return()` to the underlying bus iterator so its
2789
+ // parked resolver (if any) is spliced out of `waiters`. Without
2790
+ // this, breaking out of `for await (const t of bus.text)` would
2791
+ // orphan the inner waiter and lose the next event.
2792
+ async return() {
2793
+ await iter.return?.();
2794
+ return { value: void 0, done: true };
2795
+ }
2796
+ };
2797
+ }
2798
+ };
2799
+ }
2800
+ /** Iterator over `{ askId, agent?, text }` for every token across root and
2801
+ * nested asks. Useful for split-pane UIs that render each sub-agent's
2802
+ * output in its own lane. `agent` is undefined when the token was emitted
2803
+ * outside any ask (rare — synthesized test fixtures). */
2804
+ get textByAsk() {
2805
+ const self = this;
2806
+ return {
2807
+ [Symbol.asyncIterator]() {
2808
+ const iter = self[Symbol.asyncIterator]();
2809
+ return {
2810
+ async next() {
2811
+ while (true) {
2812
+ const { value, done } = await iter.next();
2813
+ if (done) {
2814
+ return {
2815
+ value: void 0,
2816
+ done: true
2817
+ };
2818
+ }
2819
+ if (value.type === "token") {
2820
+ return {
2821
+ value: { askId: value.askId, agent: value.agent, text: value.data },
2822
+ done: false
2823
+ };
2824
+ }
2825
+ }
2826
+ },
2827
+ async return() {
2828
+ await iter.return?.();
2829
+ return {
2830
+ value: void 0,
2831
+ done: true
2832
+ };
2833
+ }
2834
+ };
2835
+ }
2836
+ };
2837
+ }
2838
+ /**
2839
+ * Coalescing iterator over `partial_object` events — yields the latest
2840
+ * payload per `askId`. When a newer `partial_object` arrives for an
2841
+ * `askId` while the consumer is busy, the older value is silently
2842
+ * superseded; the consumer sees only the most recent state per ask on
2843
+ * its next `.next()` await.
2844
+ *
2845
+ * Memory bound: O(active asks holding a pending partial), not O(events).
2846
+ * This is what UIs streaming structured output actually want — rendering
2847
+ * every intermediate snapshot just to overwrite it ms later is wasted
2848
+ * work, and unbounded queueing of intermediate JSON snapshots is the
2849
+ * memory-pressure scenario this view is designed to avoid. In practice
2850
+ * "active asks" is small (1-10 even for fan-out workflows); a workflow
2851
+ * fanning out to 10k concurrent asks each producing partials would
2852
+ * accumulate ≈10k pending entries (~1 MB), which is the realistic
2853
+ * ceiling for this view.
2854
+ *
2855
+ * Schema-retry awareness: each yielded value carries the 1-indexed
2856
+ * `attempt` field from the underlying `partial_object` event. When a
2857
+ * schema check fails and the agent retries, the runtime emits
2858
+ * `pipeline(status: 'failed')`; this view drops any undrained pending
2859
+ * partial for the affected ask so the consumer never sees stale
2860
+ * attempt-N snapshots after attempt-N+1 has begun. Mirrors the
2861
+ * per-attempt buffer reset that `AxlStream.fullText` applies to
2862
+ * tokens. (Validate-stage and guardrail retries trigger the same
2863
+ * reset.)
2864
+ *
2865
+ * Malformed `partial_object` events (those missing `data.object`) are
2866
+ * silently filtered — the variant shape requires the field, so an
2867
+ * undefined value is a producer bug rather than a meaningful state.
2868
+ *
2869
+ * Implementation note: this is a listener-based view (subscribes to the
2870
+ * bus's `partial_object` channel via `.on()`), not an iterator-based
2871
+ * filter like `.text` / `.lifecycle` / `.textByAsk`. As a result it does
2872
+ * NOT race with the main async iterator — running `for await (const e of bus)`
2873
+ * concurrently with `for await (const p of bus.partialObjects)` works
2874
+ * cleanly (each `partial_object` event fires the listener AND queues for
2875
+ * iterators).
2876
+ *
2877
+ * Termination: yields any pending coalesced values first, then `done: true`
2878
+ * once the bus is finished (via `_finish()`).
2879
+ */
2880
+ get partialObjects() {
2881
+ const self = this;
2882
+ return {
2883
+ [Symbol.asyncIterator]() {
2884
+ const pending = /* @__PURE__ */ new Map();
2885
+ let resolveWaiter = null;
2886
+ let finished = self.finished;
2887
+ let unsubscribed = false;
2888
+ const wakeWaiter = () => {
2889
+ const r = resolveWaiter;
2890
+ resolveWaiter = null;
2891
+ r?.();
2892
+ };
2893
+ const recordPartial = (event) => {
2894
+ if (event.data?.object === void 0) return;
2895
+ const askId = event.askId ?? "";
2896
+ pending.set(askId, {
2897
+ askId,
2898
+ agent: event.agent,
2899
+ object: event.data.object,
2900
+ attempt: event.attempt
2901
+ });
2902
+ };
2903
+ for (const [askId, value] of self.latestPartialByAsk) {
2904
+ pending.set(askId, value);
2905
+ }
2906
+ const handler = (event) => {
2907
+ if (event.type !== "partial_object") return;
2908
+ recordPartial(event);
2909
+ wakeWaiter();
2910
+ };
2911
+ self.bus.on("partial_object", handler);
2912
+ const pipelineHandler = (event) => {
2913
+ if (event.type !== "pipeline") return;
2914
+ if (event.status !== "failed") return;
2915
+ const askId = event.askId ?? "";
2916
+ pending.delete(askId);
2917
+ };
2918
+ self.bus.on("pipeline", pipelineHandler);
2919
+ const unsubscribe = () => {
2920
+ if (unsubscribed) return;
2921
+ unsubscribed = true;
2922
+ self.bus.off("partial_object", handler);
2923
+ self.bus.off("pipeline", pipelineHandler);
2924
+ unsubscribeFinish();
2925
+ };
2926
+ const unsubscribeFinish = self._onFinish(() => {
2927
+ finished = true;
2928
+ self.bus.off("partial_object", handler);
2929
+ self.bus.off("pipeline", pipelineHandler);
2930
+ wakeWaiter();
2931
+ });
2932
+ return {
2933
+ async next() {
2934
+ while (true) {
2935
+ if (pending.size > 0) {
2936
+ const it = pending.entries().next();
2937
+ if (!it.done) {
2938
+ const [key, value] = it.value;
2939
+ pending.delete(key);
2940
+ return { value, done: false };
2941
+ }
2942
+ }
2943
+ if (finished) {
2944
+ unsubscribe();
2945
+ return { value: void 0, done: true };
2946
+ }
2947
+ await new Promise((res) => {
2948
+ resolveWaiter = res;
2949
+ });
2950
+ }
2951
+ },
2952
+ async return() {
2953
+ unsubscribe();
2954
+ return { value: void 0, done: true };
2955
+ },
2956
+ [Symbol.asyncIterator]() {
2957
+ return this;
2958
+ }
2959
+ };
2960
+ }
2961
+ };
2962
+ }
2963
+ /** Iterator over structural lifecycle events only — skips per-token chatter
2964
+ * and progressive `partial_object` emissions. Useful for waterfall UIs
2965
+ * and any consumer that wants the "what happened" timeline. */
2966
+ get lifecycle() {
2967
+ const self = this;
2968
+ return {
2969
+ [Symbol.asyncIterator]() {
2970
+ const iter = self[Symbol.asyncIterator]();
2971
+ return {
2972
+ async next() {
2973
+ while (true) {
2974
+ const { value, done } = await iter.next();
2975
+ if (done) return { value: void 0, done: true };
2976
+ if (LIFECYCLE_TYPES.has(value.type)) return { value, done: false };
2977
+ }
2978
+ },
2979
+ async return() {
2980
+ await iter.return?.();
2981
+ return { value: void 0, done: true };
2982
+ }
2983
+ };
2984
+ }
2985
+ };
2986
+ }
2987
+ /**
2988
+ * Push an event onto the bus. Emits on the typed channel (for `.on()`
2989
+ * subscribers) and queues for async iterators. No-op once `_finish()` has
2990
+ * been called.
2991
+ *
2992
+ * Subclasses (AxlStream) override to add token accounting and Readable
2993
+ * fan-out before delegating to this implementation.
2994
+ *
2995
+ * @internal — called by the runtime / WorkflowContext / AxlStream.
2996
+ */
2997
+ _push(event) {
2998
+ if (this.finished) return;
2999
+ try {
3000
+ this.bus.emit(event.type, event);
3001
+ } catch (err) {
3002
+ console.error(
3003
+ `[axl] AxlEventBus listener for "${event.type}" threw; ignoring:`,
3004
+ err instanceof Error ? err.message : String(err)
3005
+ );
3006
+ }
3007
+ if (event.type === "partial_object" && event.data?.object !== void 0) {
3008
+ const askId = event.askId ?? "";
3009
+ this.latestPartialByAsk.set(askId, {
3010
+ askId,
3011
+ agent: event.agent,
3012
+ object: event.data.object,
3013
+ attempt: event.attempt
3014
+ });
3015
+ } else if (event.type === "pipeline" && event.status === "failed") {
3016
+ this.latestPartialByAsk.delete(event.askId ?? "");
3017
+ }
3018
+ const waiter = this.waiters.shift();
3019
+ if (waiter) {
3020
+ waiter({ value: event, done: false });
3021
+ return;
3022
+ }
3023
+ if (this.eventQueue.length >= this.maxQueued && !TERMINAL_TYPES.has(event.type)) {
3024
+ this.handleOverflow(event);
3025
+ return;
3026
+ }
3027
+ this.eventQueue.push(event);
3028
+ }
3029
+ /** Apply the overflow policy when the queue is at `maxQueued` and a
3030
+ * non-terminal event arrives. Terminal events bypass this path entirely
3031
+ * (checked by the caller). */
3032
+ handleOverflow(event) {
3033
+ if (this.onOverflow === "throw") {
3034
+ throw new EventStreamOverflowError(this.maxQueued, event.type);
3035
+ }
3036
+ let droppedIdx = -1;
3037
+ for (let i = 0; i < this.eventQueue.length; i++) {
3038
+ if (!TERMINAL_TYPES.has(this.eventQueue[i].type)) {
3039
+ droppedIdx = i;
3040
+ break;
3041
+ }
3042
+ }
3043
+ if (!this.overflowWarned) {
3044
+ this.overflowWarned = true;
3045
+ console.warn(
3046
+ `[axl] AxlEventBus queue exceeded maxQueued=${this.maxQueued}` + (droppedIdx >= 0 ? `; dropping oldest non-terminal events (consumer is slower than producer).` : `; queue is all terminals (preserved past cap so no terminal is lost).`) + ` This warning fires once per bus instance.`
3047
+ );
3048
+ }
3049
+ if (droppedIdx >= 0) {
3050
+ this.eventQueue.splice(droppedIdx, 1);
3051
+ }
3052
+ this.eventQueue.push(event);
3053
+ }
3054
+ /**
3055
+ * Mark the bus as finished. Wakes any pending iterators with `done: true`
3056
+ * and drops further `_push` calls. Idempotent.
3057
+ *
3058
+ * Does not synthesize a terminal `done` AxlEvent — callers (AxlStream) that
3059
+ * want one push it via `_push` before calling `_finish`.
3060
+ *
3061
+ * Late iterators that connect after `_finish()` still drain any events
3062
+ * remaining in `eventQueue` (the `next()` short-circuit in
3063
+ * `[Symbol.asyncIterator]` checks the queue before checking `finished`),
3064
+ * then receive `done: true`.
3065
+ *
3066
+ * @internal
3067
+ */
3068
+ _finish() {
3069
+ if (this.finished) return;
3070
+ this.finished = true;
3071
+ for (const w of this.waiters) {
3072
+ w({ value: void 0, done: true });
3073
+ }
3074
+ this.waiters.length = 0;
3075
+ const callbacks = this.finishCallbacks;
3076
+ this.finishCallbacks = [];
3077
+ for (const cb of callbacks) {
3078
+ try {
3079
+ cb();
3080
+ } catch (err) {
3081
+ console.error("[axl] AxlEventBus finish callback threw:", err);
3082
+ }
3083
+ }
3084
+ }
3085
+ /** Register a one-shot callback fired when `_finish()` is called.
3086
+ * Returns an unsubscribe function. If the bus is already finished,
3087
+ * the callback is invoked synchronously and the returned unsubscribe
3088
+ * is a no-op.
3089
+ *
3090
+ * Used by listener-based curated views (e.g., `partialObjects`) that
3091
+ * need a termination signal independent of any synthesized `done`
3092
+ * event. Iterator consumers don't need this — `[Symbol.asyncIterator]`
3093
+ * resolves with `done: true` automatically.
3094
+ *
3095
+ * @internal */
3096
+ _onFinish(cb) {
3097
+ if (this.finished) {
3098
+ cb();
3099
+ return () => {
3100
+ };
3101
+ }
3102
+ this.finishCallbacks.push(cb);
3103
+ return () => {
3104
+ const i = this.finishCallbacks.indexOf(cb);
3105
+ if (i >= 0) this.finishCallbacks.splice(i, 1);
3106
+ };
3107
+ }
3108
+ /** Whether `event` is one of the `AxlEventType` names this bus emits.
3109
+ * Subclasses (e.g., `AxlStream`) consult this to route AxlEvent names to
3110
+ * the bus and other names (`'close'`, `'data'`, ...) to a wider emitter. */
3111
+ knowsEventName(event) {
3112
+ return STREAM_EVENTS.has(event);
3113
+ }
3114
+ /** Whether `_finish()` has been called. */
3115
+ get isFinished() {
3116
+ return this.finished;
3117
+ }
3118
+ };
3119
+
2526
3120
  // src/redaction.ts
2527
3121
  var REDACTED = "[redacted]";
2528
3122
  function walkObjectOneLevel(obj, preserveKeys) {
@@ -2888,6 +3482,103 @@ var WorkflowContext = class _WorkflowContext {
2888
3482
  awaitHumanHandler;
2889
3483
  onAgentStart;
2890
3484
  onAgentCallComplete;
3485
+ /** Shared mutable slot for the lazy `AxlEventBus`. The slot is shared
3486
+ * by reference across parent/child contexts so a late allocation
3487
+ * (consumer subscribes after a child context exists) is visible
3488
+ * everywhere. Until first access `_busRef.current` is undefined and
3489
+ * `emitEvent` skips the bus fan-out entirely (zero overhead for
3490
+ * contexts that never observe). */
3491
+ _busRef;
3492
+ _eventStreamOptions;
3493
+ /** Removes the constructor-registered `'abort'` listener from
3494
+ * `this.signal` so a long-lived signal (e.g., a request-scoped or
3495
+ * process-wide signal reused across many `runtime.createContext()` /
3496
+ * `runtime.execute()` calls) doesn't accumulate listeners forever.
3497
+ * Called from `disposeEvents()` and from the workflow_end/error
3498
+ * emission in `emitEvent`. Undefined when no abort listener was
3499
+ * registered (no signal, signal already aborted, or non-root
3500
+ * context). */
3501
+ abortListenerCleanup;
3502
+ /**
3503
+ * Iterable + EventEmitter view over every `AxlEvent` emitted by this
3504
+ * context (and its child contexts — agent-as-tool nested asks bubble up).
3505
+ *
3506
+ * Use cases:
3507
+ * - **Inside a workflow handler**: subscribe before the first `ctx.ask()`
3508
+ * to observe `partial_object`, `agent_call_*`, `tool_call_*`, etc.
3509
+ * *between* asks. `ctx.events.partialObjects` is the coalescing view
3510
+ * designed for streaming-structured-output UIs.
3511
+ * - **Ad-hoc contexts** from `runtime.createContext()`: the same
3512
+ * iterable replaces the legacy `onToken` / `onToolCall` /
3513
+ * `onAgentStart` callbacks.
3514
+ * - **Cross-execution**: prefer `runtime.on('trace', …)` instead — that
3515
+ * fans out across all executions, while `ctx.events` is per-context.
3516
+ *
3517
+ * Lazy: the bus is allocated on first access. The bus's iterator
3518
+ * queue retains events emitted before any consumer iterates, so a
3519
+ * late `for await (const e of ctx.events)` still drains queued
3520
+ * events. The `partialObjects` view additionally seeds from a
3521
+ * per-bus `latestPartialByAsk` map, so a late subscriber may see
3522
+ * the latest coalesced state per ask even when earlier events were
3523
+ * already drained by another iterator. **Neither rescues a late
3524
+ * subscriber from the streaming-gate behavior** (see below).
3525
+ *
3526
+ * **Subscribe before the first `ctx.ask()`.** The streaming code
3527
+ * path inside `ctx.ask()` activates only when an observer is
3528
+ * present at the time the ask starts (`_streamingEnabled` — either
3529
+ * the legacy `onToken` callback is set or `ctx.events` has been
3530
+ * allocated). If you allocate `ctx.events` AFTER a `ctx.ask()` has
3531
+ * begun, that in-flight ask will not stream `token` /
3532
+ * `partial_object` events at all (the agent loop went through
3533
+ * `provider.chat` instead of `provider.stream`). Subsequent asks
3534
+ * will stream normally — the gate is re-checked per ask. The
3535
+ * unambiguous pattern is `const events = ctx.events;` on the first
3536
+ * line of the handler.
3537
+ *
3538
+ * Auto-termination: `emitEvent` calls `_eventBus._finish()` after
3539
+ * emitting `workflow_end` or `error`, so iterators terminate
3540
+ * cleanly with `done: true`. For ad-hoc contexts that never run a
3541
+ * workflow (no terminal event), pass `signal: AbortSignal.timeout(...)`
3542
+ * to `runtime.createContext` (the bus auto-disposes on abort) or
3543
+ * call `ctx.disposeEvents()` when done observing.
3544
+ */
3545
+ get events() {
3546
+ if (!this._busRef.current) {
3547
+ this._busRef.current = new AxlEventBus(this._eventStreamOptions);
3548
+ if (this.signal?.aborted) {
3549
+ this._busRef.current._finish();
3550
+ }
3551
+ }
3552
+ return this._busRef.current;
3553
+ }
3554
+ /** Manually finish the `ctx.events` bus, terminating any active
3555
+ * iterators with `done: true`. Idempotent. Useful for ad-hoc contexts
3556
+ * from `runtime.createContext()` that never emit a `workflow_end` /
3557
+ * `error` terminal — observers iterating `for await (const e of
3558
+ * ctx.events)` would otherwise hang. Workflow-driven contexts
3559
+ * (`runtime.execute` / `runtime.stream`) terminate automatically and
3560
+ * don't need this.
3561
+ *
3562
+ * Also removes the constructor-registered abort listener so a
3563
+ * long-lived `signal` reused across many ad-hoc contexts doesn't
3564
+ * accumulate listeners. */
3565
+ disposeEvents() {
3566
+ this._busRef.current?._finish();
3567
+ this.abortListenerCleanup?.();
3568
+ }
3569
+ /** True if any observer wants per-token streaming for asks started
3570
+ * from this context: either the legacy `onToken` callback is set
3571
+ * (e.g., `runtime.stream()` plumbs a sentinel) OR `ctx.events` has
3572
+ * been allocated (a workflow-handler consumer subscribed before
3573
+ * the ask). `ctx.ask` reads this to decide whether to enter the
3574
+ * streaming code path; the gate is re-checked per ask, so a
3575
+ * consumer subscribing AFTER the first ask started still gets
3576
+ * streaming on the next one. The check `_busRef.current !== undefined`
3577
+ * is a one-way flag — `_finish` does not unset the reference, so
3578
+ * once an observer was present, every subsequent ask streams. */
3579
+ get _streamingEnabled() {
3580
+ return this.onToken !== void 0 || this._busRef.current !== void 0;
3581
+ }
2891
3582
  constructor(init) {
2892
3583
  this.input = init.input;
2893
3584
  this.executionId = init.executionId;
@@ -2911,6 +3602,19 @@ var WorkflowContext = class _WorkflowContext {
2911
3602
  this.awaitHumanHandler = init.awaitHumanHandler;
2912
3603
  this.onAgentStart = init.onAgentStart;
2913
3604
  this.onAgentCallComplete = init.onAgentCallComplete;
3605
+ this._busRef = init._busRef ?? { current: void 0 };
3606
+ this._eventStreamOptions = init.eventStreamOptions;
3607
+ if (init._busRef === void 0 && this.signal && !this.signal.aborted) {
3608
+ const signal = this.signal;
3609
+ const onAbort = () => {
3610
+ this._busRef.current?._finish();
3611
+ };
3612
+ signal.addEventListener("abort", onAbort, { once: true });
3613
+ this.abortListenerCleanup = () => {
3614
+ signal.removeEventListener("abort", onAbort);
3615
+ this.abortListenerCleanup = void 0;
3616
+ };
3617
+ }
2914
3618
  this.autoCheckpointCounters = init.autoCheckpointCounters ?? {
2915
3619
  byAgent: /* @__PURE__ */ new Map(),
2916
3620
  root: 0
@@ -2965,7 +3669,14 @@ var WorkflowContext = class _WorkflowContext {
2965
3669
  // Share the parent's auto-checkpoint counters so internal checkpoints
2966
3670
  // emitted from `ctx.ask` / spawn / race / parallel / map in this
2967
3671
  // child context don't collide with the parent's in the state store.
2968
- autoCheckpointCounters: this.autoCheckpointCounters
3672
+ autoCheckpointCounters: this.autoCheckpointCounters,
3673
+ // Share the parent's bus slot (mutable ref). Late allocation in
3674
+ // either parent or child propagates because both read the same
3675
+ // slot. Nested-ask events (agent-as-tool pattern) bubble up to
3676
+ // the parent's iterator with `parentAskId`/`depth` correlation
3677
+ // intact.
3678
+ _busRef: this._busRef,
3679
+ eventStreamOptions: this._eventStreamOptions
2969
3680
  // Isolated: sessionHistory (empty)
2970
3681
  });
2971
3682
  }
@@ -3101,15 +3812,26 @@ var WorkflowContext = class _WorkflowContext {
3101
3812
  };
3102
3813
  throw err;
3103
3814
  } finally {
3104
- this.emitEvent({
3105
- type: "ask_end",
3106
- outcome: outcome ?? {
3107
- ok: false,
3108
- error: "ask_end emitted without outcome \u2014 internal bug"
3109
- },
3110
- cost: frame.askCost.value,
3111
- duration: Date.now() - askStart
3112
- });
3815
+ try {
3816
+ this.emitEvent({
3817
+ type: "ask_end",
3818
+ outcome: outcome ?? {
3819
+ ok: false,
3820
+ error: "ask_end emitted without outcome \u2014 internal bug"
3821
+ },
3822
+ cost: frame.askCost.value,
3823
+ duration: Date.now() - askStart
3824
+ });
3825
+ } catch (emitErr) {
3826
+ if (emitErr instanceof EventStreamOverflowError && outcome && !outcome.ok) {
3827
+ console.error(
3828
+ "[axl] ask_end emit overflowed during error path; preserving original error:",
3829
+ emitErr.message
3830
+ );
3831
+ } else {
3832
+ throw emitErr;
3833
+ }
3834
+ }
3113
3835
  }
3114
3836
  });
3115
3837
  },
@@ -3346,7 +4068,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3346
4068
  this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
3347
4069
  let response;
3348
4070
  try {
3349
- if (this.onToken) {
4071
+ if (this._streamingEnabled) {
3350
4072
  let content2 = "";
3351
4073
  const toolCalls = [];
3352
4074
  const toolCallBuffers = /* @__PURE__ */ new Map();
@@ -3361,7 +4083,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3361
4083
  if (chunk.type === "text_delta") {
3362
4084
  content2 += chunk.content;
3363
4085
  this.emitEvent({ type: "token", data: chunk.content });
3364
- this.onToken(chunk.content, callbackMeta);
4086
+ this.onToken?.(chunk.content, callbackMeta);
3365
4087
  if (partialObjectEnabled) {
3366
4088
  for (const ch of chunk.content) {
3367
4089
  if (escaped) {
@@ -4076,19 +4798,21 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4076
4798
  attempt: lastStartAttempt,
4077
4799
  maxAttempts: lastStartMaxAttempts
4078
4800
  });
4079
- this.pushAssistantToSessionHistory(content, response.providerMetadata);
4801
+ this.pushAssistantToSessionHistory(content, agent2._name, response.providerMetadata);
4080
4802
  return validated ?? content;
4081
4803
  }
4082
4804
  throw new MaxTurnsError("ctx.ask()", maxTurns);
4083
4805
  }
4084
4806
  /**
4085
4807
  * Push the final assistant message into session history, preserving providerMetadata
4086
- * (e.g., Gemini thought signatures needed for multi-turn reasoning context).
4808
+ * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
4809
+ * stamping the agent name for observability + future per-agent scoping.
4087
4810
  */
4088
- pushAssistantToSessionHistory(content, providerMetadata) {
4811
+ pushAssistantToSessionHistory(content, agentName, providerMetadata) {
4089
4812
  this.sessionHistory.push({
4090
4813
  role: "assistant",
4091
4814
  content,
4815
+ agent: agentName,
4092
4816
  ...providerMetadata ? { providerMetadata } : {}
4093
4817
  });
4094
4818
  }
@@ -5274,16 +5998,48 @@ ${summaryResponse.content}`
5274
5998
  data
5275
5999
  };
5276
6000
  const finalEvent = this.config.trace?.redact ? redactEvent(event) : event;
6001
+ let overflowErr;
5277
6002
  if (this.onTrace) {
5278
6003
  try {
5279
6004
  this.onTrace(finalEvent);
5280
6005
  } catch (err) {
5281
- console.error(
5282
- "[axl] onTrace handler threw; trace event dropped:",
5283
- err instanceof Error ? err.message : String(err)
5284
- );
6006
+ if (err instanceof EventStreamOverflowError) {
6007
+ overflowErr = err;
6008
+ } else {
6009
+ console.error(
6010
+ "[axl] onTrace handler threw; trace event dropped:",
6011
+ err instanceof Error ? err.message : String(err)
6012
+ );
6013
+ }
6014
+ }
6015
+ }
6016
+ const bus = this._busRef.current;
6017
+ if (bus) {
6018
+ try {
6019
+ bus._push(finalEvent);
6020
+ } catch (err) {
6021
+ if (err instanceof EventStreamOverflowError) {
6022
+ if (!overflowErr) {
6023
+ overflowErr = err;
6024
+ } else {
6025
+ console.error(
6026
+ '[axl] ctx.events bus also overflowed for event "' + finalEvent.type + '"; first overflow already in flight:',
6027
+ err.message
6028
+ );
6029
+ }
6030
+ } else {
6031
+ console.error(
6032
+ "[axl] AxlEventBus._push threw unexpectedly:",
6033
+ err instanceof Error ? err.message : String(err)
6034
+ );
6035
+ }
5285
6036
  }
5286
6037
  }
6038
+ if (finalEvent.type === "workflow_end" || finalEvent.type === "error") {
6039
+ bus?._finish();
6040
+ this.abortListenerCleanup?.();
6041
+ }
6042
+ if (overflowErr) throw overflowErr;
5287
6043
  }
5288
6044
  };
5289
6045
 
@@ -5887,56 +6643,28 @@ var Session = class _Session {
5887
6643
  get id() {
5888
6644
  return this.sessionId;
5889
6645
  }
5890
- async send(workflowName, input) {
6646
+ async send(workflowName, input, options) {
5891
6647
  if (this.closed) throw new Error("Session has been ended");
5892
- const history = await this.store.getSession(this.sessionId);
5893
- let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
5894
- const maxMessages = this.options.history?.maxMessages;
5895
- if (maxMessages && history.length > maxMessages) {
5896
- if (this.options.history?.summarize) {
5897
- const summaryModel = this.options.history?.summaryModel;
5898
- if (!summaryModel) {
5899
- throw new Error("SessionOptions.history.summaryModel is required when summarize is true");
5900
- }
5901
- const messagesToDrop = history.slice(0, history.length - maxMessages);
5902
- const toSummarize = cachedSummary ? [
5903
- { role: "system", content: `Previous conversation summary: ${cachedSummary}` },
5904
- ...messagesToDrop
5905
- ] : messagesToDrop;
5906
- const summary = await this.runtime.summarizeMessages(toSummarize, summaryModel);
5907
- await this.store.saveSessionMeta(this.sessionId, "summaryCache", summary);
5908
- cachedSummary = summary;
5909
- }
5910
- const trimmed = history.slice(-maxMessages);
5911
- history.length = 0;
5912
- history.push(...trimmed);
5913
- }
5914
- const userMessage = {
5915
- role: "user",
5916
- content: typeof input === "string" ? input : JSON.stringify(input)
5917
- };
5918
- history.push(userMessage);
5919
- const result = await this.runtime.execute(workflowName, input, {
5920
- metadata: {
5921
- sessionId: this.sessionId,
5922
- sessionHistory: history,
5923
- ...cachedSummary ? { summaryCache: cachedSummary } : {}
5924
- }
5925
- });
5926
- const lastMsg = history[history.length - 1];
5927
- if (!(lastMsg && lastMsg.role === "assistant")) {
5928
- history.push({
5929
- role: "assistant",
5930
- content: typeof result === "string" ? result : JSON.stringify(result)
5931
- });
5932
- }
5933
- if (this.options.persist !== false) {
5934
- await this.store.saveSession(this.sessionId, history);
5935
- }
6648
+ options?.signal?.throwIfAborted?.();
6649
+ return this.runtime._serializeSession(
6650
+ this.sessionId,
6651
+ () => this.sendImpl(workflowName, input, options?.signal, options?.events)
6652
+ );
6653
+ }
6654
+ async sendImpl(workflowName, input, signal, events) {
6655
+ const { history, metadata } = await this.prepareHistory(input);
6656
+ const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
6657
+ await this.commitHistory(history, result);
5936
6658
  return result;
5937
6659
  }
5938
- async stream(workflowName, input) {
5939
- if (this.closed) throw new Error("Session has been ended");
6660
+ /** Read history + summary, apply maxMessages limit (with optional
6661
+ * summarization), and push the new user message. Returns the mutable
6662
+ * `history` array (which `executeAgentCall` will append the assistant
6663
+ * reply to in-place) and the `metadata` payload to forward to
6664
+ * `runtime.execute()` / `runtime.stream()`. Shared by `sendImpl` and
6665
+ * `streamImpl` so future fixes (e.g., to summarization wiring) land in
6666
+ * one place. */
6667
+ async prepareHistory(input) {
5940
6668
  const history = await this.store.getSession(this.sessionId);
5941
6669
  let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
5942
6670
  const maxMessages = this.options.history?.maxMessages;
@@ -5959,41 +6687,83 @@ var Session = class _Session {
5959
6687
  history.length = 0;
5960
6688
  history.push(...trimmed);
5961
6689
  }
5962
- const userMessage = {
6690
+ history.push({
5963
6691
  role: "user",
5964
6692
  content: typeof input === "string" ? input : JSON.stringify(input)
5965
- };
5966
- history.push(userMessage);
5967
- const axlStream = this.runtime.stream(workflowName, input, {
5968
- metadata: {
5969
- sessionId: this.sessionId,
5970
- sessionHistory: history,
5971
- ...cachedSummary ? { summaryCache: cachedSummary } : {}
5972
- }
5973
6693
  });
5974
- const updateHistory = async (result) => {
5975
- const lastMsg = history[history.length - 1];
5976
- if (!(lastMsg && lastMsg.role === "assistant")) {
5977
- history.push({
5978
- role: "assistant",
5979
- content: typeof result === "string" ? result : JSON.stringify(result)
5980
- });
5981
- }
5982
- if (this.options.persist !== false) {
5983
- await this.store.saveSession(this.sessionId, history);
5984
- }
6694
+ const metadata = {
6695
+ sessionId: this.sessionId,
6696
+ sessionHistory: history,
6697
+ ...cachedSummary ? { summaryCache: cachedSummary } : {}
5985
6698
  };
5986
- axlStream.on("done", (event) => {
5987
- const data = event.data;
5988
- updateHistory(data.result).catch((err) => {
6699
+ return { history, metadata };
6700
+ }
6701
+ /** Push the assistant reply (if `executeAgentCall` didn't already) and
6702
+ * persist. Shared by `sendImpl` and `streamImpl`. The fallback assistant
6703
+ * push has no agent context and so leaves `agent` undefined — see the
6704
+ * `ChatMessage.agent` field doc. */
6705
+ async commitHistory(history, result) {
6706
+ const lastMsg = history[history.length - 1];
6707
+ if (!(lastMsg && lastMsg.role === "assistant")) {
6708
+ history.push({
6709
+ role: "assistant",
6710
+ content: typeof result === "string" ? result : JSON.stringify(result)
6711
+ });
6712
+ }
6713
+ if (this.options.persist !== false) {
6714
+ await this.store.saveSession(this.sessionId, history);
6715
+ }
6716
+ }
6717
+ async stream(workflowName, input, options) {
6718
+ if (this.closed) throw new Error("Session has been ended");
6719
+ options?.signal?.throwIfAborted?.();
6720
+ let resolveReady;
6721
+ let rejectReady;
6722
+ const streamReady = new Promise((res, rej) => {
6723
+ resolveReady = res;
6724
+ rejectReady = rej;
6725
+ });
6726
+ void this.runtime._serializeSession(
6727
+ this.sessionId,
6728
+ () => this.streamImpl(
6729
+ workflowName,
6730
+ input,
6731
+ resolveReady,
6732
+ rejectReady,
6733
+ options?.signal,
6734
+ options?.events
6735
+ )
6736
+ ).catch(() => {
6737
+ });
6738
+ return streamReady;
6739
+ }
6740
+ async streamImpl(workflowName, input, resolveReady, rejectReady, signal, events) {
6741
+ let history;
6742
+ let axlStream;
6743
+ try {
6744
+ const prepared = await this.prepareHistory(input);
6745
+ history = prepared.history;
6746
+ axlStream = this.runtime.stream(workflowName, input, {
6747
+ metadata: prepared.metadata,
6748
+ signal,
6749
+ events
6750
+ });
6751
+ } catch (err) {
6752
+ rejectReady(err);
6753
+ throw err;
6754
+ }
6755
+ resolveReady(axlStream);
6756
+ const completion = axlStream.promise.then(
6757
+ (result) => this.commitHistory(history, result).catch((err) => {
5989
6758
  this.runtime.emit("error", {
5990
6759
  type: "session_history_save_failed",
5991
6760
  sessionId: this.sessionId,
5992
6761
  error: err instanceof Error ? err.message : String(err)
5993
6762
  });
5994
- });
5995
- });
5996
- return axlStream;
6763
+ }),
6764
+ () => void 0
6765
+ );
6766
+ await completion;
5997
6767
  }
5998
6768
  async history() {
5999
6769
  return this.store.getSession(this.sessionId);
@@ -6004,99 +6774,71 @@ var Session = class _Session {
6004
6774
  }
6005
6775
  async end() {
6006
6776
  this.closed = true;
6007
- if (this.options.persist !== false) {
6008
- await this.store.deleteSession(this.sessionId);
6009
- }
6777
+ return this.runtime._serializeSession(this.sessionId, async () => {
6778
+ if (this.options.persist !== false) {
6779
+ await this.store.deleteSession(this.sessionId);
6780
+ }
6781
+ });
6010
6782
  }
6011
- async fork(newId) {
6012
- const history = await this.store.getSession(this.sessionId);
6013
- const forked = new _Session(newId, this.runtime, this.store, this.options);
6014
- await this.store.saveSession(newId, [...history]);
6015
- const summaryCache = await this.store.getSessionMeta(this.sessionId, "summaryCache");
6016
- if (summaryCache !== null) {
6017
- await this.store.saveSessionMeta(newId, "summaryCache", summaryCache);
6018
- }
6019
- const handoffHistory = await this.store.getSessionMeta(this.sessionId, "handoffHistory");
6020
- if (handoffHistory !== null) {
6021
- await this.store.saveSessionMeta(newId, "handoffHistory", handoffHistory);
6022
- }
6023
- return forked;
6783
+ async fork(newId, options) {
6784
+ if (newId === this.sessionId) {
6785
+ throw new Error(`Session.fork: newId must differ from source id (${this.sessionId})`);
6786
+ }
6787
+ const [first, second] = [this.sessionId, newId].sort();
6788
+ return this.runtime._serializeSession(
6789
+ first,
6790
+ () => this.runtime._serializeSession(second, async () => {
6791
+ if (!options?.overwrite) {
6792
+ const existing = await this.store.getSession(newId);
6793
+ if (existing.length > 0) {
6794
+ throw new Error(
6795
+ `Session.fork: target id "${newId}" already has history. Pass { overwrite: true } to replace it, or choose a different id.`
6796
+ );
6797
+ }
6798
+ }
6799
+ const history = await this.store.getSession(this.sessionId);
6800
+ const forked = new _Session(newId, this.runtime, this.store, this.options);
6801
+ await this.store.saveSession(newId, [...history]);
6802
+ const summaryCache = await this.store.getSessionMeta(this.sessionId, "summaryCache");
6803
+ if (summaryCache !== null) {
6804
+ await this.store.saveSessionMeta(newId, "summaryCache", summaryCache);
6805
+ }
6806
+ const handoffHistory = await this.store.getSessionMeta(this.sessionId, "handoffHistory");
6807
+ if (handoffHistory !== null) {
6808
+ await this.store.saveSessionMeta(newId, "handoffHistory", handoffHistory);
6809
+ }
6810
+ if (this.store.getAllMemory && this.store.saveMemory) {
6811
+ const sourceScope = `session:${this.sessionId}`;
6812
+ const targetScope = `session:${newId}`;
6813
+ const entries = await this.store.getAllMemory(sourceScope);
6814
+ for (const { key, value } of entries) {
6815
+ await this.store.saveMemory(targetScope, key, value);
6816
+ }
6817
+ }
6818
+ return forked;
6819
+ })
6820
+ );
6024
6821
  }
6025
6822
  };
6026
6823
 
6027
6824
  // src/stream.ts
6028
6825
  var import_node_stream = require("stream");
6029
- var import_node_events = require("events");
6030
-
6031
- // src/types.ts
6032
- var AXL_EVENT_TYPES = [
6033
- // Workflow lifecycle
6034
- "workflow_start",
6035
- "workflow_end",
6036
- // Ask boundary
6037
- "ask_start",
6038
- "ask_end",
6039
- // Agent turn lifecycle
6040
- "agent_call_start",
6041
- "agent_call_end",
6042
- // Content delivery (stream-only)
6043
- "token",
6044
- // Tool invocation lifecycle
6045
- "tool_call_start",
6046
- "tool_call_end",
6047
- // Single-point tool events
6048
- "tool_approval",
6049
- "tool_denied",
6050
- // Delegation
6051
- "delegate",
6052
- // Handoff (spans two asks; not AskScoped).
6053
- // `handoff_start` emits before the target ask begins; always fired.
6054
- // `handoff_return` emits when control returns to source; roundtrip only
6055
- // (oneway handoffs have no return trip — the target's `ask_end` IS the
6056
- // end of the chain).
6057
- "handoff_start",
6058
- "handoff_return",
6059
- // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
6060
- "pipeline",
6061
- // Progressive structured output — added in PR 2; reserved here.
6062
- "partial_object",
6063
- // Verification
6064
- "verify",
6065
- // Observability
6066
- "log",
6067
- "memory_remember",
6068
- "memory_recall",
6069
- "memory_forget",
6070
- // Durable execution checkpoints (`ctx.checkpoint`)
6071
- "checkpoint_save",
6072
- "checkpoint_replay",
6073
- // Human-in-the-loop (`ctx.awaitHuman`)
6074
- "await_human",
6075
- "await_human_resolved",
6076
- // Legacy gate events — emitted by current code; collapsed into `pipeline`
6077
- // in PR 2 (spec/16-streaming-wire-reliability §4.2).
6078
- "guardrail",
6079
- "schema_check",
6080
- "validate",
6081
- // Terminal workflow markers
6082
- "done",
6083
- "error"
6084
- ];
6085
-
6086
- // src/stream.ts
6087
- var AxlStream = class _AxlStream extends import_node_stream.Readable {
6088
- bus = new import_node_events.EventEmitter();
6826
+ var AxlStream = class extends import_node_stream.Readable {
6827
+ /** Composed event bus — owns iterator queue, EventEmitter gating, and
6828
+ * curated views (`.text`, `.lifecycle`, `.textByAsk`). Private; the
6829
+ * public surface is via the delegating methods below. */
6830
+ events;
6089
6831
  /**
6090
- * Per-ask token buffers split into "in-progress" and "committed"
6091
- * halves so `fullText` only includes tokens from attempts that
6092
- * actually won. Scoping is per-`askId` so concurrent root-level asks
6093
- * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
6094
- * interleave each other's tokens — each branch's chunks stay
6095
- * contiguous, and a `pipeline(failed)` on one branch only discards
6096
- * THAT branch's in-progress buffer (previously: shared buffer caused
6097
- * a failure on one branch to discard a peer's in-flight successful
6098
- * tokens). Insertion order in the Maps reflects which ask emitted
6099
- * its first token first, which is what `fullText` joins on.
6832
+ * Per-ask token buffers split into "in-progress" and "committed" halves
6833
+ * so `fullText` only includes tokens from attempts that actually won.
6834
+ * Scoping is per-`askId` so concurrent root-level asks
6835
+ * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't interleave
6836
+ * each other's tokens — each branch's chunks stay contiguous, and a
6837
+ * `pipeline(failed)` on one branch only discards THAT branch's
6838
+ * in-progress buffer (previously: shared buffer caused a failure on
6839
+ * one branch to discard a peer's in-flight successful tokens).
6840
+ * Insertion order in the Maps reflects which ask emitted its first
6841
+ * token first, which is what `fullText` joins on.
6100
6842
  *
6101
6843
  * On `pipeline(status: 'committed')` for an ask, that ask's
6102
6844
  * in-progress entry flushes to its committed entry. On
@@ -6110,189 +6852,98 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6110
6852
  attemptByAsk = /* @__PURE__ */ new Map();
6111
6853
  committedByAsk = /* @__PURE__ */ new Map();
6112
6854
  result = void 0;
6113
- finished = false;
6114
6855
  promise;
6115
- eventQueue = [];
6116
- waiters = [];
6117
- constructor() {
6856
+ resolvePromise;
6857
+ rejectPromise;
6858
+ constructor(options) {
6118
6859
  super({ objectMode: true, read() {
6119
6860
  } });
6120
- let resolvePromise;
6121
- let rejectPromise;
6861
+ this.events = new AxlEventBus(options, () => this.destroy());
6122
6862
  this.promise = new Promise((resolve2, reject) => {
6123
- resolvePromise = resolve2;
6124
- rejectPromise = reject;
6125
- });
6126
- this.bus.on("__resolve", (value) => resolvePromise(value));
6127
- this.bus.on("__reject", (error) => rejectPromise(error));
6128
- this.bus.on("error", () => {
6863
+ this.resolvePromise = resolve2;
6864
+ this.rejectPromise = reject;
6129
6865
  });
6130
6866
  this.promise.catch(() => {
6131
6867
  });
6132
6868
  }
6133
- /** Wire-format event names callers can subscribe to via `.on(name, fn)`.
6134
- * Derived from the canonical `AXL_EVENT_TYPES` tuple — adding a new
6135
- * variant in `types.ts` automatically extends the subscribable set. */
6136
- static STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
6869
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6137
6870
  on(event, handler) {
6138
- if (_AxlStream.STREAM_EVENTS.has(event)) {
6139
- this.bus.on(event, handler);
6871
+ if (this.events.knowsEventName(event)) {
6872
+ this.events.on(event, handler);
6140
6873
  } else {
6141
6874
  super.on(event, handler);
6142
6875
  }
6143
6876
  return this;
6144
6877
  }
6878
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6145
6879
  off(event, handler) {
6146
- if (_AxlStream.STREAM_EVENTS.has(event)) {
6147
- this.bus.off(event, handler);
6880
+ if (this.events.knowsEventName(event)) {
6881
+ this.events.off(event, handler);
6148
6882
  } else {
6149
6883
  super.off(event, handler);
6150
6884
  }
6151
6885
  return this;
6152
6886
  }
6887
+ // Override Readable's [Symbol.asyncIterator] to delegate to the bus.
6888
+ // No explicit return type — structural inference keeps it compatible
6889
+ // with Readable's own signature while preserving the richer
6890
+ // iterator-with-dispose shape for direct consumers.
6153
6891
  [Symbol.asyncIterator]() {
6154
- const self = this;
6155
- return {
6156
- next: () => {
6157
- if (self.eventQueue.length > 0) {
6158
- return Promise.resolve({ value: self.eventQueue.shift(), done: false });
6159
- }
6160
- if (self.finished && self.eventQueue.length === 0) {
6161
- return Promise.resolve({ value: void 0, done: true });
6162
- }
6163
- return new Promise((resolve2) => {
6164
- self.waiters.push(resolve2);
6165
- });
6166
- },
6167
- [Symbol.asyncIterator]() {
6168
- return this;
6169
- },
6170
- [Symbol.asyncDispose]() {
6171
- self.destroy();
6172
- return Promise.resolve();
6173
- }
6174
- };
6892
+ return this.events[Symbol.asyncIterator]();
6175
6893
  }
6176
6894
  get text() {
6177
- const self = this;
6178
- return {
6179
- [Symbol.asyncIterator]() {
6180
- const iter = self[Symbol.asyncIterator]();
6181
- return {
6182
- async next() {
6183
- while (true) {
6184
- const { value, done } = await iter.next();
6185
- if (done) return { value: void 0, done: true };
6186
- if (value.type === "token" && isRootLevel(value)) {
6187
- return { value: value.data, done: false };
6188
- }
6189
- }
6190
- }
6191
- };
6192
- }
6193
- };
6895
+ return this.events.text;
6194
6896
  }
6195
- /**
6196
- * Iterator over `{ askId, text }` pairs — one emission per token chunk,
6197
- * tagged with the ask frame that produced it. Complements `.text`
6198
- * (root-only stream): consumers building a split-pane UI that shows
6199
- * each sub-agent's output in its own lane can group by `askId`
6200
- * without iterating the raw stream and hand-filtering on `event.type`.
6201
- *
6202
- * Nested and root tokens both flow through. The `agent` field names
6203
- * the producing agent for UI labelling; it's undefined when the token
6204
- * was emitted outside any ask (rare — synthesized test fixtures).
6205
- */
6206
6897
  get textByAsk() {
6207
- const self = this;
6208
- return {
6209
- [Symbol.asyncIterator]() {
6210
- const iter = self[Symbol.asyncIterator]();
6211
- return {
6212
- async next() {
6213
- while (true) {
6214
- const { value, done } = await iter.next();
6215
- if (done) {
6216
- return {
6217
- value: void 0,
6218
- done: true
6219
- };
6220
- }
6221
- if (value.type === "token") {
6222
- return {
6223
- value: { askId: value.askId, agent: value.agent, text: value.data },
6224
- done: false
6225
- };
6226
- }
6227
- }
6228
- }
6229
- };
6230
- }
6231
- };
6898
+ return this.events.textByAsk;
6232
6899
  }
6233
- /**
6234
- * Iterator over structural lifecycle events only — skips per-token chatter
6235
- * and progressive partial_object emissions. Useful for waterfall UIs and
6236
- * any consumer that wants the "what happened" timeline without per-chunk
6237
- * noise. Renamed from `.steps` in the unified-event-model migration
6238
- * because these are events, not pipeline steps.
6239
- */
6240
6900
  get lifecycle() {
6241
- const lifecycleTypes = /* @__PURE__ */ new Set([
6242
- "ask_start",
6243
- "ask_end",
6244
- "agent_call_start",
6245
- "agent_call_end",
6246
- "tool_call_start",
6247
- "tool_call_end",
6248
- "tool_approval",
6249
- "tool_denied",
6250
- "handoff_start",
6251
- "handoff_return",
6252
- "delegate",
6253
- "pipeline",
6254
- "verify",
6255
- "workflow_start",
6256
- "workflow_end",
6257
- // Durable-execution checkpoints — structural points in the timeline.
6258
- "checkpoint_save",
6259
- "checkpoint_replay",
6260
- // Human-in-the-loop — pause/resume are major timeline landmarks.
6261
- "await_human",
6262
- "await_human_resolved"
6263
- ]);
6264
- const self = this;
6265
- return {
6266
- [Symbol.asyncIterator]() {
6267
- const iter = self[Symbol.asyncIterator]();
6268
- return {
6269
- async next() {
6270
- while (true) {
6271
- const { value, done } = await iter.next();
6272
- if (done) return { value: void 0, done: true };
6273
- if (lifecycleTypes.has(value.type)) return { value, done: false };
6274
- }
6275
- }
6276
- };
6277
- }
6278
- };
6901
+ return this.events.lifecycle;
6902
+ }
6903
+ /** Coalescing iterator over `partial_object` events — yields the latest
6904
+ * object payload per `askId`. Designed for streaming-structured-output
6905
+ * UIs: when the producer outpaces the consumer, intermediate snapshots
6906
+ * are silently superseded and the next `.next()` await sees only the
6907
+ * most recent state per ask. Listener-based, so does NOT race with the
6908
+ * main `for await (const e of stream)` iterator. See
6909
+ * `AxlEventBus.partialObjects` for full semantics. The yielded
6910
+ * `CoalescedPartialObject` shape is shared with the bus so the two
6911
+ * surfaces can never drift on the `attempt` field. */
6912
+ get partialObjects() {
6913
+ return this.events.partialObjects;
6279
6914
  }
6280
6915
  pipe(destination, options) {
6281
6916
  const shouldEnd = options?.end !== false;
6282
- this.bus.on("token", (event) => {
6917
+ this.events.on("token", (event) => {
6283
6918
  if (event.type === "token") destination.write(event.data);
6284
6919
  });
6285
- this.bus.on("done", () => {
6920
+ this.events.on("done", () => {
6286
6921
  if (shouldEnd) destination.end();
6287
6922
  });
6288
- this.bus.on("error", () => {
6923
+ this.events.on("error", () => {
6289
6924
  if (shouldEnd) destination.end();
6290
6925
  });
6291
6926
  return destination;
6292
6927
  }
6293
- /** Push an event onto the stream. Called by the runtime. */
6928
+ /** Push an event onto the stream. Called by the runtime.
6929
+ *
6930
+ * Side-effect ordering: `accountForToken` (fullText buffers) →
6931
+ * `events._push` (bus.emit + iterator waiter/queue) → `Readable.push`
6932
+ * (object-mode buffer for `.pipe()` consumers and `'data'` listeners).
6933
+ * Pre-refactor the order was `bus.emit → Readable.push → waiter/queue`;
6934
+ * the new order resolves async iterators before Readable.push. The
6935
+ * difference is invisible to all in-tree consumers — `.pipe()` here
6936
+ * routes through the bus's `'token'` listener (line 129), and existing
6937
+ * tests pass — but it's intentional rather than accidental. */
6294
6938
  _push(event) {
6295
- if (this.finished) return;
6939
+ if (this.events.isFinished) return;
6940
+ this.accountForToken(event);
6941
+ this.events._push(event);
6942
+ this.push(event);
6943
+ }
6944
+ /** Token accumulation and pipeline commit/discard accounting for
6945
+ * `fullText`. Extracted from `_push` so the bus delegation reads cleanly. */
6946
+ accountForToken(event) {
6296
6947
  if (event.type === "token" && isRootLevel(event)) {
6297
6948
  const key = event.askId ?? "";
6298
6949
  const attempt = this.attemptByAsk.get(key);
@@ -6301,6 +6952,7 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6301
6952
  } else {
6302
6953
  this.attemptByAsk.set(key, [event.data]);
6303
6954
  }
6955
+ return;
6304
6956
  }
6305
6957
  if (event.type === "pipeline" && isRootLevel(event)) {
6306
6958
  const key = event.askId ?? "";
@@ -6314,18 +6966,11 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6314
6966
  } else if (event.status === "failed") {
6315
6967
  this.attemptByAsk.set(key, []);
6316
6968
  }
6969
+ return;
6317
6970
  }
6318
6971
  if (event.type === "ask_end" && isRootLevel(event) && !event.outcome.ok) {
6319
6972
  this.attemptByAsk.set(event.askId ?? "", []);
6320
6973
  }
6321
- this.bus.emit(event.type, event);
6322
- this.push(event);
6323
- const waiter = this.waiters.shift();
6324
- if (waiter) {
6325
- waiter({ value: event, done: false });
6326
- } else {
6327
- this.eventQueue.push(event);
6328
- }
6329
6974
  }
6330
6975
  /**
6331
6976
  * Signal successful completion.
@@ -6336,8 +6981,7 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6336
6981
  * on error paths that threw before `execInfo` was assigned (review S4).
6337
6982
  */
6338
6983
  _done(result, executionId) {
6339
- if (this.finished) return;
6340
- this.finished = true;
6984
+ if (this.events.isFinished) return;
6341
6985
  this.result = result;
6342
6986
  const doneEvent = {
6343
6987
  type: "done",
@@ -6346,27 +6990,17 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6346
6990
  timestamp: Date.now(),
6347
6991
  data: { result }
6348
6992
  };
6349
- this.bus.emit("done", doneEvent);
6993
+ this.events._push(doneEvent);
6350
6994
  this.push(doneEvent);
6351
6995
  this.push(null);
6352
- const waiter = this.waiters.shift();
6353
- if (waiter) {
6354
- waiter({ value: doneEvent, done: false });
6355
- } else {
6356
- this.eventQueue.push(doneEvent);
6357
- }
6358
- for (const w of this.waiters) {
6359
- w({ value: void 0, done: true });
6360
- }
6361
- this.waiters.length = 0;
6362
- this.bus.emit("__resolve", result);
6996
+ this.events._finish();
6997
+ this.resolvePromise(result);
6363
6998
  }
6364
6999
  /** Signal an error. `executionId` is required for the same reason as
6365
7000
  * `_done`: terminal events must carry a real id even when the
6366
7001
  * failure happens before any real trace event fires (review S4). */
6367
7002
  _error(error, executionId) {
6368
- if (this.finished) return;
6369
- this.finished = true;
7003
+ if (this.events.isFinished) return;
6370
7004
  const errorEvent = {
6371
7005
  type: "error",
6372
7006
  executionId,
@@ -6374,20 +7008,11 @@ var AxlStream = class _AxlStream extends import_node_stream.Readable {
6374
7008
  timestamp: Date.now(),
6375
7009
  data: { message: error.message, name: error.name }
6376
7010
  };
6377
- this.bus.emit("error", errorEvent);
7011
+ this.events._push(errorEvent);
6378
7012
  this.push(errorEvent);
6379
7013
  this.push(null);
6380
- const waiter = this.waiters.shift();
6381
- if (waiter) {
6382
- waiter({ value: errorEvent, done: false });
6383
- } else {
6384
- this.eventQueue.push(errorEvent);
6385
- }
6386
- for (const w of this.waiters) {
6387
- w({ value: void 0, done: true });
6388
- }
6389
- this.waiters.length = 0;
6390
- this.bus.emit("__reject", error);
7014
+ this.events._finish();
7015
+ this.rejectPromise(error);
6391
7016
  }
6392
7017
  /** Concatenated root-only text from committed attempts plus the
6393
7018
  * current in-flight attempt(s). Retried (gate-rejected) attempts
@@ -6987,6 +7612,18 @@ function pushEventBounded(execInfo, event, cap) {
6987
7612
  }
6988
7613
  }
6989
7614
  var costScopeStorage = new import_node_async_hooks2.AsyncLocalStorage();
7615
+ function forwardAbortSignal(external, internal) {
7616
+ if (!external) return () => {
7617
+ };
7618
+ if (external.aborted) {
7619
+ internal.abort();
7620
+ return () => {
7621
+ };
7622
+ }
7623
+ const onAbort = () => internal.abort();
7624
+ external.addEventListener("abort", onAbort, { once: true });
7625
+ return () => external.removeEventListener("abort", onAbort);
7626
+ }
6990
7627
  var AxlRuntime = class extends import_node_events2.EventEmitter {
6991
7628
  config;
6992
7629
  workflows = /* @__PURE__ */ new Map();
@@ -7003,12 +7640,23 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7003
7640
  spanManager = new NoopSpanManager();
7004
7641
  historicalExecutions = /* @__PURE__ */ new Map();
7005
7642
  historicalExecutionsLoadPromise = null;
7643
+ /** Per-session in-process serialization. Each entry is a chain of pending
7644
+ * work for that session id — `Session.send` / `Session.stream` await the
7645
+ * prior entry before reading history, so concurrent calls produce a
7646
+ * sequenced conversation log instead of last-writer-wins corruption.
7647
+ * Cross-process locking is NOT provided; use distinct session ids when
7648
+ * multiple processes share a store. */
7649
+ sessionLocks = /* @__PURE__ */ new Map();
7006
7650
  evalHistory = [];
7007
7651
  evalHistoryLoadPromise = null;
7008
7652
  /** Resolved cap on `ExecutionInfo.events` per execution. Cached on
7009
7653
  * the instance so per-event onTrace handlers don't reach into
7010
7654
  * `this.config.state` on every emission. */
7011
7655
  maxEventsPerExecution;
7656
+ /** Tracks executionIds for which we've already warned about a malformed
7657
+ * `events` field — keeps the log to one line per row instead of one per
7658
+ * rebuild tick. */
7659
+ warnedMalformedExecutions = /* @__PURE__ */ new Set();
7012
7660
  constructor(config) {
7013
7661
  super();
7014
7662
  this.config = resolveConfig(config ?? {});
@@ -7094,8 +7742,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7094
7742
  * Side effects on `execInfo`: sets `status` / `completedAt` /
7095
7743
  * `duration` / `result` (or `error`) before emitting `workflow_end`.
7096
7744
  * On throw: emits `workflow_end({status: 'failed', aborted?})` and
7097
- * persists before rethrowing. `abortControllers.delete` runs in
7098
- * `finally` regardless of outcome.
7745
+ * persists before rethrowing. `abortControllers.delete` and
7746
+ * `cleanupAbortForwarder` (if provided) both run in `finally`
7747
+ * regardless of outcome.
7099
7748
  *
7100
7749
  * Caller responsibilities:
7101
7750
  * - Allocate `executionId` and register it in `abortControllers` map
@@ -7105,9 +7754,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7105
7754
  * - Pass `span` if the call site is inside a tracer span — this
7106
7755
  * helper sets `axl.workflow.cost` / `.duration` attributes on
7107
7756
  * success.
7757
+ * - Pass `cleanupAbortForwarder` (the cleanup fn returned from
7758
+ * `forwardAbortSignal`) so the listener on the user's external
7759
+ * signal is removed when the workflow completes — see the
7760
+ * helper's docstring for the leak this prevents.
7108
7761
  */
7109
7762
  async runWorkflowBody(args) {
7110
- const { workflow: workflow2, ctx, execInfo, validated, span } = args;
7763
+ const { workflow: workflow2, ctx, execInfo, validated, span, cleanupAbortForwarder } = args;
7111
7764
  try {
7112
7765
  ctx._emitWorkflowStart(validated);
7113
7766
  const raw = await workflow2.handler(ctx);
@@ -7144,6 +7797,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7144
7797
  throw err;
7145
7798
  } finally {
7146
7799
  this.abortControllers.delete(execInfo.executionId);
7800
+ cleanupAbortForwarder?.();
7147
7801
  }
7148
7802
  }
7149
7803
  /**
@@ -7296,12 +7950,31 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7296
7950
  return result;
7297
7951
  }
7298
7952
  /** Get all execution info (running + completed + historical). */
7953
+ /** Normalize an `ExecutionInfo` loaded from a `StateStore` so the
7954
+ * `events: AxlEvent[]` type contract holds for downstream consumers
7955
+ * (Studio aggregators, REST routes, redaction). Custom or legacy stores
7956
+ * may persist rows without `events` (or with a non-array value) — without
7957
+ * this guard, a single malformed row crashes every iterator over
7958
+ * `exec.events` and takes down dependent features. Mutates the row
7959
+ * in place; logs at most one warning per `executionId`. */
7960
+ normalizeStoredExecution(exec) {
7961
+ if (!Array.isArray(exec.events)) {
7962
+ if (!this.warnedMalformedExecutions.has(exec.executionId)) {
7963
+ this.warnedMalformedExecutions.add(exec.executionId);
7964
+ console.warn(
7965
+ `[axl] StateStore returned execution ${exec.executionId} with non-array events (got ${exec.events === null ? "null" : typeof exec.events}); coercing to []`
7966
+ );
7967
+ }
7968
+ exec.events = [];
7969
+ }
7970
+ return exec;
7971
+ }
7299
7972
  async getExecutions() {
7300
7973
  if (!this.historicalExecutionsLoadPromise && this.stateStore.listExecutions) {
7301
7974
  this.historicalExecutionsLoadPromise = this.stateStore.listExecutions().then((stored) => {
7302
7975
  for (const exec of stored) {
7303
7976
  if (!this.executions.has(exec.executionId) && !this.historicalExecutions.has(exec.executionId)) {
7304
- this.historicalExecutions.set(exec.executionId, exec);
7977
+ this.historicalExecutions.set(exec.executionId, this.normalizeStoredExecution(exec));
7305
7978
  }
7306
7979
  }
7307
7980
  }).catch(() => {
@@ -7342,6 +8015,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7342
8015
  onToolCall: options?.onToolCall,
7343
8016
  onAgentStart: options?.onAgentStart,
7344
8017
  awaitHumanHandler: options?.awaitHumanHandler,
8018
+ eventStreamOptions: options?.events,
7345
8019
  onTrace: (event) => {
7346
8020
  this.emit("trace", event);
7347
8021
  this.outputAxlEvent(event);
@@ -7374,6 +8048,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7374
8048
  const executionId = (0, import_node_crypto3.randomUUID)();
7375
8049
  const controller = new AbortController();
7376
8050
  this.abortControllers.set(executionId, controller);
8051
+ const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
7377
8052
  this.registerWithCostScope(executionId);
7378
8053
  const execInfo = {
7379
8054
  executionId,
@@ -7394,6 +8069,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7394
8069
  providerRegistry: this.providerRegistry,
7395
8070
  sessionHistory,
7396
8071
  signal: controller.signal,
8072
+ eventStreamOptions: options?.events,
7397
8073
  onTrace: (event) => {
7398
8074
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7399
8075
  execInfo.totalCost += eventCostContribution(event);
@@ -7455,15 +8131,17 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7455
8131
  ctx,
7456
8132
  execInfo,
7457
8133
  validated,
7458
- span
8134
+ span,
8135
+ cleanupAbortForwarder
7459
8136
  })
7460
8137
  );
7461
8138
  }
7462
8139
  /** Execute a workflow and return a stream. */
7463
8140
  stream(name, input, options) {
7464
- const axlStream = new AxlStream();
8141
+ const axlStream = new AxlStream(options?.events);
7465
8142
  const controller = new AbortController();
7466
8143
  axlStream.on("close", () => controller.abort());
8144
+ const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
7467
8145
  const executionId = (0, import_node_crypto3.randomUUID)();
7468
8146
  this.abortControllers.set(executionId, controller);
7469
8147
  let execInfo;
@@ -7491,6 +8169,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7491
8169
  providerRegistry: this.providerRegistry,
7492
8170
  sessionHistory,
7493
8171
  signal: controller.signal,
8172
+ eventStreamOptions: options?.events,
7494
8173
  // Sentinel `onToken` so the streaming code path in WorkflowContext
7495
8174
  // is entered (it's gated on `this.onToken` being defined). The
7496
8175
  // actual `token` AxlEvents are emitted via `emitEvent` and reach
@@ -7559,7 +8238,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7559
8238
  ctx: wfCtx,
7560
8239
  execInfo,
7561
8240
  validated,
7562
- span
8241
+ span,
8242
+ cleanupAbortForwarder
7563
8243
  })
7564
8244
  );
7565
8245
  };
@@ -7572,6 +8252,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7572
8252
  this.persistExecution(execInfo);
7573
8253
  }
7574
8254
  this.abortControllers.delete(executionId);
8255
+ cleanupAbortForwarder();
7575
8256
  axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
7576
8257
  });
7577
8258
  return axlStream;
@@ -7580,12 +8261,55 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7580
8261
  session(id, options) {
7581
8262
  return new Session(id, this, this.stateStore, options);
7582
8263
  }
7583
- /** Gracefully shut down the runtime, aborting in-flight executions and closing state stores and MCP servers. */
8264
+ /** @internal Serialize work per session id within this runtime instance.
8265
+ * Subsequent calls await the prior task's settlement (success or failure)
8266
+ * before running. Used by `Session.send` / `Session.stream` / `end` /
8267
+ * `fork` to eliminate read-modify-write races on `StateStore.saveSession`
8268
+ * (and on `delete`/`getSession` ordering vs in-flight saves).
8269
+ *
8270
+ * ⚠️ NOT REENTRANT. Calling this from inside an `fn` already running
8271
+ * under `_serializeSession(sameId, ...)` will deadlock — the inner
8272
+ * call awaits the outer's settlement, the outer awaits the inner's
8273
+ * return. `Session.fork` deliberately calls with TWO different ids
8274
+ * (source and target) and never the same id twice. Any future caller
8275
+ * must follow the same rule. */
8276
+ async _serializeSession(id, fn) {
8277
+ const prev = this.sessionLocks.get(id);
8278
+ if (prev !== void 0) {
8279
+ this.emit("session_lock_contended", { sessionId: id });
8280
+ }
8281
+ const base = prev ?? Promise.resolve();
8282
+ const ours = base.then(fn, fn);
8283
+ const chained = ours.then(
8284
+ () => {
8285
+ },
8286
+ () => {
8287
+ }
8288
+ );
8289
+ this.sessionLocks.set(id, chained);
8290
+ void chained.finally(() => {
8291
+ if (this.sessionLocks.get(id) === chained) {
8292
+ this.sessionLocks.delete(id);
8293
+ }
8294
+ });
8295
+ return ours;
8296
+ }
8297
+ /** Gracefully shut down the runtime, aborting in-flight executions and
8298
+ * closing state stores and MCP servers. Drains in-flight per-session
8299
+ * work (Session.send / Session.stream) before closing the state store.
8300
+ *
8301
+ * This is a drain of EXISTING work, not a barrier against new calls —
8302
+ * callers should stop accepting work (e.g., close their HTTP server)
8303
+ * before invoking `shutdown()`. A `Session.send` invoked after the
8304
+ * drain snapshot is taken can still race a closed state store. */
7584
8305
  async shutdown() {
7585
8306
  for (const [, controller] of this.abortControllers) {
7586
8307
  controller.abort();
7587
8308
  }
7588
8309
  this.abortControllers.clear();
8310
+ if (this.sessionLocks.size > 0) {
8311
+ await Promise.allSettled([...this.sessionLocks.values()]);
8312
+ }
7589
8313
  const errors = [];
7590
8314
  const safeClose = async (label, fn) => {
7591
8315
  try {
@@ -7621,8 +8345,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7621
8345
  if (this.stateStore.getExecution) {
7622
8346
  const stored = await this.stateStore.getExecution(executionId);
7623
8347
  if (stored) {
7624
- this.historicalExecutions.set(executionId, stored);
7625
- return stored;
8348
+ const normalized = this.normalizeStoredExecution(stored);
8349
+ this.historicalExecutions.set(executionId, normalized);
8350
+ return normalized;
7626
8351
  }
7627
8352
  }
7628
8353
  return void 0;
@@ -8112,7 +8837,14 @@ async function ensureTsxRegistered() {
8112
8837
  return tsxRegistered;
8113
8838
  }
8114
8839
  async function importModule(filePath, _parentURL) {
8115
- if (needsTsxLoader(filePath)) {
8840
+ const url = (0, import_node_url.pathToFileURL)(filePath).href;
8841
+ if (!needsTsxLoader(filePath)) {
8842
+ return await import(url);
8843
+ }
8844
+ try {
8845
+ return await import(url);
8846
+ } catch (err) {
8847
+ if (!isMissingLoaderError(err)) throw err;
8116
8848
  const registered = await ensureTsxRegistered();
8117
8849
  if (!registered) {
8118
8850
  throw new Error(
@@ -8122,8 +8854,14 @@ async function importModule(filePath, _parentURL) {
8122
8854
  disabled, install it explicitly: npm install -D tsx`
8123
8855
  );
8124
8856
  }
8857
+ return await import(url);
8125
8858
  }
8126
- return await import((0, import_node_url.pathToFileURL)(filePath).href);
8859
+ }
8860
+ function isMissingLoaderError(err) {
8861
+ if (!(err instanceof Error)) return false;
8862
+ const code = err.code;
8863
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") return true;
8864
+ return /Cannot use import statement outside a module/i.test(err.message);
8127
8865
  }
8128
8866
  var MAX_GLOB_DEPTH = 20;
8129
8867
  function expandGlob(pattern, cwd) {
@@ -8536,11 +9274,13 @@ function cosineSimilarity2(a, b) {
8536
9274
  AXL_EVENT_TYPES,
8537
9275
  AnthropicProvider,
8538
9276
  AxlError,
9277
+ AxlEventBus,
8539
9278
  AxlRuntime,
8540
9279
  AxlStream,
8541
9280
  BudgetExceededError,
8542
9281
  CONFIG_CANDIDATES,
8543
9282
  COST_BEARING_LEAF_TYPES,
9283
+ EventStreamOverflowError,
8544
9284
  GeminiProvider,
8545
9285
  GuardrailError,
8546
9286
  InMemoryVectorStore,