@agents24/react 0.2.26 → 0.2.28

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.
@@ -47,8 +47,8 @@
47
47
  },
48
48
  "schemaVersion": "agents24.compatibility.v1",
49
49
  "targetOperationCounts": {
50
- "client": 31,
51
- "node": 139,
52
- "python": 132
50
+ "client": 36,
51
+ "node": 196,
52
+ "python": 190
53
53
  }
54
54
  }
@@ -0,0 +1,18 @@
1
+ /** Ephemeral display only: these fragments never enter stored messages or replay cursors. */
2
+ import type { AssistantPreview } from "@agents24/client/protocol";
3
+ import type { RuntimeEvent } from "@agents24/client";
4
+ import type { ChatMessage } from "./model";
5
+ export type AssistantPreviewState = {
6
+ runId: string;
7
+ key: string;
8
+ segmentId: string;
9
+ committed: number;
10
+ dispatchId: string;
11
+ generation: number;
12
+ chunks: Record<number, string>;
13
+ retired: string[];
14
+ };
15
+ export declare function acceptAssistantPreview(current: AssistantPreviewState | null | undefined, frame: AssistantPreview | null): AssistantPreviewState | null;
16
+ export declare function commitAssistantPreview(current: AssistantPreviewState | null | undefined, event: RuntimeEvent): AssistantPreviewState | null;
17
+ export declare function previewText(state: AssistantPreviewState | null | undefined): string;
18
+ export declare function messagesWithAssistantPreview(messages: ChatMessage[], state: AssistantPreviewState | null | undefined): ChatMessage[];
package/dist/index.js CHANGED
@@ -637,6 +637,79 @@ function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
637
637
  };
638
638
  }
639
639
 
640
+ // src/assistant-preview.ts
641
+ var keyOf = (frame) => `${frame.dispatch_id}:${frame.generation}:${frame.segment_id}`;
642
+ function acceptAssistantPreview(current, frame) {
643
+ if (!frame) return current ? { ...current, chunks: {} } : null;
644
+ const key = keyOf(frame);
645
+ if (current?.runId === frame.run_id && current.dispatchId === frame.dispatch_id && current.generation > frame.generation) return current;
646
+ if (current?.runId === frame.run_id && current.retired.includes(key)) return current;
647
+ const state = current?.runId === frame.run_id && current.key === key ? current : {
648
+ runId: frame.run_id,
649
+ dispatchId: frame.dispatch_id,
650
+ generation: frame.generation,
651
+ key,
652
+ segmentId: frame.segment_id,
653
+ committed: -1,
654
+ chunks: {},
655
+ retired: current?.runId === frame.run_id ? [...current.retired, current.key].slice(-8) : []
656
+ };
657
+ if (frame.chunk_index <= state.committed || Object.keys(state.chunks).length >= 32 || Object.values(state.chunks).reduce((size, text2) => size + text2.length, 0) + frame.text.length > 65536) return state;
658
+ return { ...state, chunks: { ...state.chunks, [frame.chunk_index]: frame.text } };
659
+ }
660
+ function commitAssistantPreview(current, event) {
661
+ if (event.event === "run.accepted" && current && current.runId !== event.run_id) return null;
662
+ if (current && current.runId !== event.run_id) return current;
663
+ if (["assistant.completed", "run.completed", "run.failed", "run.cancelled", "run.waiting", "run.interrupted", "run.paused", "run.snapshot"].includes(event.event)) return null;
664
+ const ack = event.payload.preview_ack;
665
+ if (event.event !== "assistant.delta" || !ack || typeof ack.dispatch_id !== "string" || typeof ack.segment_id !== "string" || !Number.isSafeInteger(ack.generation) || !Number.isSafeInteger(ack.chunk_index)) return current || null;
666
+ const key = keyOf(ack);
667
+ if (current && current.key !== key) return current;
668
+ const committed = Math.max(current?.committed ?? -1, ack.chunk_index);
669
+ return {
670
+ runId: event.run_id,
671
+ dispatchId: ack.dispatch_id,
672
+ generation: ack.generation,
673
+ key,
674
+ segmentId: ack.segment_id,
675
+ committed,
676
+ retired: current?.retired || [],
677
+ chunks: Object.fromEntries(Object.entries(current?.chunks || {}).filter(([index]) => Number(index) > committed))
678
+ };
679
+ }
680
+ function previewText(state) {
681
+ if (!state) return "";
682
+ let text2 = "";
683
+ for (let index = state.committed + 1; Object.hasOwn(state.chunks, index); index++) text2 += state.chunks[index];
684
+ return text2;
685
+ }
686
+ function messagesWithAssistantPreview(messages, state) {
687
+ const tail = previewText(state);
688
+ if (!state || !tail) return messages;
689
+ let index = -1;
690
+ for (let i = messages.length - 1; i >= 0; i--) {
691
+ if (messages[i].role === "assistant" && messages[i].runId === state.runId && !messages[i].isFinal) {
692
+ index = i;
693
+ break;
694
+ }
695
+ }
696
+ const base = index < 0 ? {
697
+ id: `preview-${state.runId}`,
698
+ role: "assistant",
699
+ runId: state.runId,
700
+ content: "",
701
+ parts: [],
702
+ createdAt: /* @__PURE__ */ new Date(0),
703
+ isFinal: false
704
+ } : messages[index];
705
+ const parts = [...base.parts];
706
+ const last = parts[parts.length - 1];
707
+ if (last?.type === "text") parts[parts.length - 1] = { ...last, text: last.text + tail };
708
+ else parts.push({ id: `preview-${state.segmentId}`, type: "text", kind: "text", text: tail });
709
+ const rendered = { ...base, content: base.content + tail, parts };
710
+ return index < 0 ? [...messages, rendered] : messages.map((message, i) => i === index ? rendered : message);
711
+ }
712
+
640
713
  // src/hitl-resolution.ts
