@runtypelabs/persona 4.20.0 → 4.21.1

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.
Files changed (52) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-CG-bLFmk.d.cts → types-B_JdVK9o.d.cts} +2 -0
  4. package/dist/animations/{types-CG-bLFmk.d.ts → types-B_JdVK9o.d.ts} +2 -0
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +2 -0
  8. package/dist/animations-extra.d.ts +2 -0
  9. package/dist/approval-ui.d.cts +22 -0
  10. package/dist/approval-ui.d.ts +22 -0
  11. package/dist/artifacts-ui.d.cts +22 -0
  12. package/dist/artifacts-ui.d.ts +22 -0
  13. package/dist/codegen.cjs +1 -1
  14. package/dist/codegen.js +1 -1
  15. package/dist/context-mentions.d.cts +22 -0
  16. package/dist/context-mentions.d.ts +22 -0
  17. package/dist/event-stream-view.d.cts +22 -0
  18. package/dist/event-stream-view.d.ts +22 -0
  19. package/dist/forms-ui.d.cts +46 -4
  20. package/dist/forms-ui.d.ts +46 -4
  21. package/dist/history-view.d.cts +2 -0
  22. package/dist/history-view.d.ts +2 -0
  23. package/dist/index.cjs +63 -63
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +50 -4
  26. package/dist/index.d.ts +50 -4
  27. package/dist/index.global.js +48 -48
  28. package/dist/index.global.js.map +1 -1
  29. package/dist/index.js +63 -63
  30. package/dist/index.js.map +1 -1
  31. package/dist/session-reconnect.d.cts +22 -0
  32. package/dist/session-reconnect.d.ts +22 -0
  33. package/dist/smart-dom-reader.d.cts +22 -0
  34. package/dist/smart-dom-reader.d.ts +22 -0
  35. package/dist/theme-editor-preview.cjs +65 -65
  36. package/dist/theme-editor-preview.d.cts +22 -0
  37. package/dist/theme-editor-preview.d.ts +22 -0
  38. package/dist/theme-editor-preview.js +43 -43
  39. package/dist/theme-editor.d.cts +22 -0
  40. package/dist/theme-editor.d.ts +22 -0
  41. package/dist/webmcp-runtime.d.cts +16 -0
  42. package/dist/webmcp-runtime.d.ts +16 -0
  43. package/package.json +1 -1
  44. package/src/client-visitor-history.test.ts +473 -3
  45. package/src/client.test.ts +141 -3
  46. package/src/client.ts +157 -39
  47. package/src/generated/runtype-openapi-contract.ts +2 -0
  48. package/src/session.ts +139 -103
  49. package/src/session.webmcp.test.ts +48 -1
  50. package/src/types.ts +30 -0
  51. package/src/ui.history-lifecycle.test.ts +2 -2
  52. package/src/ui.ts +95 -7
@@ -911,6 +911,60 @@ describe('AgentWidgetClient - Agent Payload Building', () => {
911
911
  });
912
912
  });
913
913
 
