@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.js CHANGED
@@ -2358,6 +2358,598 @@ function isRootLevel(event) {
2358
2358
  return (d ?? 0) === 0;
2359
2359
  }
2360
2360
 
2361
+ // src/event-stream.ts
2362
+ import { EventEmitter } from "events";
2363
+
2364
+ // src/types.ts
2365
+ var AXL_EVENT_TYPES = [
2366
+ // Workflow lifecycle
2367
+ "workflow_start",
2368
+ "workflow_end",
2369
+ // Ask boundary
2370
+ "ask_start",
2371
+ "ask_end",
2372
+ // Agent turn lifecycle
2373
+ "agent_call_start",
2374
+ "agent_call_end",
2375
+ // Content delivery (stream-only)
2376
+ "token",
2377
+ // Tool invocation lifecycle
2378
+ "tool_call_start",
2379
+ "tool_call_end",
2380
+ // Single-point tool events
2381
+ "tool_approval",
2382
+ "tool_denied",
2383
+ // Delegation
2384
+ "delegate",
2385
+ // Handoff (spans two asks; not AskScoped).
2386
+ // `handoff_start` emits before the target ask begins; always fired.
2387
+ // `handoff_return` emits when control returns to source; roundtrip only
2388
+ // (oneway handoffs have no return trip — the target's `ask_end` IS the
2389
+ // end of the chain).
2390
+ "handoff_start",
2391
+ "handoff_return",
2392
+ // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
2393
+ "pipeline",
2394
+ // Progressive structured output — added in PR 2; reserved here.
2395
+ "partial_object",
2396
+ // Verification
2397
+ "verify",
2398
+ // Observability
2399
+ "log",
2400
+ "memory_remember",
2401
+ "memory_recall",
2402
+ "memory_forget",
2403
+ // Durable execution checkpoints (`ctx.checkpoint`)
2404
+ "checkpoint_save",
2405
+ "checkpoint_replay",
2406
+ // Human-in-the-loop (`ctx.awaitHuman`)
2407
+ "await_human",
2408
+ "await_human_resolved",
2409
+ // Legacy gate events — emitted by current code; collapsed into `pipeline`
2410
+ // in PR 2 (spec/16-streaming-wire-reliability §4.2).
2411
+ "guardrail",
2412
+ "schema_check",
2413
+ "validate",
2414
+ // Terminal workflow markers
2415
+ "done",
2416
+ "error"
2417
+ ];
2418
+
2419
+ // src/event-stream.ts
2420
+ var STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
2421
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
2422
+ "done",
2423
+ "error",
2424
+ "workflow_end"
2425
+ ]);
2426
+ var LIFECYCLE_TYPES = /* @__PURE__ */ new Set([
2427
+ "ask_start",
2428
+ "ask_end",
2429
+ "agent_call_start",
2430
+ "agent_call_end",
2431
+ "tool_call_start",
2432
+ "tool_call_end",
2433
+ "tool_approval",
2434
+ "tool_denied",
2435
+ "handoff_start",
2436
+ "handoff_return",
2437
+ "delegate",
2438
+ "pipeline",
2439
+ "verify",
2440
+ "workflow_start",
2441
+ "workflow_end",
2442
+ "checkpoint_save",
2443
+ "checkpoint_replay",
2444
+ "await_human",
2445
+ "await_human_resolved"
2446
+ ]);
2447
+ var EventStreamOverflowError = class extends Error {
2448
+ maxQueued;
2449
+ eventType;
2450
+ constructor(maxQueued, eventType) {
2451
+ super(
2452
+ `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.`
2453
+ );
2454
+ this.name = "EventStreamOverflowError";
2455
+ this.maxQueued = maxQueued;
2456
+ this.eventType = eventType;
2457
+ }
2458
+ };
2459
+ var DEFAULT_MAX_QUEUED = 1e4;
2460
+ var DEFAULT_OVERFLOW = "drop-oldest-non-terminal";
2461
+ var AxlEventBus = class {
2462
+ bus = new EventEmitter();
2463
+ eventQueue = [];
2464
+ waiters = [];
2465
+ finished = false;
2466
+ maxQueued;
2467
+ onOverflow;
2468
+ overflowWarned = false;
2469
+ /** Callbacks fired exactly once when `_finish()` is called. Used by
2470
+ * curated views (e.g., `partialObjects`) that are listener-based and
2471
+ * need a termination signal independent of any synthetic `done` event. */
2472
+ finishCallbacks = [];
2473
+ /**
2474
+ * Latest coalesced partial-object snapshot per `askId`, maintained on
2475
+ * every `_push` regardless of consumer state. Seeds the
2476
+ * `partialObjects` view's `pending` map at subscription time so a
2477
+ * late subscriber recovers the most recent state per ask even if
2478
+ * earlier `partial_object` events were already delivered to a prior
2479
+ * iterator's waiter (and thus removed from `eventQueue`).
2480
+ *
2481
+ * The pre-fix snapshot walked `eventQueue` directly — that misses
2482
+ * events already drained by past consumers. The map walks past
2483
+ * `_push` calls instead, which is the actual definition of "what's
2484
+ * the latest partial per ask". `pipeline(status: 'failed')` clears
2485
+ * the entry for the affected askId so a discarded attempt can't
2486
+ * surface to a future subscriber.
2487
+ *
2488
+ * Memory bound: O(active asks holding a pending partial). Cleared
2489
+ * on `pipeline(failed)` (per ask) and otherwise grows with the
2490
+ * number of distinct askIds that emitted at least one partial.
2491
+ */
2492
+ latestPartialByAsk = /* @__PURE__ */ new Map();
2493
+ /**
2494
+ * Hook fired when an iterator obtained from `[Symbol.asyncIterator]`
2495
+ * is `await using`-disposed. Set via the second constructor argument.
2496
+ * Used by `AxlStream` to call `Readable.destroy()` on its owning
2497
+ * stream so disposing the iterator cascades to the Readable contract.
2498
+ * Set once at construction; never reassigned.
2499
+ *
2500
+ * Replaces the previous `AxlStreamEventBus extends AxlEventBus`
2501
+ * subclass — composition over inheritance for what is essentially a
2502
+ * single function-pointer hook.
2503
+ *
2504
+ * @internal
2505
+ */
2506
+ onIteratorDispose;
2507
+ /**
2508
+ * @param options Public stream options (queue cap, overflow policy).
2509
+ * @param onIteratorDispose @internal Invoked from `_disposeIterator`
2510
+ * when an iterator obtained from this bus is `await using`-disposed.
2511
+ * Used by `AxlStream` to forward to `Readable.destroy()`.
2512
+ */
2513
+ constructor(options, onIteratorDispose) {
2514
+ const requestedMax = options?.maxQueued ?? DEFAULT_MAX_QUEUED;
2515
+ if (typeof requestedMax !== "number" || Number.isNaN(requestedMax) || requestedMax !== Infinity && requestedMax < 1) {
2516
+ throw new Error(
2517
+ `EventStreamOptions.maxQueued must be >= 1 or Infinity (got ${String(requestedMax)}). To disable the cap, pass Infinity.`
2518
+ );
2519
+ }
2520
+ this.maxQueued = requestedMax;
2521
+ this.onOverflow = options?.onOverflow ?? DEFAULT_OVERFLOW;
2522
+ this.onIteratorDispose = onIteratorDispose;
2523
+ this.bus.on("error", () => {
2524
+ });
2525
+ }
2526
+ unknownEventWarned = false;
2527
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2528
+ on(event, handler) {
2529
+ if (STREAM_EVENTS.has(event)) {
2530
+ this.bus.on(event, handler);
2531
+ } else {
2532
+ this.warnUnknownEventName("on", event);
2533
+ }
2534
+ return this;
2535
+ }
2536
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2537
+ off(event, handler) {
2538
+ if (STREAM_EVENTS.has(event)) {
2539
+ this.bus.off(event, handler);
2540
+ } else {
2541
+ this.warnUnknownEventName("off", event);
2542
+ }
2543
+ return this;
2544
+ }
2545
+ /** One-shot warn on subscribe/unsubscribe for an event name outside
2546
+ * `AXL_EVENT_TYPES`. Pre-fix this branch silently dropped, which
2547
+ * meant a typo like `'agent_call_ended'` produced no signal. */
2548
+ warnUnknownEventName(method, event) {
2549
+ if (this.unknownEventWarned) return;
2550
+ this.unknownEventWarned = true;
2551
+ console.warn(
2552
+ `[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.`
2553
+ );
2554
+ }
2555
+ // No explicit return-type annotation: structural inference yields a type
2556
+ // assignable to Readable's `() => AsyncIterator<any, undefined, any>` when
2557
+ // AxlStream delegates here, while preserving the richer iterator-with-dispose
2558
+ // shape for direct AxlEventBus consumers.
2559
+ [Symbol.asyncIterator]() {
2560
+ const self = this;
2561
+ let myResolver = null;
2562
+ return {
2563
+ next: () => {
2564
+ if (self.eventQueue.length > 0) {
2565
+ return Promise.resolve({ value: self.eventQueue.shift(), done: false });
2566
+ }
2567
+ if (self.finished && self.eventQueue.length === 0) {
2568
+ return Promise.resolve({ value: void 0, done: true });
2569
+ }
2570
+ return new Promise((resolve2) => {
2571
+ const wrapped = (value) => {
2572
+ myResolver = null;
2573
+ resolve2(value);
2574
+ };
2575
+ myResolver = wrapped;
2576
+ self.waiters.push(wrapped);
2577
+ });
2578
+ },
2579
+ return: () => {
2580
+ if (myResolver) {
2581
+ const idx = self.waiters.indexOf(myResolver);
2582
+ if (idx >= 0) self.waiters.splice(idx, 1);
2583
+ const r = myResolver;
2584
+ myResolver = null;
2585
+ r({ value: void 0, done: true });
2586
+ }
2587
+ return Promise.resolve({ value: void 0, done: true });
2588
+ },
2589
+ [Symbol.asyncIterator]() {
2590
+ return this;
2591
+ },
2592
+ [Symbol.asyncDispose]: () => self._disposeIterator()
2593
+ };
2594
+ }
2595
+ /** Hook for iterator disposal. Calls the constructor-supplied
2596
+ * `onIteratorDispose` callback if any. The default behavior is a
2597
+ * no-op when no callback was passed — a plain bus has no
2598
+ * Node-stream resource to release. */
2599
+ _disposeIterator() {
2600
+ this.onIteratorDispose?.();
2601
+ return Promise.resolve();
2602
+ }
2603
+ /** Iterator over root-only `token` chunks. Skips nested-ask tokens
2604
+ * (consumers wanting nested can iterate the full bus and filter on
2605
+ * `event.depth >= 1`). */
2606
+ get text() {
2607
+ const self = this;
2608
+ return {
2609
+ [Symbol.asyncIterator]() {
2610
+ const iter = self[Symbol.asyncIterator]();
2611
+ return {
2612
+ async next() {
2613
+ while (true) {
2614
+ const { value, done } = await iter.next();
2615
+ if (done) return { value: void 0, done: true };
2616
+ if (value.type === "token" && isRootLevel(value)) {
2617
+ return { value: value.data, done: false };
2618
+ }
2619
+ }
2620
+ },
2621
+ // Propagate `return()` to the underlying bus iterator so its
2622
+ // parked resolver (if any) is spliced out of `waiters`. Without
2623
+ // this, breaking out of `for await (const t of bus.text)` would
2624
+ // orphan the inner waiter and lose the next event.
2625
+ async return() {
2626
+ await iter.return?.();
2627
+ return { value: void 0, done: true };
2628
+ }
2629
+ };
2630
+ }
2631
+ };
2632
+ }
2633
+ /** Iterator over `{ askId, agent?, text }` for every token across root and
2634
+ * nested asks. Useful for split-pane UIs that render each sub-agent's
2635
+ * output in its own lane. `agent` is undefined when the token was emitted
2636
+ * outside any ask (rare — synthesized test fixtures). */
2637
+ get textByAsk() {
2638
+ const self = this;
2639
+ return {
2640
+ [Symbol.asyncIterator]() {
2641
+ const iter = self[Symbol.asyncIterator]();
2642
+ return {
2643
+ async next() {
2644
+ while (true) {
2645
+ const { value, done } = await iter.next();
2646
+ if (done) {
2647
+ return {
2648
+ value: void 0,
2649
+ done: true
2650
+ };
2651
+ }
2652
+ if (value.type === "token") {
2653
+ return {
2654
+ value: { askId: value.askId, agent: value.agent, text: value.data },
2655
+ done: false
2656
+ };
2657
+ }
2658
+ }
2659
+ },
2660
+ async return() {
2661
+ await iter.return?.();
2662
+ return {
2663
+ value: void 0,
2664
+ done: true
2665
+ };
2666
+ }
2667
+ };
2668
+ }
2669
+ };
2670
+ }
2671
+ /**
2672
+ * Coalescing iterator over `partial_object` events — yields the latest
2673
+ * payload per `askId`. When a newer `partial_object` arrives for an
2674
+ * `askId` while the consumer is busy, the older value is silently
2675
+ * superseded; the consumer sees only the most recent state per ask on
2676
+ * its next `.next()` await.
2677
+ *
2678
+ * Memory bound: O(active asks holding a pending partial), not O(events).
2679
+ * This is what UIs streaming structured output actually want — rendering
2680
+ * every intermediate snapshot just to overwrite it ms later is wasted
2681
+ * work, and unbounded queueing of intermediate JSON snapshots is the
2682
+ * memory-pressure scenario this view is designed to avoid. In practice
2683
+ * "active asks" is small (1-10 even for fan-out workflows); a workflow
2684
+ * fanning out to 10k concurrent asks each producing partials would
2685
+ * accumulate ≈10k pending entries (~1 MB), which is the realistic
2686
+ * ceiling for this view.
2687
+ *
2688
+ * Schema-retry awareness: each yielded value carries the 1-indexed
2689
+ * `attempt` field from the underlying `partial_object` event. When a
2690
+ * schema check fails and the agent retries, the runtime emits
2691
+ * `pipeline(status: 'failed')`; this view drops any undrained pending
2692
+ * partial for the affected ask so the consumer never sees stale
2693
+ * attempt-N snapshots after attempt-N+1 has begun. Mirrors the
2694
+ * per-attempt buffer reset that `AxlStream.fullText` applies to
2695
+ * tokens. (Validate-stage and guardrail retries trigger the same
2696
+ * reset.)
2697
+ *
2698
+ * Malformed `partial_object` events (those missing `data.object`) are
2699
+ * silently filtered — the variant shape requires the field, so an
2700
+ * undefined value is a producer bug rather than a meaningful state.
2701
+ *
2702
+ * Implementation note: this is a listener-based view (subscribes to the
2703
+ * bus's `partial_object` channel via `.on()`), not an iterator-based
2704
+ * filter like `.text` / `.lifecycle` / `.textByAsk`. As a result it does
2705
+ * NOT race with the main async iterator — running `for await (const e of bus)`
2706
+ * concurrently with `for await (const p of bus.partialObjects)` works
2707
+ * cleanly (each `partial_object` event fires the listener AND queues for
2708
+ * iterators).
2709
+ *
2710
+ * Termination: yields any pending coalesced values first, then `done: true`
2711
+ * once the bus is finished (via `_finish()`).
2712
+ */
2713
+ get partialObjects() {
2714
+ const self = this;
2715
+ return {
2716
+ [Symbol.asyncIterator]() {
2717
+ const pending = /* @__PURE__ */ new Map();
2718
+ let resolveWaiter = null;
2719
+ let finished = self.finished;
2720
+ let unsubscribed = false;
2721
+ const wakeWaiter = () => {
2722
+ const r = resolveWaiter;
2723
+ resolveWaiter = null;
2724
+ r?.();
2725
+ };
2726
+ const recordPartial = (event) => {
2727
+ if (event.data?.object === void 0) return;
2728
+ const askId = event.askId ?? "";
2729
+ pending.set(askId, {
2730
+ askId,
2731
+ agent: event.agent,
2732
+ object: event.data.object,
2733
+ attempt: event.attempt
2734
+ });
2735
+ };
2736
+ for (const [askId, value] of self.latestPartialByAsk) {
2737
+ pending.set(askId, value);
2738
+ }
2739
+ const handler = (event) => {
2740
+ if (event.type !== "partial_object") return;
2741
+ recordPartial(event);
2742
+ wakeWaiter();
2743
+ };
2744
+ self.bus.on("partial_object", handler);
2745
+ const pipelineHandler = (event) => {
2746
+ if (event.type !== "pipeline") return;
2747
+ if (event.status !== "failed") return;
2748
+ const askId = event.askId ?? "";
2749
+ pending.delete(askId);
2750
+ };
2751
+ self.bus.on("pipeline", pipelineHandler);
2752
+ const unsubscribe = () => {
2753
+ if (unsubscribed) return;
2754
+ unsubscribed = true;
2755
+ self.bus.off("partial_object", handler);
2756
+ self.bus.off("pipeline", pipelineHandler);
2757
+ unsubscribeFinish();
2758
+ };
2759
+ const unsubscribeFinish = self._onFinish(() => {
2760
+ finished = true;
2761
+ self.bus.off("partial_object", handler);
2762
+ self.bus.off("pipeline", pipelineHandler);
2763
+ wakeWaiter();
2764
+ });
2765
+ return {
2766
+ async next() {
2767
+ while (true) {
2768
+ if (pending.size > 0) {
2769
+ const it = pending.entries().next();
2770
+ if (!it.done) {
2771
+ const [key, value] = it.value;
2772
+ pending.delete(key);
2773
+ return { value, done: false };
2774
+ }
2775
+ }
2776
+ if (finished) {
2777
+ unsubscribe();
2778
+ return { value: void 0, done: true };
2779
+ }
2780
+ await new Promise((res) => {
2781
+ resolveWaiter = res;
2782
+ });
2783
+ }
2784
+ },
2785
+ async return() {
2786
+ unsubscribe();
2787
+ return { value: void 0, done: true };
2788
+ },
2789
+ [Symbol.asyncIterator]() {
2790
+ return this;
2791
+ }
2792
+ };
2793
+ }
2794
+ };
2795
+ }
2796
+ /** Iterator over structural lifecycle events only — skips per-token chatter
2797
+ * and progressive `partial_object` emissions. Useful for waterfall UIs
2798
+ * and any consumer that wants the "what happened" timeline. */
2799
+ get lifecycle() {
2800
+ const self = this;
2801
+ return {
2802
+ [Symbol.asyncIterator]() {
2803
+ const iter = self[Symbol.asyncIterator]();
2804
+ return {
2805
+ async next() {
2806
+ while (true) {
2807
+ const { value, done } = await iter.next();
2808
+ if (done) return { value: void 0, done: true };
2809
+ if (LIFECYCLE_TYPES.has(value.type)) return { value, done: false };
2810
+ }
2811
+ },
2812
+ async return() {
2813
+ await iter.return?.();
2814
+ return { value: void 0, done: true };
2815
+ }
2816
+ };
2817
+ }
2818
+ };
2819
+ }
2820
+ /**
2821
+ * Push an event onto the bus. Emits on the typed channel (for `.on()`
2822
+ * subscribers) and queues for async iterators. No-op once `_finish()` has
2823
+ * been called.
2824
+ *
2825
+ * Subclasses (AxlStream) override to add token accounting and Readable
2826
+ * fan-out before delegating to this implementation.
2827
+ *
2828
+ * @internal — called by the runtime / WorkflowContext / AxlStream.
2829
+ */
2830
+ _push(event) {
2831
+ if (this.finished) return;
2832
+ try {
2833
+ this.bus.emit(event.type, event);
2834
+ } catch (err) {
2835
+ console.error(
2836
+ `[axl] AxlEventBus listener for "${event.type}" threw; ignoring:`,
2837
+ err instanceof Error ? err.message : String(err)
2838
+ );
2839
+ }
2840
+ if (event.type === "partial_object" && event.data?.object !== void 0) {
2841
+ const askId = event.askId ?? "";
2842
+ this.latestPartialByAsk.set(askId, {
2843
+ askId,
2844
+ agent: event.agent,
2845
+ object: event.data.object,
2846
+ attempt: event.attempt
2847
+ });
2848
+ } else if (event.type === "pipeline" && event.status === "failed") {
2849
+ this.latestPartialByAsk.delete(event.askId ?? "");
2850
+ }
2851
+ const waiter = this.waiters.shift();
2852
+ if (waiter) {
2853
+ waiter({ value: event, done: false });
2854
+ return;
2855
+ }
2856
+ if (this.eventQueue.length >= this.maxQueued && !TERMINAL_TYPES.has(event.type)) {
2857
+ this.handleOverflow(event);
2858
+ return;
2859
+ }
2860
+ this.eventQueue.push(event);
2861
+ }
2862
+ /** Apply the overflow policy when the queue is at `maxQueued` and a
2863
+ * non-terminal event arrives. Terminal events bypass this path entirely
2864
+ * (checked by the caller). */
2865
+ handleOverflow(event) {
2866
+ if (this.onOverflow === "throw") {
2867
+ throw new EventStreamOverflowError(this.maxQueued, event.type);
2868
+ }
2869
+ let droppedIdx = -1;
2870
+ for (let i = 0; i < this.eventQueue.length; i++) {
2871
+ if (!TERMINAL_TYPES.has(this.eventQueue[i].type)) {
2872
+ droppedIdx = i;
2873
+ break;
2874
+ }
2875
+ }
2876
+ if (!this.overflowWarned) {
2877
+ this.overflowWarned = true;
2878
+ console.warn(
2879
+ `[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.`
2880
+ );
2881
+ }
2882
+ if (droppedIdx >= 0) {
2883
+ this.eventQueue.splice(droppedIdx, 1);
2884
+ }
2885
+ this.eventQueue.push(event);
2886
+ }
2887
+ /**
2888
+ * Mark the bus as finished. Wakes any pending iterators with `done: true`
2889
+ * and drops further `_push` calls. Idempotent.
2890
+ *
2891
+ * Does not synthesize a terminal `done` AxlEvent — callers (AxlStream) that
2892
+ * want one push it via `_push` before calling `_finish`.
2893
+ *
2894
+ * Late iterators that connect after `_finish()` still drain any events
2895
+ * remaining in `eventQueue` (the `next()` short-circuit in
2896
+ * `[Symbol.asyncIterator]` checks the queue before checking `finished`),
2897
+ * then receive `done: true`.
2898
+ *
2899
+ * @internal
2900
+ */
2901
+ _finish() {
2902
+ if (this.finished) return;
2903
+ this.finished = true;
2904
+ for (const w of this.waiters) {
2905
+ w({ value: void 0, done: true });
2906
+ }
2907
+ this.waiters.length = 0;
2908
+ const callbacks = this.finishCallbacks;
2909
+ this.finishCallbacks = [];
2910
+ for (const cb of callbacks) {
2911
+ try {
2912
+ cb();
2913
+ } catch (err) {
2914
+ console.error("[axl] AxlEventBus finish callback threw:", err);
2915
+ }
2916
+ }
2917
+ }
2918
+ /** Register a one-shot callback fired when `_finish()` is called.
2919
+ * Returns an unsubscribe function. If the bus is already finished,
2920
+ * the callback is invoked synchronously and the returned unsubscribe
2921
+ * is a no-op.
2922
+ *
2923
+ * Used by listener-based curated views (e.g., `partialObjects`) that
2924
+ * need a termination signal independent of any synthesized `done`
2925
+ * event. Iterator consumers don't need this — `[Symbol.asyncIterator]`
2926
+ * resolves with `done: true` automatically.
2927
+ *
2928
+ * @internal */
2929
+ _onFinish(cb) {
2930
+ if (this.finished) {
2931
+ cb();
2932
+ return () => {
2933
+ };
2934
+ }
2935
+ this.finishCallbacks.push(cb);
2936
+ return () => {
2937
+ const i = this.finishCallbacks.indexOf(cb);
2938
+ if (i >= 0) this.finishCallbacks.splice(i, 1);
2939
+ };
2940
+ }
2941
+ /** Whether `event` is one of the `AxlEventType` names this bus emits.
2942
+ * Subclasses (e.g., `AxlStream`) consult this to route AxlEvent names to
2943
+ * the bus and other names (`'close'`, `'data'`, ...) to a wider emitter. */
2944
+ knowsEventName(event) {
2945
+ return STREAM_EVENTS.has(event);
2946
+ }
2947
+ /** Whether `_finish()` has been called. */
2948
+ get isFinished() {
2949
+ return this.finished;
2950
+ }
2951
+ };
2952
+
2361
2953
  // src/redaction.ts
