@webless/agent 0.2.10 → 0.2.12

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/embed.cjs CHANGED
@@ -96,7 +96,8 @@ function createAgentRuntimeCapability(options) {
96
96
  const response = await fetchImplementation(`${options.runtimeOrigin}/webless/v1/bootstrap`, {
97
97
  body: JSON.stringify({
98
98
  clientSessionId: options.visitorSessionId,
99
- indexId: options.indexId
99
+ indexId: options.indexId,
100
+ version: options.version
100
101
  }),
101
102
  headers: { "content-type": "application/json" },
102
103
  method: "POST"
@@ -237,6 +238,54 @@ function clearPersistedAgentSession(visitorSessionId, options) {
237
238
  }
238
239
 
239
240
  // src/runtime/client.ts
241
+ function isTurnBoundary(event) {
242
+ return event.type === "session.waiting" || event.type === "session.completed" || event.type === "session.failed";
243
+ }
244
+ function applyMessageEvent(event, rendered, handlers) {
245
+ const step = mapStepLabel(event);
246
+ if (step) handlers.onStep?.(step.label, step.detail);
247
+ if (event.type === "session.failed") {
248
+ throw new Error(event.data.message || event.data.code);
249
+ }
250
+ if (event.type === "message.completed") {
251
+ handlers.onComplete?.();
252
+ }
253
+ if (event.type !== "message.appended") return rendered;
254
+ const { messageDelta, messageSoFar } = event.data;
255
+ let delta = messageDelta;
256
+ let next = rendered;
257
+ if (messageSoFar.startsWith(rendered)) {
258
+ delta = messageSoFar.slice(rendered.length);
259
+ next = messageSoFar;
260
+ } else if (messageDelta) {
261
+ next += messageDelta;
262
+ }
263
+ if (delta) handlers.onDelta(delta);
264
+ return next;
265
+ }
266
+ function latestTurnEvents(events) {
267
+ let startIndex = -1;
268
+ for (let index = events.length - 1; index >= 0; index -= 1) {
269
+ if (events[index]?.type === "message.received") {
270
+ startIndex = index;
271
+ break;
272
+ }
273
+ }
274
+ return startIndex >= 0 ? events.slice(startIndex) : [];
275
+ }
276
+ function renderTurn(events) {
277
+ let rendered = "";
278
+ for (const event of events) {
279
+ if (event.type !== "message.appended") continue;
280
+ const { messageDelta, messageSoFar } = event.data;
281
+ if (messageSoFar.startsWith(rendered)) {
282
+ rendered = messageSoFar;
283
+ } else if (messageDelta) {
284
+ rendered += messageDelta;
285
+ }
286
+ }
287
+ return rendered;
288
+ }
240
289
  function mapStepLabel(event) {
241
290
  if (event.type !== "step.started") return null;
242
291
  const stepIndex = event.data.stepIndex;
@@ -255,6 +304,7 @@ var AgentSession = class {
255
304
  this.capability = createAgentRuntimeCapability({
256
305
  indexId,
257
306
  runtimeOrigin,
307
+ version,
258
308
  visitorSessionId
259
309
  });
260
310
  }
@@ -272,8 +322,13 @@ var AgentSession = class {
272
322
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
273
323
  }
274
324
  reset() {
275
- void this.activeResponse?.cancel().catch(() => {
276
- });
325
+ if (this.activeResponse) {
326
+ void this.activeResponse.cancel().catch(() => {
327
+ });
328
+ } else {
329
+ void this.session?.cancel().catch(() => {
330
+ });
331
+ }
277
332
  this.activeResponse = void 0;
278
333
  this.session = void 0;
279
334
  clearPersistedAgentSession(this.visitorSessionId, this.storeOptions);
@@ -298,7 +353,8 @@ var AgentSession = class {
298
353
  if (this.client && this.clientHost === config.host) {
299
354
  return this.client;
300
355
  }
301
- this.reset();
356
+ this.activeResponse = void 0;
357
+ this.session = void 0;
302
358
  this.client = new import_client2.Client({
303
359
  auth: { bearer: () => this.capability.getAccessToken() },
304
360
  host: config.host,
@@ -347,28 +403,20 @@ var AgentSession = class {
347
403
  this.persistSessionCursor(session);
348
404
  }
349
405
  this.activeResponse = response;
406
+ let streamIndex = session?.state.streamIndex ?? 0;
350
407
  let rendered = "";
351
408
  try {
352
409
  for await (const event of response) {
353
410
  if (signal.aborted) break;
354
- const step = mapStepLabel(event);
355
- if (step) handlers.onStep?.(step.label, step.detail);
356
- if (event.type === "message.appended") {
357
- const { messageDelta, messageSoFar } = event.data;
358
- let delta = messageDelta;
359
- if (messageSoFar.startsWith(rendered)) {
360
- delta = messageSoFar.slice(rendered.length);
361
- rendered = messageSoFar;
362
- } else if (messageDelta) {
363
- rendered += messageDelta;
364
- }
365
- if (delta) handlers.onDelta(delta);
366
- }
367
- if (event.type === "message.completed") {
368
- handlers.onComplete?.();
369
- }
370
- if (event.type === "session.failed") {
371
- throw new Error(event.data.message || event.data.code);
411
+ rendered = applyMessageEvent(event, rendered, handlers);
412
+ streamIndex += 1;
413
+ if (session) {
414
+ savePersistedAgentSession(
415
+ this.visitorSessionId,
416
+ session.state.sessionId,
417
+ streamIndex,
418
+ this.storeOptions
419
+ );
372
420
  }
373
421
  }
374
422
  } finally {
@@ -380,15 +428,83 @@ var AgentSession = class {
380
428
  if (!rendered.trim() && !signal.aborted) {
381
429
  throw new Error("Empty response from runtime");
382
430
  }
383
- if (signal.aborted) {
384
- await response.cancel().catch(() => {
385
- });
431
+ return rendered.trim();
432
+ }
433
+ async resumeTurn(message, signal, handlers, initialText = "") {
434
+ const persisted = loadPersistedAgentSession(this.visitorSessionId, this.storeOptions);
435
+ if (!persisted) return null;
436
+ const client = this.ensureClient();
437
+ const attached = client.sessions.attach(persisted.sessionId, {
438
+ streamIndex: persisted.streamIndex
439
+ });
440
+ const snapshot = await withCapabilityRefresh(
441
+ this.capability,
442
+ () => attached.snapshot({ signal })
443
+ );
444
+ const turnEvents = latestTurnEvents(snapshot.events);
445
+ const received = turnEvents[0];
446
+ if (received?.type !== "message.received" || received.data.message !== message) {
447
+ return null;
448
+ }
449
+ let rendered = renderTurn(turnEvents);
450
+ if (rendered.startsWith(initialText)) {
451
+ const missedText = rendered.slice(initialText.length);
452
+ if (missedText) handlers.onDelta(missedText);
453
+ } else if (initialText.startsWith(rendered)) {
454
+ rendered = initialText;
455
+ } else if (!initialText.startsWith(rendered)) {
456
+ rendered = initialText + rendered;
457
+ }
458
+ let session = client.sessions.attach(snapshot.session.sessionId, {
459
+ streamIndex: snapshot.session.streamIndex
460
+ });
461
+ this.session = session;
462
+ this.persistSessionCursor(session);
463
+ let snapshotBoundary;
464
+ for (let index = turnEvents.length - 1; index >= 0; index -= 1) {
465
+ const event = turnEvents[index];
466
+ if (event && isTurnBoundary(event)) {
467
+ snapshotBoundary = event;
468
+ break;
469
+ }
470
+ }
471
+ if (snapshotBoundary) {
472
+ if (snapshotBoundary.type === "session.failed") {
473
+ throw new Error(snapshotBoundary.data.message || snapshotBoundary.data.code);
474
+ }
475
+ handlers.onComplete?.();
476
+ if (!rendered.trim()) throw new Error("Empty response from runtime");
477
+ return rendered.trim();
478
+ }
479
+ let streamIndex = snapshot.session.streamIndex;
480
+ for await (const event of session.stream({ signal })) {
481
+ if (signal.aborted) break;
482
+ rendered = applyMessageEvent(event, rendered, handlers);
483
+ streamIndex += 1;
484
+ savePersistedAgentSession(
485
+ this.visitorSessionId,
486
+ session.state.sessionId,
487
+ streamIndex,
488
+ this.storeOptions
489
+ );
490
+ if (isTurnBoundary(event)) break;
491
+ }
492
+ session = client.sessions.attach(session.state.sessionId, { streamIndex });
493
+ this.session = session;
494
+ this.persistSessionCursor(session);
495
+ if (!rendered.trim() && !signal.aborted) {
496
+ throw new Error("Empty response from runtime");
386
497
  }
387
498
  return rendered.trim();
388
499
  }
389
500
  cancelActive() {
390
- this.activeResponse?.cancel().catch(() => {
391
- });
501
+ if (this.activeResponse) {
502
+ this.activeResponse.cancel().catch(() => {
503
+ });
504
+ } else {
505
+ this.session?.cancel().catch(() => {
506
+ });
507
+ }
392
508
  }
393
509
  };
394
510
  function createAgentClient(options) {
@@ -422,6 +538,12 @@ function createAgentClient(options) {
422
538
  sendOptions.signal ?? new AbortController().signal,
423
539
  sendOptions.handlers
424
540
  ),
541
+ resumeTurn: (resumeOptions) => session.resumeTurn(
542
+ resumeOptions.message,
543
+ resumeOptions.signal ?? new AbortController().signal,
544
+ resumeOptions.handlers,
545
+ resumeOptions.initialText
546
+ ),
425
547
  reset: () => session.reset(),
426
548
  cancelActive: () => session.cancelActive(),
427
549
  getActiveSessionId: () => session.getActiveSessionId()
@@ -450,6 +572,58 @@ function formatAgentError(error) {
450
572
  return "Runtime request failed";
451
573
  }
452
574
 
575
+ // src/react/persisted-conversation.ts
576
+ var CONVERSATION_VERSION = 1;
577
+ function conversationKey(storageKeyPrefix, visitorSessionId) {
578
+ return `${storageKeyPrefix}:conversation:${visitorSessionId}`;
579
+ }
580
+ function parseMessage(value) {
581
+ if (typeof value !== "object" || value === null) return null;
582
+ const record = value;
583
+ if (typeof record.id !== "string" || record.role !== "agent" && record.role !== "visitor" || typeof record.text !== "string" || typeof record.createdAt !== "number" || !Number.isFinite(record.createdAt)) {
584
+ return null;
585
+ }
586
+ return {
587
+ id: record.id,
588
+ role: record.role,
589
+ text: record.text,
590
+ createdAt: record.createdAt
591
+ };
592
+ }
593
+ function loadPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
594
+ if (typeof sessionStorage === "undefined") return null;
595
+ const raw = sessionStorage.getItem(conversationKey(storageKeyPrefix, visitorSessionId));
596
+ if (!raw) return null;
597
+ try {
598
+ const value = JSON.parse(raw);
599
+ if (typeof value !== "object" || value === null) return null;
600
+ const record = value;
601
+ if (record.version !== CONVERSATION_VERSION || !Array.isArray(record.messages) || typeof record.pending !== "boolean" || typeof record.streamingText !== "string") {
602
+ return null;
603
+ }
604
+ const messages = record.messages.map(parseMessage);
605
+ if (messages.some((message) => message === null)) return null;
606
+ return {
607
+ messages: messages.filter((message) => message !== null),
608
+ pending: record.pending,
609
+ streamingText: record.streamingText
610
+ };
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+ function savePersistedAgentConversation(storageKeyPrefix, visitorSessionId, conversation) {
616
+ if (typeof sessionStorage === "undefined") return;
617
+ sessionStorage.setItem(
618
+ conversationKey(storageKeyPrefix, visitorSessionId),
619
+ JSON.stringify({ version: CONVERSATION_VERSION, ...conversation })
620
+ );
621
+ }
622
+ function clearPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
623
+ if (typeof sessionStorage === "undefined") return;
624
+ sessionStorage.removeItem(conversationKey(storageKeyPrefix, visitorSessionId));
625
+ }
626
+
453
627
  // src/react/hooks/useAgentChat.ts
454
628
  var GREETING_MESSAGE = {
455
629
  id: "greeting",
@@ -466,6 +640,15 @@ var INITIAL_STATE = {
466
640
  streamingText: "",
467
641
  error: null
468
642
  };
643
+ function stateFromConversation(conversation) {
644
+ if (!conversation || conversation.messages.length === 0) return INITIAL_STATE;
645
+ return {
646
+ ...INITIAL_STATE,
647
+ messages: conversation.messages,
648
+ phase: conversation.pending ? conversation.streamingText ? "streaming" : "thinking" : "complete",
649
+ streamingText: conversation.streamingText
650
+ };
651
+ }
469
652
  var STATUS_SEQUENCE = [
470
653
  { id: "s1", label: "Starting Eve session", ms: 400 },
471
654
  { id: "s2", label: "Connecting to runtime", ms: 500 }
@@ -497,8 +680,6 @@ function useAgentChat({
497
680
  visitorSessionId,
498
681
  storageKeyPrefix
499
682
  }) {
500
- const [state, setState] = (0, import_react.useState)(INITIAL_STATE);
501
- const runRef = (0, import_react.useRef)(null);
502
683
  const resolvedStorageKeyPrefix = (0, import_react.useMemo)(
503
684
  () => storageKeyPrefix?.trim() || buildAgentStorageKeyPrefix({ customerId, indexId, version, runtimeOrigin }),
504
685
  [customerId, indexId, runtimeOrigin, storageKeyPrefix, version]
@@ -507,6 +688,12 @@ function useAgentChat({
507
688
  () => visitorSessionId?.trim() || getOrCreateVisitorSessionId({ storageKeyPrefix: resolvedStorageKeyPrefix }),
508
689
  [resolvedStorageKeyPrefix, visitorSessionId]
509
690
  );
691
+ const [state, setState] = (0, import_react.useState)(
692
+ () => stateFromConversation(
693
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
694
+ )
695
+ );
696
+ const runRef = (0, import_react.useRef)(null);
510
697
  const clientRef = (0, import_react.useRef)(
511
698
  createAgentClient({
512
699
  customerId,
@@ -517,20 +704,15 @@ function useAgentChat({
517
704
  storageKeyPrefix: resolvedStorageKeyPrefix
518
705
  })
519
706
  );
520
- const identityRef = (0, import_react.useRef)(null);
521
707
  const identityKey = `${customerId ?? ""}|${indexId}|${version ?? "published"}|${runtimeOrigin ?? ""}|${resolvedStorageKeyPrefix}|${visitorId}`;
708
+ const identityRef = (0, import_react.useRef)(identityKey);
522
709
  (0, import_react.useEffect)(() => {
523
- if (identityRef.current === null) {
524
- identityRef.current = identityKey;
525
- return;
526
- }
527
710
  if (identityRef.current === identityKey) {
528
711
  return;
529
712
  }
530
713
  identityRef.current = identityKey;
531
714
  runRef.current?.abort();
532
715
  runRef.current = null;
533
- clientRef.current.reset();
534
716
  clientRef.current = createAgentClient({
535
717
  customerId,
536
718
  indexId,
@@ -539,81 +721,81 @@ function useAgentChat({
539
721
  visitorSessionId: visitorId,
540
722
  storageKeyPrefix: resolvedStorageKeyPrefix
541
723
  });
542
- setState(INITIAL_STATE);
724
+ setState(
725
+ stateFromConversation(
726
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
727
+ )
728
+ );
543
729
  }, [customerId, identityKey, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]);
730
+ (0, import_react.useEffect)(() => {
731
+ if (!hasVisitorMessages(state.messages)) return;
732
+ savePersistedAgentConversation(resolvedStorageKeyPrefix, visitorId, {
733
+ messages: state.messages,
734
+ pending: isAgentBusy(state.phase),
735
+ streamingText: state.streamingText
736
+ });
737
+ }, [resolvedStorageKeyPrefix, state.messages, state.phase, state.streamingText, visitorId]);
544
738
  const reset = (0, import_react.useCallback)(() => {
545
739
  runRef.current?.abort();
546
740
  runRef.current = null;
547
741
  clientRef.current.reset();
742
+ clearPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId);
548
743
  setState(INITIAL_STATE);
549
- }, []);
550
- const submit = (0, import_react.useCallback)(
551
- async (visitorText) => {
552
- runRef.current?.abort();
553
- clientRef.current.cancelActive();
554
- const controller = new AbortController();
555
- runRef.current = controller;
744
+ }, [resolvedStorageKeyPrefix, visitorId]);
745
+ const runTurn = (0, import_react.useCallback)(
746
+ async (input) => {
747
+ const { controller, initialText = "", resume, visitorText } = input;
556
748
  const { signal } = controller;
557
749
  const isActiveRun = () => runRef.current === controller && !signal.aborted;
558
- const visitorMessage = {
559
- id: `visitor-${Date.now()}`,
560
- role: "visitor",
561
- text: visitorText,
562
- createdAt: Date.now()
563
- };
564
- setState((prev) => ({
565
- ...prev,
566
- phase: "thinking",
567
- messages: [...prev.messages, visitorMessage],
568
- toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
569
- journey: null,
570
- followUps: [],
571
- streamingText: "",
572
- error: null
573
- }));
574
750
  try {
575
- let streamStarted = false;
576
- const planningPromise = runStatusSequence(signal, (step) => {
751
+ let streamStarted = Boolean(initialText);
752
+ let streamed = initialText;
753
+ const planningPromise = resume ? Promise.resolve() : runStatusSequence(signal, (step) => {
577
754
  if (streamStarted || !isActiveRun()) return;
578
755
  setState((prev) => ({ ...prev, phase: "running-tools", toolSteps: [step] }));
579
756
  });
580
- let streamed = "";
581
- const finalText = await clientRef.current.sendTurn(visitorText, {
582
- signal,
583
- handlers: {
584
- onStep: (label, detail) => {
585
- if (streamStarted || !isActiveRun()) return;
757
+ const handlers = {
758
+ onStep: (label, detail) => {
759
+ if (streamStarted || !isActiveRun()) return;
760
+ setState((prev) => ({
761
+ ...prev,
762
+ phase: "running-tools",
763
+ toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
764
+ }));
765
+ },
766
+ onDelta: (delta) => {
767
+ if (!isActiveRun()) return;
768
+ void planningPromise.catch(() => {
769
+ });
770
+ if (!streamStarted) {
771
+ streamStarted = true;
586
772
  setState((prev) => ({
587
773
  ...prev,
588
- phase: "running-tools",
589
- toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
774
+ phase: "streaming",
775
+ toolSteps: [],
776
+ streamingText: ""
590
777
  }));
591
- },
592
- onDelta: (delta) => {
593
- if (!isActiveRun()) return;
594
- void planningPromise.catch(() => {
595
- });
596
- if (!streamStarted) {
597
- streamStarted = true;
598
- setState((prev) => ({
599
- ...prev,
600
- phase: "streaming",
601
- toolSteps: [],
602
- streamingText: ""
603
- }));
604
- }
605
- streamed += delta;
606
- setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
607
- },
608
- onComplete: () => {
609
- if (!isActiveRun()) return;
610
- streamStarted = true;
611
778
  }
779
+ streamed += delta;
780
+ setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
781
+ },
782
+ onComplete: () => {
783
+ if (!isActiveRun()) return;
784
+ streamStarted = true;
612
785
  }
613
- });
786
+ };
787
+ let finalText = resume ? await clientRef.current.resumeTurn({
788
+ handlers,
789
+ initialText,
790
+ message: visitorText,
791
+ signal
792
+ }) : await clientRef.current.sendTurn(visitorText, { handlers, signal });
793
+ if (resume && finalText === null) {
794
+ finalText = await clientRef.current.sendTurn(visitorText, { handlers, signal });
795
+ }
614
796
  await planningPromise.catch(() => {
615
797
  });
616
- if (!isActiveRun()) return;
798
+ if (!isActiveRun() || finalText === null) return;
617
799
  const agentMessage = {
618
800
  id: `agent-${Date.now()}`,
619
801
  role: "agent",
@@ -629,6 +811,7 @@ function useAgentChat({
629
811
  followUps: [],
630
812
  journey: null
631
813
  }));
814
+ runRef.current = null;
632
815
  } catch (error) {
633
816
  if (error instanceof DOMException && error.name === "AbortError") return;
634
817
  if (!isActiveRun()) return;
@@ -641,14 +824,66 @@ function useAgentChat({
641
824
  streamingText: "",
642
825
  error: message
643
826
  }));
827
+ runRef.current = null;
644
828
  }
645
829
  },
646
- [customerId, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]
830
+ []
647
831
  );
832
+ const submit = (0, import_react.useCallback)(
833
+ async (visitorText) => {
834
+ if (runRef.current) {
835
+ runRef.current.abort();
836
+ clientRef.current.cancelActive();
837
+ }
838
+ const controller = new AbortController();
839
+ runRef.current = controller;
840
+ const visitorMessage = {
841
+ id: `visitor-${Date.now()}`,
842
+ role: "visitor",
843
+ text: visitorText,
844
+ createdAt: Date.now()
845
+ };
846
+ setState((prev) => ({
847
+ ...prev,
848
+ phase: "thinking",
849
+ messages: [...prev.messages, visitorMessage],
850
+ toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
851
+ journey: null,
852
+ followUps: [],
853
+ streamingText: "",
854
+ error: null
855
+ }));
856
+ await runTurn({ controller, resume: false, visitorText });
857
+ },
858
+ [runTurn]
859
+ );
860
+ (0, import_react.useEffect)(() => {
861
+ const conversation = loadPersistedAgentConversation(
862
+ resolvedStorageKeyPrefix,
863
+ visitorId
864
+ );
865
+ if (!conversation?.pending) return;
866
+ const visitorMessage = [...conversation.messages].reverse().find((message) => message.role === "visitor");
867
+ if (!visitorMessage) return;
868
+ const controller = new AbortController();
869
+ runRef.current = controller;
870
+ void runTurn({
871
+ controller,
872
+ initialText: conversation.streamingText,
873
+ resume: true,
874
+ visitorText: visitorMessage.text
875
+ });
876
+ return () => {
877
+ if (runRef.current === controller) {
878
+ runRef.current = null;
879
+ }
880
+ controller.abort();
881
+ };
882
+ }, [identityKey, resolvedStorageKeyPrefix, runTurn, visitorId]);
648
883
  (0, import_react.useEffect)(() => {
649
884
  return () => {
650
885
  runRef.current?.abort();
651
- clientRef.current.cancelActive();
886
+ runRef.current = null;
652
887
  };
653
888
  }, []);
654
889
  return {
@@ -669,6 +904,9 @@ function createIdleSuggestions() {
669
904
  { id: "idle-3", label: "What should I ask you?" }
670
905
  ];
671
906
  }
907
+ function isAgentBusy(phase) {
908
+ return phase === "thinking" || phase === "running-tools" || phase === "streaming";
909
+ }
672
910
 
673
911
  // src/react/hooks/useIsMobile.ts
674
912
  var import_react2 = require("react");