@runtypelabs/persona 4.21.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/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +63 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.global.js +47 -47
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +63 -63
- package/dist/index.js.map +1 -1
- package/dist/theme-editor-preview.cjs +62 -62
- package/dist/theme-editor-preview.js +44 -44
- package/package.json +1 -1
- package/src/client-visitor-history.test.ts +131 -0
- package/src/ui.ts +28 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.21.
|
|
3
|
+
"version": "4.21.1",
|
|
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",
|
|
@@ -825,6 +825,137 @@ describe('controller wiring', () => {
|
|
|
825
825
|
mount.remove();
|
|
826
826
|
});
|
|
827
827
|
|
|
828
|
+
it('persists the latest durable cursor with the transcript before page exit', async () => {
|
|
829
|
+
await seedToken('cvt_stored');
|
|
830
|
+
installFetch([ok({ sessionId: 'sess_ui' })]);
|
|
831
|
+
const mount = document.createElement('div');
|
|
832
|
+
document.body.appendChild(mount);
|
|
833
|
+
const controller = createAgentExperience(mount, {
|
|
834
|
+
apiUrl: API_URL,
|
|
835
|
+
clientToken: CLIENT_TOKEN,
|
|
836
|
+
launcher: { enabled: false },
|
|
837
|
+
});
|
|
838
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
839
|
+
|
|
840
|
+
let streamController!: ReadableStreamDefaultController<Uint8Array>;
|
|
841
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
842
|
+
start(value) {
|
|
843
|
+
streamController = value;
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
const connected = controller.connectStream(stream);
|
|
847
|
+
const encoder = new TextEncoder();
|
|
848
|
+
streamController.enqueue(
|
|
849
|
+
encoder.encode(
|
|
850
|
+
'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_exit_latest","id":"text_exit_latest","delta":"Hi"}\n\n'
|
|
851
|
+
)
|
|
852
|
+
);
|
|
853
|
+
await vi.waitFor(() =>
|
|
854
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
855
|
+
executionId: 'exec_exit_latest',
|
|
856
|
+
after: '7',
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
streamController.enqueue(
|
|
861
|
+
encoder.encode(
|
|
862
|
+
'id: 8\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_exit_latest","id":"text_exit_latest","delta":" there"}\n\n'
|
|
863
|
+
)
|
|
864
|
+
);
|
|
865
|
+
await vi.waitFor(() =>
|
|
866
|
+
expect(controller.getMessages().find((message) => message.role === 'assistant')?.content).toBe(
|
|
867
|
+
'Hi there'
|
|
868
|
+
)
|
|
869
|
+
);
|
|
870
|
+
await vi.waitFor(() =>
|
|
871
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
872
|
+
executionId: 'exec_exit_latest',
|
|
873
|
+
after: '8',
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
878
|
+
|
|
879
|
+
streamController.close();
|
|
880
|
+
await connected;
|
|
881
|
+
controller.destroy();
|
|
882
|
+
mount.remove();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('keeps async transcript snapshots aligned with the durable cursor', async () => {
|
|
886
|
+
await seedToken('cvt_stored');
|
|
887
|
+
installFetch([ok({ sessionId: 'sess_ui' })]);
|
|
888
|
+
const saves: AgentWidgetStoredState[] = [];
|
|
889
|
+
const mount = document.createElement('div');
|
|
890
|
+
document.body.appendChild(mount);
|
|
891
|
+
const controller = createAgentExperience(mount, {
|
|
892
|
+
apiUrl: API_URL,
|
|
893
|
+
clientToken: CLIENT_TOKEN,
|
|
894
|
+
launcher: { enabled: false },
|
|
895
|
+
storageAdapter: {
|
|
896
|
+
save: async (state) => {
|
|
897
|
+
await Promise.resolve();
|
|
898
|
+
saves.push(state);
|
|
899
|
+
},
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
await vi.waitFor(() => expect(requests).toHaveLength(1));
|
|
903
|
+
|
|
904
|
+
let streamController!: ReadableStreamDefaultController<Uint8Array>;
|
|
905
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
906
|
+
start(value) {
|
|
907
|
+
streamController = value;
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
const connected = controller.connectStream(stream);
|
|
911
|
+
const encoder = new TextEncoder();
|
|
912
|
+
streamController.enqueue(
|
|
913
|
+
encoder.encode(
|
|
914
|
+
'id: 7\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_async","id":"text_async","delta":"Hi"}\n\n'
|
|
915
|
+
)
|
|
916
|
+
);
|
|
917
|
+
await vi.waitFor(() =>
|
|
918
|
+
expect(controller.getPersistentMetadata().durableResume).toEqual({
|
|
919
|
+
executionId: 'exec_async',
|
|
920
|
+
after: '7',
|
|
921
|
+
})
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
streamController.enqueue(
|
|
925
|
+
encoder.encode(
|
|
926
|
+
'id: 8\nevent: text_delta\ndata: {"type":"text_delta","executionId":"exec_async","id":"text_async","delta":" there"}\n\n'
|
|
927
|
+
)
|
|
928
|
+
);
|
|
929
|
+
let firstCompleteTranscript: AgentWidgetStoredState | undefined;
|
|
930
|
+
await vi.waitFor(() => {
|
|
931
|
+
firstCompleteTranscript = saves.find((state) =>
|
|
932
|
+
state.messages?.some(
|
|
933
|
+
(message) => message.role === 'assistant' && message.content === 'Hi there'
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
expect(firstCompleteTranscript).toBeDefined();
|
|
937
|
+
});
|
|
938
|
+
expect(firstCompleteTranscript?.metadata?.durableResume).toEqual({
|
|
939
|
+
executionId: 'exec_async',
|
|
940
|
+
after: '8',
|
|
941
|
+
});
|
|
942
|
+
await Promise.resolve();
|
|
943
|
+
const savesBeforeExit = saves.length;
|
|
944
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
945
|
+
await Promise.resolve();
|
|
946
|
+
expect(saves).toHaveLength(savesBeforeExit);
|
|
947
|
+
|
|
948
|
+
streamController.enqueue(
|
|
949
|
+
encoder.encode(
|
|
950
|
+
'id: 9\nevent: execution_complete\ndata: {"type":"execution_complete","executionId":"exec_async","kind":"agent","success":true}\n\n'
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
streamController.close();
|
|
954
|
+
await connected;
|
|
955
|
+
controller.destroy();
|
|
956
|
+
mount.remove();
|
|
957
|
+
});
|
|
958
|
+
|
|
828
959
|
it('uses Persona-owned stored recovery state to reopen the durable conversation', async () => {
|
|
829
960
|
await seedToken('cvt_stored');
|
|
830
961
|
global.fetch = vi.fn(async (url: string | URL | Request, options?: RequestInit) => {
|
package/src/ui.ts
CHANGED
|
@@ -1054,6 +1054,8 @@ export const createAgentExperience = (
|
|
|
1054
1054
|
messagePersistenceDisabled
|
|
1055
1055
|
? null
|
|
1056
1056
|
: (config.storageAdapter ?? createLocalStorageAdapter());
|
|
1057
|
+
const personaOwnsClientTokenRecovery =
|
|
1058
|
+
Boolean(config.clientToken) && typeof config.reconnectStream !== "function";
|
|
1057
1059
|
let persistentMetadata: Record<string, unknown> = {};
|
|
1058
1060
|
let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
|
|
1059
1061
|
// Resolves once stored state has been applied (or its load failed). Every
|
|
@@ -5313,6 +5315,17 @@ export const createAgentExperience = (
|
|
|
5313
5315
|
? getMessagesForPersistence()
|
|
5314
5316
|
: [];
|
|
5315
5317
|
|
|
5318
|
+
const durableHandle =
|
|
5319
|
+
personaOwnsClientTokenRecovery && session?.getResumableHandle();
|
|
5320
|
+
if (durableHandle) {
|
|
5321
|
+
persistentMetadata = {
|
|
5322
|
+
...persistentMetadata,
|
|
5323
|
+
durableResume: {
|
|
5324
|
+
executionId: durableHandle.executionId,
|
|
5325
|
+
after: durableHandle.lastEventId,
|
|
5326
|
+
},
|
|
5327
|
+
};
|
|
5328
|
+
}
|
|
5316
5329
|
const payload: AgentWidgetStoredState = {
|
|
5317
5330
|
messages,
|
|
5318
5331
|
metadata: persistentMetadata,
|
|
@@ -8384,8 +8397,6 @@ export const createAgentExperience = (
|
|
|
8384
8397
|
? { executionId: candidate.executionId, after: candidate.after }
|
|
8385
8398
|
: null;
|
|
8386
8399
|
};
|
|
8387
|
-
const personaOwnsClientTokenRecovery =
|
|
8388
|
-
Boolean(config.clientToken) && typeof config.reconnectStream !== "function";
|
|
8389
8400
|
if (config.clientToken) {
|
|
8390
8401
|
config = {
|
|
8391
8402
|
...config,
|
|
@@ -8498,17 +8509,11 @@ export const createAgentExperience = (
|
|
|
8498
8509
|
const hostExecutionState = config.onExecutionState;
|
|
8499
8510
|
const recoveryWindow = mount.ownerDocument.defaultView;
|
|
8500
8511
|
let pageExitInProgress = false;
|
|
8501
|
-
const markPageExit = () =>
|
|
8502
|
-
|
|
8503
|
-
};
|
|
8504
|
-
const markPageActive = () => {
|
|
8505
|
-
pageExitInProgress = false;
|
|
8506
|
-
};
|
|
8507
|
-
recoveryWindow?.addEventListener("beforeunload", markPageExit);
|
|
8512
|
+
const markPageExit = () => (pageExitInProgress = true);
|
|
8513
|
+
const markPageActive = () => (pageExitInProgress = false);
|
|
8508
8514
|
recoveryWindow?.addEventListener("pagehide", markPageExit);
|
|
8509
8515
|
recoveryWindow?.addEventListener("pageshow", markPageActive);
|
|
8510
8516
|
destroyCallbacks.push(() => {
|
|
8511
|
-
recoveryWindow?.removeEventListener("beforeunload", markPageExit);
|
|
8512
8517
|
recoveryWindow?.removeEventListener("pagehide", markPageExit);
|
|
8513
8518
|
recoveryWindow?.removeEventListener("pageshow", markPageActive);
|
|
8514
8519
|
});
|
|
@@ -8638,6 +8643,18 @@ export const createAgentExperience = (
|
|
|
8638
8643
|
let activeStreamingTextCandidate: { index: number; id: string } | null = null;
|
|
8639
8644
|
let pendingStreamingTextMessages: AgentWidgetMessage[] | null = null;
|
|
8640
8645
|
let streamingTextRAF: number | null = null;
|
|
8646
|
+
let pendingDurablePersistenceMessages: AgentWidgetMessage[] | null = null;
|
|
8647
|
+
|
|
8648
|
+
const persistMessages = (messages: AgentWidgetMessage[]) => {
|
|
8649
|
+
if (!personaOwnsClientTokenRecovery) return persistState(messages);
|
|
8650
|
+
pendingDurablePersistenceMessages = messages;
|
|
8651
|
+
queueMicrotask(() => {
|
|
8652
|
+
if (pendingDurablePersistenceMessages === messages) {
|
|
8653
|
+
pendingDurablePersistenceMessages = null;
|
|
8654
|
+
persistState(messages);
|
|
8655
|
+
}
|
|
8656
|
+
});
|
|
8657
|
+
};
|
|
8641
8658
|
|
|
8642
8659
|
const applyMessagesChanged = (messages: AgentWidgetMessage[]) => {
|
|
8643
8660
|
lastAppliedMessages = messages;
|
|
@@ -8698,7 +8715,7 @@ export const createAgentExperience = (
|
|
|
8698
8715
|
}
|
|
8699
8716
|
|
|
8700
8717
|
voiceState.lastUserMessageWasVoice = Boolean(lastUserMessage?.viaVoice);
|
|
8701
|
-
|
|
8718
|
+
persistMessages(messages);
|
|
8702
8719
|
syncComposerBarPeek();
|
|
8703
8720
|
};
|
|
8704
8721
|
|