914
+ it('proves the exact recovery visitor on durable client-token chat', async () => {
915
+ let capturedHeaders: Record<string, string> | undefined;
916
+ global.fetch = vi.fn().mockImplementation(async (_url: string, options: RequestInit) => {
917
+ capturedHeaders = options.headers as Record<string, string>;
918
+ const encoder = new TextEncoder();
919
+ return {
920
+ ok: true,
921
+ body: new ReadableStream({
922
+ start(controller) {
923
+ controller.enqueue(encoder.encode(sseEvent('agent_complete', {
924
+ executionId: 'exec_1',
925
+ agentId: 'agent_123',
926
+ success: true,
927
+ iterations: 1,
928
+ completedAt: new Date().toISOString(),
929
+ seq: 1,
930
+ })));
931
+ controller.close();
932
+ },
933
+ }),
934
+ };
935
+ });
936
+
937
+ const client = new AgentWidgetClient({
938
+ apiUrl: 'https://api.runtype.com',
939
+ clientToken: 'ct_live_demo',
940
+ agentId: 'agent_123',
941
+ });
942
+ (client as unknown as {
943
+ clientSession: {
944
+ sessionId: string;
945
+ expiresAt: Date;
946
+ durableRecovery: { enabled: boolean };
947
+ };
948
+ }).clientSession = {
949
+ sessionId: 'sess_durable',
950
+ expiresAt: new Date(Date.now() + 10 * 60_000),
951
+ durableRecovery: { enabled: true },
952
+ };
953
+ (client as unknown as { readVisitorToken: () => Promise<string> }).readVisitorToken =
954
+ async () => 'cvt_owner';
955
+
956
+ await client.dispatch({
957
+ messages: [{
958
+ id: 'usr_1',
959
+ role: 'user',
960
+ content: 'Hello durable agent',
961
+ createdAt: '2025-01-01T00:00:00.000Z',
962
+ }],
963
+ }, () => {});
964
+
965
+ expect(capturedHeaders?.['X-Visitor-Token']).toBe('cvt_owner');
966
+ });
967
+
914
968
  it('should build agent payload with agent config', async () => {
915
969
  let capturedPayload: any = null;
916
970
  global.fetch = vi.fn().mockImplementation(async (_url: string, options: any) => {
@@ -3011,7 +3065,7 @@ describe('AgentWidgetClient.resumeFlow', () => {
3011
3065
  expect(capturedUrl).toBe('http://localhost:43111/api/chat/dispatch/resume');
3012
3066
  });
3013
3067
 
3014
- it('routes to /v1/client/resume with sessionId in client-token mode (core#3889)', async () => {
3068
+ it('routes durable client-token resume with the live session, visitor proof, and cursor', async () => {
3015
3069
  let capturedUrl: string | undefined;
3016
3070
  let capturedBody: Record<string, unknown> | undefined;
3017
3071
  let capturedHeaders: Record<string, string> | undefined;
@@ -3027,12 +3081,21 @@ describe('AgentWidgetClient.resumeFlow', () => {
3027
3081
  apiUrl: 'https://api.runtype.com',
3028
3082
  });
3029
3083
  // Simulate an initialized client session (resumeFlow reads sessionId off it).
3030
- (client as unknown as { clientSession: { sessionId: string; expiresAt: Date } }).clientSession = {
3084
+ (client as unknown as {
3085
+ clientSession: {
3086
+ sessionId: string;
3087
+ expiresAt: Date;
3088
+ durableRecovery: { enabled: boolean };
3089
+ };
3090
+ }).clientSession = {
3031
3091
  sessionId: 'cs_123',
3032
3092
  expiresAt: new Date(Date.now() + 60_000),
3093
+ durableRecovery: { enabled: true },
3033
3094
  };
3095
+ (client as unknown as { readVisitorToken: () => Promise<string> }).readVisitorToken =
3096
+ async () => 'cvt_resume';
3034
3097
 
3035
- await client.resumeFlow('exec_xyz', { toolu_A: { ok: true } });
3098
+ await client.resumeFlow('exec_xyz', { toolu_A: { ok: true } }, { after: '18' });
3036
3099
 
3037
3100
  // Session-authed sibling of /v1/client/chat: no Bearer key, sessionId in body.
3038
3101
  expect(capturedUrl).toBe('https://api.runtype.com/v1/client/resume');
@@ -3041,8 +3104,10 @@ describe('AgentWidgetClient.resumeFlow', () => {
3041
3104
  toolOutputs: { toolu_A: { ok: true } },
3042
3105
  streamResponse: true,
3043
3106
  sessionId: 'cs_123',
3107
+ after: '18',
3044
3108
  });
3045
3109
  expect(capturedHeaders!['Authorization']).toBeUndefined();
3110
+ expect(capturedHeaders!['X-Visitor-Token']).toBe('cvt_resume');
3046
3111
  });
3047
3112
 
3048
3113
  it('strips a trailing /v1/dispatch from apiUrl when building the client resume URL', async () => {
@@ -4611,3 +4676,76 @@ describe('AgentWidgetClient - mention context', () => {
4611
4676
  expect(get().context).toBeUndefined();
4612
4677
  });
4613
4678
  });
4679
+
4680
+ describe('AgentWidgetClient - agent text runs through the configured stream parser', () => {
4681
+ // Regression: agent-mode text_delta used to append the raw delta straight onto
4682
+ // `content`, bypassing `streamParser` entirely. Demos that pin a server-side
4683
+ // agent AND reply with a JSON envelope (the page-context / smart-dom-reader
4684
+ // demo) therefore rendered the raw `{"text": ...}` in the bubble, and never
4685
+ // populated `rawContent`, so the action manager saw nothing to dispatch.
4686
+ const envelopeStream = (execId: string, chunks: string[]) => [
4687
+ sseEvent('execution_start', { kind: 'agent', executionId: execId, agentId: 'virtual', agentName: 'Test', maxTurns: 1, startedAt: new Date().toISOString(), seq: 1 }),
4688
+ sseEvent('turn_start', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', seq: 2 }),
4689
+ sseEvent('text_start', { executionId: execId, id: 'text_1', role: 'assistant', seq: 3 }),
4690
+ ...chunks.map((delta, i) =>
4691
+ sseEvent('text_delta', { executionId: execId, id: 'text_1', delta, seq: 4 + i })
4692
+ ),
4693
+ sseEvent('text_complete', { executionId: execId, id: 'text_1', seq: 90 }),
4694
+ sseEvent('turn_complete', { executionId: execId, id: 'turn_1', iteration: 1, role: 'assistant', completedAt: new Date().toISOString(), seq: 91 }),
4695
+ sseEvent('execution_complete', { kind: 'agent', executionId: execId, success: true, completedAt: new Date().toISOString(), seq: 92 }),
4696
+ ];
4697
+
4698
+ const lastAssistant = (events: AgentWidgetEvent[]): AgentWidgetMessage | null => {
4699
+ for (let i = events.length - 1; i >= 0; i--) {
4700
+ const e = events[i];
4701
+ if (e.type === 'message' && e.message.role === 'assistant') return e.message;
4702
+ }
4703
+ return null;
4704
+ };
4705
+
4706
+ const runAgent = async (
4707
+ chunks: string[],
4708
+ config: Record<string, unknown> = {}
4709
+ ): Promise<AgentWidgetMessage | null> => {
4710
+ global.fetch = createRawStreamFetch(envelopeStream('exec_env', chunks));
4711
+ const events: AgentWidgetEvent[] = [];
4712
+ const client = new AgentWidgetClient({
4713
+ apiUrl: 'http://localhost:8000',
4714
+ agent: { name: 'Test', model: 'openai:gpt-4o-mini', systemPrompt: 'test' },
4715
+ ...config,
4716
+ } as any);
4717
+ await client.dispatch(
4718
+ { messages: [{ id: 'u1', role: 'user', content: 'Hi', createdAt: new Date().toISOString() }] },
4719
+ (e) => events.push(e)
4720
+ );
4721
+ return lastAssistant(events);
4722
+ };
4723
+
4724
+ it('extracts `text` from a JSON envelope streamed across agent text_delta chunks', async () => {
4725
+ const msg = await runAgent(
4726
+ ['{"text": "In the ', '**Featured drop**', ', we have 2 items."}'],
4727
+ { streamParser: () => createJsonStreamParser() }
4728
+ );
4729
+
4730
+ expect(msg?.content).toBe('In the **Featured drop**, we have 2 items.');
4731
+ // The bubble must not show the envelope itself.
4732
+ expect(msg?.content).not.toContain('{"text"');
4733
+ });
4734
+
4735
+ it('keeps the raw envelope on rawContent so the action manager can dispatch it', async () => {
4736
+ const envelope = '{"action": "add_to_cart", "product": "headphones", "text": "Added it!"}';
4737
+ const msg = await runAgent([envelope], {
4738
+ streamParser: () => createJsonStreamParser(),
4739
+ });
4740
+
4741
+ expect(msg?.content).toBe('Added it!');
4742
+ expect(msg?.rawContent).toBe(envelope);
4743
+ });
4744
+
4745
+ it('still plain-appends agent prose when no custom parser is configured', async () => {
4746
+ const msg = await runAgent(['Hello', ' World']);
4747
+
4748
+ expect(msg?.content).toBe('Hello World');
4749
+ expect(msg?.rawContent).toBeUndefined();
4750
+ });
4751
+ });
package/src/client.ts CHANGED
@@ -566,6 +566,54 @@ export class AgentWidgetClient {
566
566
  return this.clientSession;
567
567
  }
568
568
 
569
+ /** Persona's built-in reconnect transport for client-token sessions. */
570
+ public async reconnectClientTokenStream(ctx: {
571
+ executionId: string;
572
+ after: string;
573
+ signal: AbortSignal;
574
+ }): Promise<Response> {
575
+ let session = await this.initSession();
576
+ let recovered = false;
577
+
578
+ for (;;) {
579
+ const conversationId = session.conversationId;
580
+ const visitorToken = await this.readVisitorToken();
581
+ if (session.durableRecovery?.enabled !== true || !conversationId || !visitorToken) {
582
+ throw new HistoryClientError(
583
+ 'visitor_token_missing',
584
+ 'Durable recovery is not enabled for this client session'
585
+ );
586
+ }
587
+ const path =
588
+ `${this.clientApiBase()}/v1/client/conversations/` +
589
+ `${encodeURIComponent(conversationId)}/executions/` +
590
+ `${encodeURIComponent(ctx.executionId)}/events`;
591
+ const query = new URLSearchParams({
592
+ sessionId: session.sessionId,
593
+ after: ctx.after,
594
+ });
595
+ const response = await fetch(`${path}?${query}`, {
596
+ method: 'GET',
597
+ headers: {
598
+ 'X-Persona-Version': VERSION,
599
+ 'X-Visitor-Token': visitorToken,
600
+ },
601
+ signal: ctx.signal,
602
+ });
603
+ if (response.status === 401 && !recovered) {
604
+ recovered = true;
605
+ session = await this.recoverFromUnauthorized(
606
+ await this.readErrorCode(response),
607
+ null,
608
+ true
609
+ );
610
+ continue;
611
+ }
612
+ if (!response.ok) throw await this.historyErrorFor(response, true);
613
+ return response;
614
+ }
615
+ }
616
+
569
617
  /**
570
618
  * Initialize session for client token mode.
571
619
  * Called automatically on first message if not already initialized.
@@ -709,13 +757,18 @@ export class AgentWidgetClient {
709
757
  */
710
758
  private async createClientSession(opts: {
711
759
  conversationId?: string;
760
+ durableResume?: boolean;
712
761
  identityProof?: string | null;
713
762
  storedSessionId?: string | null;
714
763
  omitVisitorFields?: boolean;
715
764
  }): Promise<ClientSession> {
716
765
  const historyCapable = this.isHistoryCapable() && !opts.omitVisitorFields;
766
+ // Recovery is negotiated independently from the history UI. New servers
767
+ // return an explicit capability bit; old strict servers reject the
768
+ // additive request field, which the fallback below handles once.
769
+ const durableRecoveryRequested = !opts.omitVisitorFields;
717
770
  let visitorToken: string | null = null;
718
- if (historyCapable) {
771
+ if (historyCapable || durableRecoveryRequested) {
719
772
  // Never read persisted state before the controller's stored-state gate.
720
773
  await this.historyInternals.historyBootstrapReady;
721
774
  visitorToken = await this.readVisitorToken();
@@ -723,12 +776,16 @@ export class AgentWidgetClient {
723
776
 
724
777
  const routed = this.routing();
725
778
  const sessionTargetId = routed.agentId ?? routed.flowId;
726
- const resumeConversationId = historyCapable ? opts.conversationId : undefined;
779
+ const resumeConversationId =
780
+ historyCapable || opts.durableResume ? opts.conversationId : undefined;
727
781
  const requestBody: Record<string, unknown> = {
728
782
  token: this.config.clientToken,
729
783
  ...(sessionTargetId && { flowId: sessionTargetId }),
730
784
  ...(historyCapable && { visitorHistory: true }),
731
- ...(historyCapable && visitorToken ? { visitorToken } : {}),
785
+ ...(durableRecoveryRequested && { durableRecovery: true }),
786
+ ...((historyCapable || durableRecoveryRequested) && visitorToken
787
+ ? { visitorToken }
788
+ : {}),
732
789
  // Independent of the strict conversationId/sessionId union: a proof also
733
790
  // binds the visitor on an ordinary init.
734
791
  ...(historyCapable && opts.identityProof
@@ -741,7 +798,7 @@ export class AgentWidgetClient {
741
798
  : {}),
742
799
  };
743
800
 
744
- const response = await fetch(this.getClientApiUrl('init'), {
801
+ let response = await fetch(this.getClientApiUrl('init'), {
745
802
  method: 'POST',
746
803
  headers: {
747
804
  'Content-Type': 'application/json',
@@ -750,6 +807,22 @@ export class AgentWidgetClient {
750
807
  body: JSON.stringify(requestBody),
751
808
  });
752
809
 
810
+ // Rolling/self-hosted compatibility: pre-recovery servers use a strict
811
+ // init schema and answer 400 to the additive field. Retry once without it;
812
+ // the absent response capability keeps Persona on ordinary streaming.
813
+ if (response.status === 400 && durableRecoveryRequested) {
814
+ const fallbackBody = { ...requestBody };
815
+ delete fallbackBody.durableRecovery;
816
+ response = await fetch(this.getClientApiUrl('init'), {
817
+ method: 'POST',
818
+ headers: {
819
+ 'Content-Type': 'application/json',
820
+ 'X-Persona-Version': VERSION,
821
+ },
822
+ body: JSON.stringify(fallbackBody),
823
+ });
824
+ }
825
+
753
826
  if (!response.ok) {
754
827
  const error = await response.json().catch(() => ({ error: 'Session initialization failed' }));
755
828
  if (historyCapable && response.status === 403 && error.error === 'visitor_history_disabled') {
@@ -763,7 +836,17 @@ export class AgentWidgetClient {
763
836
  }
764
837
  return this.createClientSession({ ...opts, omitVisitorFields: true });
765
838
  }
766
- if (historyCapable && response.status === 401 && error.error === 'visitor_required') {
839
+ if (response.status === 403 && error.error === 'durable_recovery_disabled') {
840
+ throw new HistoryClientError(
841
+ 'history_disabled',
842
+ 'Durable recovery is disabled for this surface'
843
+ );
844
+ }
845
+ if (
846
+ (historyCapable || opts.durableResume) &&
847
+ response.status === 401 &&
848
+ error.error === 'visitor_required'
849
+ ) {
767
850
  throw new HistoryClientError('visitor_required', 'Visitor credential no longer resolves');
768
851
  }
769
852
  if (resumeConversationId && response.status === 404) {
@@ -797,6 +880,7 @@ export class AgentWidgetClient {
797
880
  ...(data.conversationRevision
798
881
  ? { conversationRevision: data.conversationRevision }
799
882
  : {}),
883
+ ...(data.durableRecovery ? { durableRecovery: data.durableRecovery } : {}),
800
884
  ...(data.visitor ? { visitor: data.visitor } : {}),
801
885
  config: {
802
886
  welcomeMessage: data.config.welcomeMessage,
@@ -807,15 +891,13 @@ export class AgentWidgetClient {
807
891
  }
808
892
 
809
893
  private async _doInitSession(): Promise<ClientSession> {
810
- if (!this.isHistoryCapable()) {
811
- const storedSessionId = this.config.getStoredSessionId?.() || null;
812
- const session = await this.createClientSession({ storedSessionId });
813
- this.config.setStoredSessionId?.(session.sessionId);
814
- return session;
815
- }
816
-
817
894
  await this.historyInternals.historyBootstrapReady;
818
895
  const previousConversationId = this.config.getStoredConversationId?.() || null;
896
+ const durableResume =
897
+ this.historyInternals.shouldResumeDurableConversation?.() === true;
898
+ if (!this.isHistoryCapable() && !durableResume) {
899
+ return this.ordinaryInit(previousConversationId, false);
900
+ }
819
901
  const storedToken = await this.readVisitorToken();
820
902
 
821
903
  // Boot resume: reopening the record beats replaying a possibly idle-expired
@@ -824,15 +906,23 @@ export class AgentWidgetClient {
824
906
  try {
825
907
  const resumed = await this.createClientSession({
826
908
  conversationId: previousConversationId,
909
+ durableResume,
827
910
  });
911
+ if (durableResume && resumed.durableRecovery?.enabled !== true) {
912
+ this.historyInternals.setStoredResumableHandle?.(null);
913
+ }
828
914
  return this.finishInit(resumed, previousConversationId, false);
829
915
  } catch (error) {
830
916
  if (
831
917
  !isHistoryClientError(error, 'not_found') &&
832
- !isHistoryClientError(error, 'visitor_required')
918
+ !isHistoryClientError(error, 'visitor_required') &&
919
+ !isHistoryClientError(error, 'history_disabled')
833
920
  ) {
834
921
  throw error;
835
922
  }
923
+ if (durableResume) {
924
+ this.historyInternals.setStoredResumableHandle?.(null);
925
+ }
836
926
  // Record gone or credential dead: exactly one ordinary fallback, never a loop.
837
927
  return this.ordinaryInit(previousConversationId, true);
838
928
  }
@@ -851,7 +941,7 @@ export class AgentWidgetClient {
851
941
  continuityBroken: boolean
852
942
  ): Promise<ClientSession> {
853
943
  const store = this.historyInternals.visitorStore;
854
- if (store && this.isHistoryCapable() && !(await this.readVisitorToken())) {
944
+ if (store && !(await this.readVisitorToken())) {
855
945
  return store.withFirstInitLock(() =>
856
946
  this.doOrdinaryInit(previousConversationId, continuityBroken)
857
947
  );
@@ -893,7 +983,11 @@ export class AgentWidgetClient {
893
983
  * unowned; one immediate re-init with `{visitorToken, sessionId}` claims it.
894
984
  */
895
985
  private async claimFirstConversation(first: ClientSession): Promise<ClientSession> {
896
- if (!first.visitor?.token || this.claimInFlight || !this.isHistoryCapable()) {
986
+ if (
987
+ !first.visitor?.token ||
988
+ this.claimInFlight ||
989
+ (!this.isHistoryCapable() && first.durableRecovery?.enabled !== true)
990
+ ) {
897
991
  return first;
898
992
  }
899
993
  this.claimInFlight = true;
@@ -1754,6 +1848,10 @@ export class AgentWidgetClient {
1754
1848
 
1755
1849
  // Build the standard payload to get context/metadata from middleware
1756
1850
  const basePayload = await this.buildPayload(options.messages);
1851
+ const recoveryVisitorToken =
1852
+ session.durableRecovery?.enabled === true
1853
+ ? await this.readVisitorToken()
1854
+ : null;
1757
1855
 
1758
1856
  // Build the chat request payload with message IDs for feedback tracking
1759
1857
  // Filter out sessionId from metadata if present (it's only for local storage)
@@ -1813,6 +1911,9 @@ export class AgentWidgetClient {
1813
1911
  headers: {
1814
1912
  'Content-Type': 'application/json',
1815
1913
  'X-Persona-Version': VERSION,
1914
+ ...(recoveryVisitorToken
1915
+ ? { 'X-Visitor-Token': recoveryVisitorToken }
1916
+ : {}),
1816
1917
  },
1817
1918
  body: JSON.stringify(chatRequest),
1818
1919
  signal: options.signal,
@@ -2221,7 +2322,7 @@ export class AgentWidgetClient {
2221
2322
  public async resumeFlow(
2222
2323
  executionId: string,
2223
2324
  toolOutputs: Record<string, unknown>,
2224
- options?: { streamResponse?: boolean; signal?: AbortSignal }
2325
+ options?: { streamResponse?: boolean; signal?: AbortSignal; after?: string }
2225
2326
  ): Promise<Response> {
2226
2327
  const isClientToken = this.isClientTokenMode();
2227
2328
  const url = isClientToken
@@ -2236,12 +2337,18 @@ export class AgentWidgetClient {
2236
2337
  // trusting the possibly-stale `this.clientSession`. (core#3889; BugBot
2237
2338
  // PR #214 r3367875360.)
2238
2339
  let resumeSessionId: string | undefined;
2340
+ let resumeVisitorToken: string | null = null;
2239
2341
  if (isClientToken) {
2240
- resumeSessionId = (await this.initSession()).sessionId;
2342
+ const session = await this.initSession();
2343
+ resumeSessionId = session.sessionId;
2344
+ if (session.durableRecovery?.enabled === true) {
2345
+ resumeVisitorToken = await this.readVisitorToken();
2346
+ }
2241
2347
  }
2242
2348
 
2243
2349
  let headers: Record<string, string> = {
2244
2350
  'Content-Type': 'application/json',
2351
+ ...(resumeVisitorToken ? { 'X-Visitor-Token': resumeVisitorToken } : {}),
2245
2352
  ...this.headers
2246
2353
  };
2247
2354
  if (this.getHeaders) {
@@ -2252,6 +2359,7 @@ export class AgentWidgetClient {
2252
2359
  executionId,
2253
2360
  toolOutputs,
2254
2361
  streamResponse: options?.streamResponse ?? true,
2362
+ ...(options?.after ? { after: options.after } : {}),
2255
2363
  };
2256
2364
  // Thread the (refreshed) sessionId through like `/v1/client/chat` does.
2257
2365
  if (resumeSessionId) {
@@ -2737,9 +2845,10 @@ export class AgentWidgetClient {
2737
2845
  // bubble-id segmentation on the wire in place of the legacy `partId`:
2738
2846
  // a new block id means a new bubble, sealed at `text_complete`/tool boundaries.
2739
2847
  let currentTextBlockId: string | null = null;
2740
- // Raw text accumulated for the open flow block before its bubble is
2741
- // materialized — lets a whitespace-only block resolve without a stray bubble.
2742
- let pendingFlowRaw = "";
2848
+ // Raw text accumulated for the open text block, on both the flow and agent
2849
+ // paths — lets a whitespace-only flow block resolve without a stray bubble,
2850
+ // and gives the structured-content parser the whole block on either path.
2851
+ let pendingTextRaw = "";
2743
2852
  // Nested flow-as-tool attribution (PR #4602): a text/reasoning block whose
2744
2853
  // `parentToolCallId` matches a `tool_start.toolCallId` belongs to a flow
2745
2854
  // running as that tool. Keyed by the wire block id, these route the block's
@@ -3134,20 +3243,20 @@ export class AgentWidgetClient {
3134
3243
  finalizeCleanup();
3135
3244
  };
3136
3245
 
3137
- // === Unified flow text channel ===
3138
- // Flow prompt-step text streams as `text_delta` blocks (segmented by
3139
- // `text_start`/`text_complete`) and can be structured JSON, so each block
3140
- // runs through the per-bubble structured-content parser — agent text stays
3141
- // plain. This is the legacy step_delta parser core, re-keyed from `partId`
3142
- // to the wire block-id bubble. The caller materializes the bubble lazily
3143
- // (whitespace-only blocks around tool boundaries never leave a stray bubble)
3144
- // and `step_complete.result.response` reconciles the authoritative final.
3246
+ // === Unified text channel ===
3247
+ // Prompt-step and agent text both stream as `text_delta` blocks (segmented by
3248
+ // `text_start`/`text_complete`) and can be structured JSON, so each block runs
3249
+ // through the per-bubble structured-content parser. This is the legacy
3250
+ // step_delta parser core, re-keyed from `partId` to the wire block-id bubble.
3251
+ // The caller materializes the bubble lazily (whitespace-only blocks around
3252
+ // tool boundaries never leave a stray bubble) and `step_complete.result.response`
3253
+ // reconciles the authoritative final.
3145
3254
  let lastSealedFlowBubble: AgentWidgetMessage | null = null;
3146
3255
 
3147
- // Stream one accumulated chunk of flow block text through the parser, setting
3256
+ // Stream one accumulated chunk of block text through the parser, setting
3148
3257
  // display `content` (extracted) + `rawContent` (raw) and emitting. Mirrors the
3149
3258
  // legacy step_delta chunk path; plain text bypasses the structured parser.
3150
- const applyFlowTextChunk = (
3259
+ const applyTextChunk = (
3151
3260
  assistant: AgentWidgetMessage,
3152
3261
  accumulatedRaw: string,
3153
3262
  chunk: string,
@@ -3773,7 +3882,7 @@ export class AgentWidgetClient {
3773
3882
  }
3774
3883
  currentTextBlockId =
3775
3884
  typeof payload.id === "string" ? payload.id : currentTextBlockId;
3776
- pendingFlowRaw = "";
3885
+ pendingTextRaw = "";
3777
3886
  } else if (payloadType === "text_delta") {
3778
3887
  // Nested flow-as-tool text: route to the parent tool's row, through the
3779
3888
  // same structured-content parser, never the top-level assistant channel.
@@ -3793,7 +3902,7 @@ export class AgentWidgetClient {
3793
3902
  executionId: payload.executionId,
3794
3903
  parentToolId: nestedParent,
3795
3904
  };
3796
- applyFlowTextChunk(nested, nestedRaw, nestedDelta, undefined);
3905
+ applyTextChunk(nested, nestedRaw, nestedDelta, undefined);
3797
3906
  continue;
3798
3907
  }
3799
3908
  currentTextBlockId =
@@ -3804,27 +3913,36 @@ export class AgentWidgetClient {
3804
3913
  // block-id bubble. Materialize lazily so a whitespace-only block
3805
3914
  // (newlines around a tool boundary) never leaves a stray bubble.
3806
3915
  const delta = typeof payload.delta === "string" ? payload.delta : "";
3807
- pendingFlowRaw += delta;
3808
- if (pendingFlowRaw.trim() === "") continue;
3916
+ pendingTextRaw += delta;
3917
+ if (pendingTextRaw.trim() === "") continue;
3809
3918
  const assistant = ensureAssistantMessage();
3810
3919
  assistant.agentMetadata = {
3811
3920
  executionId: payload.executionId,
3812
3921
  iteration: payload.iteration,
3813
3922
  };
3814
- applyFlowTextChunk(assistant, pendingFlowRaw, delta, undefined);
3923
+ applyTextChunk(assistant, pendingTextRaw, delta, undefined);
3815
3924
  lastAssistantInTurn = assistant;
3816
3925
  continue;
3817
3926
  }
3927
+ // Agent text can be structured JSON too, whenever the host configures a
3928
+ // custom `streamParser` (e.g. the page-context demo's `{"text": ...}` /
3929
+ // `{"action": "add_to_cart", ...}` envelope). Accumulate the open block's
3930
+ // raw text and run it through the same structured-content path the flow
3931
+ // branch uses, so the parser sees the whole block and `rawContent` stays
3932
+ // populated for the action manager. With the default plain-text parser
3933
+ // this is byte-identical to appending the delta, so agent demos that
3934
+ // stream prose are unaffected.
3935
+ const agentDelta = typeof payload.delta === "string" ? payload.delta : "";
3936
+ pendingTextRaw += agentDelta;
3818
3937
  const assistant = ensureAssistantMessage();
3819
- assistant.content += payload.delta ?? '';
3820
3938
  assistant.agentMetadata = {
3821
3939
  executionId: payload.executionId,
3822
3940
  iteration: payload.iteration,
3823
3941
  turnId: openTurnId ?? undefined,
3824
3942
  agentName: agentExecution?.agentName
3825
3943
  };
3944
+ applyTextChunk(assistant, pendingTextRaw, agentDelta, undefined);
3826
3945
  lastAssistantInTurn = assistant;
3827
- emitMessage(assistant);
3828
3946
  } else if (payloadType === "text_complete") {
3829
3947
  // Nested flow-as-tool text block close: seal its parent-tool-row message.
3830
3948
  const completeBlockId = typeof payload.id === "string" ? payload.id : null;
@@ -3857,7 +3975,7 @@ export class AgentWidgetClient {
3857
3975
  assistantMessage = null;
3858
3976
  }
3859
3977
  currentTextBlockId = null;
3860
- pendingFlowRaw = "";
3978
+ pendingTextRaw = "";
3861
3979
  } else if (payloadType === "step_complete") {
3862
3980
  // Only process completions for prompt steps, not tool/context steps
3863
3981
  const stepType = (payload as any).stepType;
@@ -4201,7 +4319,7 @@ export class AgentWidgetClient {
4201
4319
  assistantMessage = null;
4202
4320
  }
4203
4321
  currentTextBlockId = null;
4204
- pendingFlowRaw = "";
4322
+ pendingTextRaw = "";
4205
4323
  lastSealedFlowBubble = null;
4206
4324
 
4207
4325
  // `terminal: true` marks this as a graceful finish (not a drop). The
@@ -58,8 +58,10 @@ export type RuntypeExecutionStreamEvent = ({
58
58
  message: string;
59
59
  };
60
60
  executionId: string;
61
+ finalOutput?: string;
61
62
  kind: "agent" | "flow";
62
63
  seq: number;
64
+ stopReason?: string;
63
65
  type: "execution_error";
64
66
  upgradeUrl?: string;
65
67
  }) | ({