2362
2954
  var REDACTED = "[redacted]";
2363
2955
  function walkObjectOneLevel(obj, preserveKeys) {
@@ -2723,6 +3315,103 @@ var WorkflowContext = class _WorkflowContext {
2723
3315
  awaitHumanHandler;
2724
3316
  onAgentStart;
2725
3317
  onAgentCallComplete;
3318
+ /** Shared mutable slot for the lazy `AxlEventBus`. The slot is shared
3319
+ * by reference across parent/child contexts so a late allocation
3320
+ * (consumer subscribes after a child context exists) is visible
3321
+ * everywhere. Until first access `_busRef.current` is undefined and
3322
+ * `emitEvent` skips the bus fan-out entirely (zero overhead for
3323
+ * contexts that never observe). */
3324
+ _busRef;
3325
+ _eventStreamOptions;
3326
+ /** Removes the constructor-registered `'abort'` listener from
3327
+ * `this.signal` so a long-lived signal (e.g., a request-scoped or
3328
+ * process-wide signal reused across many `runtime.createContext()` /
3329
+ * `runtime.execute()` calls) doesn't accumulate listeners forever.
3330
+ * Called from `disposeEvents()` and from the workflow_end/error
3331
+ * emission in `emitEvent`. Undefined when no abort listener was
3332
+ * registered (no signal, signal already aborted, or non-root
3333
+ * context). */
3334
+ abortListenerCleanup;
3335
+ /**
3336
+ * Iterable + EventEmitter view over every `AxlEvent` emitted by this
3337
+ * context (and its child contexts — agent-as-tool nested asks bubble up).
3338
+ *
3339
+ * Use cases:
3340
+ * - **Inside a workflow handler**: subscribe before the first `ctx.ask()`
3341
+ * to observe `partial_object`, `agent_call_*`, `tool_call_*`, etc.
3342
+ * *between* asks. `ctx.events.partialObjects` is the coalescing view
3343
+ * designed for streaming-structured-output UIs.
3344
+ * - **Ad-hoc contexts** from `runtime.createContext()`: the same
3345
+ * iterable replaces the legacy `onToken` / `onToolCall` /
3346
+ * `onAgentStart` callbacks.
3347
+ * - **Cross-execution**: prefer `runtime.on('trace', …)` instead — that
3348
+ * fans out across all executions, while `ctx.events` is per-context.
3349
+ *
3350
+ * Lazy: the bus is allocated on first access. The bus's iterator
3351
+ * queue retains events emitted before any consumer iterates, so a
3352
+ * late `for await (const e of ctx.events)` still drains queued
3353
+ * events. The `partialObjects` view additionally seeds from a
3354
+ * per-bus `latestPartialByAsk` map, so a late subscriber may see
3355
+ * the latest coalesced state per ask even when earlier events were
3356
+ * already drained by another iterator. **Neither rescues a late
3357
+ * subscriber from the streaming-gate behavior** (see below).
3358
+ *
3359
+ * **Subscribe before the first `ctx.ask()`.** The streaming code
3360
+ * path inside `ctx.ask()` activates only when an observer is
3361
+ * present at the time the ask starts (`_streamingEnabled` — either
3362
+ * the legacy `onToken` callback is set or `ctx.events` has been
3363
+ * allocated). If you allocate `ctx.events` AFTER a `ctx.ask()` has
3364
+ * begun, that in-flight ask will not stream `token` /
3365
+ * `partial_object` events at all (the agent loop went through
3366
+ * `provider.chat` instead of `provider.stream`). Subsequent asks
3367
+ * will stream normally — the gate is re-checked per ask. The
3368
+ * unambiguous pattern is `const events = ctx.events;` on the first
3369
+ * line of the handler.
3370
+ *
3371
+ * Auto-termination: `emitEvent` calls `_eventBus._finish()` after
3372
+ * emitting `workflow_end` or `error`, so iterators terminate
3373
+ * cleanly with `done: true`. For ad-hoc contexts that never run a
3374
+ * workflow (no terminal event), pass `signal: AbortSignal.timeout(...)`
3375
+ * to `runtime.createContext` (the bus auto-disposes on abort) or
3376
+ * call `ctx.disposeEvents()` when done observing.
3377
+ */
3378
+ get events() {
3379
+ if (!this._busRef.current) {
3380
+ this._busRef.current = new AxlEventBus(this._eventStreamOptions);
3381
+ if (this.signal?.aborted) {
3382
+ this._busRef.current._finish();
3383
+ }
3384
+ }
3385
+ return this._busRef.current;
3386
+ }
3387
+ /** Manually finish the `ctx.events` bus, terminating any active
3388
+ * iterators with `done: true`. Idempotent. Useful for ad-hoc contexts
3389
+ * from `runtime.createContext()` that never emit a `workflow_end` /
3390
+ * `error` terminal — observers iterating `for await (const e of
3391
+ * ctx.events)` would otherwise hang. Workflow-driven contexts
3392
+ * (`runtime.execute` / `runtime.stream`) terminate automatically and
3393
+ * don't need this.
3394
+ *
3395
+ * Also removes the constructor-registered abort listener so a
3396
+ * long-lived `signal` reused across many ad-hoc contexts doesn't
3397
+ * accumulate listeners. */
3398
+ disposeEvents() {
3399
+ this._busRef.current?._finish();
3400
+ this.abortListenerCleanup?.();
3401
+ }
3402
+ /** True if any observer wants per-token streaming for asks started
3403
+ * from this context: either the legacy `onToken` callback is set
3404
+ * (e.g., `runtime.stream()` plumbs a sentinel) OR `ctx.events` has
3405
+ * been allocated (a workflow-handler consumer subscribed before
3406
+ * the ask). `ctx.ask` reads this to decide whether to enter the
3407
+ * streaming code path; the gate is re-checked per ask, so a
3408
+ * consumer subscribing AFTER the first ask started still gets
3409
+ * streaming on the next one. The check `_busRef.current !== undefined`
3410
+ * is a one-way flag — `_finish` does not unset the reference, so
3411
+ * once an observer was present, every subsequent ask streams. */
3412
+ get _streamingEnabled() {
3413
+ return this.onToken !== void 0 || this._busRef.current !== void 0;
3414
+ }
2726
3415
  constructor(init) {
2727
3416
  this.input = init.input;
2728
3417
  this.executionId = init.executionId;
@@ -2746,6 +3435,19 @@ var WorkflowContext = class _WorkflowContext {
2746
3435
  this.awaitHumanHandler = init.awaitHumanHandler;
2747
3436
  this.onAgentStart = init.onAgentStart;
2748
3437
  this.onAgentCallComplete = init.onAgentCallComplete;
3438
+ this._busRef = init._busRef ?? { current: void 0 };
3439
+ this._eventStreamOptions = init.eventStreamOptions;
3440
+ if (init._busRef === void 0 && this.signal && !this.signal.aborted) {
3441
+ const signal = this.signal;
3442
+ const onAbort = () => {
3443
+ this._busRef.current?._finish();
3444
+ };
3445
+ signal.addEventListener("abort", onAbort, { once: true });
3446
+ this.abortListenerCleanup = () => {
3447
+ signal.removeEventListener("abort", onAbort);
3448
+ this.abortListenerCleanup = void 0;
3449
+ };
3450
+ }
2749
3451
  this.autoCheckpointCounters = init.autoCheckpointCounters ?? {
2750
3452
  byAgent: /* @__PURE__ */ new Map(),
2751
3453
  root: 0
@@ -2800,7 +3502,14 @@ var WorkflowContext = class _WorkflowContext {
2800
3502
  // Share the parent's auto-checkpoint counters so internal checkpoints
2801
3503
  // emitted from `ctx.ask` / spawn / race / parallel / map in this
2802
3504
  // child context don't collide with the parent's in the state store.
2803
- autoCheckpointCounters: this.autoCheckpointCounters
3505
+ autoCheckpointCounters: this.autoCheckpointCounters,
3506
+ // Share the parent's bus slot (mutable ref). Late allocation in
3507
+ // either parent or child propagates because both read the same
3508
+ // slot. Nested-ask events (agent-as-tool pattern) bubble up to
3509
+ // the parent's iterator with `parentAskId`/`depth` correlation
3510
+ // intact.
3511
+ _busRef: this._busRef,
3512
+ eventStreamOptions: this._eventStreamOptions
2804
3513
  // Isolated: sessionHistory (empty)
2805
3514
  });
2806
3515
  }
@@ -2936,15 +3645,26 @@ var WorkflowContext = class _WorkflowContext {
2936
3645
  };
2937
3646
  throw err;
2938
3647
  } finally {
2939
- this.emitEvent({
2940
- type: "ask_end",
2941
- outcome: outcome ?? {
2942
- ok: false,
2943
- error: "ask_end emitted without outcome \u2014 internal bug"
2944
- },
2945
- cost: frame.askCost.value,
2946
- duration: Date.now() - askStart
2947
- });
3648
+ try {
3649
+ this.emitEvent({
3650
+ type: "ask_end",
3651
+ outcome: outcome ?? {
3652
+ ok: false,
3653
+ error: "ask_end emitted without outcome \u2014 internal bug"
3654
+ },
3655
+ cost: frame.askCost.value,
3656
+ duration: Date.now() - askStart
3657
+ });
3658
+ } catch (emitErr) {
3659
+ if (emitErr instanceof EventStreamOverflowError && outcome && !outcome.ok) {
3660
+ console.error(
3661
+ "[axl] ask_end emit overflowed during error path; preserving original error:",
3662
+ emitErr.message
3663
+ );
3664
+ } else {
3665
+ throw emitErr;
3666
+ }
3667
+ }
2948
3668
  }
2949
3669
  });
