@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.
@@ -45,7 +45,8 @@ function createAgentRuntimeCapability(options) {
45
45
  const response = await fetchImplementation(`${options.runtimeOrigin}/webless/v1/bootstrap`, {
46
46
  body: JSON.stringify({
47
47
  clientSessionId: options.visitorSessionId,
48
- indexId: options.indexId
48
+ indexId: options.indexId,
49
+ version: options.version
49
50
  }),
50
51
  headers: { "content-type": "application/json" },
51
52
  method: "POST"
@@ -186,6 +187,54 @@ function clearPersistedAgentSession(visitorSessionId, options) {
186
187
  }
187
188
 
188
189
  // src/runtime/client.ts
190
+ function isTurnBoundary(event) {
191
+ return event.type === "session.waiting" || event.type === "session.completed" || event.type === "session.failed";
192
+ }
193
+ function applyMessageEvent(event, rendered, handlers) {
194
+ const step = mapStepLabel(event);
195
+ if (step) handlers.onStep?.(step.label, step.detail);
196
+ if (event.type === "session.failed") {
197
+ throw new Error(event.data.message || event.data.code);
198
+ }
199
+ if (event.type === "message.completed") {
200
+ handlers.onComplete?.();
201
+ }
202
+ if (event.type !== "message.appended") return rendered;
203
+ const { messageDelta, messageSoFar } = event.data;
204
+ let delta = messageDelta;
205
+ let next = rendered;
206
+ if (messageSoFar.startsWith(rendered)) {
207
+ delta = messageSoFar.slice(rendered.length);
208
+ next = messageSoFar;
209
+ } else if (messageDelta) {
210
+ next += messageDelta;
211
+ }
212
+ if (delta) handlers.onDelta(delta);
213
+ return next;
214
+ }
215
+ function latestTurnEvents(events) {
216
+ let startIndex = -1;
217
+ for (let index = events.length - 1; index >= 0; index -= 1) {
218
+ if (events[index]?.type === "message.received") {
219
+ startIndex = index;
220
+ break;
221
+ }
222
+ }
223
+ return startIndex >= 0 ? events.slice(startIndex) : [];
224
+ }
225
+ function renderTurn(events) {
226
+ let rendered = "";
227
+ for (const event of events) {
228
+ if (event.type !== "message.appended") continue;
229
+ const { messageDelta, messageSoFar } = event.data;
230
+ if (messageSoFar.startsWith(rendered)) {
231
+ rendered = messageSoFar;
232
+ } else if (messageDelta) {
233
+ rendered += messageDelta;
234
+ }
235
+ }
236
+ return rendered;
237
+ }
189
238
  function mapStepLabel(event) {
190
239
  if (event.type !== "step.started") return null;
191
240
  const stepIndex = event.data.stepIndex;
@@ -204,6 +253,7 @@ var AgentSession = class {
204
253
  this.capability = createAgentRuntimeCapability({
205
254
  indexId,
206
255
  runtimeOrigin,
256
+ version,
207
257
  visitorSessionId
208
258
  });
209
259
  }
@@ -221,8 +271,13 @@ var AgentSession = class {
221
271
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
222
272
  }
223
273
  reset() {
224
- void this.activeResponse?.cancel().catch(() => {
225
- });
274
+ if (this.activeResponse) {
275
+ void this.activeResponse.cancel().catch(() => {
276
+ });
277
+ } else {
278
+ void this.session?.cancel().catch(() => {
279
+ });
280
+ }
226
281
  this.activeResponse = void 0;
227
282
  this.session = void 0;
228
283
  clearPersistedAgentSession(this.visitorSessionId, this.storeOptions);
@@ -247,7 +302,8 @@ var AgentSession = class {
247
302
  if (this.client && this.clientHost === config.host) {
248
303
  return this.client;
249
304
  }
250
- this.reset();
305
+ this.activeResponse = void 0;
306
+ this.session = void 0;
251
307
  this.client = new Client({
252
308
  auth: { bearer: () => this.capability.getAccessToken() },
253
309
  host: config.host,
@@ -296,28 +352,20 @@ var AgentSession = class {
296
352
  this.persistSessionCursor(session);
297
353
  }
298
354
  this.activeResponse = response;
355
+ let streamIndex = session?.state.streamIndex ?? 0;
299
356
  let rendered = "";
300
357
  try {
301
358
  for await (const event of response) {
302
359
  if (signal.aborted) break;
303
- const step = mapStepLabel(event);
304
- if (step) handlers.onStep?.(step.label, step.detail);
305
- if (event.type === "message.appended") {
306
- const { messageDelta, messageSoFar } = event.data;
307
- let delta = messageDelta;
308
- if (messageSoFar.startsWith(rendered)) {
309
- delta = messageSoFar.slice(rendered.length);
310
- rendered = messageSoFar;
311
- } else if (messageDelta) {
312
- rendered += messageDelta;
313
- }
314
- if (delta) handlers.onDelta(delta);
315
- }
316
- if (event.type === "message.completed") {
317
- handlers.onComplete?.();
318
- }
319
- if (event.type === "session.failed") {
320
- throw new Error(event.data.message || event.data.code);
360
+ rendered = applyMessageEvent(event, rendered, handlers);
361
+ streamIndex += 1;
362
+ if (session) {
363
+ savePersistedAgentSession(
364
+ this.visitorSessionId,
365
+ session.state.sessionId,
366
+ streamIndex,
367
+ this.storeOptions
368
+ );
321
369
  }
322
370
  }
323
371
  } finally {
@@ -329,15 +377,83 @@ var AgentSession = class {
329
377
  if (!rendered.trim() && !signal.aborted) {
330
378
  throw new Error("Empty response from runtime");
331
379
  }
332
- if (signal.aborted) {
333
- await response.cancel().catch(() => {
334
- });
380
+ return rendered.trim();
381
+ }
382
+ async resumeTurn(message, signal, handlers, initialText = "") {
383
+ const persisted = loadPersistedAgentSession(this.visitorSessionId, this.storeOptions);
384
+ if (!persisted) return null;
385
+ const client = this.ensureClient();
386
+ const attached = client.sessions.attach(persisted.sessionId, {
387
+ streamIndex: persisted.streamIndex
388
+ });
389
+ const snapshot = await withCapabilityRefresh(
390
+ this.capability,
391
+ () => attached.snapshot({ signal })
392
+ );
393
+ const turnEvents = latestTurnEvents(snapshot.events);
394
+ const received = turnEvents[0];
395
+ if (received?.type !== "message.received" || received.data.message !== message) {
396
+ return null;
397
+ }
398
+ let rendered = renderTurn(turnEvents);
399
+ if (rendered.startsWith(initialText)) {
400
+ const missedText = rendered.slice(initialText.length);
401
+ if (missedText) handlers.onDelta(missedText);
402
+ } else if (initialText.startsWith(rendered)) {
403
+ rendered = initialText;
404
+ } else if (!initialText.startsWith(rendered)) {
405
+ rendered = initialText + rendered;
406
+ }
407
+ let session = client.sessions.attach(snapshot.session.sessionId, {
408
+ streamIndex: snapshot.session.streamIndex
409
+ });
410
+ this.session = session;
411
+ this.persistSessionCursor(session);
412
+ let snapshotBoundary;
413
+ for (let index = turnEvents.length - 1; index >= 0; index -= 1) {
414
+ const event = turnEvents[index];
415
+ if (event && isTurnBoundary(event)) {
416
+ snapshotBoundary = event;
417
+ break;
418
+ }
419
+ }
420
+ if (snapshotBoundary) {
421
+ if (snapshotBoundary.type === "session.failed") {
422
+ throw new Error(snapshotBoundary.data.message || snapshotBoundary.data.code);
423
+ }
424
+ handlers.onComplete?.();
425
+ if (!rendered.trim()) throw new Error("Empty response from runtime");
426
+ return rendered.trim();
427
+ }
428
+ let streamIndex = snapshot.session.streamIndex;
429
+ for await (const event of session.stream({ signal })) {
430
+ if (signal.aborted) break;
431
+ rendered = applyMessageEvent(event, rendered, handlers);
432
+ streamIndex += 1;
433
+ savePersistedAgentSession(
434
+ this.visitorSessionId,
435
+ session.state.sessionId,
436
+ streamIndex,
437
+ this.storeOptions
438
+ );
439
+ if (isTurnBoundary(event)) break;
440
+ }
441
+ session = client.sessions.attach(session.state.sessionId, { streamIndex });
442
+ this.session = session;
443
+ this.persistSessionCursor(session);
444
+ if (!rendered.trim() && !signal.aborted) {
445
+ throw new Error("Empty response from runtime");
335
446
  }
336
447
  return rendered.trim();
337
448
  }
338
449
  cancelActive() {
339
- this.activeResponse?.cancel().catch(() => {
340
- });
450
+ if (this.activeResponse) {
451
+ this.activeResponse.cancel().catch(() => {
452
+ });
453
+ } else {
454
+ this.session?.cancel().catch(() => {
455
+ });
456
+ }
341
457
  }
342
458
  };
343
459
  function createAgentClient(options) {
@@ -371,6 +487,12 @@ function createAgentClient(options) {
371
487
  sendOptions.signal ?? new AbortController().signal,
372
488
  sendOptions.handlers
373
489
  ),
490
+ resumeTurn: (resumeOptions) => session.resumeTurn(
491
+ resumeOptions.message,
492
+ resumeOptions.signal ?? new AbortController().signal,
493
+ resumeOptions.handlers,
494
+ resumeOptions.initialText
495
+ ),
374
496
  reset: () => session.reset(),
375
497
  cancelActive: () => session.cancelActive(),
376
498
  getActiveSessionId: () => session.getActiveSessionId()
@@ -399,6 +521,58 @@ function formatAgentError(error) {
399
521
  return "Runtime request failed";
400
522
  }
401
523
 
524
+ // src/react/persisted-conversation.ts
525
+ var CONVERSATION_VERSION = 1;
526
+ function conversationKey(storageKeyPrefix, visitorSessionId) {
527
+ return `${storageKeyPrefix}:conversation:${visitorSessionId}`;
528
+ }
529
+ function parseMessage(value) {
530
+ if (typeof value !== "object" || value === null) return null;
531
+ const record = value;
532
+ if (typeof record.id !== "string" || record.role !== "agent" && record.role !== "visitor" || typeof record.text !== "string" || typeof record.createdAt !== "number" || !Number.isFinite(record.createdAt)) {
533
+ return null;
534
+ }
535
+ return {
536
+ id: record.id,
537
+ role: record.role,
538
+ text: record.text,
539
+ createdAt: record.createdAt
540
+ };
541
+ }
542
+ function loadPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
543
+ if (typeof sessionStorage === "undefined") return null;
544
+ const raw = sessionStorage.getItem(conversationKey(storageKeyPrefix, visitorSessionId));
545
+ if (!raw) return null;
546
+ try {
547
+ const value = JSON.parse(raw);
548
+ if (typeof value !== "object" || value === null) return null;
549
+ const record = value;
550
+ if (record.version !== CONVERSATION_VERSION || !Array.isArray(record.messages) || typeof record.pending !== "boolean" || typeof record.streamingText !== "string") {
551
+ return null;
552
+ }
553
+ const messages = record.messages.map(parseMessage);
554
+ if (messages.some((message) => message === null)) return null;
555
+ return {
556
+ messages: messages.filter((message) => message !== null),
557
+ pending: record.pending,
558
+ streamingText: record.streamingText
559
+ };
560
+ } catch {
561
+ return null;
562
+ }
563
+ }
564
+ function savePersistedAgentConversation(storageKeyPrefix, visitorSessionId, conversation) {
565
+ if (typeof sessionStorage === "undefined") return;
566
+ sessionStorage.setItem(
567
+ conversationKey(storageKeyPrefix, visitorSessionId),
568
+ JSON.stringify({ version: CONVERSATION_VERSION, ...conversation })
569
+ );
570
+ }
571
+ function clearPersistedAgentConversation(storageKeyPrefix, visitorSessionId) {
572
+ if (typeof sessionStorage === "undefined") return;
573
+ sessionStorage.removeItem(conversationKey(storageKeyPrefix, visitorSessionId));
574
+ }
575
+
402
576
  // src/react/hooks/useAgentChat.ts
403
577
  var GREETING_MESSAGE = {
404
578
  id: "greeting",
@@ -415,6 +589,15 @@ var INITIAL_STATE = {
415
589
  streamingText: "",
416
590
  error: null
417
591
  };
592
+ function stateFromConversation(conversation) {
593
+ if (!conversation || conversation.messages.length === 0) return INITIAL_STATE;
594
+ return {
595
+ ...INITIAL_STATE,
596
+ messages: conversation.messages,
597
+ phase: conversation.pending ? conversation.streamingText ? "streaming" : "thinking" : "complete",
598
+ streamingText: conversation.streamingText
599
+ };
600
+ }
418
601
  var STATUS_SEQUENCE = [
419
602
  { id: "s1", label: "Starting Eve session", ms: 400 },
420
603
  { id: "s2", label: "Connecting to runtime", ms: 500 }
@@ -446,8 +629,6 @@ function useAgentChat({
446
629
  visitorSessionId,
447
630
  storageKeyPrefix
448
631
  }) {
449
- const [state, setState] = useState(INITIAL_STATE);
450
- const runRef = useRef(null);
451
632
  const resolvedStorageKeyPrefix = useMemo(
452
633
  () => storageKeyPrefix?.trim() || buildAgentStorageKeyPrefix({ customerId, indexId, version, runtimeOrigin }),
453
634
  [customerId, indexId, runtimeOrigin, storageKeyPrefix, version]
@@ -456,6 +637,12 @@ function useAgentChat({
456
637
  () => visitorSessionId?.trim() || getOrCreateVisitorSessionId({ storageKeyPrefix: resolvedStorageKeyPrefix }),
457
638
  [resolvedStorageKeyPrefix, visitorSessionId]
458
639
  );
640
+ const [state, setState] = useState(
641
+ () => stateFromConversation(
642
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
643
+ )
644
+ );
645
+ const runRef = useRef(null);
459
646
  const clientRef = useRef(
460
647
  createAgentClient({
461
648
  customerId,
@@ -466,20 +653,15 @@ function useAgentChat({
466
653
  storageKeyPrefix: resolvedStorageKeyPrefix
467
654
  })
468
655
  );
469
- const identityRef = useRef(null);
470
656
  const identityKey = `${customerId ?? ""}|${indexId}|${version ?? "published"}|${runtimeOrigin ?? ""}|${resolvedStorageKeyPrefix}|${visitorId}`;
657
+ const identityRef = useRef(identityKey);
471
658
  useEffect(() => {
472
- if (identityRef.current === null) {
473
- identityRef.current = identityKey;
474
- return;
475
- }
476
659
  if (identityRef.current === identityKey) {
477
660
  return;
478
661
  }
479
662
  identityRef.current = identityKey;
480
663
  runRef.current?.abort();
481
664
  runRef.current = null;
482
- clientRef.current.reset();
483
665
  clientRef.current = createAgentClient({
484
666
  customerId,
485
667
  indexId,
@@ -488,81 +670,81 @@ function useAgentChat({
488
670
  visitorSessionId: visitorId,
489
671
  storageKeyPrefix: resolvedStorageKeyPrefix
490
672
  });
491
- setState(INITIAL_STATE);
673
+ setState(
674
+ stateFromConversation(
675
+ loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId)
676
+ )
677
+ );
492
678
  }, [customerId, identityKey, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]);
679
+ useEffect(() => {
680
+ if (!hasVisitorMessages(state.messages)) return;
681
+ savePersistedAgentConversation(resolvedStorageKeyPrefix, visitorId, {
682
+ messages: state.messages,
683
+ pending: isAgentBusy(state.phase),
684
+ streamingText: state.streamingText
685
+ });
686
+ }, [resolvedStorageKeyPrefix, state.messages, state.phase, state.streamingText, visitorId]);
493
687
  const reset = useCallback(() => {
494
688
  runRef.current?.abort();
495
689
  runRef.current = null;
496
690
  clientRef.current.reset();
691
+ clearPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId);
497
692
  setState(INITIAL_STATE);
498
- }, []);
499
- const submit = useCallback(
500
- async (visitorText) => {
501
- runRef.current?.abort();
502
- clientRef.current.cancelActive();
503
- const controller = new AbortController();
504
- runRef.current = controller;
693
+ }, [resolvedStorageKeyPrefix, visitorId]);
694
+ const runTurn = useCallback(
695
+ async (input) => {
696
+ const { controller, initialText = "", resume, visitorText } = input;
505
697
  const { signal } = controller;
506
698
  const isActiveRun = () => runRef.current === controller && !signal.aborted;
507
- const visitorMessage = {
508
- id: `visitor-${Date.now()}`,
509
- role: "visitor",
510
- text: visitorText,
511
- createdAt: Date.now()
512
- };
513
- setState((prev) => ({
514
- ...prev,
515
- phase: "thinking",
516
- messages: [...prev.messages, visitorMessage],
517
- toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
518
- journey: null,
519
- followUps: [],
520
- streamingText: "",
521
- error: null
522
- }));
523
699
  try {
524
- let streamStarted = false;
525
- const planningPromise = runStatusSequence(signal, (step) => {
700
+ let streamStarted = Boolean(initialText);
701
+ let streamed = initialText;
702
+ const planningPromise = resume ? Promise.resolve() : runStatusSequence(signal, (step) => {
526
703
  if (streamStarted || !isActiveRun()) return;
527
704
  setState((prev) => ({ ...prev, phase: "running-tools", toolSteps: [step] }));
528
705
  });
529
- let streamed = "";
530
- const finalText = await clientRef.current.sendTurn(visitorText, {
531
- signal,
532
- handlers: {
533
- onStep: (label, detail) => {
534
- if (streamStarted || !isActiveRun()) return;
706
+ const handlers = {
707
+ onStep: (label, detail) => {
708
+ if (streamStarted || !isActiveRun()) return;
709
+ setState((prev) => ({
710
+ ...prev,
711
+ phase: "running-tools",
712
+ toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
713
+ }));
714
+ },
715
+ onDelta: (delta) => {
716
+ if (!isActiveRun()) return;
717
+ void planningPromise.catch(() => {
718
+ });
719
+ if (!streamStarted) {
720
+ streamStarted = true;
535
721
  setState((prev) => ({
536
722
  ...prev,
537
- phase: "running-tools",
538
- toolSteps: [{ id: `step-${label}`, label, detail, state: "active" }]
723
+ phase: "streaming",
724
+ toolSteps: [],
725
+ streamingText: ""
539
726
  }));
540
- },
541
- onDelta: (delta) => {
542
- if (!isActiveRun()) return;
543
- void planningPromise.catch(() => {
544
- });
545
- if (!streamStarted) {
546
- streamStarted = true;
547
- setState((prev) => ({
548
- ...prev,
549
- phase: "streaming",
550
- toolSteps: [],
551
- streamingText: ""
552
- }));
553
- }
554
- streamed += delta;
555
- setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
556
- },
557
- onComplete: () => {
558
- if (!isActiveRun()) return;
559
- streamStarted = true;
560
727
  }
728
+ streamed += delta;
729
+ setState((prev) => ({ ...prev, phase: "streaming", streamingText: streamed }));
730
+ },
731
+ onComplete: () => {
732
+ if (!isActiveRun()) return;
733
+ streamStarted = true;
561
734
  }
562
- });
735
+ };
736
+ let finalText = resume ? await clientRef.current.resumeTurn({
737
+ handlers,
738
+ initialText,
739
+ message: visitorText,
740
+ signal
741
+ }) : await clientRef.current.sendTurn(visitorText, { handlers, signal });
742
+ if (resume && finalText === null) {
743
+ finalText = await clientRef.current.sendTurn(visitorText, { handlers, signal });
744
+ }
563
745
  await planningPromise.catch(() => {
564
746
  });
565
- if (!isActiveRun()) return;
747
+ if (!isActiveRun() || finalText === null) return;
566
748
  const agentMessage = {
567
749
  id: `agent-${Date.now()}`,
568
750
  role: "agent",
@@ -578,6 +760,7 @@ function useAgentChat({
578
760
  followUps: [],
579
761
  journey: null
580
762
  }));
763
+ runRef.current = null;
581
764
  } catch (error) {
582
765
  if (error instanceof DOMException && error.name === "AbortError") return;
583
766
  if (!isActiveRun()) return;
@@ -590,14 +773,66 @@ function useAgentChat({
590
773
  streamingText: "",
591
774
  error: message
592
775
  }));
776
+ runRef.current = null;
593
777
  }
594
778
  },
595
- [customerId, indexId, runtimeOrigin, resolvedStorageKeyPrefix, version, visitorId]
779
+ []
596
780
  );
781
+ const submit = useCallback(
782
+ async (visitorText) => {
783
+ if (runRef.current) {
784
+ runRef.current.abort();
785
+ clientRef.current.cancelActive();
786
+ }
787
+ const controller = new AbortController();
788
+ runRef.current = controller;
789
+ const visitorMessage = {
790
+ id: `visitor-${Date.now()}`,
791
+ role: "visitor",
792
+ text: visitorText,
793
+ createdAt: Date.now()
794
+ };
795
+ setState((prev) => ({
796
+ ...prev,
797
+ phase: "thinking",
798
+ messages: [...prev.messages, visitorMessage],
799
+ toolSteps: [{ id: "s1", label: "Starting Eve session", state: "active" }],
800
+ journey: null,
801
+ followUps: [],
802
+ streamingText: "",
803
+ error: null
804
+ }));
805
+ await runTurn({ controller, resume: false, visitorText });
806
+ },
807
+ [runTurn]
808
+ );
809
+ useEffect(() => {
810
+ const conversation = loadPersistedAgentConversation(
811
+ resolvedStorageKeyPrefix,
812
+ visitorId
813
+ );
814
+ if (!conversation?.pending) return;
815
+ const visitorMessage = [...conversation.messages].reverse().find((message) => message.role === "visitor");
816
+ if (!visitorMessage) return;
817
+ const controller = new AbortController();
818
+ runRef.current = controller;
819
+ void runTurn({
820
+ controller,
821
+ initialText: conversation.streamingText,
822
+ resume: true,
823
+ visitorText: visitorMessage.text
824
+ });
825
+ return () => {
826
+ if (runRef.current === controller) {
827
+ runRef.current = null;
828
+ }
829
+ controller.abort();
830
+ };
831
+ }, [identityKey, resolvedStorageKeyPrefix, runTurn, visitorId]);
597
832
  useEffect(() => {
598
833
  return () => {
599
834
  runRef.current?.abort();
600
- clientRef.current.cancelActive();
835
+ runRef.current = null;
601
836
  };
602
837
  }, []);
603
838
  return {
@@ -1188,4 +1423,4 @@ export {
1188
1423
  AssistEdgeTab,
1189
1424
  AgentWidget
1190
1425
  };
1191
- //# sourceMappingURL=chunk-DSNTEDXA.js.map
1426
+ //# sourceMappingURL=chunk-K52TJUV6.js.map