@webless/agent 0.2.10 → 0.2.11

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