2950
3670
  },
@@ -3181,7 +3901,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3181
3901
  this.onAgentStart?.({ agent: agent2._name, model: modelUri }, callbackMeta);
3182
3902
  let response;
3183
3903
  try {
3184
- if (this.onToken) {
3904
+ if (this._streamingEnabled) {
3185
3905
  let content2 = "";
3186
3906
  const toolCalls = [];
3187
3907
  const toolCallBuffers = /* @__PURE__ */ new Map();
@@ -3196,7 +3916,7 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3196
3916
  if (chunk.type === "text_delta") {
3197
3917
  content2 += chunk.content;
3198
3918
  this.emitEvent({ type: "token", data: chunk.content });
3199
- this.onToken(chunk.content, callbackMeta);
3919
+ this.onToken?.(chunk.content, callbackMeta);
3200
3920
  if (partialObjectEnabled) {
3201
3921
  for (const ch of chunk.content) {
3202
3922
  if (escaped) {
@@ -3911,19 +4631,21 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3911
4631
  attempt: lastStartAttempt,
3912
4632
  maxAttempts: lastStartMaxAttempts
3913
4633
  });
3914
- this.pushAssistantToSessionHistory(content, response.providerMetadata);
4634
+ this.pushAssistantToSessionHistory(content, agent2._name, response.providerMetadata);
3915
4635
  return validated ?? content;
3916
4636
  }
3917
4637
  throw new MaxTurnsError("ctx.ask()", maxTurns);
3918
4638
  }
3919
4639
  /**
3920
4640
  * Push the final assistant message into session history, preserving providerMetadata
3921
- * (e.g., Gemini thought signatures needed for multi-turn reasoning context).
4641
+ * (e.g., Gemini thought signatures needed for multi-turn reasoning context) and
4642
+ * stamping the agent name for observability + future per-agent scoping.
3922
4643
  */
3923
- pushAssistantToSessionHistory(content, providerMetadata) {
4644
+ pushAssistantToSessionHistory(content, agentName, providerMetadata) {
3924
4645
  this.sessionHistory.push({
3925
4646
  role: "assistant",
3926
4647
  content,
4648
+ agent: agentName,
3927
4649
  ...providerMetadata ? { providerMetadata } : {}
3928
4650
  });
3929
4651
  }
@@ -5109,16 +5831,48 @@ ${summaryResponse.content}`
5109
5831
  data
5110
5832
  };
5111
5833
  const finalEvent = this.config.trace?.redact ? redactEvent(event) : event;
5834
+ let overflowErr;
5112
5835
  if (this.onTrace) {
5113
5836
  try {
5114
5837
  this.onTrace(finalEvent);
5115
5838
  } catch (err) {
5116
- console.error(
5117
- "[axl] onTrace handler threw; trace event dropped:",
5118
- err instanceof Error ? err.message : String(err)
5119
- );
5839
+ if (err instanceof EventStreamOverflowError) {
5840
+ overflowErr = err;
5841
+ } else {
5842
+ console.error(
5843
+ "[axl] onTrace handler threw; trace event dropped:",
5844
+ err instanceof Error ? err.message : String(err)
5845
+ );
5846
+ }
5847
+ }
5848
+ }
5849
+ const bus = this._busRef.current;
5850
+ if (bus) {
5851
+ try {
5852
+ bus._push(finalEvent);
5853
+ } catch (err) {
5854
+ if (err instanceof EventStreamOverflowError) {
5855
+ if (!overflowErr) {
5856
+ overflowErr = err;
5857
+ } else {
5858
+ console.error(
5859
+ '[axl] ctx.events bus also overflowed for event "' + finalEvent.type + '"; first overflow already in flight:',
5860
+ err.message
5861
+ );
5862
+ }
5863
+ } else {
5864
+ console.error(
5865
+ "[axl] AxlEventBus._push threw unexpectedly:",
5866
+ err instanceof Error ? err.message : String(err)
5867
+ );
5868
+ }
5120
5869
  }
5121
5870
  }
5871
+ if (finalEvent.type === "workflow_end" || finalEvent.type === "error") {
5872
+ bus?._finish();
5873
+ this.abortListenerCleanup?.();
5874
+ }
5875
+ if (overflowErr) throw overflowErr;
5122
5876
  }
5123
5877
  };
5124
5878
 
@@ -5722,56 +6476,28 @@ var Session = class _Session {
5722
6476
  get id() {
5723
6477
  return this.sessionId;
5724
6478
  }
5725
- async send(workflowName, input) {
6479
+ async send(workflowName, input, options) {
5726
6480
  if (this.closed) throw new Error("Session has been ended");
5727
- const history = await this.store.getSession(this.sessionId);
5728
- let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
5729
- const maxMessages = this.options.history?.maxMessages;
5730
- if (maxMessages && history.length > maxMessages) {
5731
- if (this.options.history?.summarize) {
5732
- const summaryModel = this.options.history?.summaryModel;
5733
- if (!summaryModel) {
5734
- throw new Error("SessionOptions.history.summaryModel is required when summarize is true");
5735
- }
5736
- const messagesToDrop = history.slice(0, history.length - maxMessages);
5737
- const toSummarize = cachedSummary ? [
5738
- { role: "system", content: `Previous conversation summary: ${cachedSummary}` },
5739
- ...messagesToDrop
5740
- ] : messagesToDrop;
5741
- const summary = await this.runtime.summarizeMessages(toSummarize, summaryModel);
5742
- await this.store.saveSessionMeta(this.sessionId, "summaryCache", summary);
5743
- cachedSummary = summary;
5744
- }
5745
- const trimmed = history.slice(-maxMessages);
5746
- history.length = 0;
5747
- history.push(...trimmed);
5748
- }
5749
- const userMessage = {
5750
- role: "user",
5751
- content: typeof input === "string" ? input : JSON.stringify(input)
5752
- };
5753
- history.push(userMessage);
5754
- const result = await this.runtime.execute(workflowName, input, {
5755
- metadata: {
5756
- sessionId: this.sessionId,
5757
- sessionHistory: history,
5758
- ...cachedSummary ? { summaryCache: cachedSummary } : {}
5759
- }
5760
- });
5761
- const lastMsg = history[history.length - 1];
5762
- if (!(lastMsg && lastMsg.role === "assistant")) {
5763
- history.push({
5764
- role: "assistant",
5765
- content: typeof result === "string" ? result : JSON.stringify(result)
5766
- });
5767
- }
5768
- if (this.options.persist !== false) {
5769
- await this.store.saveSession(this.sessionId, history);
5770
- }
6481
+ options?.signal?.throwIfAborted?.();
6482
+ return this.runtime._serializeSession(
6483
+ this.sessionId,
6484
+ () => this.sendImpl(workflowName, input, options?.signal, options?.events)
6485
+ );
6486
+ }
6487
+ async sendImpl(workflowName, input, signal, events) {
6488
+ const { history, metadata } = await this.prepareHistory(input);
6489
+ const result = await this.runtime.execute(workflowName, input, { metadata, signal, events });
6490
+ await this.commitHistory(history, result);
5771
6491
  return result;
5772
6492
  }
5773
- async stream(workflowName, input) {
5774
- if (this.closed) throw new Error("Session has been ended");
6493
+ /** Read history + summary, apply maxMessages limit (with optional
6494
+ * summarization), and push the new user message. Returns the mutable
6495
+ * `history` array (which `executeAgentCall` will append the assistant
6496
+ * reply to in-place) and the `metadata` payload to forward to
6497
+ * `runtime.execute()` / `runtime.stream()`. Shared by `sendImpl` and
6498
+ * `streamImpl` so future fixes (e.g., to summarization wiring) land in
6499
+ * one place. */
6500
+ async prepareHistory(input) {
5775
6501
  const history = await this.store.getSession(this.sessionId);
5776
6502
  let cachedSummary = await this.store.getSessionMeta(this.sessionId, "summaryCache");
5777
6503
  const maxMessages = this.options.history?.maxMessages;
@@ -5794,41 +6520,83 @@ var Session = class _Session {
5794
6520
  history.length = 0;
5795
6521
  history.push(...trimmed);
5796
6522
  }
5797
- const userMessage = {
6523
+ history.push({
5798
6524
  role: "user",
5799
6525
  content: typeof input === "string" ? input : JSON.stringify(input)
5800
- };
5801
- history.push(userMessage);
5802
- const axlStream = this.runtime.stream(workflowName, input, {
5803
- metadata: {
5804
- sessionId: this.sessionId,
5805
- sessionHistory: history,
5806
- ...cachedSummary ? { summaryCache: cachedSummary } : {}
5807
- }
5808
6526
  });
5809
- const updateHistory = async (result) => {
5810
- const lastMsg = history[history.length - 1];
5811
- if (!(lastMsg && lastMsg.role === "assistant")) {
5812
- history.push({
5813
- role: "assistant",
5814
- content: typeof result === "string" ? result : JSON.stringify(result)
5815
- });
5816
- }
5817
- if (this.options.persist !== false) {
5818
- await this.store.saveSession(this.sessionId, history);
5819
- }
6527
+ const metadata = {
6528
+ sessionId: this.sessionId,
6529
+ sessionHistory: history,
6530
+ ...cachedSummary ? { summaryCache: cachedSummary } : {}
5820
6531
  };
5821
- axlStream.on("done", (event) => {
5822
- const data = event.data;
5823
- updateHistory(data.result).catch((err) => {
6532
+ return { history, metadata };
6533
+ }
6534
+ /** Push the assistant reply (if `executeAgentCall` didn't already) and
6535
+ * persist. Shared by `sendImpl` and `streamImpl`. The fallback assistant
6536
+ * push has no agent context and so leaves `agent` undefined — see the
6537
+ * `ChatMessage.agent` field doc. */
6538
+ async commitHistory(history, result) {
6539
+ const lastMsg = history[history.length - 1];
6540
+ if (!(lastMsg && lastMsg.role === "assistant")) {
6541
+ history.push({
6542
+ role: "assistant",
6543
+ content: typeof result === "string" ? result : JSON.stringify(result)
6544
+ });
6545
+ }
6546
+ if (this.options.persist !== false) {
6547
+ await this.store.saveSession(this.sessionId, history);
6548
+ }
6549
+ }
6550
+ async stream(workflowName, input, options) {
6551
+ if (this.closed) throw new Error("Session has been ended");
6552
+ options?.signal?.throwIfAborted?.();
6553
+ let resolveReady;
6554
+ let rejectReady;
6555
+ const streamReady = new Promise((res, rej) => {
6556
+ resolveReady = res;
6557
+ rejectReady = rej;
6558
+ });
6559
+ void this.runtime._serializeSession(
6560
+ this.sessionId,
6561
+ () => this.streamImpl(
6562
+ workflowName,
6563
+ input,
6564
+ resolveReady,
6565
+ rejectReady,
6566
+ options?.signal,
6567
+ options?.events
6568
+ )
6569
+ ).catch(() => {
6570
+ });
6571
+ return streamReady;
6572
+ }
6573
+ async streamImpl(workflowName, input, resolveReady, rejectReady, signal, events) {
6574
+ let history;
6575
+ let axlStream;
6576
+ try {
6577
+ const prepared = await this.prepareHistory(input);
6578
+ history = prepared.history;
6579
+ axlStream = this.runtime.stream(workflowName, input, {
6580
+ metadata: prepared.metadata,
6581
+ signal,
6582
+ events
6583
+ });
6584
+ } catch (err) {
6585
+ rejectReady(err);
6586
+ throw err;
6587
+ }
6588
+ resolveReady(axlStream);
6589
+ const completion = axlStream.promise.then(
6590
+ (result) => this.commitHistory(history, result).catch((err) => {
5824
6591
  this.runtime.emit("error", {
5825
6592
  type: "session_history_save_failed",
5826
6593
  sessionId: this.sessionId,
5827
6594
  error: err instanceof Error ? err.message : String(err)
5828
6595
  });
5829
- });
5830
- });
5831
- return axlStream;
6596
+ }),
6597
+ () => void 0
6598
+ );
6599
+ await completion;
5832
6600
  }
5833
6601
  async history() {
5834
6602
  return this.store.getSession(this.sessionId);
@@ -5839,99 +6607,71 @@ var Session = class _Session {
5839
6607
  }
5840
6608
  async end() {
5841
6609
  this.closed = true;
5842
- if (this.options.persist !== false) {
5843
- await this.store.deleteSession(this.sessionId);
5844
- }
6610
+ return this.runtime._serializeSession(this.sessionId, async () => {
6611
+ if (this.options.persist !== false) {
6612
+ await this.store.deleteSession(this.sessionId);
6613
+ }
6614
+ });
5845
6615
  }
5846
- async fork(newId) {
5847
- const history = await this.store.getSession(this.sessionId);
5848
- const forked = new _Session(newId, this.runtime, this.store, this.options);
5849
- await this.store.saveSession(newId, [...history]);
5850
- const summaryCache = await this.store.getSessionMeta(this.sessionId, "summaryCache");
5851
- if (summaryCache !== null) {
5852
- await this.store.saveSessionMeta(newId, "summaryCache", summaryCache);
5853
- }
5854
- const handoffHistory = await this.store.getSessionMeta(this.sessionId, "handoffHistory");
5855
- if (handoffHistory !== null) {
5856
- await this.store.saveSessionMeta(newId, "handoffHistory", handoffHistory);
5857
- }
5858
- return forked;
6616
+ async fork(newId, options) {
6617
+ if (newId === this.sessionId) {
6618
+ throw new Error(`Session.fork: newId must differ from source id (${this.sessionId})`);
6619
+ }
6620
+ const [first, second] = [this.sessionId, newId].sort();
6621
+ return this.runtime._serializeSession(
6622
+ first,
6623
+ () => this.runtime._serializeSession(second, async () => {
6624
+ if (!options?.overwrite) {
6625
+ const existing = await this.store.getSession(newId);
6626
+ if (existing.length > 0) {
6627
+ throw new Error(
6628
+ `Session.fork: target id "${newId}" already has history. Pass { overwrite: true } to replace it, or choose a different id.`
6629
+ );
6630
+ }
6631
+ }
6632
+ const history = await this.store.getSession(this.sessionId);
6633
+ const forked = new _Session(newId, this.runtime, this.store, this.options);
6634
+ await this.store.saveSession(newId, [...history]);
6635
+ const summaryCache = await this.store.getSessionMeta(this.sessionId, "summaryCache");
6636
+ if (summaryCache !== null) {
6637
+ await this.store.saveSessionMeta(newId, "summaryCache", summaryCache);
6638
+ }
6639
+ const handoffHistory = await this.store.getSessionMeta(this.sessionId, "handoffHistory");
6640
+ if (handoffHistory !== null) {
6641
+ await this.store.saveSessionMeta(newId, "handoffHistory", handoffHistory);
6642
+ }
6643
+ if (this.store.getAllMemory && this.store.saveMemory) {
6644
+ const sourceScope = `session:${this.sessionId}`;
6645
+ const targetScope = `session:${newId}`;
6646
+ const entries = await this.store.getAllMemory(sourceScope);
6647
+ for (const { key, value } of entries) {
6648
+ await this.store.saveMemory(targetScope, key, value);
6649
+ }
6650
+ }
6651
+ return forked;
6652
+ })
6653
+ );
5859
6654
  }
5860
6655
  };
5861
6656
 
5862
6657
  // src/stream.ts
5863
6658
  import { Readable } from "stream";
5864
- import { EventEmitter } from "events";
5865
-
5866
- // src/types.ts
5867
- var AXL_EVENT_TYPES = [
5868
- // Workflow lifecycle
5869
- "workflow_start",
5870
- "workflow_end",
5871
- // Ask boundary
5872
- "ask_start",
5873
- "ask_end",
5874
- // Agent turn lifecycle
5875
- "agent_call_start",
5876
- "agent_call_end",
5877
- // Content delivery (stream-only)
5878
- "token",
5879
- // Tool invocation lifecycle
5880
- "tool_call_start",
5881
- "tool_call_end",
5882
- // Single-point tool events
5883
- "tool_approval",
5884
- "tool_denied",
5885
- // Delegation
5886
- "delegate",
5887
- // Handoff (spans two asks; not AskScoped).
5888
- // `handoff_start` emits before the target ask begins; always fired.
5889
- // `handoff_return` emits when control returns to source; roundtrip only
5890
- // (oneway handoffs have no return trip — the target's `ask_end` IS the
5891
- // end of the chain).
5892
- "handoff_start",
5893
- "handoff_return",
5894
- // Pipeline (retry/validation lifecycle) — added in PR 2; reserved here.
5895
- "pipeline",
5896
- // Progressive structured output — added in PR 2; reserved here.
5897
- "partial_object",
5898
- // Verification
5899
- "verify",
5900
- // Observability
5901
- "log",
5902
- "memory_remember",
5903
- "memory_recall",
5904
- "memory_forget",
5905
- // Durable execution checkpoints (`ctx.checkpoint`)
5906
- "checkpoint_save",
5907
- "checkpoint_replay",
5908
- // Human-in-the-loop (`ctx.awaitHuman`)
5909
- "await_human",
5910
- "await_human_resolved",
5911
- // Legacy gate events — emitted by current code; collapsed into `pipeline`
5912
- // in PR 2 (spec/16-streaming-wire-reliability §4.2).
5913
- "guardrail",
5914
- "schema_check",
5915
- "validate",
5916
- // Terminal workflow markers
5917
- "done",
5918
- "error"
5919
- ];
5920
-
5921
- // src/stream.ts
5922
- var AxlStream = class _AxlStream extends Readable {
5923
- bus = new EventEmitter();
6659
+ var AxlStream = class extends Readable {
6660
+ /** Composed event bus — owns iterator queue, EventEmitter gating, and
6661
+ * curated views (`.text`, `.lifecycle`, `.textByAsk`). Private; the
6662
+ * public surface is via the delegating methods below. */
6663
+ events;
5924
6664
  /**
5925
- * Per-ask token buffers split into "in-progress" and "committed"
5926
- * halves so `fullText` only includes tokens from attempts that
5927
- * actually won. Scoping is per-`askId` so concurrent root-level asks
5928
- * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
5929
- * interleave each other's tokens — each branch's chunks stay
5930
- * contiguous, and a `pipeline(failed)` on one branch only discards
5931
- * THAT branch's in-progress buffer (previously: shared buffer caused
5932
- * a failure on one branch to discard a peer's in-flight successful
5933
- * tokens). Insertion order in the Maps reflects which ask emitted
5934
- * its first token first, which is what `fullText` joins on.
6665
+ * Per-ask token buffers split into "in-progress" and "committed" halves
6666
+ * so `fullText` only includes tokens from attempts that actually won.
6667
+ * Scoping is per-`askId` so concurrent root-level asks
6668
+ * (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't interleave
6669
+ * each other's tokens — each branch's chunks stay contiguous, and a
6670
+ * `pipeline(failed)` on one branch only discards THAT branch's
6671
+ * in-progress buffer (previously: shared buffer caused a failure on
6672
+ * one branch to discard a peer's in-flight successful tokens).
6673
+ * Insertion order in the Maps reflects which ask emitted its first
6674
+ * token first, which is what `fullText` joins on.
5935
6675
  *
5936
6676
  * On `pipeline(status: 'committed')` for an ask, that ask's
5937
6677
  * in-progress entry flushes to its committed entry. On
@@ -5945,189 +6685,98 @@ var AxlStream = class _AxlStream extends Readable {
5945
6685
  attemptByAsk = /* @__PURE__ */ new Map();
5946
6686
  committedByAsk = /* @__PURE__ */ new Map();
5947
6687
  result = void 0;
5948
- finished = false;
5949
6688
  promise;
5950
- eventQueue = [];
5951
- waiters = [];
5952
- constructor() {
6689
+ resolvePromise;
6690
+ rejectPromise;
6691
+ constructor(options) {
5953
6692
  super({ objectMode: true, read() {
5954
6693
  } });
5955
- let resolvePromise;
5956
- let rejectPromise;
6694
+ this.events = new AxlEventBus(options, () => this.destroy());
5957
6695
  this.promise = new Promise((resolve2, reject) => {
5958
- resolvePromise = resolve2;
5959
- rejectPromise = reject;
5960
- });
5961
- this.bus.on("__resolve", (value) => resolvePromise(value));
5962
- this.bus.on("__reject", (error) => rejectPromise(error));
5963
- this.bus.on("error", () => {
6696
+ this.resolvePromise = resolve2;
6697
+ this.rejectPromise = reject;
5964
6698
  });
5965
6699
  this.promise.catch(() => {
5966
6700
  });
5967
6701
  }
5968
- /** Wire-format event names callers can subscribe to via `.on(name, fn)`.
5969
- * Derived from the canonical `AXL_EVENT_TYPES` tuple — adding a new
5970
- * variant in `types.ts` automatically extends the subscribable set. */
5971
- static STREAM_EVENTS = new Set(AXL_EVENT_TYPES);
6702
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5972
6703
  on(event, handler) {
5973
- if (_AxlStream.STREAM_EVENTS.has(event)) {
5974
- this.bus.on(event, handler);
6704
+ if (this.events.knowsEventName(event)) {
6705
+ this.events.on(event, handler);
5975
6706
  } else {
5976
6707
  super.on(event, handler);
5977
6708
  }
5978
6709
  return this;
5979
6710
  }
6711
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5980
6712
  off(event, handler) {
5981
- if (_AxlStream.STREAM_EVENTS.has(event)) {
5982
- this.bus.off(event, handler);
6713
+ if (this.events.knowsEventName(event)) {
6714
+ this.events.off(event, handler);
5983
6715
  } else {
5984
6716
  super.off(event, handler);
5985
6717
  }
5986
6718
  return this;
5987
6719
  }
6720
+ // Override Readable's [Symbol.asyncIterator] to delegate to the bus.
6721
+ // No explicit return type — structural inference keeps it compatible
6722
+ // with Readable's own signature while preserving the richer
6723
+ // iterator-with-dispose shape for direct consumers.
5988
6724
  [Symbol.asyncIterator]() {
5989
- const self = this;
5990
- return {
5991
- next: () => {
5992
- if (self.eventQueue.length > 0) {
5993
- return Promise.resolve({ value: self.eventQueue.shift(), done: false });
5994
- }
5995
- if (self.finished && self.eventQueue.length === 0) {
5996
- return Promise.resolve({ value: void 0, done: true });
5997
- }
5998
- return new Promise((resolve2) => {
5999
- self.waiters.push(resolve2);
6000
- });
6001
- },
6002
- [Symbol.asyncIterator]() {
6003
- return this;
6004
- },
6005
- [Symbol.asyncDispose]() {
6006
- self.destroy();
6007
- return Promise.resolve();
6008
- }
6009
- };
6725
+ return this.events[Symbol.asyncIterator]();
6010
6726
  }
6011
6727
  get text() {
6012
- const self = this;
6013
- return {
6014
- [Symbol.asyncIterator]() {
6015
- const iter = self[Symbol.asyncIterator]();
6016
- return {
6017
- async next() {
6018
- while (true) {
6019
- const { value, done } = await iter.next();
6020
- if (done) return { value: void 0, done: true };
6021
- if (value.type === "token" && isRootLevel(value)) {
6022
- return { value: value.data, done: false };
6023
- }
6024
- }
6025
- }
6026
- };
6027
- }
6028
- };
6728
+ return this.events.text;
6029
6729
  }
6030
- /**
6031
- * Iterator over `{ askId, text }` pairs — one emission per token chunk,
6032
- * tagged with the ask frame that produced it. Complements `.text`
6033
- * (root-only stream): consumers building a split-pane UI that shows
6034
- * each sub-agent's output in its own lane can group by `askId`
6035
- * without iterating the raw stream and hand-filtering on `event.type`.
6036
- *
6037
- * Nested and root tokens both flow through. The `agent` field names
6038
- * the producing agent for UI labelling; it's undefined when the token
6039
- * was emitted outside any ask (rare — synthesized test fixtures).
6040
- */
6041
6730
  get textByAsk() {
6042
- const self = this;
6043
- return {
6044
- [Symbol.asyncIterator]() {
6045
- const iter = self[Symbol.asyncIterator]();
6046
- return {
6047
- async next() {
6048
- while (true) {
6049
- const { value, done } = await iter.next();
6050
- if (done) {
6051
- return {
6052
- value: void 0,
6053
- done: true
6054
- };
6055
- }
6056
- if (value.type === "token") {
6057
- return {
6058
- value: { askId: value.askId, agent: value.agent, text: value.data },
6059
- done: false
6060
- };
6061
- }
6062
- }
6063
- }
6064
- };
6065
- }
6066
- };
6731
+ return this.events.textByAsk;
6067
6732
  }
6068
- /**
6069
- * Iterator over structural lifecycle events only — skips per-token chatter
6070
- * and progressive partial_object emissions. Useful for waterfall UIs and
6071
- * any consumer that wants the "what happened" timeline without per-chunk
6072
- * noise. Renamed from `.steps` in the unified-event-model migration
6073
- * because these are events, not pipeline steps.
6074
- */
6075
6733
  get lifecycle() {
6076
- const lifecycleTypes = /* @__PURE__ */ new Set([
6077
- "ask_start",
6078
- "ask_end",
6079
- "agent_call_start",
6080
- "agent_call_end",
6081
- "tool_call_start",
6082
- "tool_call_end",
6083
- "tool_approval",
6084
- "tool_denied",
6085
- "handoff_start",
6086
- "handoff_return",
6087
- "delegate",
6088
- "pipeline",
6089
- "verify",
6090
- "workflow_start",
6091
- "workflow_end",
6092
- // Durable-execution checkpoints — structural points in the timeline.
6093
- "checkpoint_save",
6094
- "checkpoint_replay",
6095
- // Human-in-the-loop — pause/resume are major timeline landmarks.
6096
- "await_human",
6097
- "await_human_resolved"
6098
- ]);
6099
- const self = this;
6100
- return {
6101
- [Symbol.asyncIterator]() {
6102
- const iter = self[Symbol.asyncIterator]();
6103
- return {
6104
- async next() {
6105
- while (true) {
6106
- const { value, done } = await iter.next();
6107
- if (done) return { value: void 0, done: true };
6108
- if (lifecycleTypes.has(value.type)) return { value, done: false };
6109
- }
6110
- }
6111
- };
6112
- }
6113
- };
6734
+ return this.events.lifecycle;
6735
+ }
6736
+ /** Coalescing iterator over `partial_object` events — yields the latest
6737
+ * object payload per `askId`. Designed for streaming-structured-output
6738
+ * UIs: when the producer outpaces the consumer, intermediate snapshots
6739
+ * are silently superseded and the next `.next()` await sees only the
6740
+ * most recent state per ask. Listener-based, so does NOT race with the
6741
+ * main `for await (const e of stream)` iterator. See
6742
+ * `AxlEventBus.partialObjects` for full semantics. The yielded
6743
+ * `CoalescedPartialObject` shape is shared with the bus so the two
6744
+ * surfaces can never drift on the `attempt` field. */
6745
+ get partialObjects() {
6746
+ return this.events.partialObjects;
6114
6747
  }
6115
6748
  pipe(destination, options) {
6116
6749
  const shouldEnd = options?.end !== false;
6117
- this.bus.on("token", (event) => {
6750
+ this.events.on("token", (event) => {
6118
6751
  if (event.type === "token") destination.write(event.data);
6119
6752
  });
6120
- this.bus.on("done", () => {
6753
+ this.events.on("done", () => {
6121
6754
  if (shouldEnd) destination.end();
6122
6755
  });
6123
- this.bus.on("error", () => {
6756
+ this.events.on("error", () => {
6124
6757
  if (shouldEnd) destination.end();
6125
6758
  });
6126
6759
  return destination;
6127
6760
  }
6128
- /** Push an event onto the stream. Called by the runtime. */
6761
+ /** Push an event onto the stream. Called by the runtime.
6762
+ *
6763
+ * Side-effect ordering: `accountForToken` (fullText buffers) →
6764
+ * `events._push` (bus.emit + iterator waiter/queue) → `Readable.push`
6765
+ * (object-mode buffer for `.pipe()` consumers and `'data'` listeners).
6766
+ * Pre-refactor the order was `bus.emit → Readable.push → waiter/queue`;
6767
+ * the new order resolves async iterators before Readable.push. The
6768
+ * difference is invisible to all in-tree consumers — `.pipe()` here
6769
+ * routes through the bus's `'token'` listener (line 129), and existing
6770
+ * tests pass — but it's intentional rather than accidental. */
6129
6771
  _push(event) {
6130
- if (this.finished) return;
6772
+ if (this.events.isFinished) return;
6773
+ this.accountForToken(event);
6774
+ this.events._push(event);
6775
+ this.push(event);
6776
+ }
6777
+ /** Token accumulation and pipeline commit/discard accounting for
6778
+ * `fullText`. Extracted from `_push` so the bus delegation reads cleanly. */
6779
+ accountForToken(event) {
6131
6780
  if (event.type === "token" && isRootLevel(event)) {
6132
6781
  const key = event.askId ?? "";
6133
6782
  const attempt = this.attemptByAsk.get(key);
@@ -6136,6 +6785,7 @@ var AxlStream = class _AxlStream extends Readable {
6136
6785
  } else {
6137
6786
  this.attemptByAsk.set(key, [event.data]);
6138
6787
  }
6788
+ return;
6139
6789
  }
6140
6790
  if (event.type === "pipeline" && isRootLevel(event)) {
6141
6791
  const key = event.askId ?? "";
@@ -6149,18 +6799,11 @@ var AxlStream = class _AxlStream extends Readable {
6149
6799
  } else if (event.status === "failed") {
6150
6800
  this.attemptByAsk.set(key, []);
6151
6801
  }
6802
+ return;
6152
6803
  }
6153
6804
  if (event.type === "ask_end" && isRootLevel(event) && !event.outcome.ok) {
6154
6805
  this.attemptByAsk.set(event.askId ?? "", []);
6155
6806
  }
6156
- this.bus.emit(event.type, event);
6157
- this.push(event);
6158
- const waiter = this.waiters.shift();
6159
- if (waiter) {
6160
- waiter({ value: event, done: false });
6161
- } else {
6162
- this.eventQueue.push(event);
6163
- }
6164
6807
  }
6165
6808
  /**
6166
6809
  * Signal successful completion.
@@ -6171,8 +6814,7 @@ var AxlStream = class _AxlStream extends Readable {
6171
6814
  * on error paths that threw before `execInfo` was assigned (review S4).
6172
6815
  */
6173
6816
  _done(result, executionId) {
6174
- if (this.finished) return;
6175
- this.finished = true;
6817
+ if (this.events.isFinished) return;
6176
6818
  this.result = result;
6177
6819
  const doneEvent = {
6178
6820
  type: "done",
@@ -6181,27 +6823,17 @@ var AxlStream = class _AxlStream extends Readable {
6181
6823
  timestamp: Date.now(),
6182
6824
  data: { result }
6183
6825
  };
6184
- this.bus.emit("done", doneEvent);
6826
+ this.events._push(doneEvent);
6185
6827
  this.push(doneEvent);
6186
6828
  this.push(null);
6187
- const waiter = this.waiters.shift();
6188
- if (waiter) {
6189
- waiter({ value: doneEvent, done: false });
6190
- } else {
6191
- this.eventQueue.push(doneEvent);
6192
- }
6193
- for (const w of this.waiters) {
6194
- w({ value: void 0, done: true });
6195
- }
6196
- this.waiters.length = 0;
6197
- this.bus.emit("__resolve", result);
6829
+ this.events._finish();
6830
+ this.resolvePromise(result);
6198
6831
  }
6199
6832
  /** Signal an error. `executionId` is required for the same reason as
6200
6833
  * `_done`: terminal events must carry a real id even when the
6201
6834
  * failure happens before any real trace event fires (review S4). */
6202
6835
  _error(error, executionId) {
6203
- if (this.finished) return;
6204
- this.finished = true;
6836
+ if (this.events.isFinished) return;
6205
6837
  const errorEvent = {
6206
6838
  type: "error",
6207
6839
  executionId,
@@ -6209,20 +6841,11 @@ var AxlStream = class _AxlStream extends Readable {
6209
6841
  timestamp: Date.now(),
6210
6842
  data: { message: error.message, name: error.name }
6211
6843
  };
6212
- this.bus.emit("error", errorEvent);
6844
+ this.events._push(errorEvent);
6213
6845
  this.push(errorEvent);
6214
6846
  this.push(null);
6215
- const waiter = this.waiters.shift();
6216
- if (waiter) {
6217
- waiter({ value: errorEvent, done: false });
6218
- } else {
6219
- this.eventQueue.push(errorEvent);
6220
- }
6221
- for (const w of this.waiters) {
6222
- w({ value: void 0, done: true });
6223
- }
6224
- this.waiters.length = 0;
6225
- this.bus.emit("__reject", error);
6847
+ this.events._finish();
6848
+ this.rejectPromise(error);
6226
6849
  }
6227
6850
  /** Concatenated root-only text from committed attempts plus the
6228
6851
  * current in-flight attempt(s). Retried (gate-rejected) attempts
@@ -6821,6 +7444,18 @@ function pushEventBounded(execInfo, event, cap) {
6821
7444
  }
6822
7445
  }
6823
7446
  var costScopeStorage = new AsyncLocalStorage2();
7447
+ function forwardAbortSignal(external, internal) {
7448
+ if (!external) return () => {
7449
+ };
7450
+ if (external.aborted) {
7451
+ internal.abort();
7452
+ return () => {
7453
+ };
7454
+ }
7455
+ const onAbort = () => internal.abort();
7456
+ external.addEventListener("abort", onAbort, { once: true });
7457
+ return () => external.removeEventListener("abort", onAbort);
7458
+ }
6824
7459
  var AxlRuntime = class extends EventEmitter2 {
6825
7460
  config;
6826
7461
  workflows = /* @__PURE__ */ new Map();
@@ -6837,12 +7472,23 @@ var AxlRuntime = class extends EventEmitter2 {
6837
7472
  spanManager = new NoopSpanManager();
6838
7473
  historicalExecutions = /* @__PURE__ */ new Map();
6839
7474
  historicalExecutionsLoadPromise = null;
7475
+ /** Per-session in-process serialization. Each entry is a chain of pending
7476
+ * work for that session id — `Session.send` / `Session.stream` await the
7477
+ * prior entry before reading history, so concurrent calls produce a
7478
+ * sequenced conversation log instead of last-writer-wins corruption.
7479
+ * Cross-process locking is NOT provided; use distinct session ids when
7480
+ * multiple processes share a store. */
7481
+ sessionLocks = /* @__PURE__ */ new Map();
6840
7482
  evalHistory = [];
6841
7483
  evalHistoryLoadPromise = null;
6842
7484
  /** Resolved cap on `ExecutionInfo.events` per execution. Cached on
6843
7485
  * the instance so per-event onTrace handlers don't reach into
6844
7486
  * `this.config.state` on every emission. */
6845
7487
  maxEventsPerExecution;
7488
+ /** Tracks executionIds for which we've already warned about a malformed
7489
+ * `events` field — keeps the log to one line per row instead of one per
7490
+ * rebuild tick. */
7491
+ warnedMalformedExecutions = /* @__PURE__ */ new Set();
6846
7492
  constructor(config) {
6847
7493
  super();
6848
7494
  this.config = resolveConfig(config ?? {});
@@ -6928,8 +7574,9 @@ var AxlRuntime = class extends EventEmitter2 {
6928
7574
  * Side effects on `execInfo`: sets `status` / `completedAt` /
6929
7575
  * `duration` / `result` (or `error`) before emitting `workflow_end`.
6930
7576
  * On throw: emits `workflow_end({status: 'failed', aborted?})` and
6931
- * persists before rethrowing. `abortControllers.delete` runs in
6932
- * `finally` regardless of outcome.
7577
+ * persists before rethrowing. `abortControllers.delete` and
7578
+ * `cleanupAbortForwarder` (if provided) both run in `finally`
7579
+ * regardless of outcome.
6933
7580
  *
6934
7581
  * Caller responsibilities:
6935
7582
  * - Allocate `executionId` and register it in `abortControllers` map
@@ -6939,9 +7586,13 @@ var AxlRuntime = class extends EventEmitter2 {
6939
7586
  * - Pass `span` if the call site is inside a tracer span — this
6940
7587
  * helper sets `axl.workflow.cost` / `.duration` attributes on
6941
7588
  * success.
7589
+ * - Pass `cleanupAbortForwarder` (the cleanup fn returned from
7590
+ * `forwardAbortSignal`) so the listener on the user's external
7591
+ * signal is removed when the workflow completes — see the
7592
+ * helper's docstring for the leak this prevents.
6942
7593
  */
6943
7594
  async runWorkflowBody(args) {
6944
- const { workflow: workflow2, ctx, execInfo, validated, span } = args;
7595
+ const { workflow: workflow2, ctx, execInfo, validated, span, cleanupAbortForwarder } = args;
6945
7596
  try {
6946
7597
  ctx._emitWorkflowStart(validated);
6947
7598
  const raw = await workflow2.handler(ctx);
@@ -6978,6 +7629,7 @@ var AxlRuntime = class extends EventEmitter2 {
6978
7629
  throw err;
6979
7630
  } finally {
6980
7631
  this.abortControllers.delete(execInfo.executionId);
7632
+ cleanupAbortForwarder?.();
6981
7633
  }
6982
7634
  }
6983
7635
  /**
@@ -7130,12 +7782,31 @@ var AxlRuntime = class extends EventEmitter2 {
7130
7782
  return result;
7131
7783
  }
7132
7784
  /** Get all execution info (running + completed + historical). */
7785
+ /** Normalize an `ExecutionInfo` loaded from a `StateStore` so the
7786
+ * `events: AxlEvent[]` type contract holds for downstream consumers
7787
+ * (Studio aggregators, REST routes, redaction). Custom or legacy stores
7788
+ * may persist rows without `events` (or with a non-array value) — without
7789
+ * this guard, a single malformed row crashes every iterator over
7790
+ * `exec.events` and takes down dependent features. Mutates the row
7791
+ * in place; logs at most one warning per `executionId`. */
7792
+ normalizeStoredExecution(exec) {
7793
+ if (!Array.isArray(exec.events)) {
7794
+ if (!this.warnedMalformedExecutions.has(exec.executionId)) {
7795
+ this.warnedMalformedExecutions.add(exec.executionId);
7796
+ console.warn(
7797
+ `[axl] StateStore returned execution ${exec.executionId} with non-array events (got ${exec.events === null ? "null" : typeof exec.events}); coercing to []`
7798
+ );
7799
+ }
7800
+ exec.events = [];
7801
+ }
7802
+ return exec;
7803
+ }
7133
7804
  async getExecutions() {
7134
7805
  if (!this.historicalExecutionsLoadPromise && this.stateStore.listExecutions) {
7135
7806
  this.historicalExecutionsLoadPromise = this.stateStore.listExecutions().then((stored) => {
7136
7807
  for (const exec of stored) {
7137
7808
  if (!this.executions.has(exec.executionId) && !this.historicalExecutions.has(exec.executionId)) {
7138
- this.historicalExecutions.set(exec.executionId, exec);
7809
+ this.historicalExecutions.set(exec.executionId, this.normalizeStoredExecution(exec));
7139
7810
  }
7140
7811
  }
7141
7812
  }).catch(() => {
@@ -7176,6 +7847,7 @@ var AxlRuntime = class extends EventEmitter2 {
7176
7847
  onToolCall: options?.onToolCall,
7177
7848
  onAgentStart: options?.onAgentStart,
7178
7849
  awaitHumanHandler: options?.awaitHumanHandler,
7850
+ eventStreamOptions: options?.events,
7179
7851
  onTrace: (event) => {
7180
7852
  this.emit("trace", event);
7181
7853
  this.outputAxlEvent(event);
@@ -7208,6 +7880,7 @@ var AxlRuntime = class extends EventEmitter2 {
7208
7880
  const executionId = randomUUID3();
7209
7881
  const controller = new AbortController();
7210
7882
  this.abortControllers.set(executionId, controller);
7883
+ const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
7211
7884
  this.registerWithCostScope(executionId);
7212
7885
  const execInfo = {
7213
7886
  executionId,
@@ -7228,6 +7901,7 @@ var AxlRuntime = class extends EventEmitter2 {
7228
7901
  providerRegistry: this.providerRegistry,
7229
7902
  sessionHistory,
7230
7903
  signal: controller.signal,
7904
+ eventStreamOptions: options?.events,
7231
7905
  onTrace: (event) => {
7232
7906
  pushEventBounded(execInfo, event, this.maxEventsPerExecution);
7233
7907
  execInfo.totalCost += eventCostContribution(event);
@@ -7289,15 +7963,17 @@ var AxlRuntime = class extends EventEmitter2 {
7289
7963
  ctx,
7290
7964
  execInfo,
7291
7965
  validated,
7292
- span
7966
+ span,
7967
+ cleanupAbortForwarder
7293
7968
  })
7294
7969
  );
7295
7970
  }
7296
7971
  /** Execute a workflow and return a stream. */
7297
7972
  stream(name, input, options) {
7298
- const axlStream = new AxlStream();
7973
+ const axlStream = new AxlStream(options?.events);
7299
7974
  const controller = new AbortController();
7300
7975
  axlStream.on("close", () => controller.abort());
7976
+ const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
7301
7977
  const executionId = randomUUID3();
7302
7978
  this.abortControllers.set(executionId, controller);
7303
7979
  let execInfo;
@@ -7325,6 +8001,7 @@ var AxlRuntime = class extends EventEmitter2 {
7325
8001
  providerRegistry: this.providerRegistry,
7326
8002
  sessionHistory,
7327
8003
  signal: controller.signal,
8004
+ eventStreamOptions: options?.events,
7328
8005
  // Sentinel `onToken` so the streaming code path in WorkflowContext
7329
8006
  // is entered (it's gated on `this.onToken` being defined). The
7330
8007
  // actual `token` AxlEvents are emitted via `emitEvent` and reach
@@ -7393,7 +8070,8 @@ var AxlRuntime = class extends EventEmitter2 {
7393
8070
  ctx: wfCtx,
7394
8071
  execInfo,
7395
8072
  validated,
7396
- span
8073
+ span,
8074
+ cleanupAbortForwarder
7397
8075
  })
7398
8076
  );
7399
8077
  };
@@ -7406,6 +8084,7 @@ var AxlRuntime = class extends EventEmitter2 {
7406
8084
  this.persistExecution(execInfo);
7407
8085
  }
7408
8086
  this.abortControllers.delete(executionId);
8087
+ cleanupAbortForwarder();
7409
8088
  axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
7410
8089
  });
7411
8090
  return axlStream;
@@ -7414,12 +8093,55 @@ var AxlRuntime = class extends EventEmitter2 {
7414
8093
  session(id, options) {
7415
8094
  return new Session(id, this, this.stateStore, options);
7416
8095
  }
7417
- /** Gracefully shut down the runtime, aborting in-flight executions and closing state stores and MCP servers. */
8096
+ /** @internal Serialize work per session id within this runtime instance.
8097
+ * Subsequent calls await the prior task's settlement (success or failure)
8098
+ * before running. Used by `Session.send` / `Session.stream` / `end` /
8099
+ * `fork` to eliminate read-modify-write races on `StateStore.saveSession`
8100
+ * (and on `delete`/`getSession` ordering vs in-flight saves).
8101
+ *
8102
+ * ⚠️ NOT REENTRANT. Calling this from inside an `fn` already running
8103
+ * under `_serializeSession(sameId, ...)` will deadlock — the inner
8104
+ * call awaits the outer's settlement, the outer awaits the inner's
8105
+ * return. `Session.fork` deliberately calls with TWO different ids
8106
+ * (source and target) and never the same id twice. Any future caller
8107
+ * must follow the same rule. */
8108
+ async _serializeSession(id, fn) {
8109
+ const prev = this.sessionLocks.get(id);
8110
+ if (prev !== void 0) {
8111
+ this.emit("session_lock_contended", { sessionId: id });
8112
+ }
8113
+ const base = prev ?? Promise.resolve();
8114
+ const ours = base.then(fn, fn);
8115
+ const chained = ours.then(
8116
+ () => {
8117
+ },
8118
+ () => {
8119
+ }
8120
+ );
8121
+ this.sessionLocks.set(id, chained);
8122
+ void chained.finally(() => {
8123
+ if (this.sessionLocks.get(id) === chained) {
8124
+ this.sessionLocks.delete(id);
8125
+ }
8126
+ });
8127
+ return ours;
8128
+ }
8129
+ /** Gracefully shut down the runtime, aborting in-flight executions and
8130
+ * closing state stores and MCP servers. Drains in-flight per-session
8131
+ * work (Session.send / Session.stream) before closing the state store.
8132
+ *
8133
+ * This is a drain of EXISTING work, not a barrier against new calls —
8134
+ * callers should stop accepting work (e.g., close their HTTP server)
8135
+ * before invoking `shutdown()`. A `Session.send` invoked after the
8136
+ * drain snapshot is taken can still race a closed state store. */
7418
8137
  async shutdown() {
7419
8138
  for (const [, controller] of this.abortControllers) {
7420
8139
  controller.abort();
7421
8140
  }
7422
8141
  this.abortControllers.clear();
8142
+ if (this.sessionLocks.size > 0) {
8143
+ await Promise.allSettled([...this.sessionLocks.values()]);
8144
+ }
7423
8145
  const errors = [];
7424
8146
  const safeClose = async (label, fn) => {
7425
8147
  try {
@@ -7455,8 +8177,9 @@ var AxlRuntime = class extends EventEmitter2 {
7455
8177
  if (this.stateStore.getExecution) {
7456
8178
  const stored = await this.stateStore.getExecution(executionId);
7457
8179
  if (stored) {
7458
- this.historicalExecutions.set(executionId, stored);
7459
- return stored;
8180
+ const normalized = this.normalizeStoredExecution(stored);
8181
+ this.historicalExecutions.set(executionId, normalized);
8182
+ return normalized;
7460
8183
  }
7461
8184
  }
7462
8185
  return void 0;
@@ -7946,7 +8669,14 @@ async function ensureTsxRegistered() {
7946
8669
  return tsxRegistered;
7947
8670
  }
7948
8671
  async function importModule(filePath, _parentURL) {
7949
- if (needsTsxLoader(filePath)) {
8672
+ const url = pathToFileURL(filePath).href;
8673
+ if (!needsTsxLoader(filePath)) {
8674
+ return await import(url);
8675
+ }
8676
+ try {
8677
+ return await import(url);
8678
+ } catch (err) {
8679
+ if (!isMissingLoaderError(err)) throw err;
7950
8680
  const registered = await ensureTsxRegistered();
7951
8681
  if (!registered) {
7952
8682
  throw new Error(
@@ -7956,8 +8686,14 @@ async function importModule(filePath, _parentURL) {
7956
8686
  disabled, install it explicitly: npm install -D tsx`
7957
8687
  );
7958
8688
  }
8689
+ return await import(url);
7959
8690
  }
7960
- return await import(pathToFileURL(filePath).href);
8691
+ }
8692
+ function isMissingLoaderError(err) {
8693
+ if (!(err instanceof Error)) return false;
8694
+ const code = err.code;
8695
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION") return true;
8696
+ return /Cannot use import statement outside a module/i.test(err.message);
7961
8697
  }
7962
8698
  var MAX_GLOB_DEPTH = 20;
7963
8699
  function expandGlob(pattern, cwd) {
@@ -8369,11 +9105,13 @@ export {
8369
9105
  AXL_EVENT_TYPES,
8370
9106
  AnthropicProvider,
8371
9107
  AxlError,
9108
+ AxlEventBus,
8372
9109
  AxlRuntime,
8373
9110
  AxlStream,
8374
9111
  BudgetExceededError,
8375
9112
  CONFIG_CANDIDATES,
8376
9113
  COST_BEARING_LEAF_TYPES,
9114
+ EventStreamOverflowError,
8377
9115
  GeminiProvider,
8378
9116
  GuardrailError,
8379
9117
  InMemoryVectorStore,