@axlsdk/axl 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -1
- package/dist/index.cjs +1086 -370
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -55
- package/dist/index.d.ts +547 -55
- package/dist/index.js +1084 -370
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
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.
|
|
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
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
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
|
+
}
|
|
5285
6014
|
}
|
|
5286
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
|
+
}
|
|
6036
|
+
}
|
|
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
|
|
|
@@ -5885,58 +6641,30 @@ var Session = class _Session {
|
|
|
5885
6641
|
closed = false;
|
|
5886
6642
|
options;
|
|
5887
6643
|
get id() {
|
|
5888
|
-
return this.sessionId;
|
|
5889
|
-
}
|
|
5890
|
-
async send(workflowName, input) {
|
|
5891
|
-
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
|
-
}
|
|
5936
|
-
return result;
|
|
6644
|
+
return this.sessionId;
|
|
5937
6645
|
}
|
|
5938
|
-
async
|
|
6646
|
+
async send(workflowName, input, options) {
|
|
5939
6647
|
if (this.closed) throw new Error("Session has been ended");
|
|
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);
|
|
6658
|
+
return result;
|
|
6659
|
+
}
|
|
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
|
-
|
|
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
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
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
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6008
|
-
|
|
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
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
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
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
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
|
-
*
|
|
6092
|
-
*
|
|
6093
|
-
* (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
|
|
6094
|
-
*
|
|
6095
|
-
*
|
|
6096
|
-
*
|
|
6097
|
-
*
|
|
6098
|
-
*
|
|
6099
|
-
*
|
|
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
|
-
|
|
6116
|
-
|
|
6117
|
-
constructor() {
|
|
6856
|
+
resolvePromise;
|
|
6857
|
+
rejectPromise;
|
|
6858
|
+
constructor(options) {
|
|
6118
6859
|
super({ objectMode: true, read() {
|
|
6119
6860
|
} });
|
|
6120
|
-
|
|
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
|
-
|
|
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 (
|
|
6139
|
-
this.
|
|
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 (
|
|
6147
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
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.
|
|
6917
|
+
this.events.on("token", (event) => {
|
|
6283
6918
|
if (event.type === "token") destination.write(event.data);
|
|
6284
6919
|
});
|
|
6285
|
-
this.
|
|
6920
|
+
this.events.on("done", () => {
|
|
6286
6921
|
if (shouldEnd) destination.end();
|
|
6287
6922
|
});
|
|
6288
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
6993
|
+
this.events._push(doneEvent);
|
|
6350
6994
|
this.push(doneEvent);
|
|
6351
6995
|
this.push(null);
|
|
6352
|
-
|
|
6353
|
-
|
|
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.
|
|
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.
|
|
7011
|
+
this.events._push(errorEvent);
|
|
6378
7012
|
this.push(errorEvent);
|
|
6379
7013
|
this.push(null);
|
|
6380
|
-
|
|
6381
|
-
|
|
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,6 +7640,13 @@ 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
|
|
@@ -7094,8 +7738,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7094
7738
|
* Side effects on `execInfo`: sets `status` / `completedAt` /
|
|
7095
7739
|
* `duration` / `result` (or `error`) before emitting `workflow_end`.
|
|
7096
7740
|
* On throw: emits `workflow_end({status: 'failed', aborted?})` and
|
|
7097
|
-
* persists before rethrowing. `abortControllers.delete`
|
|
7098
|
-
* `
|
|
7741
|
+
* persists before rethrowing. `abortControllers.delete` and
|
|
7742
|
+
* `cleanupAbortForwarder` (if provided) both run in `finally`
|
|
7743
|
+
* regardless of outcome.
|
|
7099
7744
|
*
|
|
7100
7745
|
* Caller responsibilities:
|
|
7101
7746
|
* - Allocate `executionId` and register it in `abortControllers` map
|
|
@@ -7105,9 +7750,13 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7105
7750
|
* - Pass `span` if the call site is inside a tracer span — this
|
|
7106
7751
|
* helper sets `axl.workflow.cost` / `.duration` attributes on
|
|
7107
7752
|
* success.
|
|
7753
|
+
* - Pass `cleanupAbortForwarder` (the cleanup fn returned from
|
|
7754
|
+
* `forwardAbortSignal`) so the listener on the user's external
|
|
7755
|
+
* signal is removed when the workflow completes — see the
|
|
7756
|
+
* helper's docstring for the leak this prevents.
|
|
7108
7757
|
*/
|
|
7109
7758
|
async runWorkflowBody(args) {
|
|
7110
|
-
const { workflow: workflow2, ctx, execInfo, validated, span } = args;
|
|
7759
|
+
const { workflow: workflow2, ctx, execInfo, validated, span, cleanupAbortForwarder } = args;
|
|
7111
7760
|
try {
|
|
7112
7761
|
ctx._emitWorkflowStart(validated);
|
|
7113
7762
|
const raw = await workflow2.handler(ctx);
|
|
@@ -7144,6 +7793,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7144
7793
|
throw err;
|
|
7145
7794
|
} finally {
|
|
7146
7795
|
this.abortControllers.delete(execInfo.executionId);
|
|
7796
|
+
cleanupAbortForwarder?.();
|
|
7147
7797
|
}
|
|
7148
7798
|
}
|
|
7149
7799
|
/**
|
|
@@ -7342,6 +7992,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7342
7992
|
onToolCall: options?.onToolCall,
|
|
7343
7993
|
onAgentStart: options?.onAgentStart,
|
|
7344
7994
|
awaitHumanHandler: options?.awaitHumanHandler,
|
|
7995
|
+
eventStreamOptions: options?.events,
|
|
7345
7996
|
onTrace: (event) => {
|
|
7346
7997
|
this.emit("trace", event);
|
|
7347
7998
|
this.outputAxlEvent(event);
|
|
@@ -7374,6 +8025,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7374
8025
|
const executionId = (0, import_node_crypto3.randomUUID)();
|
|
7375
8026
|
const controller = new AbortController();
|
|
7376
8027
|
this.abortControllers.set(executionId, controller);
|
|
8028
|
+
const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
|
|
7377
8029
|
this.registerWithCostScope(executionId);
|
|
7378
8030
|
const execInfo = {
|
|
7379
8031
|
executionId,
|
|
@@ -7394,6 +8046,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7394
8046
|
providerRegistry: this.providerRegistry,
|
|
7395
8047
|
sessionHistory,
|
|
7396
8048
|
signal: controller.signal,
|
|
8049
|
+
eventStreamOptions: options?.events,
|
|
7397
8050
|
onTrace: (event) => {
|
|
7398
8051
|
pushEventBounded(execInfo, event, this.maxEventsPerExecution);
|
|
7399
8052
|
execInfo.totalCost += eventCostContribution(event);
|
|
@@ -7455,15 +8108,17 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7455
8108
|
ctx,
|
|
7456
8109
|
execInfo,
|
|
7457
8110
|
validated,
|
|
7458
|
-
span
|
|
8111
|
+
span,
|
|
8112
|
+
cleanupAbortForwarder
|
|
7459
8113
|
})
|
|
7460
8114
|
);
|
|
7461
8115
|
}
|
|
7462
8116
|
/** Execute a workflow and return a stream. */
|
|
7463
8117
|
stream(name, input, options) {
|
|
7464
|
-
const axlStream = new AxlStream();
|
|
8118
|
+
const axlStream = new AxlStream(options?.events);
|
|
7465
8119
|
const controller = new AbortController();
|
|
7466
8120
|
axlStream.on("close", () => controller.abort());
|
|
8121
|
+
const cleanupAbortForwarder = forwardAbortSignal(options?.signal, controller);
|
|
7467
8122
|
const executionId = (0, import_node_crypto3.randomUUID)();
|
|
7468
8123
|
this.abortControllers.set(executionId, controller);
|
|
7469
8124
|
let execInfo;
|
|
@@ -7491,6 +8146,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7491
8146
|
providerRegistry: this.providerRegistry,
|
|
7492
8147
|
sessionHistory,
|
|
7493
8148
|
signal: controller.signal,
|
|
8149
|
+
eventStreamOptions: options?.events,
|
|
7494
8150
|
// Sentinel `onToken` so the streaming code path in WorkflowContext
|
|
7495
8151
|
// is entered (it's gated on `this.onToken` being defined). The
|
|
7496
8152
|
// actual `token` AxlEvents are emitted via `emitEvent` and reach
|
|
@@ -7559,7 +8215,8 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7559
8215
|
ctx: wfCtx,
|
|
7560
8216
|
execInfo,
|
|
7561
8217
|
validated,
|
|
7562
|
-
span
|
|
8218
|
+
span,
|
|
8219
|
+
cleanupAbortForwarder
|
|
7563
8220
|
})
|
|
7564
8221
|
);
|
|
7565
8222
|
};
|
|
@@ -7572,6 +8229,7 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7572
8229
|
this.persistExecution(execInfo);
|
|
7573
8230
|
}
|
|
7574
8231
|
this.abortControllers.delete(executionId);
|
|
8232
|
+
cleanupAbortForwarder();
|
|
7575
8233
|
axlStream._error(err instanceof Error ? err : new Error(String(err)), executionId);
|
|
7576
8234
|
});
|
|
7577
8235
|
return axlStream;
|
|
@@ -7580,12 +8238,55 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
|
|
|
7580
8238
|
session(id, options) {
|
|
7581
8239
|
return new Session(id, this, this.stateStore, options);
|
|
7582
8240
|
}
|
|
7583
|
-
/**
|
|
8241
|
+
/** @internal Serialize work per session id within this runtime instance.
|
|
8242
|
+
* Subsequent calls await the prior task's settlement (success or failure)
|
|
8243
|
+
* before running. Used by `Session.send` / `Session.stream` / `end` /
|
|
8244
|
+
* `fork` to eliminate read-modify-write races on `StateStore.saveSession`
|
|
8245
|
+
* (and on `delete`/`getSession` ordering vs in-flight saves).
|
|
8246
|
+
*
|
|
8247
|
+
* ⚠️ NOT REENTRANT. Calling this from inside an `fn` already running
|
|
8248
|
+
* under `_serializeSession(sameId, ...)` will deadlock — the inner
|
|
8249
|
+
* call awaits the outer's settlement, the outer awaits the inner's
|
|
8250
|
+
* return. `Session.fork` deliberately calls with TWO different ids
|
|
8251
|
+
* (source and target) and never the same id twice. Any future caller
|
|
8252
|
+
* must follow the same rule. */
|
|
8253
|
+
async _serializeSession(id, fn) {
|
|
8254
|
+
const prev = this.sessionLocks.get(id);
|
|
8255
|
+
if (prev !== void 0) {
|
|
8256
|
+
this.emit("session_lock_contended", { sessionId: id });
|
|
8257
|
+
}
|
|
8258
|
+
const base = prev ?? Promise.resolve();
|
|
8259
|
+
const ours = base.then(fn, fn);
|
|
8260
|
+
const chained = ours.then(
|
|
8261
|
+
() => {
|
|
8262
|
+
},
|
|
8263
|
+
() => {
|
|
8264
|
+
}
|
|
8265
|
+
);
|
|
8266
|
+
this.sessionLocks.set(id, chained);
|
|
8267
|
+
void chained.finally(() => {
|
|
8268
|
+
if (this.sessionLocks.get(id) === chained) {
|
|
8269
|
+
this.sessionLocks.delete(id);
|
|
8270
|
+
}
|
|
8271
|
+
});
|
|
8272
|
+
return ours;
|
|
8273
|
+
}
|
|
8274
|
+
/** Gracefully shut down the runtime, aborting in-flight executions and
|
|
8275
|
+
* closing state stores and MCP servers. Drains in-flight per-session
|
|
8276
|
+
* work (Session.send / Session.stream) before closing the state store.
|
|
8277
|
+
*
|
|
8278
|
+
* This is a drain of EXISTING work, not a barrier against new calls —
|
|
8279
|
+
* callers should stop accepting work (e.g., close their HTTP server)
|
|
8280
|
+
* before invoking `shutdown()`. A `Session.send` invoked after the
|
|
8281
|
+
* drain snapshot is taken can still race a closed state store. */
|
|
7584
8282
|
async shutdown() {
|
|
7585
8283
|
for (const [, controller] of this.abortControllers) {
|
|
7586
8284
|
controller.abort();
|
|
7587
8285
|
}
|
|
7588
8286
|
this.abortControllers.clear();
|
|
8287
|
+
if (this.sessionLocks.size > 0) {
|
|
8288
|
+
await Promise.allSettled([...this.sessionLocks.values()]);
|
|
8289
|
+
}
|
|
7589
8290
|
const errors = [];
|
|
7590
8291
|
const safeClose = async (label, fn) => {
|
|
7591
8292
|
try {
|
|
@@ -8112,7 +8813,14 @@ async function ensureTsxRegistered() {
|
|
|
8112
8813
|
return tsxRegistered;
|
|
8113
8814
|
}
|
|
8114
8815
|
async function importModule(filePath, _parentURL) {
|
|
8115
|
-
|
|
8816
|
+
const url = (0, import_node_url.pathToFileURL)(filePath).href;
|
|
8817
|
+
if (!needsTsxLoader(filePath)) {
|
|
8818
|
+
return await import(url);
|
|
8819
|
+
}
|
|
8820
|
+
try {
|
|
8821
|
+
return await import(url);
|
|
8822
|
+
} catch (err) {
|
|
8823
|
+
if (!isMissingLoaderError(err)) throw err;
|
|
8116
8824
|
const registered = await ensureTsxRegistered();
|
|
8117
8825
|
if (!registered) {
|
|
8118
8826
|
throw new Error(
|
|
@@ -8122,8 +8830,14 @@ async function importModule(filePath, _parentURL) {
|
|
|
8122
8830
|
disabled, install it explicitly: npm install -D tsx`
|
|
8123
8831
|
);
|
|
8124
8832
|
}
|
|
8833
|
+
return await import(url);
|
|
8125
8834
|
}
|
|
8126
|
-
|
|
8835
|
+
}
|
|
8836
|
+
function isMissingLoaderError(err) {
|
|
8837
|
+
if (!(err instanceof Error)) return false;
|
|
8838
|
+
const code = err.code;
|
|
8839
|
+
if (code === "ERR_UNKNOWN_FILE_EXTENSION") return true;
|
|
8840
|
+
return /Cannot use import statement outside a module/i.test(err.message);
|
|
8127
8841
|
}
|
|
8128
8842
|
var MAX_GLOB_DEPTH = 20;
|
|
8129
8843
|
function expandGlob(pattern, cwd) {
|
|
@@ -8536,11 +9250,13 @@ function cosineSimilarity2(a, b) {
|
|
|
8536
9250
|
AXL_EVENT_TYPES,
|
|
8537
9251
|
AnthropicProvider,
|
|
8538
9252
|
AxlError,
|
|
9253
|
+
AxlEventBus,
|
|
8539
9254
|
AxlRuntime,
|
|
8540
9255
|
AxlStream,
|
|
8541
9256
|
BudgetExceededError,
|
|
8542
9257
|
CONFIG_CANDIDATES,
|
|
8543
9258
|
COST_BEARING_LEAF_TYPES,
|
|
9259
|
+
EventStreamOverflowError,
|
|
8544
9260
|
GeminiProvider,
|
|
8545
9261
|
GuardrailError,
|
|
8546
9262
|
InMemoryVectorStore,
|