@axlsdk/axl 0.17.3 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -163,6 +163,7 @@ __export(index_exports, {
163
163
  registerConditions: () => registerConditions,
164
164
  resolveRuntime: () => resolveRuntime,
165
165
  resolveThinkingOptions: () => resolveThinkingOptions,
166
+ stringStreamFromEvents: () => stringStreamFromEvents,
166
167
  tool: () => tool,
167
168
  workflow: () => workflow,
168
169
  zodToJsonSchema: () => zodToJsonSchema
@@ -2504,6 +2505,314 @@ var PartialParser = class {
2504
2505
  }
2505
2506
  };
2506
2507
 
2508
+ // src/streaming-walker.ts
2509
+ var MAX_DEPTH2 = 256;
2510
+ function encodeSegment(s) {
2511
+ if (typeof s === "number") return String(s);
2512
+ return s.replace(/~/g, "~0").replace(/\//g, "~1");
2513
+ }
2514
+ function computePath(stack) {
2515
+ let path = "";
2516
+ for (const frame of stack) {
2517
+ if (frame.type === "object") {
2518
+ path += "/" + encodeSegment(frame.currentKey ?? "");
2519
+ } else {
2520
+ path += "/" + encodeSegment(frame.currentIndex);
2521
+ }
2522
+ }
2523
+ return path;
2524
+ }
2525
+ var StreamingWalker = class {
2526
+ constructor(callbacks) {
2527
+ this.callbacks = callbacks;
2528
+ }
2529
+ state = "pre-json";
2530
+ stack = [];
2531
+ // String parsing state — survives across chunks so escapes can split.
2532
+ escaped = false;
2533
+ /** When > 0, we're collecting `\\uXXXX` hex digits. 4→0 as digits arrive. */
2534
+ unicodePending = 0;
2535
+ /** Accumulator for `\\uXXXX` digits. Cleared when the escape resolves. */
2536
+ unicodeBuf = "";
2537
+ // Per-string accumulators.
2538
+ keyBuf = "";
2539
+ deltaBuf = "";
2540
+ /** Cached JSON Pointer to the value currently being read. Recomputed
2541
+ * on entry to `read-value-string` (the only state that needs it). */
2542
+ currentPath = "";
2543
+ /** Set true when a structural boundary lands. Caller reads via
2544
+ * `consumeBoundary()` after each chunk to decide on partial_object emit. */
2545
+ boundaryPending = false;
2546
+ /** Walk `text` char-by-char, advancing state and accumulating deltas.
2547
+ * Calls `onStringDelta` at end-of-chunk for any pending value text. */
2548
+ processChunk(text) {
2549
+ for (let i = 0; i < text.length; i++) {
2550
+ if (this.state === "done") return;
2551
+ this.processChar(text[i]);
2552
+ }
2553
+ if (this.deltaBuf.length > 0 && this.state === "read-value-string") {
2554
+ this.callbacks.onStringDelta?.(this.currentPath, this.deltaBuf);
2555
+ this.deltaBuf = "";
2556
+ }
2557
+ }
2558
+ /** Read-and-clear the structural-boundary flag. Caller checks this
2559
+ * after `processChunk` to decide whether to emit `partial_object`. */
2560
+ consumeBoundary() {
2561
+ const v = this.boundaryPending;
2562
+ this.boundaryPending = false;
2563
+ return v;
2564
+ }
2565
+ /** True when the walker has decided it's done with this stream
2566
+ * (root closed or fatal malformed input). Caller may use this to
2567
+ * short-circuit further work. */
2568
+ get isDone() {
2569
+ return this.state === "done";
2570
+ }
2571
+ // ── Per-char dispatch ───────────────────────────────────────────────────
2572
+ processChar(ch) {
2573
+ switch (this.state) {
2574
+ case "pre-json":
2575
+ if (ch === "{") {
2576
+ if (!this.pushFrame({ type: "object" })) return;
2577
+ this.state = "expect-key-or-close";
2578
+ }
2579
+ return;
2580
+ case "expect-key-or-close":
2581
+ if (isWhitespace(ch)) return;
2582
+ if (ch === "}") {
2583
+ this.popFrame();
2584
+ return;
2585
+ }
2586
+ if (ch === '"') {
2587
+ this.keyBuf = "";
2588
+ this.escaped = false;
2589
+ this.unicodePending = 0;
2590
+ this.unicodeBuf = "";
2591
+ this.state = "read-key";
2592
+ return;
2593
+ }
2594
+ this.state = "done";
2595
+ return;
2596
+ case "read-key":
2597
+ this.processStringChar(
2598
+ ch,
2599
+ /* isValue */
2600
+ false
2601
+ );
2602
+ return;
2603
+ case "expect-colon":
2604
+ if (isWhitespace(ch)) return;
2605
+ if (ch === ":") {
2606
+ this.state = "expect-value";
2607
+ return;
2608
+ }
2609
+ this.state = "done";
2610
+ return;
2611
+ case "expect-value":
2612
+ if (isWhitespace(ch)) return;
2613
+ if (ch === "{") {
2614
+ if (!this.pushFrame({ type: "object" })) return;
2615
+ this.state = "expect-key-or-close";
2616
+ return;
2617
+ }
2618
+ if (ch === "[") {
2619
+ if (!this.pushFrame({ type: "array", currentIndex: 0 })) return;
2620
+ this.state = "expect-value";
2621
+ return;
2622
+ }
2623
+ if (ch === "]") {
2624
+ this.popFrame();
2625
+ return;
2626
+ }
2627
+ if (ch === '"') {
2628
+ this.currentPath = computePath(this.stack);
2629
+ this.deltaBuf = "";
2630
+ this.escaped = false;
2631
+ this.unicodePending = 0;
2632
+ this.unicodeBuf = "";
2633
+ this.state = "read-value-string";
2634
+ return;
2635
+ }
2636
+ if (isValueStartChar(ch)) {
2637
+ this.state = "read-value-other";
2638
+ return;
2639
+ }
2640
+ this.state = "done";
2641
+ return;
2642
+ case "read-value-string":
2643
+ this.processStringChar(
2644
+ ch,
2645
+ /* isValue */
2646
+ true
2647
+ );
2648
+ return;
2649
+ case "read-value-other":
2650
+ if (ch === "," || ch === "}" || ch === "]" || isWhitespace(ch)) {
2651
+ this.state = "expect-comma-close";
2652
+ this.processChar(ch);
2653
+ return;
2654
+ }
2655
+ return;
2656
+ case "expect-comma-close":
2657
+ if (isWhitespace(ch)) return;
2658
+ if (ch === ",") {
2659
+ this.boundaryPending = true;
2660
+ const top = this.stack[this.stack.length - 1];
2661
+ if (!top) {
2662
+ this.state = "done";
2663
+ return;
2664
+ }
2665
+ if (top.type === "object") {
2666
+ top.currentKey = void 0;
2667
+ this.state = "expect-key-or-close";
2668
+ } else {
2669
+ top.currentIndex += 1;
2670
+ this.state = "expect-value";
2671
+ }
2672
+ return;
2673
+ }
2674
+ if (ch === "}" || ch === "]") {
2675
+ this.popFrame();
2676
+ return;
2677
+ }
2678
+ this.state = "done";
2679
+ return;
2680
+ case "done":
2681
+ return;
2682
+ }
2683
+ }
2684
+ // ── String state machine (shared between key and value reads) ───────────
2685
+ /**
2686
+ * One char inside a string. Translates JSON escapes, accumulates
2687
+ * `\\uXXXX` digits across chunks, and routes the resolved char to
2688
+ * either `keyBuf` (when reading a key) or `deltaBuf` (when reading a
2689
+ * value).
2690
+ */
2691
+ processStringChar(ch, isValue) {
2692
+ if (this.unicodePending > 0) {
2693
+ if (isHex(ch)) {
2694
+ this.unicodeBuf += ch;
2695
+ this.unicodePending -= 1;
2696
+ if (this.unicodePending === 0) {
2697
+ const code = parseInt(this.unicodeBuf, 16);
2698
+ this.unicodeBuf = "";
2699
+ this.appendChar(String.fromCharCode(code), isValue);
2700
+ }
2701
+ } else {
2702
+ this.state = "done";
2703
+ }
2704
+ return;
2705
+ }
2706
+ if (this.escaped) {
2707
+ this.escaped = false;
2708
+ switch (ch) {
2709
+ case '"':
2710
+ this.appendChar('"', isValue);
2711
+ return;
2712
+ case "\\":
2713
+ this.appendChar("\\", isValue);
2714
+ return;
2715
+ case "/":
2716
+ this.appendChar("/", isValue);
2717
+ return;
2718
+ case "b":
2719
+ this.appendChar("\b", isValue);
2720
+ return;
2721
+ case "f":
2722
+ this.appendChar("\f", isValue);
2723
+ return;
2724
+ case "n":
2725
+ this.appendChar("\n", isValue);
2726
+ return;
2727
+ case "r":
2728
+ this.appendChar("\r", isValue);
2729
+ return;
2730
+ case "t":
2731
+ this.appendChar(" ", isValue);
2732
+ return;
2733
+ case "u":
2734
+ this.unicodePending = 4;
2735
+ this.unicodeBuf = "";
2736
+ return;
2737
+ default:
2738
+ this.state = "done";
2739
+ return;
2740
+ }
2741
+ }
2742
+ if (ch === "\\") {
2743
+ this.escaped = true;
2744
+ return;
2745
+ }
2746
+ if (ch === '"') {
2747
+ if (isValue) {
2748
+ if (this.deltaBuf.length > 0) {
2749
+ this.callbacks.onStringDelta?.(this.currentPath, this.deltaBuf);
2750
+ this.deltaBuf = "";
2751
+ }
2752
+ this.state = "expect-comma-close";
2753
+ } else {
2754
+ const top = this.stack[this.stack.length - 1];
2755
+ if (!top || top.type !== "object") {
2756
+ this.state = "done";
2757
+ return;
2758
+ }
2759
+ top.currentKey = this.keyBuf;
2760
+ this.keyBuf = "";
2761
+ this.state = "expect-colon";
2762
+ }
2763
+ return;
2764
+ }
2765
+ this.appendChar(ch, isValue);
2766
+ }
2767
+ appendChar(c, isValue) {
2768
+ if (isValue) {
2769
+ this.deltaBuf += c;
2770
+ } else {
2771
+ this.keyBuf += c;
2772
+ }
2773
+ }
2774
+ // ── Frame stack ─────────────────────────────────────────────────────────
2775
+ /** Push a frame onto the stack. Returns false if the stack is at the
2776
+ * depth cap — in which case the walker is also moved to `done` so the
2777
+ * caller's subsequent state assignment must be guarded. */
2778
+ pushFrame(frame) {
2779
+ if (this.stack.length >= MAX_DEPTH2) {
2780
+ this.state = "done";
2781
+ return false;
2782
+ }
2783
+ this.stack.push(frame);
2784
+ return true;
2785
+ }
2786
+ /** Pop a frame on `}` or `]`, fire boundary, transition to next state.
2787
+ * Defensive on empty stack — the state machine never invokes this
2788
+ * without an enclosing container today (callers gate on state, and
2789
+ * state transitions out of `pre-json` / `done` only after pushing),
2790
+ * but a future state-machine bug could land us here with nothing to
2791
+ * pop. Fail closed (move to `done`, no spurious boundary fire). */
2792
+ popFrame() {
2793
+ if (this.stack.length === 0) {
2794
+ this.state = "done";
2795
+ return;
2796
+ }
2797
+ this.stack.pop();
2798
+ this.boundaryPending = true;
2799
+ if (this.stack.length === 0) {
2800
+ this.state = "done";
2801
+ } else {
2802
+ this.state = "expect-comma-close";
2803
+ }
2804
+ }
2805
+ };
2806
+ function isWhitespace(ch) {
2807
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
2808
+ }
2809
+ function isHex(ch) {
2810
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
2811
+ }
2812
+ function isValueStartChar(ch) {
2813
+ return ch === "-" || ch >= "0" && ch <= "9" || ch === "t" || ch === "f" || ch === "n";
2814
+ }
2815
+
2507
2816
  // src/event-utils.ts
2508
2817
  var COST_BEARING_LEAF_TYPES = [
2509
2818
  "agent_call_end",
@@ -2560,6 +2869,12 @@ var AXL_EVENT_TYPES = [
2560
2869
  "pipeline",
2561
2870
  // Progressive structured output — added in PR 2; reserved here.
2562
2871
  "partial_object",
2872
+ // Character-level deltas inside string values of progressive structured
2873
+ // output. Same gating as `partial_object` (schema set, no tools, root is
2874
+ // ZodObject); fires per text-delta chunk when the walker is inside a
2875
+ // string value at a known JSON Pointer path. Designed for chat-style
2876
+ // typewriter rendering of long string fields.
2877
+ "string_delta",
2563
2878
  // Verification
2564
2879
  "verify",
2565
2880
  // Observability
@@ -2657,6 +2972,27 @@ var AxlEventBus = class {
2657
2972
  * number of distinct askIds that emitted at least one partial.
2658
2973
  */
2659
2974
  latestPartialByAsk = /* @__PURE__ */ new Map();
2975
+ /**
2976
+ * Per-ask, per-path running accumulator for `string_delta` events,
2977
+ * updated on every `_push(string_delta)` regardless of consumer state.
2978
+ * Used to seed late `stringStream` subscribers with the current text-
2979
+ * so-far per (askId, path) pair, so a UI binding to `/summary` mid-
2980
+ * stream renders the field's current state on first event instead of
2981
+ * waiting for the next chunk.
2982
+ *
2983
+ * Cleared per-askId on `pipeline(failed)` (discarded attempt — old
2984
+ * deltas must not leak into the next attempt's view) and on `ask_end`
2985
+ * (frees memory; stream-event accumulators don't outlive the ask).
2986
+ *
2987
+ * Memory bound: O(active asks × distinct paths × total chars per
2988
+ * field). For typical chat workflows (1 ask, 1-3 streaming string
2989
+ * fields) this is a few KB. The clear-on-`ask_end` cap keeps a
2990
+ * long-running runtime from accumulating per-ask entries forever
2991
+ * (the way `latestPartialByAsk` does — partial snapshots are tiny by
2992
+ * comparison so that map's growth is acceptable; string accumulators
2993
+ * can be 4 KB+ each, hence the more aggressive cleanup here).
2994
+ */
2995
+ stringStreamByAsk = /* @__PURE__ */ new Map();
2660
2996
  /**
2661
2997
  * Hook fired when an iterator obtained from `[Symbol.asyncIterator]`
2662
2998
  * is `await using`-disposed. Set via the second constructor argument.
@@ -2960,6 +3296,141 @@ var AxlEventBus = class {
2960
3296
  }
2961
3297
  };
2962
3298
  }
3299
+ /**
3300
+ * Listener-based view over `string_delta` events with optional
3301
+ * filtering by `path` and/or `askId`. Yields `StringStreamEvent`s
3302
+ * carrying `delta` (the new chars) and `accumulated` (the full text-
3303
+ * so-far for that ask + path).
3304
+ *
3305
+ * Designed for chat-style typewriter rendering of long string fields:
3306
+ * a React component binding to `path: '/summary'` sets its text to
3307
+ * `event.accumulated` on every yield, getting smooth char-by-char
3308
+ * progression with no parser of its own.
3309
+ *
3310
+ * Late-subscriber seeding: at subscribe time, the bus's per-ask
3311
+ * accumulator is walked and one synthetic `StringStreamEvent` is
3312
+ * yielded per matching (askId, path) entry, with `delta ===
3313
+ * accumulated === <full text-so-far>`. After that, live deltas flow
3314
+ * normally. A consumer who attaches mid-ask sees the field's current
3315
+ * state on first iteration, then incremental updates.
3316
+ *
3317
+ * Like `partialObjects`, this view is listener-based — it does NOT
3318
+ * race the main async iterator (`for await (const e of bus)`).
3319
+ * Running both concurrently each receives every matching event.
3320
+ *
3321
+ * Schema-retry awareness: on `pipeline(status: 'failed')`, the
3322
+ * accumulator and any pending events for the affected ask are
3323
+ * dropped, so a consumer iterating during attempt-N never observes
3324
+ * stale text after attempt-N+1 begins. Mirrors `partialObjects`'
3325
+ * per-attempt reset.
3326
+ *
3327
+ * Termination: yields any pending events first (live + seeded), then
3328
+ * `done: true` once the bus is finished via `_finish()`.
3329
+ */
3330
+ stringStream(opts) {
3331
+ const self = this;
3332
+ const filterPath = opts?.path;
3333
+ const filterAskId = opts?.askId;
3334
+ if (filterPath !== void 0 && filterPath !== "" && !filterPath.startsWith("/")) {
3335
+ throw new Error(
3336
+ `AxlEventBus.stringStream({ path }) \u2014 path must start with '/' (RFC 6901 JSON Pointer); got ${JSON.stringify(filterPath)}. Examples: '/summary', '/sources/0/title'.`
3337
+ );
3338
+ }
3339
+ return {
3340
+ [Symbol.asyncIterator]() {
3341
+ const pending = [];
3342
+ let resolveWaiter = null;
3343
+ let finished = self.finished;
3344
+ let unsubscribed = false;
3345
+ const wakeWaiter = () => {
3346
+ const r = resolveWaiter;
3347
+ resolveWaiter = null;
3348
+ r?.();
3349
+ };
3350
+ const matchesFilter = (askId, path) => {
3351
+ if (filterAskId !== void 0 && askId !== filterAskId) return false;
3352
+ if (filterPath !== void 0 && path !== filterPath) return false;
3353
+ return true;
3354
+ };
3355
+ for (const [askId, paths] of self.stringStreamByAsk) {
3356
+ for (const [path, acc] of paths) {
3357
+ if (!matchesFilter(askId, path)) continue;
3358
+ pending.push({
3359
+ askId,
3360
+ agent: acc.agent,
3361
+ path,
3362
+ delta: acc.text,
3363
+ accumulated: acc.text,
3364
+ attempt: acc.attempt
3365
+ });
3366
+ }
3367
+ }
3368
+ const handler = (event) => {
3369
+ if (event.type !== "string_delta") return;
3370
+ const askId = event.askId ?? "";
3371
+ if (!matchesFilter(askId, event.data.path)) return;
3372
+ const paths = self.stringStreamByAsk.get(askId);
3373
+ const acc = paths?.get(event.data.path);
3374
+ const accumulated = acc?.text ?? event.data.delta;
3375
+ pending.push({
3376
+ askId,
3377
+ agent: event.agent,
3378
+ path: event.data.path,
3379
+ delta: event.data.delta,
3380
+ accumulated,
3381
+ attempt: event.attempt
3382
+ });
3383
+ wakeWaiter();
3384
+ };
3385
+ self.bus.on("string_delta", handler);
3386
+ const pipelineHandler = (event) => {
3387
+ if (event.type !== "pipeline") return;
3388
+ if (event.status !== "failed") return;
3389
+ const askId = event.askId ?? "";
3390
+ for (let i = pending.length - 1; i >= 0; i--) {
3391
+ if (pending[i].askId === askId) pending.splice(i, 1);
3392
+ }
3393
+ };
3394
+ self.bus.on("pipeline", pipelineHandler);
3395
+ const unsubscribe = () => {
3396
+ if (unsubscribed) return;
3397
+ unsubscribed = true;
3398
+ self.bus.off("string_delta", handler);
3399
+ self.bus.off("pipeline", pipelineHandler);
3400
+ unsubscribeFinish();
3401
+ };
3402
+ const unsubscribeFinish = self._onFinish(() => {
3403
+ finished = true;
3404
+ self.bus.off("string_delta", handler);
3405
+ self.bus.off("pipeline", pipelineHandler);
3406
+ wakeWaiter();
3407
+ });
3408
+ return {
3409
+ async next() {
3410
+ while (true) {
3411
+ if (pending.length > 0) {
3412
+ return { value: pending.shift(), done: false };
3413
+ }
3414
+ if (finished) {
3415
+ unsubscribe();
3416
+ return { value: void 0, done: true };
3417
+ }
3418
+ await new Promise((res) => {
3419
+ resolveWaiter = res;
3420
+ });
3421
+ }
3422
+ },
3423
+ async return() {
3424
+ unsubscribe();
3425
+ return { value: void 0, done: true };
3426
+ },
3427
+ [Symbol.asyncIterator]() {
3428
+ return this;
3429
+ }
3430
+ };
3431
+ }
3432
+ };
3433
+ }
2963
3434
  /** Iterator over structural lifecycle events only — skips per-token chatter
2964
3435
  * and progressive `partial_object` emissions. Useful for waterfall UIs
2965
3436
  * and any consumer that wants the "what happened" timeline. */
@@ -2996,14 +3467,6 @@ var AxlEventBus = class {
2996
3467
  */
2997
3468
  _push(event) {
2998
3469
  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
3470
  if (event.type === "partial_object" && event.data?.object !== void 0) {
3008
3471
  const askId = event.askId ?? "";
3009
3472
  this.latestPartialByAsk.set(askId, {
@@ -3012,8 +3475,33 @@ var AxlEventBus = class {
3012
3475
  object: event.data.object,
3013
3476
  attempt: event.attempt
3014
3477
  });
3478
+ } else if (event.type === "string_delta") {
3479
+ const askId = event.askId ?? "";
3480
+ let paths = this.stringStreamByAsk.get(askId);
3481
+ if (!paths) {
3482
+ paths = /* @__PURE__ */ new Map();
3483
+ this.stringStreamByAsk.set(askId, paths);
3484
+ }
3485
+ const existing = paths.get(event.data.path);
3486
+ paths.set(event.data.path, {
3487
+ agent: event.agent,
3488
+ text: (existing?.text ?? "") + event.data.delta,
3489
+ attempt: event.attempt
3490
+ });
3015
3491
  } else if (event.type === "pipeline" && event.status === "failed") {
3016
- this.latestPartialByAsk.delete(event.askId ?? "");
3492
+ const askId = event.askId ?? "";
3493
+ this.latestPartialByAsk.delete(askId);
3494
+ this.stringStreamByAsk.delete(askId);
3495
+ } else if (event.type === "ask_end") {
3496
+ this.stringStreamByAsk.delete(event.askId ?? "");
3497
+ }
3498
+ try {
3499
+ this.bus.emit(event.type, event);
3500
+ } catch (err) {
3501
+ console.error(
3502
+ `[axl] AxlEventBus listener for "${event.type}" threw; ignoring:`,
3503
+ err instanceof Error ? err.message : String(err)
3504
+ );
3017
3505
  }
3018
3506
  const waiter = this.waiters.shift();
3019
3507
  if (waiter) {
@@ -3231,6 +3719,9 @@ var REDACTION_RULES = {
3231
3719
  handoff_return: passthrough,
3232
3720
  pipeline: (e) => e.status === "failed" ? { ...e, reason: REDACTED } : e,
3233
3721
  partial_object: (e) => ({ ...e, data: { ...e.data, object: REDACTED } }),
3722
+ // `path` is structural (schema-shape, no PII); `delta` is user content.
3723
+ // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3724
+ string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3234
3725
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
3235
3726
  log: (e) => {
3236
3727
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
@@ -4074,36 +4565,26 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
4074
4565
  const toolCallBuffers = /* @__PURE__ */ new Map();
4075
4566
  let streamProviderMetadata;
4076
4567
  let thinkingContent = "";
4077
- const partialObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod.z.ZodObject;
4568
+ const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof import_zod.z.ZodObject;
4078
4569
  const currentAttempt = schemaRetries + 1;
4079
- let inString = false;
4080
- let escaped = false;
4081
- let boundaryPending = false;
4570
+ const walker = streamingObjectEnabled ? new StreamingWalker({
4571
+ onStringDelta: (path, delta) => {
4572
+ this.emitEvent({
4573
+ type: "string_delta",
4574
+ agent: agent2._name,
4575
+ attempt: currentAttempt,
4576
+ data: { path, delta }
4577
+ });
4578
+ }
4579
+ }) : void 0;
4082
4580
  for await (const chunk of provider.stream(currentMessages, chatOptions)) {
4083
4581
  if (chunk.type === "text_delta") {
4084
4582
  content2 += chunk.content;
4085
4583
  this.emitEvent({ type: "token", data: chunk.content });
4086
4584
  this.onToken?.(chunk.content, callbackMeta);
4087
- if (partialObjectEnabled) {
4088
- for (const ch of chunk.content) {
4089
- if (escaped) {
4090
- escaped = false;
4091
- continue;
4092
- }
4093
- if (ch === "\\") {
4094
- escaped = true;
4095
- continue;
4096
- }
4097
- if (ch === '"') {
4098
- inString = !inString;
4099
- continue;
4100
- }
4101
- if (!inString && (ch === "," || ch === "}" || ch === "]")) {
4102
- boundaryPending = true;
4103
- }
4104
- }
4105
- if (boundaryPending) {
4106
- boundaryPending = false;
4585
+ if (walker) {
4586
+ walker.processChunk(chunk.content);
4587
+ if (walker.consumeBoundary()) {
4107
4588
  let parsed;
4108
4589
  try {
4109
4590
  parsed = parsePartialJson(extractJson(content2));
@@ -6912,6 +7393,20 @@ var AxlStream = class extends import_node_stream.Readable {
6912
7393
  get partialObjects() {
6913
7394
  return this.events.partialObjects;
6914
7395
  }
7396
+ /** Listener-based view over `string_delta` events, with optional filter
7397
+ * on `path` and/or `askId`. Yields `StringStreamEvent`s carrying the new
7398
+ * `delta` and full text-so-far `accumulated`. Designed for chat-style
7399
+ * typewriter rendering of long string fields — bind a UI component to
7400
+ * one path and set its text to `event.accumulated` per yield.
7401
+ *
7402
+ * Late subscribers receive a synthetic event seeded from the bus's
7403
+ * per-ask accumulator (so the field's current state renders on the
7404
+ * first iteration, not after the next char arrives). Doesn't race the
7405
+ * main async iterator. See `AxlEventBus.stringStream` for full
7406
+ * semantics. */
7407
+ stringStream(opts) {
7408
+ return this.events.stringStream(opts);
7409
+ }
6915
7410
  pipe(destination, options) {
6916
7411
  const shouldEnd = options?.end !== false;
6917
7412
  this.events.on("token", (event) => {
@@ -7567,7 +8062,6 @@ var NoopSpanManager = class {
7567
8062
  };
7568
8063
 
7569
8064
  // src/telemetry/index.ts
7570
- init_span_manager();
7571
8065
  async function createSpanManager(config) {
7572
8066
  if (!config?.enabled) {
7573
8067
  return new NoopSpanManager();
@@ -7653,6 +8147,10 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7653
8147
  * the instance so per-event onTrace handlers don't reach into
7654
8148
  * `this.config.state` on every emission. */
7655
8149
  maxEventsPerExecution;
8150
+ /** Tracks executionIds for which we've already warned about a malformed
8151
+ * `events` field — keeps the log to one line per row instead of one per
8152
+ * rebuild tick. */
8153
+ warnedMalformedExecutions = /* @__PURE__ */ new Set();
7656
8154
  constructor(config) {
7657
8155
  super();
7658
8156
  this.config = resolveConfig(config ?? {});
@@ -7946,12 +8444,31 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
7946
8444
  return result;
7947
8445
  }
7948
8446
  /** Get all execution info (running + completed + historical). */
8447
+ /** Normalize an `ExecutionInfo` loaded from a `StateStore` so the
8448
+ * `events: AxlEvent[]` type contract holds for downstream consumers
8449
+ * (Studio aggregators, REST routes, redaction). Custom or legacy stores
8450
+ * may persist rows without `events` (or with a non-array value) — without
8451
+ * this guard, a single malformed row crashes every iterator over
8452
+ * `exec.events` and takes down dependent features. Mutates the row
8453
+ * in place; logs at most one warning per `executionId`. */
8454
+ normalizeStoredExecution(exec) {
8455
+ if (!Array.isArray(exec.events)) {
8456
+ if (!this.warnedMalformedExecutions.has(exec.executionId)) {
8457
+ this.warnedMalformedExecutions.add(exec.executionId);
8458
+ console.warn(
8459
+ `[axl] StateStore returned execution ${exec.executionId} with non-array events (got ${exec.events === null ? "null" : typeof exec.events}); coercing to []`
8460
+ );
8461
+ }
8462
+ exec.events = [];
8463
+ }
8464
+ return exec;
8465
+ }
7949
8466
  async getExecutions() {
7950
8467
  if (!this.historicalExecutionsLoadPromise && this.stateStore.listExecutions) {
7951
8468
  this.historicalExecutionsLoadPromise = this.stateStore.listExecutions().then((stored) => {
7952
8469
  for (const exec of stored) {
7953
8470
  if (!this.executions.has(exec.executionId) && !this.historicalExecutions.has(exec.executionId)) {
7954
- this.historicalExecutions.set(exec.executionId, exec);
8471
+ this.historicalExecutions.set(exec.executionId, this.normalizeStoredExecution(exec));
7955
8472
  }
7956
8473
  }
7957
8474
  }).catch(() => {
@@ -8322,8 +8839,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
8322
8839
  if (this.stateStore.getExecution) {
8323
8840
  const stored = await this.stateStore.getExecution(executionId);
8324
8841
  if (stored) {
8325
- this.historicalExecutions.set(executionId, stored);
8326
- return stored;
8842
+ const normalized = this.normalizeStoredExecution(stored);
8843
+ this.historicalExecutions.set(executionId, normalized);
8844
+ return normalized;
8327
8845
  }
8328
8846
  }
8329
8847
  return void 0;
@@ -8571,7 +9089,9 @@ var AxlRuntime = class extends import_node_events2.EventEmitter {
8571
9089
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
8572
9090
  }
8573
9091
  if (capturedTraces) {
8574
- if (event.type === "token" || event.type === "partial_object") return;
9092
+ if (event.type === "token" || event.type === "partial_object" || event.type === "string_delta") {
9093
+ return;
9094
+ }
8575
9095
  if (event.type === "agent_call_start" && event.data) {
8576
9096
  const d = event.data;
8577
9097
  if ("messages" in d) {
@@ -8895,6 +9415,49 @@ async function registerConditions(conditions) {
8895
9415
  }
8896
9416
  }
8897
9417
 
9418
+ // src/string-stream-from-events.ts
9419
+ async function* stringStreamFromEvents(source, opts) {
9420
+ const filterPath = opts?.path;
9421
+ const filterAskId = opts?.askId;
9422
+ if (filterPath !== void 0 && filterPath !== "" && !filterPath.startsWith("/")) {
9423
+ throw new Error(
9424
+ `stringStreamFromEvents({ path }) \u2014 path must start with '/' (RFC 6901 JSON Pointer); got ${JSON.stringify(filterPath)}. Examples: '/summary', '/sources/0/title'.`
9425
+ );
9426
+ }
9427
+ const acc = /* @__PURE__ */ new Map();
9428
+ for await (const event of source) {
9429
+ if (event.type === "string_delta") {
9430
+ const askId = event.askId ?? "";
9431
+ let paths = acc.get(askId);
9432
+ if (!paths) {
9433
+ paths = /* @__PURE__ */ new Map();
9434
+ acc.set(askId, paths);
9435
+ }
9436
+ const existing = paths.get(event.data.path);
9437
+ const accumulated = (existing?.text ?? "") + event.data.delta;
9438
+ paths.set(event.data.path, {
9439
+ agent: event.agent,
9440
+ text: accumulated,
9441
+ attempt: event.attempt
9442
+ });
9443
+ if (filterAskId !== void 0 && askId !== filterAskId) continue;
9444
+ if (filterPath !== void 0 && event.data.path !== filterPath) continue;
9445
+ yield {
9446
+ askId,
9447
+ agent: event.agent,
9448
+ path: event.data.path,
9449
+ delta: event.data.delta,
9450
+ accumulated,
9451
+ attempt: event.attempt
9452
+ };
9453
+ } else if (event.type === "pipeline" && event.status === "failed") {
9454
+ acc.delete(event.askId ?? "");
9455
+ } else if (event.type === "ask_end") {
9456
+ acc.delete(event.askId ?? "");
9457
+ }
9458
+ }
9459
+ }
9460
+
8898
9461
  // src/state/redis.ts
8899
9462
  var RedisStore = class _RedisStore {
8900
9463
  constructor(client) {
@@ -9300,6 +9863,7 @@ function cosineSimilarity2(a, b) {
9300
9863
  registerConditions,
9301
9864
  resolveRuntime,
9302
9865
  resolveThinkingOptions,
9866
+ stringStreamFromEvents,
9303
9867
  tool,
9304
9868
  workflow,
9305
9869
  zodToJsonSchema