@runtypelabs/persona 3.21.0 → 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);
@@ -7871,8 +7880,10 @@ var AgentWidgetSession = class _AgentWidgetSession {
7871
7880
  */
7872
7881
  async connectStream(stream, options) {
7873
7882
  var _a, _b, _c;
7874
- if (this.streaming) return;
7875
- (_a = this.abortController) == null ? void 0 : _a.abort();
7883
+ if (this.streaming && !(options == null ? void 0 : options.allowReentry)) return;
7884
+ if (!(options == null ? void 0 : options.allowReentry)) {
7885
+ (_a = this.abortController) == null ? void 0 : _a.abort();
7886
+ }
7876
7887
  let hasStale = false;
7877
7888
  for (const msg of this.messages) {
7878
7889
  if (msg.streaming) {
@@ -7906,7 +7917,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7906
7917
  * and pipes the response stream through connectStream().
7907
7918
  */
7908
7919
  async resolveApproval(approval, decision) {
7909
- var _a, _b, _c;
7920
+ var _a, _b, _c, _d;
7910
7921
  const approvalMessageId = `approval-${approval.id}`;
7911
7922
  const updatedApproval = {
7912
7923
  ...approval,
@@ -7923,6 +7934,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7923
7934
  approval: updatedApproval
7924
7935
  };
7925
7936
  this.upsertMessage(updatedMessage);
7937
+ (_a = this.abortController) == null ? void 0 : _a.abort();
7938
+ this.abortController = new AbortController();
7939
+ this.setStreaming(true);
7926
7940
  const approvalConfig = this.config.approval;
7927
7941
  const onDecision = approvalConfig && typeof approvalConfig === "object" ? approvalConfig.onDecision : void 0;
7928
7942
  try {
@@ -7953,7 +7967,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7953
7967
  if (!response.ok) {
7954
7968
  const errorData = await response.json().catch(() => null);
7955
7969
  throw new Error(
7956
- (_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Approval request failed: ${response.status}`
7970
+ (_b = errorData == null ? void 0 : errorData.error) != null ? _b : `Approval request failed: ${response.status}`
7957
7971
  );
7958
7972
  }
7959
7973
  stream = response.body;
@@ -7961,23 +7975,35 @@ var AgentWidgetSession = class _AgentWidgetSession {
7961
7975
  stream = response;
7962
7976
  }
7963
7977
  if (stream) {
7964
- await this.connectStream(stream);
7965
- } else if (decision === "denied") {
7966
- this.appendMessage({
7967
- id: `denial-${approval.id}`,
7968
- role: "assistant",
7969
- content: "Tool execution was denied by user.",
7970
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7971
- streaming: false,
7972
- sequence: this.nextSequence()
7973
- });
7978
+ await this.connectStream(stream, { allowReentry: true });
7979
+ } else {
7980
+ if (decision === "denied") {
7981
+ this.appendMessage({
7982
+ id: `denial-${approval.id}`,
7983
+ role: "assistant",
7984
+ content: "Tool execution was denied by user.",
7985
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7986
+ streaming: false,
7987
+ sequence: this.nextSequence()
7988
+ });
7989
+ }
7990
+ this.setStreaming(false);
7991
+ this.abortController = null;
7974
7992
  }
7993
+ } else {
7994
+ this.setStreaming(false);
7995
+ this.abortController = null;
7975
7996
  }
7976
7997
  } catch (error) {
7977
- (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
7978
- _b,
7979
- error instanceof Error ? error : new Error(String(error))
7980
- );
7998
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
7999
+ this.setStreaming(false);
8000
+ this.abortController = null;
8001
+ if (!isAbortError) {
8002
+ (_d = (_c = this.callbacks).onError) == null ? void 0 : _d.call(
8003
+ _c,
8004
+ error instanceof Error ? error : new Error(String(error))
8005
+ );
8006
+ }
7981
8007
  }
7982
8008
  }
7983
8009
  /**
@@ -8031,7 +8057,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
8031
8057
  });
8032
8058
  }
8033
8059
  async resolveAskUserQuestion(toolMessage, answer) {
8034
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
8060
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
8035
8061
  const live = this.messages.find((m) => m.id === toolMessage.id);
8036
8062
  if (((_a = live == null ? void 0 : live.agentMetadata) == null ? void 0 : _a.askUserQuestionAnswered) === true) return;
8037
8063
  const executionId = (_b = toolMessage.agentMetadata) == null ? void 0 : _b.executionId;
@@ -8055,8 +8081,11 @@ var AgentWidgetSession = class _AgentWidgetSession {
8055
8081
  }
8056
8082
  }
8057
8083
  this.markAskUserQuestionResolved(toolMessage, structuredAnswers);
8084
+ (_h = this.abortController) == null ? void 0 : _h.abort();
8085
+ this.abortController = new AbortController();
8086
+ this.setStreaming(true);
8058
8087
  const toolCallId = toolMessage.toolCall.id;
8059
- const args = (_h = toolMessage.toolCall) == null ? void 0 : _h.args;
8088
+ const args = (_i = toolMessage.toolCall) == null ? void 0 : _i.args;
8060
8089
  const questions = Array.isArray(args == null ? void 0 : args.questions) ? args.questions : [];
8061
8090
  if (questions.length === 0) {
8062
8091
  const fallback = typeof answer === "string" ? answer : Object.entries(answer).map(
@@ -8102,17 +8131,25 @@ var AgentWidgetSession = class _AgentWidgetSession {
8102
8131
  if (!response.ok) {
8103
8132
  const errorData = await response.json().catch(() => null);
8104
8133
  throw new Error(
8105
- (_i = errorData == null ? void 0 : errorData.error) != null ? _i : `Resume failed: ${response.status}`
8134
+ (_j = errorData == null ? void 0 : errorData.error) != null ? _j : `Resume failed: ${response.status}`
8106
8135
  );
8107
8136
  }
8108
8137
  if (response.body) {
8109
- await this.connectStream(response.body);
8138
+ await this.connectStream(response.body, { allowReentry: true });
8139
+ } else {
8140
+ this.setStreaming(false);
8141
+ this.abortController = null;
8110
8142
  }
8111
8143
  } catch (error) {
8112
- (_k = (_j = this.callbacks).onError) == null ? void 0 : _k.call(
8113
- _j,
8114
- error instanceof Error ? error : new Error(String(error))
8115
- );
8144
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8145
+ this.setStreaming(false);
8146
+ this.abortController = null;
8147
+ if (!isAbortError) {
8148
+ (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
8149
+ _k,
8150
+ error instanceof Error ? error : new Error(String(error))
8151
+ );
8152
+ }
8116
8153
  }
8117
8154
  }
8118
8155
  cancel() {
@@ -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);
@@ -7760,8 +7769,10 @@ var AgentWidgetSession = class _AgentWidgetSession {
7760
7769
  */
7761
7770
  async connectStream(stream, options) {
7762
7771
  var _a, _b, _c;
7763
- if (this.streaming) return;
7764
- (_a = this.abortController) == null ? void 0 : _a.abort();
7772
+ if (this.streaming && !(options == null ? void 0 : options.allowReentry)) return;
7773
+ if (!(options == null ? void 0 : options.allowReentry)) {
7774
+ (_a = this.abortController) == null ? void 0 : _a.abort();
7775
+ }
7765
7776
  let hasStale = false;
7766
7777
  for (const msg of this.messages) {
7767
7778
  if (msg.streaming) {
@@ -7795,7 +7806,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7795
7806
  * and pipes the response stream through connectStream().
7796
7807
  */
7797
7808
  async resolveApproval(approval, decision) {
7798
- var _a, _b, _c;
7809
+ var _a, _b, _c, _d;
7799
7810
  const approvalMessageId = `approval-${approval.id}`;
7800
7811
  const updatedApproval = {
7801
7812
  ...approval,
@@ -7812,6 +7823,9 @@ var AgentWidgetSession = class _AgentWidgetSession {
7812
7823
  approval: updatedApproval
7813
7824
  };
7814
7825
  this.upsertMessage(updatedMessage);
7826
+ (_a = this.abortController) == null ? void 0 : _a.abort();
7827
+ this.abortController = new AbortController();
7828
+ this.setStreaming(true);
7815
7829
  const approvalConfig = this.config.approval;
7816
7830
  const onDecision = approvalConfig && typeof approvalConfig === "object" ? approvalConfig.onDecision : void 0;
7817
7831
  try {
@@ -7842,7 +7856,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7842
7856
  if (!response.ok) {
7843
7857
  const errorData = await response.json().catch(() => null);
7844
7858
  throw new Error(
7845
- (_a = errorData == null ? void 0 : errorData.error) != null ? _a : `Approval request failed: ${response.status}`
7859
+ (_b = errorData == null ? void 0 : errorData.error) != null ? _b : `Approval request failed: ${response.status}`
7846
7860
  );
7847
7861
  }
7848
7862
  stream = response.body;
@@ -7850,23 +7864,35 @@ var AgentWidgetSession = class _AgentWidgetSession {
7850
7864
  stream = response;
7851
7865
  }
7852
7866
  if (stream) {
7853
- await this.connectStream(stream);
7854
- } else if (decision === "denied") {
7855
- this.appendMessage({
7856
- id: `denial-${approval.id}`,
7857
- role: "assistant",
7858
- content: "Tool execution was denied by user.",
7859
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7860
- streaming: false,
7861
- sequence: this.nextSequence()
7862
- });
7867
+ await this.connectStream(stream, { allowReentry: true });
7868
+ } else {
7869
+ if (decision === "denied") {
7870
+ this.appendMessage({
7871
+ id: `denial-${approval.id}`,
7872
+ role: "assistant",
7873
+ content: "Tool execution was denied by user.",
7874
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
7875
+ streaming: false,
7876
+ sequence: this.nextSequence()
7877
+ });
7878
+ }
7879
+ this.setStreaming(false);
7880
+ this.abortController = null;
7863
7881
  }
7882
+ } else {
7883
+ this.setStreaming(false);
7884
+ this.abortController = null;
7864
7885
  }
7865
7886
  } catch (error) {
7866
- (_c = (_b = this.callbacks).onError) == null ? void 0 : _c.call(
7867
- _b,
7868
- error instanceof Error ? error : new Error(String(error))
7869
- );
7887
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
7888
+ this.setStreaming(false);
7889
+ this.abortController = null;
7890
+ if (!isAbortError) {
7891
+ (_d = (_c = this.callbacks).onError) == null ? void 0 : _d.call(
7892
+ _c,
7893
+ error instanceof Error ? error : new Error(String(error))
7894
+ );
7895
+ }
7870
7896
  }
7871
7897
  }
7872
7898
  /**
@@ -7920,7 +7946,7 @@ var AgentWidgetSession = class _AgentWidgetSession {
7920
7946
  });
7921
7947
  }
7922
7948
  async resolveAskUserQuestion(toolMessage, answer) {
7923
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
7949
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
7924
7950
  const live = this.messages.find((m) => m.id === toolMessage.id);
7925
7951
  if (((_a = live == null ? void 0 : live.agentMetadata) == null ? void 0 : _a.askUserQuestionAnswered) === true) return;
7926
7952
  const executionId = (_b = toolMessage.agentMetadata) == null ? void 0 : _b.executionId;
@@ -7944,8 +7970,11 @@ var AgentWidgetSession = class _AgentWidgetSession {
7944
7970
  }
7945
7971
  }
7946
7972
  this.markAskUserQuestionResolved(toolMessage, structuredAnswers);
7973
+ (_h = this.abortController) == null ? void 0 : _h.abort();
7974
+ this.abortController = new AbortController();
7975
+ this.setStreaming(true);
7947
7976
  const toolCallId = toolMessage.toolCall.id;
7948
- const args = (_h = toolMessage.toolCall) == null ? void 0 : _h.args;
7977
+ const args = (_i = toolMessage.toolCall) == null ? void 0 : _i.args;
7949
7978
  const questions = Array.isArray(args == null ? void 0 : args.questions) ? args.questions : [];
7950
7979
  if (questions.length === 0) {
7951
7980
  const fallback = typeof answer === "string" ? answer : Object.entries(answer).map(
@@ -7991,17 +8020,25 @@ var AgentWidgetSession = class _AgentWidgetSession {
7991
8020
  if (!response.ok) {
7992
8021
  const errorData = await response.json().catch(() => null);
7993
8022
  throw new Error(
7994
- (_i = errorData == null ? void 0 : errorData.error) != null ? _i : `Resume failed: ${response.status}`
8023
+ (_j = errorData == null ? void 0 : errorData.error) != null ? _j : `Resume failed: ${response.status}`
7995
8024
  );
7996
8025
  }
7997
8026
  if (response.body) {
7998
- await this.connectStream(response.body);
8027
+ await this.connectStream(response.body, { allowReentry: true });
8028
+ } else {
8029
+ this.setStreaming(false);
8030
+ this.abortController = null;
7999
8031
  }
8000
8032
  } catch (error) {
8001
- (_k = (_j = this.callbacks).onError) == null ? void 0 : _k.call(
8002
- _j,
8003
- error instanceof Error ? error : new Error(String(error))
8004
- );
8033
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("abort"));
8034
+ this.setStreaming(false);
8035
+ this.abortController = null;
8036
+ if (!isAbortError) {
8037
+ (_l = (_k = this.callbacks).onError) == null ? void 0 : _l.call(
8038
+ _k,
8039
+ error instanceof Error ? error : new Error(String(error))
8040
+ );
8041
+ }
8005
8042
  }
8006
8043
  }
8007
8044
  cancel() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/persona",
3
- "version": "3.21.0",
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
  // ============================================================================