@runtypelabs/persona 3.21.1 → 3.21.2

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.
@@ -4644,6 +4644,7 @@ var AgentWidgetClient = class {
4644
4644
  });
4645
4645
  };
4646
4646
  let assistantMessage = null;
4647
+ let lastAssistantInTurn = null;
4647
4648
  const assistantMessageRef = { current: null };
4648
4649
  const partIdState = { current: null };
4649
4650
  let didSplitByPartId = false;
@@ -5640,6 +5641,7 @@ var AgentWidgetClient = class {
5640
5641
  }
5641
5642
  }
5642
5643
  } else if (payloadType === "agent_turn_start") {
5644
+ lastAssistantInTurn = null;
5643
5645
  } else if (payloadType === "agent_turn_delta") {
5644
5646
  if (payload.contentType === "text") {
5645
5647
  const assistant = ensureAssistantMessage();
@@ -5650,6 +5652,7 @@ var AgentWidgetClient = class {
5650
5652
  turnId: payload.turnId,
5651
5653
  agentName: agentExecution == null ? void 0 : agentExecution.agentName
5652
5654
  };
5655
+ lastAssistantInTurn = assistant;
5653
5656
  emitMessage(assistant);
5654
5657
  } else if (payload.contentType === "thinking") {
5655
5658
  const reasoningId = (_Ha = payload.turnId) != null ? _Ha : `agent-think-${payload.iteration}`;
@@ -5694,15 +5697,21 @@ var AgentWidgetClient = class {
5694
5697
  }
5695
5698
  }
5696
5699
  const turnStopReason = payload.stopReason;
5697
- if (turnStopReason && assistantMessage !== null) {
5700
+ const stopReasonTarget = assistantMessage != null ? assistantMessage : lastAssistantInTurn;
5701
+ if (turnStopReason && stopReasonTarget !== null) {
5698
5702
  const turnId = payload.turnId;
5699
- const matchesTurn = !turnId || ((_Pa = assistantMessage.agentMetadata) == null ? void 0 : _Pa.turnId) === turnId;
5703
+ const matchesTurn = !turnId || ((_Pa = stopReasonTarget.agentMetadata) == null ? void 0 : _Pa.turnId) === turnId;
5700
5704
  if (matchesTurn) {
5701
- assistantMessage.stopReason = turnStopReason;
5702
- emitMessage(assistantMessage);
5705
+ stopReasonTarget.stopReason = turnStopReason;
5706
+ emitMessage(stopReasonTarget);
5703
5707
  }
5704
5708
  }
5705
5709
  } else if (payloadType === "agent_tool_start") {
5710
+ if (assistantMessage) {
5711
+ assistantMessage.streaming = false;
5712
+ emitMessage(assistantMessage);
5713
+ assistantMessage = null;
5714
+ }
5706
5715
  const toolId = (_Qa = payload.toolCallId) != null ? _Qa : `agent-tool-${nextSequence()}`;
5707
5716
  trackToolId(getToolCallKey(payload), toolId);
5708
5717
  const toolMessage = ensureToolMessage(toolId);
@@ -4533,6 +4533,7 @@ var AgentWidgetClient = class {
4533
4533
  });
4534
4534
  };
4535
4535
  let assistantMessage = null;
4536
+ let lastAssistantInTurn = null;
4536
4537
  const assistantMessageRef = { current: null };
4537
4538
  const partIdState = { current: null };
4538
4539
  let didSplitByPartId = false;
@@ -5529,6 +5530,7 @@ var AgentWidgetClient = class {
5529
5530
  }
5530
5531
  }
