@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/react.cjs CHANGED
@@ -80,7 +80,8 @@ function createAgentRuntimeCapability(options) {
80
80
  const response = await fetchImplementation(`${options.runtimeOrigin}/webless/v1/bootstrap`, {
81
81
  body: JSON.stringify({
82
82
  clientSessionId: options.visitorSessionId,
83
- indexId: options.indexId
83
+ indexId: options.indexId,
84
+ version: options.version
84
85
  }),
85
86
  headers: { "content-type": "application/json" },
86
87
  method: "POST"
@@ -221,6 +222,54 @@ function clearPersistedAgentSession(visitorSessionId, options) {
221
222
  }
222
223
 
223
224
  // src/runtime/client.ts
225
+ function isTurnBoundary(event) {
226
+ return event.type === "session.waiting" || event.type === "session.completed" || event.type === "session.failed";
227
+ }
228
+ function applyMessageEvent(event, rendered, handlers) {
229
+ const step = mapStepLabel(event);
230
+ if (step) handlers.onStep?.(step.label, step.detail);
231
+ if (event.type === "session.failed") {
232
+ throw new Error(event.data.message || event.data.code);
233
+ }
234
+ if (event.type === "message.completed") {
235
+ handlers.onComplete?.();
236
+ }
237
+ if (event.type !== "message.appended") return rendered;
238
+ const { messageDelta, messageSoFar } = event.data;
239
+ let delta = messageDelta;
240
+ let next = rendered;
241
+ if (messageSoFar.startsWith(rendered)) {
242
+ delta = messageSoFar.slice(rendered.length);
243
+ next = messageSoFar;
244
+ } else if (messageDelta) {
245
+ next += messageDelta;
246
+ }
247
+ if (delta) handlers.onDelta(delta);
248
+ return next;
249
+ }
250
+ function latestTurnEvents(events) {
251
+ let startIndex = -1;
252
+ for (let index = events.length - 1; index >= 0; index -= 1) {
253
+ if (events[index]?.type === "message.received") {
254
+ startIndex = index;
255
+ break;
256
+ }
257
+ }
258
+ return startIndex >= 0 ? events.slice(startIndex) : [];
259
+ }
260
+ function renderTurn(events) {
261
+ let rendered = "";
262
+ for (const event of events) {
263
+ if (event.type !== "message.appended") continue;
264
+ const { messageDelta, messageSoFar } = event.data;
265
+ if (messageSoFar.startsWith(rendered)) {
266
+ rendered = messageSoFar;
267
+ } else if (messageDelta) {
268
+ rendered += messageDelta;
269
+ }
270
+ }
271
+ return rendered;
272
+ }
224
273
  function mapStepLabel(event) {
225
274
  if (event.type !== "step.started") return null;
226
275
  const stepIndex = event.data.stepIndex;
@@ -239,6 +288,7 @@ var AgentSession = class {
239
288
  this.capability = createAgentRuntimeCapability({
240
289
  indexId,
241
290
  runtimeOrigin,
291
+ version,
242
292
  visitorSessionId
243
293
  });
244
294
  }
@@ -256,8 +306,13 @@ var AgentSession = class {
256
306
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
257
307
  }
258
308
  reset() {
259
- void this.activeResponse?.cancel().catch(() => {
260
- });
309
+ if (this.activeResponse) {
310
+ void this.activeResponse.cancel().catch(() => {
311
+ });
312
+ } else {
313
+ void this.session?.cancel().catch(() => {
314
+ });
315
+ }
261
316
  this.activeResponse = void 0;
262
317
  this.session = void 0;
263
318
  clearPersistedAgentSession(this.visitorSessionId, this.storeOptions);
@@ -282,7 +337,8 @@ var AgentSession = class {
282
337
  if (this.client && this.clientHost === config.host) {
283
338
  return this.client;
284
339
  }
285
- this.reset();
340
+ this.activeResponse = void 0;
341
+ this.session = void 0;
286
342
  this.client = new import_client2.Client({
287
343
  auth: { bearer: () => this.capability.getAccessToken() },
288
344
  host: config.host,
@@ -331,28 +387,20 @@ var AgentSession = class {
331
387
  this.persistSessionCursor(session);
332
388
  }
333
389
  this.activeResponse = response;
390
+ let streamIndex = session?.state.streamIndex ?? 0;
334
391
  let rendered = "";
335
392
  try {
336
393
  for await (const event of response) {
337
394
  if (signal.aborted) break;
338
- const step = mapStepLabel(event);
339
- if (step) handlers.onStep?.(step.label, step.detail);
340
- if (event.type === "message.appended") {
341
- const { messageDelta, messageSoFar } = event.data;
342
- let delta = messageDelta;
343
- if (messageSoFar.startsWith(rendered)) {
344
- delta = messageSoFar.slice(rendered.length);
345
- rendered = messageSoFar;
346
- } else if (messageDelta) {
347
- rendered += messageDelta;
348
- }
349
- if (delta) handlers.onDelta(delta);
350
- }
351
- if (event.type === "message.completed") {
352
- handlers.onComplete?.();
353
- }
354
- if (event.type === "session.failed") {
355
- throw new Error(event.data.message || event.data.code);
395
+ rendered = applyMessageEvent(event, rendered, handlers);
396
+ streamIndex += 1;
397
+ if (session) {
398
+ savePersistedAgentSession(
399
+ this.visitorSessionId,
400
+ session.state.sessionId,
401
+ streamIndex,
402
+ this.storeOptions
403
+ );
356
404
  }
357
405
  }
358
406
  } finally {
@@ -364,15 +412,83 @@ var AgentSession = class {
364
412
  if (!rendered.trim() && !signal.aborted) {
365
413
  throw new Error("Empty response from runtime");
366
414
  }
367
- if (signal.aborted) {
368
- await response.cancel().catch(() => {
369
- });
415
+ return rendered.trim();
416
+ }
417
+ async resumeTurn(message, signal, handlers, initialText = "") {
418
+ const persisted = loadPersistedAgentSession(this.visitorSessionId, this.storeOptions);
419
+ if (!persisted) return null;
420
+ const client = this.ensureClient();
421
+ const attached = client.sessions.attach(persisted.sessionId, {
422
+ streamIndex: persisted.streamIndex
423
+ });
424
+ const snapshot = await withCapabilityRefresh(
425
+ this.capability,
426
+ () => attached.snapshot({ signal })
427
+ );
428
+ const turnEvents = latestTurnEvents(snapshot.events);
429
+ const received = turnEvents[0];
430
+ if (received?.type !== "message.received" || received.data.message !== message) {
431
+ return null;
432
+ }
433
+ let rendered = renderTurn(turnEvents);
434
+ if (rendered.startsWith(initialText)) {
435
+ const missedText = rendered.slice(initialText.length);
436
+ if (missedText) handlers.onDelta(missedText);
437
+ } else if (initialText.startsWith(rendered)) {
438
+ rendered = initialText;
439
+ } else if (!initialText.startsWith(rendered)) {
440
+ rendered = initialText + rendered;
441
+ }
442
+ let session = client.sessions.attach(snapshot.session.sessionId, {
443
+ streamIndex: snapshot.session.streamIndex
444
+ });
445
+ this.session = session;
446
+ this.persistSessionCursor(session);
447
+ let snapshotBoundary;
448
+ for (let index = turnEvents.length - 1; index >= 0; index -= 1) {
449
+ const event = turnEvents[index];
450
+ if (event && isTurnBoundary(event)) {
451
+ snapshotBoundary = event;
452
+ break;
453
+ }
454
+ }
455
+ if (snapshotBoundary) {
456
+ if (snapshotBoundary.type === "session.failed") {
457
+ throw new Error(snapshotBoundary.data.message || snapshotBoundary.data.code);
458
+ }
459
+ handlers.onComplete?.();
460
+ if (!rendered.trim()) throw new Error("Empty response from runtime");
461
+ return rendered.trim();
462
+ }
463
+ let streamIndex = snapshot.session.streamIndex;
464
+ for await (const event of session.stream({ signal })) {
465
+ if (signal.aborted) break;
466
+ rendered = applyMessageEvent(event, rendered, handlers);
467
+ streamIndex += 1;
468
+ savePersistedAgentSession(
469
+ this.visitorSessionId,
470
+ session.state.sessionId,
471
+ streamIndex,
472
+ this.storeOptions
473
+ );
474
+ if (isTurnBoundary(event)) break;
475
+ }
476
+ session = client.sessions.attach(session.state.sessionId, { streamIndex });
477
+ this.session = session;
478
+ this.persistSessionCursor(session);
479
+ if (!rendered.trim() && !signal.aborted) {
480
+ throw new Error("Empty response from runtime");
370
481
  }
371
482
  return rendered.trim();
372
483
  }
373
484
  cancelActive() {
374
- this.activeResponse?.cancel().catch(() => {
375
- });
485
+ if (this.activeResponse) {
486
+ this.activeResponse.cancel().catch(() => {
487
+ });
488
+ } else {
489
+ this.session?.cancel().catch(() => {
490
+ });
491
+ }
376
492
  }
377
493
  };
378
494
  function createAgentClient(options) {
@@ -406,6 +522,12 @@ function createAgentClient(options) {
406
522
  sendOptions.signal ?? new AbortController().signal,
407
523
  sendOptions.handlers
408
524
  ),
525
+ resumeTurn: (resumeOptions) => session.resumeTurn(
526
+ resumeOptions.message,
527
+ resumeOptions.signal ?? new AbortController().signal,
528
+ resumeOptions.handlers,
529
+ resumeOptions.initialText
530
+ ),
409
531
  reset: () => session.reset(),
410
532
  cancelActive: () => session.cancelActive(),
411
533
  getActiveSessionId: () => session.getActiveSessionId()
@@ -434,6 +556,58 @@ function formatAgentError(error) {
434
556
  return "Runtime request failed";
435
557
  }
436
558
 
559
+ // src/react/persisted-conversation.ts
560
+ var CONVERSATION_VERSION = 1;
561
+ function conversationKey(storageKeyPrefix, visitorSessionId) {
562
+ return `${storageKeyPrefix}:conversation:${visitorSessionId}`;
563
+ }
564
+ function parseMessage(value) {
565
+ if (typeof value !== "object" || value === null) return null;
566
+ const record = value;
567
+ if (typeof record.id !== "string" || record.role !== "agent" && record.role !== "visitor" || typeof record.text !== "string" || typeof record.createdAt !== "number" || !Number.isFinite(record.createdAt)) {
568
+ return null;
569
+ }
570
+ return {
571
+ id: record.id,
572
+ role: record.role,
573
+ text: record.text,
574
+ createdAt: record.createdAt
575
+ };
576
+ }
577
+ function loadPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
578
+ if (typeof sessionStorage === "undefined") return null;
579
+ const raw = sessionStorage.getItem(conversationKey(storageKeyPrefix, visitorSessionId));
580
+ if (!raw) return null;
581
+ try {
582
+ const value = JSON.parse(raw);
583
+ if (typeof value !== "object" || value === null) return null;
584
+ const record = value;
585
+ if (record.version !== CONVERSATION_VERSION || !Array.isArray(record.messages) || typeof record.pending !== "boolean" || typeof record.streamingText !== "string") {
586
+ return null;
587
+ }
588
+ const messages = record.messages.map(parseMessage);
589
+ if (messages.some((message) => message === null)) return null;
590
+ return {
591
+ messages: messages.filter((message) => message !== null),
592
+ pending: record.pending,
593
+ streamingText: record.streamingText
594
+ };
595
+ } catch {
596
+ return null;
597
+ }
598
+ }
599
+ function savePersistedAgentConversation(storageKeyPrefix, visitorSessionId, conversation) {
600
+ if (typeof sessionStorage === "undefined") return;
601
+ sessionStorage.setItem(
602
+ conversationKey(storageKeyPrefix, visitorSessionId),
603
+ JSON.stringify({ version: CONVERSATION_VERSION, ...conversation })
604
+ );
605
+ }
606
+ function clearPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
607
+ if (typeof sessionStorage === "undefined") return;
608
+ sessionStorage.removeItem(conversationKey(storageKeyPrefix, visitorSessionId));
609
+ }
610
+
437
611
  // src/react/hooks/useAgentChat.ts
438
612
  var GREETING_MESSAGE = {
439
613
  id: "greeting",
@@ -450,6 +624,15 @@ var INITIAL_STATE = {
450
624
  streamingText: "",
451
625
  error: null
452
626
  };
627
+ function stateFromConversation(conversation) {
628
+ if (!conversation || conversation.messages.length === 0) return INITIAL_STATE;
629
+ return {
630
+ ...INITIAL_STATE,
631
+ messages: conversation.messages,
632
+ phase: conversation.pending ? conversation.streamingText ? "streaming" : "thinking" : "complete",
633
+ streamingText: conversation.streamingText
634
+ };
635
+ }
453
636
  var STATUS_SEQUENCE = [
454
637
  { id: "s1", label: "Starting Eve session", ms: 400 },
455
638
  { id: "s2", label: "Connecting to runtime", ms: 500 }
@@ -481,8 +664,6 @@ function useAgentChat({
481
664
  visitorSessionId,
482
665
  storageKeyPrefix
483
666
  }) {
484
- const [state, setState] = (0, import_react.useState)(INITIAL_STATE);
485
- const runRef = (0, import_react.useRef)(null);
486
667
  const resolvedStorageKeyPrefix = (0, import_react.useMemo)(
487
668
  () => storageKeyPrefix?.trim() || buildAgentStorageKeyPrefix({ customerId, indexId, version, runtimeOrigin }),
488
669
  [customerId, indexId, runtimeOrigin, storageKeyPrefix, version]
@@ -491,6 +672,12 @@ function useAgentChat({
491
672
  () => visitorSessionId?.trim() || getOrCreateVisitorSessionId({ storageKeyPrefix: resolvedStorageKeyPrefix }),
492
673
  [resolvedStorageKeyPrefix, visitorSessionId]
493
674
  );
675
+ const [state, setState] = (0, import_react.useState)(
676
+ () => stateFromConversation(
677
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
678
+ )
679
+ );
680
+ const runRef = (0, import_react.useRef)(null);
494
681
  const clientRef = (0, import_react.useRef)(
495
682
  createAgentClient({
496
683
  customerId,
@@ -501,20 +688,15 @@ function useAgentChat({
501
688
  storageKeyPrefix: resolvedStorageKeyPrefix
502
689
  })
503
690
  );
504
- const identityRef = (0, import_react.useRef)(null);
505
691
  const identityKey = `${customerId ?? ""}|${indexId}|${version ?? "published"}|${runtimeOrigin ?? ""}|${resolvedStorageKeyPrefix}|${visitorId}`;
692
+ const identityRef = (0, import_react.useRef)(identityKey);
506
693
  (0, import_react.useEffect)(() => {
507
- if (identityRef.current === null) {
508
- identityRef.current = identityKey;
509
- return;
510
- }
511
694
  if (identityRef.current === identityKey) {
512
695
  return;
513
696
  }
514
697
  identityRef.current = identityKey;
515
698
  runRef.current?.abort();
516
699
  runRef.current = null;
517
- clientRef.current.reset();
518
700
  clientRef.current = createAgentClient({
519
701
  customerId,
520
702
  indexId,
@@ -523,81 +705,81 @@ function useAgentChat({
523
705
  visitorSessionId: visitorId,
524
706
  storageKeyPrefix: resolvedStorageKeyPrefix
525
707
  });
526
- setState(INITIAL_STATE);
708
+ setState(
709
+ stateFromConversation(
710
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
711
+ )
712
+ );
527
713
  }, [customerId, identityKey, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]);
714
+ (0, import_react.useEffect)(() => {
715
+ if (!hasVisitorMessages(state.messages)) return;
716
+ savePersistedAgentConversation(resolvedStorageKeyPrefix, visitorId, {
717
+ messages: state.messages,
718
+ pending: isAgentBusy(state.phase),
719
+ streamingText: state.streamingText
720
+ });
721
+ }, [resolvedStorageKeyPrefix, state.messages, state.phase, state.streamingText, visitorId]);
528
722
  const reset = (0, import_react.useCallback)(() => {
529
723
  runRef.current?.abort();
530
724
  runRef.current = null;
531
725
  clientRef.current.reset();
726
+ clearPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId);
532
727
  setState(INITIAL_STATE);
533
- }, []);
534
- const submit = (0, import_react.useCallback)(
535
- async (visitorText) => {
536
- runRef.current?.abort();
537
- clientRef.current.cancelActive();
538
- const controller = new AbortController();
539
- runRef.current = controller;
728
+ }, [resolvedStorageKeyPrefix, visitorId]);
729
+ const runTurn = (0, import_react.useCallback)(
730
+ async (input) => {
731
+ const { controller, initialText = "", resume, visitorText } = input;
540
732
  const { signal } = controller;
541
733
  const isActiveRun = () => runRef.current === controller && !signal.aborted;
542
- const visitorMessage = {
543
- id: `visitor-${Date.now()}`,
544
- role: "visitor",
545
- text: visitorText,
546
- createdAt: Date.now()
547
- };
548
- setState((prev) => ({
549
- ...prev,
550
- phase: "thinking",
551
- messages: [...prev.messages, visitorMessage],
552
- toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
553
- journey: null,
554
- followUps: [],
555
- streamingText: "",
556
- error: null
557
- }));
558
734
  try {
559
- let streamStarted = false;
560
- const planningPromise = runStatusSequence(signal, (step) => {
735
+ let streamStarted = Boolean(initialText);
736
+ let streamed = initialText;
737
+ const planningPromise = resume ? Promise.resolve() : runStatusSequence(signal, (step) => {
561
738
  if (streamStarted || !isActiveRun()) return;
562
739
  setState((prev) => ({ ...prev, phase: "running-tools", toolSteps: [step] }));
563
740
  });
564
- let streamed = "";
565
- const finalText = await clientRef.current.sendTurn(visitorText, {
566
- signal,
567
- handlers: {
568
- onStep: (label, detail) => {
569
- if (streamStarted || !isActiveRun()) return;
741
+ const handlers = {
742
+ onStep: (label, detail) => {
743
+ if (streamStarted || !isActiveRun()) return;
744
+ setState((prev) => ({
745
+ ...prev,
746
+ phase: "running-tools",
747
+ toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
748
+ }));
749
+ },
750
+ onDelta: (delta) => {
751
+ if (!isActiveRun()) return;
752
+ void planningPromise.catch(() => {
753
+ });
754
+ if (!streamStarted) {
755
+ streamStarted = true;
570
756
  setState((prev) => ({
571
757
  ...prev,
572
- phase: "running-tools",
573
- toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
758
+ phase: "streaming",
759
+ toolSteps: [],
760
+ streamingText: ""
574
761
  }));
575
- },
576
- onDelta: (delta) => {
577
- if (!isActiveRun()) return;
578
- void planningPromise.catch(() => {
579
- });
580
- if (!streamStarted) {
581
- streamStarted = true;
582
- setState((prev) => ({
583
- ...prev,
584
- phase: "streaming",
585
- toolSteps: [],
586
- streamingText: ""
587
- }));
588
- }
589
- streamed += delta;
590
- setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
591
- },
592
- onComplete: () => {
593
- if (!isActiveRun()) return;
594
- streamStarted = true;
595
762
  }
763
+ streamed += delta;
764
+ setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
765
+ },
766
+ onComplete: () => {
767
+ if (!isActiveRun()) return;
768
+ streamStarted = true;
596
769
  }
597
- });
770
+ };
771
+ let finalText = resume ? await clientRef.current.resumeTurn({
772
+ handlers,
773
+ initialText,
774
+ message: visitorText,
775
+ signal
776
+ }) : await clientRef.current.sendTurn(visitorText, { handlers, signal });
777
+ if (resume && finalText === null) {
778
+ finalText = await clientRef.current.sendTurn(visitorText, { handlers, signal });
779
+ }
598
780
  await planningPromise.catch(() => {
599
781
  });
600
- if (!isActiveRun()) return;
782
+ if (!isActiveRun() || finalText === null) return;
601
783
  const agentMessage = {
602
784
  id: `agent-${Date.now()}`,
603
785
  role: "agent",
@@ -613,6 +795,7 @@ function useAgentChat({
613
795
  followUps: [],
614
796
  journey: null
615
797
  }));
798
+ runRef.current = null;
616
799
  } catch (error) {
617
800
  if (error instanceof DOMException && error.name === "AbortError") return;
618
801
  if (!isActiveRun()) return;
@@ -625,14 +808,66 @@ function useAgentChat({
625
808
  streamingText: "",
626
809
  error: message
627
810
  }));
811
+ runRef.current = null;
628
812
  }
629
813
  },
630
- [customerId, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]
814
+ []
631
815
  );
816
+ const submit = (0, import_react.useCallback)(
817
+ async (visitorText) => {
818
+ if (runRef.current) {
819
+ runRef.current.abort();
820
+ clientRef.current.cancelActive();
821
+ }
822
+ const controller = new AbortController();
823
+ runRef.current = controller;
824
+ const visitorMessage = {
825
+ id: `visitor-${Date.now()}`,
826
+ role: "visitor",
827
+ text: visitorText,
828
+ createdAt: Date.now()
829
+ };
830
+ setState((prev) => ({
831
+ ...prev,
832
+ phase: "thinking",
833
+ messages: [...prev.messages, visitorMessage],
834
+ toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
835
+ journey: null,
836
+ followUps: [],
837
+ streamingText: "",
838
+ error: null
839
+ }));
840
+ await runTurn({ controller, resume: false, visitorText });
841
+ },
842
+ [runTurn]
843
+ );
844
+ (0, import_react.useEffect)(() => {
845
+ const conversation = loadPersistedAgentConversation(
846
+ resolvedStorageKeyPrefix,
847
+ visitorId
848
+ );
849
+ if (!conversation?.pending) return;
850
+ const visitorMessage = [...conversation.messages].reverse().find((message) => message.role === "visitor");
851
+ if (!visitorMessage) return;
852
+ const controller = new AbortController();
853
+ runRef.current = controller;
854
+ void runTurn({
855
+ controller,
856
+ initialText: conversation.streamingText,
857
+ resume: true,
858
+ visitorText: visitorMessage.text
859
+ });
860
+ return () => {
861
+ if (runRef.current === controller) {
862
+ runRef.current = null;
863
+ }
864
+ controller.abort();
865
+ };
866
+ }, [identityKey, resolvedStorageKeyPrefix, runTurn, visitorId]);
632
867
  (0, import_react.useEffect)(() => {
633
868
  return () => {
634
869
  runRef.current?.abort();
635
- clientRef.current.cancelActive();
870
+ runRef.current = null;
636
871
  };
637
872
  }, []);
638
873
  return {