641
714
  function applyHitlResolution(messages, receipt) {
642
715
  return messages.map((message) => message.runId !== receipt.run_id ? message : {
@@ -856,11 +929,11 @@ function reconcileModelTurnParts(parts, event) {
856
929
  const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
857
930
  const modelTurnId = typeof payload.model_turn_id === "string" ? payload.model_turn_id : null;
858
931
  let next = parts;
859
- if (eventName === "run.snapshot" || eventName === "assistant.completed" || terminal) {
932
+ if (["run.snapshot", "run.waiting", "run.interrupted", "run.paused", "assistant.completed"].includes(eventName) || terminal) {
860
933
  next = next.filter((part) => part.kind !== "model-turn");
861
934
  } else if (eventName === "assistant.delta" && typeof payload.content === "string" && payload.content) {
862
935
  next = next.filter((part) => part.kind !== "model-turn" || part.turnKind !== "agent");
863
- } else if (eventName.startsWith("tool.") || eventName === "node.status.updated") {
936
+ } else if (eventName === "tool.started") {
864
937
  next = next.filter((part) => part.kind !== "model-turn" || part.turnKind !== "agent");
865
938
  } else if ((eventName === "model.turn.failed" || eventName === "model.turn.completed" && payload.outcome !== "tool_calls") && modelTurnId) {
866
939
  next = next.filter((part) => part.kind !== "model-turn" || part.modelTurnId !== modelTurnId);
@@ -869,6 +942,11 @@ function reconcileModelTurnParts(parts, event) {
869
942
  return active ? upsertResponseBlockParts(next, [active]) : next;
870
943
  }
871
944
  function applyRuntimeEvent(state, event) {
945
+ const next = applyCanonicalRuntimeEvent(state, event);
946
+ if (!state.assistantPreview && !event.payload.preview_ack) return next;
947
+ return { ...next, assistantPreview: commitAssistantPreview(state.assistantPreview, event) };
948
+ }
949
+ function applyCanonicalRuntimeEvent(state, event) {
872
950
  if (state.activeRunId && event.run_id && event.run_id !== state.activeRunId) {
873
951
  const previous = state.messages.find((message) => message.role === "assistant" && message.runId === event.run_id);
874
952
  if (event.event !== "run.accepted") {
@@ -906,12 +984,24 @@ function applyRuntimeEvent(state, event) {
906
984
  const distinctSegment = Boolean(segmentId && segmentId !== `${runId}:0`);
907
985
  const messageId = distinctSegment ? `${runId || "run"}-assistant-${segmentId}` : state.streamingMessageId || `${runId || "run"}-assistant`;
908
986
  const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
909
- const paused = eventName === "run.paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
910
- const suspension = eventName === "run.waiting" ? "waiting" : eventName === "run.interrupted" ? "interrupted" : null;
987
+ const snapshotStatus = eventName === "run.snapshot" ? event.payload?.status : null;
988
+ const paused = eventName === "run.paused" || snapshotStatus === "paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
989
+ const retainedSuspension = !eventName.startsWith("run.") && !["model.turn.started", "assistant.delta", "tool.started"].includes(eventName) && (state.runState === "waiting" || state.runState === "interrupted") ? state.runState : null;
990
+ const suspension = eventName === "run.waiting" || snapshotStatus === "waiting" ? "waiting" : eventName === "run.interrupted" || snapshotStatus === "interrupted" ? "interrupted" : retainedSuspension;
911
991
  const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
912
992
  const existing = state.messages.find((message) => message.role === "assistant" && message.id === messageId) || (!distinctSegment ? state.messages.find((message) => message.role === "assistant" && Boolean(runId && message.runId === runId)) : void 0);
913
993
  const sealedAfterTerminal = responseCompleted && !state.activeRunId && ["completed", "failed", "cancelled"].includes(state.runState) && existing?.runId === runId;
914
994
  const payload = event.payload || {};
995
+ const wait = payload.capacity_wait;
996
+ const capacityWait = wait && typeof wait.reason === "string" ? {
997
+ reason: wait.reason,
998
+ ...typeof wait.retry_at === "string" ? { retryAt: wait.retry_at } : {},
999
+ ...typeof wait.deadline === "string" ? { deadline: wait.deadline } : {}
1000
+ } : state.runState === "waiting" ? existing?.activity?.capacityWait : void 0;
1001
+ const activity = terminal || paused || responseCompleted || existing?.responseCompletion ? void 0 : {
1002
+ status: suspension || (cancelling ? "cancelling" : "running"),
1003
+ ...suspension === "waiting" && capacityWait ? { capacityWait } : {}
1004
+ };
915
1005
  const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
916
1006
  const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
917
1007
  const contextPayload = payload.context_window && typeof payload.context_window === "object" ? { ...payload.context_window, run_id: runId } : payload.context_window;
@@ -946,6 +1036,7 @@ function applyRuntimeEvent(state, event) {
946
1036
  content,
947
1037
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
948
1038
  isFinal: terminal || responseCompleted,
1039
+ activity,
949
1040
  responseCompletion: completion,
950
1041
  parts
951
1042
  }),
@@ -955,6 +1046,7 @@ function applyRuntimeEvent(state, event) {
955
1046
  content,
956
1047
  parts,
957
1048
  isFinal: terminal || responseCompleted || message.isFinal,
1049
+ activity,
958
1050
  responseCompletion: completion || message.responseCompletion
959
1051
  })
960
1052
  });
@@ -964,10 +1056,12 @@ function applyRuntimeEvent(state, event) {
964
1056
  return {
965
1057
  ...message,
966
1058
  isFinal: true,
1059
+ activity: void 0,
967
1060
  responseCompletion: completion,
968
1061
  ...segment ? { durableId: segment.message_id } : {}
969
1062
  };
970
1063
  });
1064
+ if (terminal || distinctSegment) messages = messages.map((message) => message.role === "assistant" && message.runId === runId && message.activity && (terminal || message.id !== (existing?.id || messageId)) ? { ...message, activity: void 0 } : message);
971
1065
  const activityStatus = suspension || (paused ? "paused" : cancelling ? "cancelling" : "running");
972
1066
  const terminalThread = runId && !sealedAfterTerminal ? {
973
1067
  active_run: terminal ? null : {
@@ -2130,6 +2224,12 @@ function useAgentChat(options = {}) {
2130
2224
  }
2131
2225
  }
2132
2226
  }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update, invalidateThreadOpen, ownOperation, captureFeedbackRead]);
2227
+ const consumePreview = React8.useCallback((frame) => {
2228
+ update((current) => {
2229
+ if (frame && current.activeRunId && current.activeRunId !== frame.run_id) return current;
2230
+ return { ...current, assistantPreview: acceptAssistantPreview(current.assistantPreview, frame) };
2231
+ });
2232
+ }, [update]);
2133
2233
  const consumeEvent = React8.useCallback((event) => {
2134
2234
  const timing = continuationTiming.current.get(event.run_id);
2135
2235
  if (timing && !timing.attached) {
@@ -2158,7 +2258,9 @@ function useAgentChat(options = {}) {
2158
2258
  clearOperationError("attach_run");
2159
2259
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
2160
2260
  try {
2161
- const result = await client.runs.attach({ runId, cursor, leaseSeconds: 60, signal: controller.signal }, (event) => {
2261
+ const result = await client.runs.attach({ runId, cursor, onPreview: (frame) => {
2262
+ if (isCurrent()) consumePreview(frame);
2263
+ }, leaseSeconds: 60, signal: controller.signal }, (event) => {
2162
2264
  if (isCurrent()) consumeEvent(event);
2163
2265
  });
2164
2266
  if (!isCurrent()) return;
@@ -2174,7 +2276,7 @@ function useAgentChat(options = {}) {
2174
2276
  } finally {
2175
2277
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
2176
2278
  }
2177
- }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update, ownOperation]);
2279
+ }, [clearOperationError, client, consumeEvent, consumePreview, eventBuffer, failOperation, refreshMessages, runtime, update, ownOperation]);
2178
2280
  attachRunRef.current = attachRun;
2179
2281
  const submit = React8.useCallback(async (input) => {
2180
2282
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
@@ -2285,6 +2387,9 @@ function useAgentChat(options = {}) {
2285
2387
  }));
2286
2388
  }
2287
2389
  const result = await client.chat.stream({
2390
+ onPreview: (frame) => {
2391
+ if (isCurrent()) consumePreview(frame);
2392
+ },
2288
2393
  leaseSeconds: 60,
2289
2394
  clientSubmissionId: ticket.client_submission_id,
2290
2395
  input: input.text.trim(),
@@ -2336,7 +2441,7 @@ function useAgentChat(options = {}) {
2336
2441
  } catch (error) {
2337
2442
  if (submissionOwner() || !isAbortLike(error)) throw error;
2338
2443
  }
2339
- }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
2444
+ }, [clearOperationError, client, consumeEvent, consumePreview, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
2340
2445
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
2341
2446
  queue,
2342
2447
  stateRef,
@@ -2563,6 +2668,7 @@ function useAgentChat(options = {}) {
2563
2668
  const hasMoreThreads = threadTotal === void 0 ? state.threads.length >= threadPageSize : state.threads.length < threadTotal;
2564
2669
  return {
2565
2670
  ...state,
2671
+ messages: messagesWithAssistantPreview(state.messages, state.assistantPreview),
2566
2672
  activeThread,
2567
2673
  pendingMessages,
2568
2674
  messageQueue,