5531
5532
  } else if (payloadType === "agent_turn_start") {
5533
+ lastAssistantInTurn = null;
5532
5534
  } else if (payloadType === "agent_turn_delta") {
5533
5535
  if (payload.contentType === "text") {
5534
5536
  const assistant = ensureAssistantMessage();
@@ -5539,6 +5541,7 @@ var AgentWidgetClient = class {
5539
5541
  turnId: payload.turnId,
5540
5542
  agentName: agentExecution == null ? void 0 : agentExecution.agentName
5541
5543
  };
5544
+ lastAssistantInTurn = assistant;
5542
5545
  emitMessage(assistant);
5543
5546
  } else if (payload.contentType === "thinking") {
5544
5547
  const reasoningId = (_Ha = payload.turnId) != null ? _Ha : `agent-think-${payload.iteration}`;
@@ -5583,15 +5586,21 @@ var AgentWidgetClient = class {
5583
5586
  }
5584
5587
  }
5585
5588
  const turnStopReason = payload.stopReason;
5586
- if (turnStopReason && assistantMessage !== null) {
5589
+ const stopReasonTarget = assistantMessage != null ? assistantMessage : lastAssistantInTurn;
5590
+ if (turnStopReason && stopReasonTarget !== null) {
5587
5591
  const turnId = payload.turnId;
5588
- const matchesTurn = !turnId || ((_Pa = assistantMessage.agentMetadata) == null ? void 0 : _Pa.turnId) === turnId;
5592
+ const matchesTurn = !turnId || ((_Pa = stopReasonTarget.agentMetadata) == null ? void 0 : _Pa.turnId) === turnId;
5589
5593
  if (matchesTurn) {
5590
- assistantMessage.stopReason = turnStopReason;
5591
- emitMessage(assistantMessage);
5594
+ stopReasonTarget.stopReason = turnStopReason;
5595
+ emitMessage(stopReasonTarget);
5592
5596
  }
5593
5597
  }
5594
5598
  } else if (payloadType === "agent_tool_start") {
5599
+ if (assistantMessage) {
5600
+ assistantMessage.streaming = false;
5601
+ emitMessage(assistantMessage);
5602
+ assistantMessage = null;
5603
+ }
5595
5604
  const toolId = (_Qa = payload.toolCallId) != null ? _Qa : `agent-tool-${nextSequence()}`;
5596
5605
  trackToolId(getToolCallKey(payload), toolId);
5597
5606
  const toolMessage = ensureToolMessage(toolId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.21.1",
3
+ "version": "3.21.2",
4
4
  "description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -2717,6 +2717,240 @@ describe('AgentWidgetClient - stopReason propagation', () => {
2717
2717
  });
2718
2718
  });
2719
2719
 
