@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CG-bLFmk.d.cts → types-B_JdVK9o.d.cts} +2 -0
- package/dist/animations/{types-CG-bLFmk.d.ts → types-B_JdVK9o.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui.d.cts +22 -0
- package/dist/approval-ui.d.ts +22 -0
- package/dist/artifacts-ui.d.cts +22 -0
- package/dist/artifacts-ui.d.ts +22 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +22 -0
- package/dist/context-mentions.d.ts +22 -0
- package/dist/event-stream-view.d.cts +22 -0
- package/dist/event-stream-view.d.ts +22 -0
- package/dist/forms-ui.d.cts +46 -4
- package/dist/forms-ui.d.ts +46 -4
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +63 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.global.js +48 -48
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +63 -63
- package/dist/index.js.map +1 -1
- package/dist/session-reconnect.d.cts +22 -0
- package/dist/session-reconnect.d.ts +22 -0
- package/dist/smart-dom-reader.d.cts +22 -0
- package/dist/smart-dom-reader.d.ts +22 -0
- package/dist/theme-editor-preview.cjs +65 -65
- package/dist/theme-editor-preview.d.cts +22 -0
- package/dist/theme-editor-preview.d.ts +22 -0
- package/dist/theme-editor-preview.js +43 -43
- package/dist/theme-editor.d.cts +22 -0
- package/dist/theme-editor.d.ts +22 -0
- package/dist/webmcp-runtime.d.cts +16 -0
- package/dist/webmcp-runtime.d.ts +16 -0
- package/package.json +1 -1
- package/src/client-visitor-history.test.ts +473 -3
- package/src/client.test.ts +141 -3
- package/src/client.ts +157 -39
- package/src/generated/runtype-openapi-contract.ts +2 -0
- package/src/session.ts +139 -103
- package/src/session.webmcp.test.ts +48 -1
- package/src/types.ts +30 -0
- package/src/ui.history-lifecycle.test.ts +2 -2
- package/src/ui.ts +95 -7
package/src/client.test.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 =
|
|
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
|
-
...(
|
|
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
|
-
|
|
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 (
|
|
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 &&
|
|
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 (
|
|
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
|
-
|
|
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
|
|
2741
|
-
//
|
|
2742
|
-
|
|
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
|
|
3138
|
-
//
|
|
3139
|
-
// `text_start`/`text_complete`) and can be structured JSON, so each block
|
|
3140
|
-
//
|
|
3141
|
-
//
|
|
3142
|
-
//
|
|
3143
|
-
//
|
|
3144
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3808
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4322
|
+
pendingTextRaw = "";
|
|
4205
4323
|
lastSealedFlowBubble = null;
|
|
4206
4324
|
|
|
4207
4325
|
// `terminal: true` marks this as a graceful finish (not a drop). The
|