@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/README.md +63 -1
- package/dist/index.cjs +1110 -370
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +559 -55
- package/dist/index.d.ts +559 -55
- package/dist/index.js +1108 -370
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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.
|
|
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
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
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
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
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
|
-
|
|
5774
|
-
|
|
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
|
-
|
|
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
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
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
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5843
|
-
|
|
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
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
const
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
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
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
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
|
-
*
|
|
5927
|
-
*
|
|
5928
|
-
* (`ctx.parallel`, `ctx.spawn`, `ctx.race`, `ctx.map`) don't
|
|
5929
|
-
*
|
|
5930
|
-
*
|
|
5931
|
-
*
|
|
5932
|
-
*
|
|
5933
|
-
*
|
|
5934
|
-
*
|
|
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
|
-
|
|
5951
|
-
|
|
5952
|
-
constructor() {
|
|
6689
|
+
resolvePromise;
|
|
6690
|
+
rejectPromise;
|
|
6691
|
+
constructor(options) {
|
|
5953
6692
|
super({ objectMode: true, read() {
|
|
5954
6693
|
} });
|
|
5955
|
-
|
|
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
|
-
|
|
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 (
|
|
5974
|
-
this.
|
|
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 (
|
|
5982
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
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.
|
|
6750
|
+
this.events.on("token", (event) => {
|
|
6118
6751
|
if (event.type === "token") destination.write(event.data);
|
|
6119
6752
|
});
|
|
6120
|
-
this.
|
|
6753
|
+
this.events.on("done", () => {
|
|
6121
6754
|
if (shouldEnd) destination.end();
|
|
6122
6755
|
});
|
|
6123
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
6826
|
+
this.events._push(doneEvent);
|
|
6185
6827
|
this.push(doneEvent);
|
|
6186
6828
|
this.push(null);
|
|
6187
|
-
|
|
6188
|
-
|
|
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.
|
|
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.
|
|
6844
|
+
this.events._push(errorEvent);
|
|
6213
6845
|
this.push(errorEvent);
|
|
6214
6846
|
this.push(null);
|
|
6215
|
-
|
|
6216
|
-
|
|
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`
|
|
6932
|
-
* `
|
|
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
|
-
/**
|
|
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.
|
|
7459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|