@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.js CHANGED
@@ -2337,6 +2337,314 @@ var PartialParser = class {
2337
2337
  }
2338
2338
  };
2339
2339
 
2340
+ // src/streaming-walker.ts
2341
+ var MAX_DEPTH2 = 256;
2342
+ function encodeSegment(s) {
2343
+ if (typeof s === "number") return String(s);
2344
+ return s.replace(/~/g, "~0").replace(/\//g, "~1");
2345
+ }
2346
+ function computePath(stack) {
2347
+ let path = "";
2348
+ for (const frame of stack) {
2349
+ if (frame.type === "object") {
2350
+ path += "/" + encodeSegment(frame.currentKey ?? "");
2351
+ } else {
2352
+ path += "/" + encodeSegment(frame.currentIndex);
2353
+ }
2354
+ }
2355
+ return path;
2356
+ }
2357
+ var StreamingWalker = class {
2358
+ constructor(callbacks) {
2359
+ this.callbacks = callbacks;
2360
+ }
2361
+ state = "pre-json";
2362
+ stack = [];
2363
+ // String parsing state — survives across chunks so escapes can split.
2364
+ escaped = false;
2365
+ /** When > 0, we're collecting `\\uXXXX` hex digits. 4→0 as digits arrive. */
2366
+ unicodePending = 0;
2367
+ /** Accumulator for `\\uXXXX` digits. Cleared when the escape resolves. */
2368
+ unicodeBuf = "";
2369
+ // Per-string accumulators.
2370
+ keyBuf = "";
2371
+ deltaBuf = "";
2372
+ /** Cached JSON Pointer to the value currently being read. Recomputed
2373
+ * on entry to `read-value-string` (the only state that needs it). */
2374
+ currentPath = "";
2375
+ /** Set true when a structural boundary lands. Caller reads via
2376
+ * `consumeBoundary()` after each chunk to decide on partial_object emit. */
2377
+ boundaryPending = false;
2378
+ /** Walk `text` char-by-char, advancing state and accumulating deltas.
2379
+ * Calls `onStringDelta` at end-of-chunk for any pending value text. */
2380
+ processChunk(text) {
2381
+ for (let i = 0; i < text.length; i++) {
2382
+ if (this.state === "done") return;
2383
+ this.processChar(text[i]);
2384
+ }
2385
+ if (this.deltaBuf.length > 0 && this.state === "read-value-string") {
2386
+ this.callbacks.onStringDelta?.(this.currentPath, this.deltaBuf);
2387
+ this.deltaBuf = "";
2388
+ }
2389
+ }
2390
+ /** Read-and-clear the structural-boundary flag. Caller checks this
2391
+ * after `processChunk` to decide whether to emit `partial_object`. */
2392
+ consumeBoundary() {
2393
+ const v = this.boundaryPending;
2394
+ this.boundaryPending = false;
2395
+ return v;
2396
+ }
2397
+ /** True when the walker has decided it's done with this stream
2398
+ * (root closed or fatal malformed input). Caller may use this to
2399
+ * short-circuit further work. */
2400
+ get isDone() {
2401
+ return this.state === "done";
2402
+ }
2403
+ // ── Per-char dispatch ───────────────────────────────────────────────────
2404
+ processChar(ch) {
2405
+ switch (this.state) {
2406
+ case "pre-json":
2407
+ if (ch === "{") {
2408
+ if (!this.pushFrame({ type: "object" })) return;
2409
+ this.state = "expect-key-or-close";
2410
+ }
2411
+ return;
2412
+ case "expect-key-or-close":
2413
+ if (isWhitespace(ch)) return;
2414
+ if (ch === "}") {
2415
+ this.popFrame();
2416
+ return;
2417
+ }
2418
+ if (ch === '"') {
2419
+ this.keyBuf = "";
2420
+ this.escaped = false;
2421
+ this.unicodePending = 0;
2422
+ this.unicodeBuf = "";
2423
+ this.state = "read-key";
2424
+ return;
2425
+ }
2426
+ this.state = "done";
2427
+ return;
2428
+ case "read-key":
2429
+ this.processStringChar(
2430
+ ch,
2431
+ /* isValue */
2432
+ false
2433
+ );
2434
+ return;
2435
+ case "expect-colon":
2436
+ if (isWhitespace(ch)) return;
2437
+ if (ch === ":") {
2438
+ this.state = "expect-value";
2439
+ return;
2440
+ }
2441
+ this.state = "done";
2442
+ return;
2443
+ case "expect-value":
2444
+ if (isWhitespace(ch)) return;
2445
+ if (ch === "{") {
2446
+ if (!this.pushFrame({ type: "object" })) return;
2447
+ this.state = "expect-key-or-close";
2448
+ return;
2449
+ }
2450
+ if (ch === "[") {
2451
+ if (!this.pushFrame({ type: "array", currentIndex: 0 })) return;
2452
+ this.state = "expect-value";
2453
+ return;
2454
+ }
2455
+ if (ch === "]") {
2456
+ this.popFrame();
2457
+ return;
2458
+ }
2459
+ if (ch === '"') {
2460
+ this.currentPath = computePath(this.stack);
2461
+ this.deltaBuf = "";
2462
+ this.escaped = false;
2463
+ this.unicodePending = 0;
2464
+ this.unicodeBuf = "";
2465
+ this.state = "read-value-string";
2466
+ return;
2467
+ }
2468
+ if (isValueStartChar(ch)) {
2469
+ this.state = "read-value-other";
2470
+ return;
2471
+ }
2472
+ this.state = "done";
2473
+ return;
2474
+ case "read-value-string":
2475
+ this.processStringChar(
2476
+ ch,
2477
+ /* isValue */
2478
+ true
2479
+ );
2480
+ return;
2481
+ case "read-value-other":
2482
+ if (ch === "," || ch === "}" || ch === "]" || isWhitespace(ch)) {
2483
+ this.state = "expect-comma-close";
2484
+ this.processChar(ch);
2485
+ return;
2486
+ }
2487
+ return;
2488
+ case "expect-comma-close":
2489
+ if (isWhitespace(ch)) return;
2490
+ if (ch === ",") {
2491
+ this.boundaryPending = true;
2492
+ const top = this.stack[this.stack.length - 1];
2493
+ if (!top) {
2494
+ this.state = "done";
2495
+ return;
2496
+ }
2497
+ if (top.type === "object") {
2498
+ top.currentKey = void 0;
2499
+ this.state = "expect-key-or-close";
2500
+ } else {
2501
+ top.currentIndex += 1;
2502
+ this.state = "expect-value";
2503
+ }
2504
+ return;
2505
+ }
2506
+ if (ch === "}" || ch === "]") {
2507
+ this.popFrame();
2508
+ return;
2509
+ }
2510
+ this.state = "done";
2511
+ return;
2512
+ case "done":
2513
+ return;
2514
+ }
2515
+ }
2516
+ // ── String state machine (shared between key and value reads) ───────────
2517
+ /**
2518
+ * One char inside a string. Translates JSON escapes, accumulates
2519
+ * `\\uXXXX` digits across chunks, and routes the resolved char to
2520
+ * either `keyBuf` (when reading a key) or `deltaBuf` (when reading a
2521
+ * value).
2522
+ */
2523
+ processStringChar(ch, isValue) {
2524
+ if (this.unicodePending > 0) {
2525
+ if (isHex(ch)) {
2526
+ this.unicodeBuf += ch;
2527
+ this.unicodePending -= 1;
2528
+ if (this.unicodePending === 0) {
2529
+ const code = parseInt(this.unicodeBuf, 16);
2530
+ this.unicodeBuf = "";
2531
+ this.appendChar(String.fromCharCode(code), isValue);
2532
+ }
2533
+ } else {
2534
+ this.state = "done";
2535
+ }
2536
+ return;
2537
+ }
2538
+ if (this.escaped) {
2539
+ this.escaped = false;
2540
+ switch (ch) {
2541
+ case '"':
2542
+ this.appendChar('"', isValue);
2543
+ return;
2544
+ case "\\":
2545
+ this.appendChar("\\", isValue);
2546
+ return;
2547
+ case "/":
2548
+ this.appendChar("/", isValue);
2549
+ return;
2550
+ case "b":
2551
+ this.appendChar("\b", isValue);
2552
+ return;
2553
+ case "f":
2554
+ this.appendChar("\f", isValue);
2555
+ return;
2556
+ case "n":
2557
+ this.appendChar("\n", isValue);
2558
+ return;
2559
+ case "r":
2560
+ this.appendChar("\r", isValue);
2561
+ return;
2562
+ case "t":
2563
+ this.appendChar(" ", isValue);
2564
+ return;
2565
+ case "u":
2566
+ this.unicodePending = 4;
2567
+ this.unicodeBuf = "";
2568
+ return;
2569
+ default:
2570
+ this.state = "done";
2571
+ return;
2572
+ }
2573
+ }
2574
+ if (ch === "\\") {
2575
+ this.escaped = true;
2576
+ return;
2577
+ }
2578
+ if (ch === '"') {
2579
+ if (isValue) {
2580
+ if (this.deltaBuf.length > 0) {
2581
+ this.callbacks.onStringDelta?.(this.currentPath, this.deltaBuf);
2582
+ this.deltaBuf = "";
2583
+ }
2584
+ this.state = "expect-comma-close";
2585
+ } else {
2586
+ const top = this.stack[this.stack.length - 1];
2587
+ if (!top || top.type !== "object") {
2588
+ this.state = "done";
2589
+ return;
2590
+ }
2591
+ top.currentKey = this.keyBuf;
2592
+ this.keyBuf = "";
2593
+ this.state = "expect-colon";
2594
+ }
2595
+ return;
2596
+ }
2597
+ this.appendChar(ch, isValue);
2598
+ }
2599
+ appendChar(c, isValue) {
2600
+ if (isValue) {
2601
+ this.deltaBuf += c;
2602
+ } else {
2603
+ this.keyBuf += c;
2604
+ }
2605
+ }
2606
+ // ── Frame stack ─────────────────────────────────────────────────────────
2607
+ /** Push a frame onto the stack. Returns false if the stack is at the
2608
+ * depth cap — in which case the walker is also moved to `done` so the
2609
+ * caller's subsequent state assignment must be guarded. */
2610
+ pushFrame(frame) {
2611
+ if (this.stack.length >= MAX_DEPTH2) {
2612
+ this.state = "done";
2613
+ return false;
2614
+ }
2615
+ this.stack.push(frame);
2616
+ return true;
2617
+ }
2618
+ /** Pop a frame on `}` or `]`, fire boundary, transition to next state.
2619
+ * Defensive on empty stack — the state machine never invokes this
2620
+ * without an enclosing container today (callers gate on state, and
2621
+ * state transitions out of `pre-json` / `done` only after pushing),
2622
+ * but a future state-machine bug could land us here with nothing to
2623
+ * pop. Fail closed (move to `done`, no spurious boundary fire). */
2624
+ popFrame() {
2625
+ if (this.stack.length === 0) {
2626
+ this.state = "done";
2627
+ return;
2628
+ }
2629
+ this.stack.pop();
2630
+ this.boundaryPending = true;
2631
+ if (this.stack.length === 0) {
2632
+ this.state = "done";
2633
+ } else {
2634
+ this.state = "expect-comma-close";
2635
+ }
2636
+ }
2637
+ };
2638
+ function isWhitespace(ch) {
2639
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r";
2640
+ }
2641
+ function isHex(ch) {
2642
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
2643
+ }
2644
+ function isValueStartChar(ch) {
2645
+ return ch === "-" || ch >= "0" && ch <= "9" || ch === "t" || ch === "f" || ch === "n";
2646
+ }
2647
+
2340
2648
  // src/event-utils.ts
2341
2649
  var COST_BEARING_LEAF_TYPES = [
2342
2650
  "agent_call_end",
@@ -2393,6 +2701,12 @@ var AXL_EVENT_TYPES = [
2393
2701
  "pipeline",
2394
2702
  // Progressive structured output — added in PR 2; reserved here.
2395
2703
  "partial_object",
2704
+ // Character-level deltas inside string values of progressive structured
2705
+ // output. Same gating as `partial_object` (schema set, no tools, root is
2706
+ // ZodObject); fires per text-delta chunk when the walker is inside a
2707
+ // string value at a known JSON Pointer path. Designed for chat-style
2708
+ // typewriter rendering of long string fields.
2709
+ "string_delta",
2396
2710
  // Verification
2397
2711
  "verify",
2398
2712
  // Observability
@@ -2490,6 +2804,27 @@ var AxlEventBus = class {
2490
2804
  * number of distinct askIds that emitted at least one partial.
2491
2805
  */
2492
2806
  latestPartialByAsk = /* @__PURE__ */ new Map();
2807
+ /**
2808
+ * Per-ask, per-path running accumulator for `string_delta` events,
2809
+ * updated on every `_push(string_delta)` regardless of consumer state.
2810
+ * Used to seed late `stringStream` subscribers with the current text-
2811
+ * so-far per (askId, path) pair, so a UI binding to `/summary` mid-
2812
+ * stream renders the field's current state on first event instead of
2813
+ * waiting for the next chunk.
2814
+ *
2815
+ * Cleared per-askId on `pipeline(failed)` (discarded attempt — old
2816
+ * deltas must not leak into the next attempt's view) and on `ask_end`
2817
+ * (frees memory; stream-event accumulators don't outlive the ask).
2818
+ *
2819
+ * Memory bound: O(active asks × distinct paths × total chars per
2820
+ * field). For typical chat workflows (1 ask, 1-3 streaming string
2821
+ * fields) this is a few KB. The clear-on-`ask_end` cap keeps a
2822
+ * long-running runtime from accumulating per-ask entries forever
2823
+ * (the way `latestPartialByAsk` does — partial snapshots are tiny by
2824
+ * comparison so that map's growth is acceptable; string accumulators
2825
+ * can be 4 KB+ each, hence the more aggressive cleanup here).
2826
+ */
2827
+ stringStreamByAsk = /* @__PURE__ */ new Map();
2493
2828
  /**
2494
2829
  * Hook fired when an iterator obtained from `[Symbol.asyncIterator]`
2495
2830
  * is `await using`-disposed. Set via the second constructor argument.
@@ -2793,6 +3128,141 @@ var AxlEventBus = class {
2793
3128
  }
2794
3129
  };
2795
3130
  }
3131
+ /**
3132
+ * Listener-based view over `string_delta` events with optional
3133
+ * filtering by `path` and/or `askId`. Yields `StringStreamEvent`s
3134
+ * carrying `delta` (the new chars) and `accumulated` (the full text-
3135
+ * so-far for that ask + path).
3136
+ *
3137
+ * Designed for chat-style typewriter rendering of long string fields:
3138
+ * a React component binding to `path: '/summary'` sets its text to
3139
+ * `event.accumulated` on every yield, getting smooth char-by-char
3140
+ * progression with no parser of its own.
3141
+ *
3142
+ * Late-subscriber seeding: at subscribe time, the bus's per-ask
3143
+ * accumulator is walked and one synthetic `StringStreamEvent` is
3144
+ * yielded per matching (askId, path) entry, with `delta ===
3145
+ * accumulated === <full text-so-far>`. After that, live deltas flow
3146
+ * normally. A consumer who attaches mid-ask sees the field's current
3147
+ * state on first iteration, then incremental updates.
3148
+ *
3149
+ * Like `partialObjects`, this view is listener-based — it does NOT
3150
+ * race the main async iterator (`for await (const e of bus)`).
3151
+ * Running both concurrently each receives every matching event.
3152
+ *
3153
+ * Schema-retry awareness: on `pipeline(status: 'failed')`, the
3154
+ * accumulator and any pending events for the affected ask are
3155
+ * dropped, so a consumer iterating during attempt-N never observes
3156
+ * stale text after attempt-N+1 begins. Mirrors `partialObjects`'
3157
+ * per-attempt reset.
3158
+ *
3159
+ * Termination: yields any pending events first (live + seeded), then
3160
+ * `done: true` once the bus is finished via `_finish()`.
3161
+ */
3162
+ stringStream(opts) {
3163
+ const self = this;
3164
+ const filterPath = opts?.path;
3165
+ const filterAskId = opts?.askId;
3166
+ if (filterPath !== void 0 && filterPath !== "" && !filterPath.startsWith("/")) {
3167
+ throw new Error(
3168
+ `AxlEventBus.stringStream({ path }) \u2014 path must start with '/' (RFC 6901 JSON Pointer); got ${JSON.stringify(filterPath)}. Examples: '/summary', '/sources/0/title'.`
3169
+ );
3170
+ }
3171
+ return {
3172
+ [Symbol.asyncIterator]() {
3173
+ const pending = [];
3174
+ let resolveWaiter = null;
3175
+ let finished = self.finished;
3176
+ let unsubscribed = false;
3177
+ const wakeWaiter = () => {
3178
+ const r = resolveWaiter;
3179
+ resolveWaiter = null;
3180
+ r?.();
3181
+ };
3182
+ const matchesFilter = (askId, path) => {
3183
+ if (filterAskId !== void 0 && askId !== filterAskId) return false;
3184
+ if (filterPath !== void 0 && path !== filterPath) return false;
3185
+ return true;
3186
+ };
3187
+ for (const [askId, paths] of self.stringStreamByAsk) {
3188
+ for (const [path, acc] of paths) {
3189
+ if (!matchesFilter(askId, path)) continue;
3190
+ pending.push({
3191
+ askId,
3192
+ agent: acc.agent,
3193
+ path,
3194
+ delta: acc.text,
3195
+ accumulated: acc.text,
3196
+ attempt: acc.attempt
3197
+ });
3198
+ }
3199
+ }
3200
+ const handler = (event) => {
3201
+ if (event.type !== "string_delta") return;
3202
+ const askId = event.askId ?? "";
3203
+ if (!matchesFilter(askId, event.data.path)) return;
3204
+ const paths = self.stringStreamByAsk.get(askId);
3205
+ const acc = paths?.get(event.data.path);
3206
+ const accumulated = acc?.text ?? event.data.delta;
3207
+ pending.push({
3208
+ askId,
3209
+ agent: event.agent,
3210
+ path: event.data.path,
3211
+ delta: event.data.delta,
3212
+ accumulated,
3213
+ attempt: event.attempt
3214
+ });
3215
+ wakeWaiter();
3216
+ };
3217
+ self.bus.on("string_delta", handler);
3218
+ const pipelineHandler = (event) => {
3219
+ if (event.type !== "pipeline") return;
3220
+ if (event.status !== "failed") return;
3221
+ const askId = event.askId ?? "";
3222
+ for (let i = pending.length - 1; i >= 0; i--) {
3223
+ if (pending[i].askId === askId) pending.splice(i, 1);
3224
+ }
3225
+ };
3226
+ self.bus.on("pipeline", pipelineHandler);
3227
+ const unsubscribe = () => {
3228
+ if (unsubscribed) return;
3229
+ unsubscribed = true;
3230
+ self.bus.off("string_delta", handler);
3231
+ self.bus.off("pipeline", pipelineHandler);
3232
+ unsubscribeFinish();
3233
+ };
3234
+ const unsubscribeFinish = self._onFinish(() => {
3235
+ finished = true;
3236
+ self.bus.off("string_delta", handler);
3237
+ self.bus.off("pipeline", pipelineHandler);
3238
+ wakeWaiter();
3239
+ });
3240
+ return {
3241
+ async next() {
3242
+ while (true) {
3243
+ if (pending.length > 0) {
3244
+ return { value: pending.shift(), done: false };
3245
+ }
3246
+ if (finished) {
3247
+ unsubscribe();
3248
+ return { value: void 0, done: true };
3249
+ }
3250
+ await new Promise((res) => {
3251
+ resolveWaiter = res;
3252
+ });
3253
+ }
3254
+ },
3255
+ async return() {
3256
+ unsubscribe();
3257
+ return { value: void 0, done: true };
3258
+ },
3259
+ [Symbol.asyncIterator]() {
3260
+ return this;
3261
+ }
3262
+ };
3263
+ }
3264
+ };
3265
+ }
2796
3266
  /** Iterator over structural lifecycle events only — skips per-token chatter
2797
3267
  * and progressive `partial_object` emissions. Useful for waterfall UIs
2798
3268
  * and any consumer that wants the "what happened" timeline. */
@@ -2829,14 +3299,6 @@ var AxlEventBus = class {
2829
3299
  */
2830
3300
  _push(event) {
2831
3301
  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
3302
  if (event.type === "partial_object" && event.data?.object !== void 0) {
2841
3303
  const askId = event.askId ?? "";
2842
3304
  this.latestPartialByAsk.set(askId, {
@@ -2845,8 +3307,33 @@ var AxlEventBus = class {
2845
3307
  object: event.data.object,
2846
3308
  attempt: event.attempt
2847
3309
  });
3310
+ } else if (event.type === "string_delta") {
3311
+ const askId = event.askId ?? "";
3312
+ let paths = this.stringStreamByAsk.get(askId);
3313
+ if (!paths) {
3314
+ paths = /* @__PURE__ */ new Map();
3315
+ this.stringStreamByAsk.set(askId, paths);
3316
+ }
3317
+ const existing = paths.get(event.data.path);
3318
+ paths.set(event.data.path, {
3319
+ agent: event.agent,
3320
+ text: (existing?.text ?? "") + event.data.delta,
3321
+ attempt: event.attempt
3322
+ });
2848
3323
  } else if (event.type === "pipeline" && event.status === "failed") {
2849
- this.latestPartialByAsk.delete(event.askId ?? "");
3324
+ const askId = event.askId ?? "";
3325
+ this.latestPartialByAsk.delete(askId);
3326
+ this.stringStreamByAsk.delete(askId);
3327
+ } else if (event.type === "ask_end") {
3328
+ this.stringStreamByAsk.delete(event.askId ?? "");
3329
+ }
3330
+ try {
3331
+ this.bus.emit(event.type, event);
3332
+ } catch (err) {
3333
+ console.error(
3334
+ `[axl] AxlEventBus listener for "${event.type}" threw; ignoring:`,
3335
+ err instanceof Error ? err.message : String(err)
3336
+ );
2850
3337
  }
2851
3338
  const waiter = this.waiters.shift();
2852
3339
  if (waiter) {
@@ -3064,6 +3551,9 @@ var REDACTION_RULES = {
3064
3551
  handoff_return: passthrough,
3065
3552
  pipeline: (e) => e.status === "failed" ? { ...e, reason: REDACTED } : e,
3066
3553
  partial_object: (e) => ({ ...e, data: { ...e.data, object: REDACTED } }),
3554
+ // `path` is structural (schema-shape, no PII); `delta` is user content.
3555
+ // Scrub only the delta — preserving path keeps "which fields stream" telemetry.
3556
+ string_delta: (e) => ({ ...e, data: { ...e.data, delta: REDACTED } }),
3067
3557
  verify: (e) => e.data.lastError !== void 0 ? { ...e, data: { ...e.data, lastError: REDACTED } } : e,
3068
3558
  log: (e) => {
3069
3559
  if (!e.data || typeof e.data !== "object" || Array.isArray(e.data)) return e;
@@ -3907,36 +4397,26 @@ ${JSON.stringify(jsonSchema, null, 2)}`;
3907
4397
  const toolCallBuffers = /* @__PURE__ */ new Map();
3908
4398
  let streamProviderMetadata;
3909
4399
  let thinkingContent = "";
3910
- const partialObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
4400
+ const streamingObjectEnabled = !!options?.schema && toolDefs.length === 0 && options.schema instanceof z.ZodObject;
3911
4401
  const currentAttempt = schemaRetries + 1;
3912
- let inString = false;
3913
- let escaped = false;
3914
- let boundaryPending = false;
4402
+ const walker = streamingObjectEnabled ? new StreamingWalker({
4403
+ onStringDelta: (path, delta) => {
4404
+ this.emitEvent({
4405
+ type: "string_delta",
4406
+ agent: agent2._name,
4407
+ attempt: currentAttempt,
4408
+ data: { path, delta }
4409
+ });
4410
+ }
4411
+ }) : void 0;
3915
4412
  for await (const chunk of provider.stream(currentMessages, chatOptions)) {
3916
4413
  if (chunk.type === "text_delta") {
3917
4414
  content2 += chunk.content;
3918
4415
  this.emitEvent({ type: "token", data: chunk.content });
3919
4416
  this.onToken?.(chunk.content, callbackMeta);
3920
- if (partialObjectEnabled) {
3921
- for (const ch of chunk.content) {
3922
- if (escaped) {
3923
- escaped = false;
3924
- continue;
3925
- }
3926
- if (ch === "\\") {
3927
- escaped = true;
3928
- continue;
3929
- }
3930
- if (ch === '"') {
3931
- inString = !inString;
3932
- continue;
3933
- }
3934
- if (!inString && (ch === "," || ch === "}" || ch === "]")) {
3935
- boundaryPending = true;
3936
- }
3937
- }
3938
- if (boundaryPending) {
3939
- boundaryPending = false;
4417
+ if (walker) {
4418
+ walker.processChunk(chunk.content);
4419
+ if (walker.consumeBoundary()) {
3940
4420
  let parsed;
3941
4421
  try {
3942
4422
  parsed = parsePartialJson(extractJson(content2));
@@ -6745,6 +7225,20 @@ var AxlStream = class extends Readable {
6745
7225
  get partialObjects() {
6746
7226
  return this.events.partialObjects;
6747
7227
  }
7228
+ /** Listener-based view over `string_delta` events, with optional filter
7229
+ * on `path` and/or `askId`. Yields `StringStreamEvent`s carrying the new
7230
+ * `delta` and full text-so-far `accumulated`. Designed for chat-style
7231
+ * typewriter rendering of long string fields — bind a UI component to
7232
+ * one path and set its text to `event.accumulated` per yield.
7233
+ *
7234
+ * Late subscribers receive a synthetic event seeded from the bus's
7235
+ * per-ask accumulator (so the field's current state renders on the
7236
+ * first iteration, not after the next char arrives). Doesn't race the
7237
+ * main async iterator. See `AxlEventBus.stringStream` for full
7238
+ * semantics. */
7239
+ stringStream(opts) {
7240
+ return this.events.stringStream(opts);
7241
+ }
6748
7242
  pipe(destination, options) {
6749
7243
  const shouldEnd = options?.end !== false;
6750
7244
  this.events.on("token", (event) => {
@@ -7404,8 +7898,8 @@ async function createSpanManager(config) {
7404
7898
  if (!config?.enabled) {
7405
7899
  return new NoopSpanManager();
7406
7900
  }
7407
- const { OTelSpanManager: OTelSpanManager2 } = await import("./span-manager-3IKXXUTZ.js");
7408
- return OTelSpanManager2.create(config);
7901
+ const { OTelSpanManager } = await import("./span-manager-3IKXXUTZ.js");
7902
+ return OTelSpanManager.create(config);
7409
7903
  }
7410
7904
 
7411
7905
  // src/runtime.ts
@@ -7485,6 +7979,10 @@ var AxlRuntime = class extends EventEmitter2 {
7485
7979
  * the instance so per-event onTrace handlers don't reach into
7486
7980
  * `this.config.state` on every emission. */
7487
7981
  maxEventsPerExecution;
7982
+ /** Tracks executionIds for which we've already warned about a malformed
7983
+ * `events` field — keeps the log to one line per row instead of one per
7984
+ * rebuild tick. */
7985
+ warnedMalformedExecutions = /* @__PURE__ */ new Set();
7488
7986
  constructor(config) {
7489
7987
  super();
7490
7988
  this.config = resolveConfig(config ?? {});
@@ -7778,12 +8276,31 @@ var AxlRuntime = class extends EventEmitter2 {
7778
8276
  return result;
7779
8277
  }
7780
8278
  /** Get all execution info (running + completed + historical). */
8279
+ /** Normalize an `ExecutionInfo` loaded from a `StateStore` so the
8280
+ * `events: AxlEvent[]` type contract holds for downstream consumers
8281
+ * (Studio aggregators, REST routes, redaction). Custom or legacy stores
8282
+ * may persist rows without `events` (or with a non-array value) — without
8283
+ * this guard, a single malformed row crashes every iterator over
8284
+ * `exec.events` and takes down dependent features. Mutates the row
8285
+ * in place; logs at most one warning per `executionId`. */
8286
+ normalizeStoredExecution(exec) {
8287
+ if (!Array.isArray(exec.events)) {
8288
+ if (!this.warnedMalformedExecutions.has(exec.executionId)) {
8289
+ this.warnedMalformedExecutions.add(exec.executionId);
8290
+ console.warn(
8291
+ `[axl] StateStore returned execution ${exec.executionId} with non-array events (got ${exec.events === null ? "null" : typeof exec.events}); coercing to []`
8292
+ );
8293
+ }
8294
+ exec.events = [];
8295
+ }
8296
+ return exec;
8297
+ }
7781
8298
  async getExecutions() {
7782
8299
  if (!this.historicalExecutionsLoadPromise && this.stateStore.listExecutions) {
7783
8300
  this.historicalExecutionsLoadPromise = this.stateStore.listExecutions().then((stored) => {
7784
8301
  for (const exec of stored) {
7785
8302
  if (!this.executions.has(exec.executionId) && !this.historicalExecutions.has(exec.executionId)) {
7786
- this.historicalExecutions.set(exec.executionId, exec);
8303
+ this.historicalExecutions.set(exec.executionId, this.normalizeStoredExecution(exec));
7787
8304
  }
7788
8305
  }
7789
8306
  }).catch(() => {
@@ -8154,8 +8671,9 @@ var AxlRuntime = class extends EventEmitter2 {
8154
8671
  if (this.stateStore.getExecution) {
8155
8672
  const stored = await this.stateStore.getExecution(executionId);
8156
8673
  if (stored) {
8157
- this.historicalExecutions.set(executionId, stored);
8158
- return stored;
8674
+ const normalized = this.normalizeStoredExecution(stored);
8675
+ this.historicalExecutions.set(executionId, normalized);
8676
+ return normalized;
8159
8677
  }
8160
8678
  }
8161
8679
  return void 0;
@@ -8403,7 +8921,9 @@ var AxlRuntime = class extends EventEmitter2 {
8403
8921
  workflowCalls.set(event.workflow, (workflowCalls.get(event.workflow) ?? 0) + 1);
8404
8922
  }
8405
8923
  if (capturedTraces) {
8406
- if (event.type === "token" || event.type === "partial_object") return;
8924
+ if (event.type === "token" || event.type === "partial_object" || event.type === "string_delta") {
8925
+ return;
8926
+ }
8407
8927
  if (event.type === "agent_call_start" && event.data) {
8408
8928
  const d = event.data;
8409
8929
  if ("messages" in d) {
@@ -8727,6 +9247,49 @@ async function registerConditions(conditions) {
8727
9247
  }
8728
9248
  }
8729
9249
 
9250
+ // src/string-stream-from-events.ts
9251
+ async function* stringStreamFromEvents(source, opts) {
9252
+ const filterPath = opts?.path;
9253
+ const filterAskId = opts?.askId;
9254
+ if (filterPath !== void 0 && filterPath !== "" && !filterPath.startsWith("/")) {
9255
+ throw new Error(
9256
+ `stringStreamFromEvents({ path }) \u2014 path must start with '/' (RFC 6901 JSON Pointer); got ${JSON.stringify(filterPath)}. Examples: '/summary', '/sources/0/title'.`
9257
+ );
9258
+ }
9259
+ const acc = /* @__PURE__ */ new Map();
9260
+ for await (const event of source) {
9261
+ if (event.type === "string_delta") {
9262
+ const askId = event.askId ?? "";
9263
+ let paths = acc.get(askId);
9264
+ if (!paths) {
9265
+ paths = /* @__PURE__ */ new Map();
9266
+ acc.set(askId, paths);
9267
+ }
9268
+ const existing = paths.get(event.data.path);
9269
+ const accumulated = (existing?.text ?? "") + event.data.delta;
9270
+ paths.set(event.data.path, {
9271
+ agent: event.agent,
9272
+ text: accumulated,
9273
+ attempt: event.attempt
9274
+ });
9275
+ if (filterAskId !== void 0 && askId !== filterAskId) continue;
9276
+ if (filterPath !== void 0 && event.data.path !== filterPath) continue;
9277
+ yield {
9278
+ askId,
9279
+ agent: event.agent,
9280
+ path: event.data.path,
9281
+ delta: event.data.delta,
9282
+ accumulated,
9283
+ attempt: event.attempt
9284
+ };
9285
+ } else if (event.type === "pipeline" && event.status === "failed") {
9286
+ acc.delete(event.askId ?? "");
9287
+ } else if (event.type === "ask_end") {
9288
+ acc.delete(event.askId ?? "");
9289
+ }
9290
+ }
9291
+ }
9292
+
8730
9293
  // src/state/redis.ts
8731
9294
  var RedisStore = class _RedisStore {
8732
9295
  constructor(client) {
@@ -9131,6 +9694,7 @@ export {
9131
9694
  registerConditions,
9132
9695
  resolveRuntime,
9133
9696
  resolveThinkingOptions,
9697
+ stringStreamFromEvents,
9134
9698
  tool,
9135
9699
  workflow,
9136
9700
  zodToJsonSchema