2720
+ // ============================================================================
2721
+ // Within-turn text/tool interleaving — assistant text bubbles must seal at
2722
+ // each agent_tool_start so the chronological text→tool→text→tool sequence
2723
+ // renders as distinct timeline entries instead of one merged bubble that
2724
+ // appears below all the tool cards.
2725
+ // ============================================================================
2726
+
2727
+ describe('AgentWidgetClient - agent_turn text/tool interleaving', () => {
2728
+ const collectMessages = (events: AgentWidgetEvent[]): AgentWidgetMessage[] => {
2729
+ const byId = new Map<string, AgentWidgetMessage>();
2730
+ const order: string[] = [];
2731
+ for (const e of events) {
2732
+ if (e.type !== 'message') continue;
2733
+ if (!byId.has(e.message.id)) order.push(e.message.id);
2734
+ byId.set(e.message.id, e.message);
2735
+ }
2736
+ return order.map((id) => byId.get(id)!);
2737
+ };
2738
+
2739
+ it('seals the assistant text bubble at each agent_tool_start so subsequent text creates a new bubble', async () => {
2740
+ const execId = 'exec_interleave';
2741
+ global.fetch = createAgentStreamFetch([
2742
+ sseEvent('agent_start', {
2743
+ executionId: execId, agentId: 'virtual', agentName: 'Test',
2744
+ maxTurns: 1, startedAt: new Date().toISOString(), seq: 1,
2745
+ }),
2746
+ sseEvent('agent_iteration_start', {
2747
+ executionId: execId, iteration: 1, maxTurns: 1,
2748
+ startedAt: new Date().toISOString(), seq: 2,
2749
+ }),
2750
+ sseEvent('agent_turn_start', {
2751
+ executionId: execId, iteration: 1, turnIndex: 0,
2752
+ role: 'assistant', turnId: 'turn_1', seq: 3,
2753
+ }),
2754
+ sseEvent('agent_turn_delta', {
2755
+ executionId: execId, iteration: 1, delta: 'before tool 1',
2756
+ contentType: 'text', turnId: 'turn_1', seq: 4,
2757
+ }),
2758
+ sseEvent('agent_tool_start', {
2759
+ executionId: execId, iteration: 1,
2760
+ toolCallId: 'call_1', toolName: 'search', toolType: 'builtin', seq: 5,
2761
+ }),
2762
+ sseEvent('agent_tool_complete', {
2763
+ executionId: execId, iteration: 1,
2764
+ toolCallId: 'call_1', toolName: 'search', success: true,
2765
+ executionTime: 10, seq: 6,
2766
+ }),
2767
+ sseEvent('agent_turn_delta', {
2768
+ executionId: execId, iteration: 1, delta: 'between tools',
2769
+ contentType: 'text', turnId: 'turn_1', seq: 7,
2770
+ }),
2771
+ sseEvent('agent_tool_start', {
2772
+ executionId: execId, iteration: 1,
2773
+ toolCallId: 'call_2', toolName: 'fetch', toolType: 'builtin', seq: 8,
2774
+ }),
2775
+ sseEvent('agent_tool_complete', {
2776
+ executionId: execId, iteration: 1,
2777
+ toolCallId: 'call_2', toolName: 'fetch', success: true,
2778
+ executionTime: 10, seq: 9,
2779
+ }),
2780
+ sseEvent('agent_turn_delta', {
2781
+ executionId: execId, iteration: 1, delta: 'after tool 2',
2782
+ contentType: 'text', turnId: 'turn_1', seq: 10,
2783
+ }),
2784
+ sseEvent('agent_turn_complete', {
2785
+ executionId: execId, iteration: 1, role: 'assistant',
2786
+ turnId: 'turn_1', completedAt: new Date().toISOString(),
2787
+ stopReason: 'end_turn', seq: 11,
2788
+ }),
2789
+ sseEvent('agent_iteration_complete', {
2790
+ executionId: execId, iteration: 1, toolCallsMade: 2,
2791
+ stopConditionMet: true, completedAt: new Date().toISOString(), seq: 12,
2792
+ }),
2793
+ sseEvent('agent_complete', {
2794
+ executionId: execId, agentId: 'virtual', success: true,
2795
+ iterations: 1, stopReason: 'complete',
2796
+ completedAt: new Date().toISOString(), seq: 13,
2797
+ }),
2798
+ ]);
2799
+
2800
+ const client = new AgentWidgetClient({
2801
+ apiUrl: 'http://localhost:8000',
2802
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
2803
+ });
2804
+ const events: AgentWidgetEvent[] = [];
2805
+ await client.dispatch(
2806
+ { messages: [{ id: 'usr_1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
2807
+ (e) => events.push(e)
2808
+ );
2809
+
2810
+ const messages = collectMessages(events);
2811
+ const assistants = messages.filter((m) => m.role === 'assistant' && m.variant !== 'tool');
2812
+ const tools = messages.filter((m) => m.variant === 'tool');
2813
+
2814
+ expect(assistants.map((m) => m.content)).toEqual([
2815
+ 'before tool 1',
2816
+ 'between tools',
2817
+ 'after tool 2',
2818
+ ]);
2819
+ expect(tools.map((m) => m.toolCall?.name)).toEqual(['search', 'fetch']);
2820
+ expect(new Set(assistants.map((m) => m.id)).size).toBe(3);
2821
+ });
2822
+
2823
+ it('attaches agent_turn_complete.stopReason to the final assistant text segment when the turn ends with text', async () => {
2824
+ const execId = 'exec_stopreason_tail_text';
2825
+ global.fetch = createAgentStreamFetch([
2826
+ sseEvent('agent_start', {
2827
+ executionId: execId, agentId: 'virtual', agentName: 'Test',
2828
+ maxTurns: 1, startedAt: new Date().toISOString(), seq: 1,
2829
+ }),
2830
+ sseEvent('agent_iteration_start', {
2831
+ executionId: execId, iteration: 1, maxTurns: 1,
2832
+ startedAt: new Date().toISOString(), seq: 2,
2833
+ }),
2834
+ sseEvent('agent_turn_start', {
2835
+ executionId: execId, iteration: 1, turnIndex: 0,
2836
+ role: 'assistant', turnId: 'turn_1', seq: 3,
2837
+ }),
2838
+ sseEvent('agent_turn_delta', {
2839
+ executionId: execId, iteration: 1, delta: 'first segment',
2840
+ contentType: 'text', turnId: 'turn_1', seq: 4,
2841
+ }),
2842
+ sseEvent('agent_tool_start', {
2843
+ executionId: execId, iteration: 1,
2844
+ toolCallId: 'call_1', toolName: 'search', toolType: 'builtin', seq: 5,
2845
+ }),
2846
+ sseEvent('agent_tool_complete', {
2847
+ executionId: execId, iteration: 1,
2848
+ toolCallId: 'call_1', toolName: 'search', success: true,
2849
+ executionTime: 10, seq: 6,
2850
+ }),
2851
+ sseEvent('agent_turn_delta', {
2852
+ executionId: execId, iteration: 1, delta: 'final segment',
2853
+ contentType: 'text', turnId: 'turn_1', seq: 7,
2854
+ }),
2855
+ sseEvent('agent_turn_complete', {
2856
+ executionId: execId, iteration: 1, role: 'assistant',
2857
+ turnId: 'turn_1', completedAt: new Date().toISOString(),
2858
+ stopReason: 'length', seq: 8,
2859
+ }),
2860
+ sseEvent('agent_iteration_complete', {
2861
+ executionId: execId, iteration: 1, toolCallsMade: 1,
2862
+ stopConditionMet: true, completedAt: new Date().toISOString(), seq: 9,
2863
+ }),
2864
+ sseEvent('agent_complete', {
2865
+ executionId: execId, agentId: 'virtual', success: true,
2866
+ iterations: 1, stopReason: 'complete',
2867
+ completedAt: new Date().toISOString(), seq: 10,
2868
+ }),
2869
+ ]);
2870
+
2871
+ const client = new AgentWidgetClient({
2872
+ apiUrl: 'http://localhost:8000',
2873
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
2874
+ });
2875
+ const events: AgentWidgetEvent[] = [];
2876
+ await client.dispatch(
2877
+ { messages: [{ id: 'usr_1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
2878
+ (e) => events.push(e)
2879
+ );
2880
+
2881
+ const assistants = collectMessages(events).filter(
2882
+ (m) => m.role === 'assistant' && m.variant !== 'tool'
2883
+ );
2884
+ expect(assistants).toHaveLength(2);
2885
+ expect(assistants[0].content).toBe('first segment');
2886
+ expect(assistants[0].stopReason).toBeUndefined();
2887
+ expect(assistants[1].content).toBe('final segment');
2888
+ expect(assistants[1].stopReason).toBe('length');
2889
+ });
2890
+
2891
+ it('attaches agent_turn_complete.stopReason to the preceding text segment when the turn ends with a tool call', async () => {
2892
+ const execId = 'exec_stopreason_tail_tool';
2893
+ global.fetch = createAgentStreamFetch([
2894
+ sseEvent('agent_start', {
2895
+ executionId: execId, agentId: 'virtual', agentName: 'Test',
2896
+ maxTurns: 1, startedAt: new Date().toISOString(), seq: 1,
2897
+ }),
2898
+ sseEvent('agent_iteration_start', {
2899
+ executionId: execId, iteration: 1, maxTurns: 1,
2900
+ startedAt: new Date().toISOString(), seq: 2,
2901
+ }),
2902
+ sseEvent('agent_turn_start', {
2903
+ executionId: execId, iteration: 1, turnIndex: 0,
2904
+ role: 'assistant', turnId: 'turn_1', seq: 3,
2905
+ }),
2906
+ sseEvent('agent_turn_delta', {
2907
+ executionId: execId, iteration: 1, delta: 'about to call tool',
2908
+ contentType: 'text', turnId: 'turn_1', seq: 4,
2909
+ }),
2910
+ sseEvent('agent_tool_start', {
2911
+ executionId: execId, iteration: 1,
2912
+ toolCallId: 'call_1', toolName: 'search', toolType: 'builtin', seq: 5,
2913
+ }),
2914
+ sseEvent('agent_tool_complete', {
2915
+ executionId: execId, iteration: 1,
2916
+ toolCallId: 'call_1', toolName: 'search', success: true,
2917
+ executionTime: 10, seq: 6,
2918
+ }),
2919
+ sseEvent('agent_turn_complete', {
2920
+ executionId: execId, iteration: 1, role: 'assistant',
2921
+ turnId: 'turn_1', completedAt: new Date().toISOString(),
2922
+ stopReason: 'max_tool_calls', seq: 7,
2923
+ }),
2924
+ sseEvent('agent_iteration_complete', {
2925
+ executionId: execId, iteration: 1, toolCallsMade: 1,
2926
+ stopConditionMet: true, completedAt: new Date().toISOString(), seq: 8,
2927
+ }),
2928
+ sseEvent('agent_complete', {
2929
+ executionId: execId, agentId: 'virtual', success: true,
2930
+ iterations: 1, stopReason: 'max_tool_calls',
2931
+ completedAt: new Date().toISOString(), seq: 9,
2932
+ }),
2933
+ ]);
2934
+
2935
+ const client = new AgentWidgetClient({
2936
+ apiUrl: 'http://localhost:8000',
2937
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
2938
+ });
2939
+ const events: AgentWidgetEvent[] = [];
2940
+ await client.dispatch(
2941
+ { messages: [{ id: 'usr_1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
2942
+ (e) => events.push(e)
2943
+ );
2944
+
2945
+ const assistants = collectMessages(events).filter(
2946
+ (m) => m.role === 'assistant' && m.variant !== 'tool'
2947
+ );
2948
+ expect(assistants).toHaveLength(1);
2949
+ expect(assistants[0].content).toBe('about to call tool');
2950
+ expect(assistants[0].stopReason).toBe('max_tool_calls');
2951
+ });
2952
+ });
2953
+
2720
2954
  // ============================================================================
2721
2955
  // step_await (LOCAL tool pause) + resumeFlow
2722
2956
  // ============================================================================
package/src/client.ts CHANGED
@@ -1125,6 +1125,11 @@ export class AgentWidgetClient {
1125
1125
  };
1126
1126
 
1127
1127
  let assistantMessage: AgentWidgetMessage | null = null;
1128
+ // Tracks the most recently touched assistant text message for the
1129
+ // current agent turn so `agent_turn_complete.stopReason` can attach
1130
+ // to the final visible text segment even after `assistantMessage`
1131
+ // has been finalized at a tool-call boundary within the turn.
1132
+ let lastAssistantInTurn: AgentWidgetMessage | null = null;
1128
1133
  // Reference to track assistant message for custom event handler
1129
1134
  const assistantMessageRef = { current: null as AgentWidgetMessage | null };
1130
1135
  // Track current partId for message segmentation at tool boundaries
@@ -2464,7 +2469,11 @@ export class AgentWidgetClient {
2464
2469
  }
2465
2470
  }
2466
2471
  } else if (payloadType === "agent_turn_start") {
2467
- // Nothing to do - turn tracking is handled by deltas
2472
+ // Reset the per-turn assistant tracker. lastAssistantInTurn is
2473
+ // used by agent_turn_complete to attach stopReason to the final
2474
+ // text segment of the turn even if that segment was sealed by an
2475
+ // intervening tool-call boundary.
2476
+ lastAssistantInTurn = null;
2468
2477
  } else if (payloadType === "agent_turn_delta") {
2469
2478
  if (payload.contentType === 'text') {
2470
2479
  // Stream text to assistant message
@@ -2476,6 +2485,7 @@ export class AgentWidgetClient {
2476
2485
  turnId: payload.turnId,
2477
2486
  agentName: agentExecution?.agentName
2478
2487
  };
2488
+ lastAssistantInTurn = assistant;
2479
2489
  emitMessage(assistant);
2480
2490
  } else if (payload.contentType === 'thinking') {
2481
2491
  // Stream thinking content to a reasoning message
@@ -2526,19 +2536,33 @@ export class AgentWidgetClient {
2526
2536
  // Attach the turn-level stopReason to the assistant message
2527
2537
  // produced by this turn. Only overwrite the current message —
2528
2538
  // prior turns already sealed their own stopReason via step_complete.
2539
+ // Falls back to lastAssistantInTurn when the current bubble was
2540
+ // sealed at a tool-call boundary mid-turn, so the notice still
2541
+ // attaches to the final visible text segment.
2529
2542
  const turnStopReason = (payload as any).stopReason as
2530
2543
  | StopReasonKind
2531
2544
  | undefined;
2532
- if (turnStopReason && assistantMessage !== null) {
2545
+ const stopReasonTarget = assistantMessage ?? lastAssistantInTurn;
2546
+ if (turnStopReason && stopReasonTarget !== null) {
2533
2547
  const turnId = payload.turnId;
2534
2548
  const matchesTurn =
2535
- !turnId || assistantMessage.agentMetadata?.turnId === turnId;
2549
+ !turnId || stopReasonTarget.agentMetadata?.turnId === turnId;
2536
2550
  if (matchesTurn) {
2537
- assistantMessage.stopReason = turnStopReason;
2538
- emitMessage(assistantMessage);
2551
+ stopReasonTarget.stopReason = turnStopReason;
2552
+ emitMessage(stopReasonTarget);
2539
2553
  }
2540
2554
  }
2541
2555
  } else if (payloadType === "agent_tool_start") {
2556
+ // Finalize any in-flight assistant text bubble so subsequent text
2557
+ // deltas in this turn create a NEW bubble. Without this, text
2558
+ // emitted before AND after a tool call accumulates into one
2559
+ // message that renders below all the tool bubbles, losing the
2560
+ // chronological text→tool→text→tool interleaving.
2561
+ if (assistantMessage) {
2562
+ assistantMessage.streaming = false;
2563
+ emitMessage(assistantMessage);
2564
+ assistantMessage = null;
2565
+ }
2542
2566
  const toolId = payload.toolCallId ?? `agent-tool-${nextSequence()}`;
2543
2567
  trackToolId(getToolCallKey(payload), toolId);
2544
2568
  const toolMessage = ensureToolMessage(toolId);