@agents24/react 0.2.20 → 0.2.22

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.
@@ -8,6 +8,8 @@ export type ChatObservationState = {
8
8
  paused: boolean;
9
9
  degraded?: boolean;
10
10
  mode?: "on-demand" | "realtime";
11
+ background?: boolean;
12
+ sidebar?: boolean;
11
13
  };
12
14
  /** One observation policy for SDK chats and platform transport adapters. */
13
15
  export declare function createChatObservation(input: {
package/dist/index.js CHANGED
@@ -17,32 +17,22 @@ function createChatObservation(input) {
17
17
  let visible = input.visibility?.isVisible() ?? true;
18
18
  let disposed = false;
19
19
  let generation = 0;
20
- let cursor;
21
- let connection;
22
20
  let recovery;
23
21
  let refreshing;
24
22
  let timer;
25
- let reconnectTimer;
26
23
  let failures = 0;
27
- let handoffAt;
28
- let buffered;
29
- const needed = () => !disposed && visible && Boolean(state.threadId) && !state.waiting && !state.paused && (state.running || state.pending || state.mode === "realtime");
30
- const clearTimer = () => {
24
+ const needed = () => !disposed && visible && Boolean(state.threadId || state.sidebar);
25
+ const interval = () => failures ? Math.min(3e4, 5e3 * 2 ** Math.min(failures, 3)) : state.running || state.pending || state.degraded || state.waiting || state.paused || state.background ? 5e3 : 3e4;
26
+ const stop = () => {
31
27
  clock.clearTimeout(timer);
32
28
  timer = void 0;
33
29
  };
34
- const stop = () => {
35
- clearTimer();
36
- clock.clearTimeout(reconnectTimer);
37
- connection?.abort();
38
- connection = void 0;
39
- };
40
- const schedule = (callback, ms) => {
41
- clearTimer();
42
- timer = clock.setTimeout(callback, ms);
30
+ const schedule = () => {
31
+ stop();
32
+ if (needed()) timer = clock.setTimeout(poll, interval());
43
33
  };
44
34
  function refresh() {
45
- if (disposed || !visible || !state.threadId) return Promise.resolve();
35
+ if (!needed()) return Promise.resolve();
46
36
  if (refreshing) return refreshing;
47
37
  const owned = generation;
48
38
  const controller = input.createAbortController();
@@ -65,137 +55,51 @@ function createChatObservation(input) {
65
55
  refreshing = promise;
66
56
  return promise;
67
57
  }
68
- function recover() {
69
- const owned = generation;
70
- return refresh().then(() => {
71
- if (owned === generation && !disposed && visible) {
72
- failures = 0;
73
- input.onResume();
74
- }
75
- }).catch((error) => {
76
- if (owned !== generation || disposed || !visible) return;
77
- failures += 1;
78
- input.onError(error);
79
- if (needed()) schedule(poll, 1e3);
80
- });
81
- }
82
58
  function poll() {
59
+ stop();
83
60
  if (!needed()) return;
84
- void recover().finally(() => {
85
- if (needed() && (state.pending && !state.running || state.degraded || failures > 0)) schedule(poll, 1e3);
86
- });
87
- }
88
- function connect() {
89
- if (!needed() || connection) return;
90
61
  const owned = generation;
91
- const controller = input.createAbortController();
92
- connection = controller;
93
- buffered = [];
94
- let ready = false;
95
- let leased = false;
96
- let hydration;
97
- const hydrate = () => hydration ??= recover().finally(() => {
98
- if (owned !== generation || controller.signal.aborted) return;
99
- const notices = buffered || [];
100
- buffered = void 0;
101
- for (const event of notices) input.onEvent(event);
102
- if (notices.length) void recover();
103
- });
104
- void input.events({
105
- threadId: state.threadId,
106
- cursor,
107
- leaseSeconds: 60,
108
- signal: controller.signal,
109
- onControl(control) {
110
- if (control.type === "lease_expired") leased = true;
111
- if (control.type === "ready") {
112
- ready = true;
113
- cursor ??= control.cursor;
114
- void hydrate().catch(input.onError);
115
- }
116
- }
117
- }, (event) => {
118
- if (owned !== generation || controller.signal.aborted) return;
119
- if (event.event === "snapshot_required") {
120
- cursor = void 0;
121
- void refresh().catch(input.onError);
122
- return;
123
- }
124
- cursor = Math.max(cursor ?? 0, event.cursor);
125
- failures = 0;
126
- if (buffered) buffered.push(event);
127
- else {
128
- input.onEvent(event);
129
- void recover();
130
- }
131
- if (!ready) {
132
- ready = true;
133
- void hydrate().catch(input.onError);
134
- }
135
- }).then(() => {
136
- if (owned !== generation || controller.signal.aborted) return;
137
- connection = void 0;
138
- if (needed()) {
139
- if (leased) connect();
140
- else {
141
- void refresh().catch(input.onError);
142
- reconnectTimer = clock.setTimeout(connect, 1e3);
143
- }
62
+ void refresh().then(() => {
63
+ if (owned === generation && needed()) {
64
+ failures = 0;
65
+ input.onResume();
144
66
  }
145
67
  }).catch((error) => {
146
- if (owned !== generation || controller.signal.aborted || isAbortLike(error)) return;
147
- connection = void 0;
68
+ if (owned !== generation || !needed()) return;
148
69
  failures += 1;
149
70
  input.onError(error);
150
- poll();
151
- reconnectTimer = clock.setTimeout(connect, Math.min(3e4, 1e3 * 2 ** Math.min(failures - 1, 5)));
71
+ }).finally(() => {
72
+ if (owned === generation) schedule();
152
73
  });
153
74
  }
154
- function reconcile() {
155
- if (!needed()) {
156
- handoffAt = void 0;
157
- stop();
158
- return;
159
- }
160
- connect();
161
- if (state.pending && !state.running) {
162
- handoffAt ??= Date.now() + 2e3;
163
- schedule(poll, Math.max(0, handoffAt - Date.now()));
164
- } else {
165
- handoffAt = void 0;
166
- if (state.degraded) schedule(poll, 1e3);
167
- else if (!failures) clearTimer();
168
- }
169
- }
170
75
  const unsubscribe = input.visibility?.subscribe(() => {
171
76
  const next = input.visibility.isVisible();
172
77
  if (next === visible) return;
173
78
  visible = next;
174
79
  if (!visible) {
80
+ generation += 1;
175
81
  stop();
176
82
  recovery?.abort();
177
83
  refreshing = void 0;
178
84
  input.onDetach();
179
- } else {
180
- reconcile();
181
- if (!needed()) void refresh().then(input.onResume).catch(input.onError);
182
- }
85
+ } else poll();
183
86
  });
184
87
  return {
185
88
  refresh,
186
89
  update(next) {
187
- if (next.threadId !== state.threadId) {
90
+ const changed = next.threadId !== state.threadId;
91
+ const accelerated = !(state.running || state.pending || state.background || state.waiting) && Boolean(next.running || next.pending || next.background || next.waiting);
92
+ if (changed) {
188
93
  generation += 1;
189
94
  stop();
190
95
  recovery?.abort();
191
96
  refreshing = void 0;
192
- cursor = void 0;
193
- buffered = void 0;
194
97
  failures = 0;
195
- handoffAt = void 0;
196
98
  }
197
99
  state = next;
198
- reconcile();
100
+ if (!needed()) stop();
101
+ else if (changed || accelerated) poll();
102
+ else if (timer === void 0 && !refreshing) schedule();
199
103
  },
200
104
  dispose() {
201
105
  disposed = true;
@@ -232,10 +136,10 @@ function useChatObservation(input) {
232
136
  if (owner.current === observation) owner.current = null;
233
137
  };
234
138
  }, [input.createAbortController, input.visibility]);
235
- const { threadId, running, waiting, pending, paused, degraded, mode } = input.state;
139
+ const { threadId, running, waiting, pending, paused, degraded, mode, background, sidebar } = input.state;
236
140
  React.useEffect(() => {
237
- owner.current?.update({ threadId, running, waiting, pending, paused, degraded, mode });
238
- }, [threadId, running, waiting, pending, paused, degraded, mode]);
141
+ owner.current?.update({ threadId, running, waiting, pending, paused, degraded, mode, background, sidebar });
142
+ }, [threadId, running, waiting, pending, paused, degraded, mode, background, sidebar]);
239
143
  return React.useCallback(() => owner.current?.refresh() ?? Promise.resolve(), []);
240
144
  }
241
145
 
@@ -258,7 +162,7 @@ function getThreadActivity(thread) {
258
162
  return {
259
163
  status,
260
164
  runId: activeRun?.run_id || thread.last_run_id || null,
261
- isSpinning: Boolean(activeRun) && !["waiting", "interrupted"].includes(status || ""),
165
+ isSpinning: Boolean(activeRun) && ["queued", "running", "cancelling"].includes(status || ""),
262
166
  isPaused: status === "paused",
263
167
  isFailed: status === "failed",
264
168
  isCompleted: status === "completed",
@@ -1027,13 +931,23 @@ function applyRuntimeEvent(state, event) {
1027
931
  ...segment ? { durableId: segment.message_id } : {}
1028
932
  };
1029
933
  });
1030
- const terminalThread = terminal ? { active_run: null, last_run_status: eventName.slice(4) } : {};
934
+ const activityStatus = suspension || (paused ? "paused" : cancelling ? "cancelling" : "running");
935
+ const terminalThread = runId && !sealedAfterTerminal ? {
936
+ active_run: terminal ? null : {
937
+ run_id: runId,
938
+ status: activityStatus,
939
+ started_at: state.threads.find((thread) => thread.id === activeThreadId)?.active_run?.started_at ?? null,
940
+ created_at: state.threads.find((thread) => thread.id === activeThreadId && thread.active_run?.run_id === runId)?.active_run?.created_at ?? event.ts,
941
+ is_cancellable: !cancelling
942
+ },
943
+ last_run_status: terminal ? eventName.slice(4) : activityStatus
944
+ } : {};
1031
945
  const threads = activeThreadId ? state.threads.some((thread) => thread.id === activeThreadId) ? state.threads.map((thread) => thread.id === activeThreadId ? {
1032
946
  ...thread,
1033
947
  messages,
1034
948
  isHydrated: true,
1035
949
  last_run_id: terminal && thread.active_run && thread.active_run.run_id !== runId ? thread.last_run_id : runId ?? thread.last_run_id,
1036
- ...!thread.active_run || thread.active_run.run_id === runId ? terminalThread : {},
950
+ ...!terminal || !thread.active_run || thread.active_run.run_id === runId ? terminalThread : {},
1037
951
  contextWindow,
1038
952
  contextCompression
1039
953
  } : thread) : [{
@@ -1471,6 +1385,53 @@ function discardOptimisticSubmission(state, message) {
1471
1385
  // src/use-agent-chat.ts
1472
1386
  import * as React7 from "react";
1473
1387
 
1388
+ // src/refresh-chat-summaries.ts
1389
+ async function refreshChatSummaries(input) {
1390
+ const before = input.current();
1391
+ const page = await input.client.threads.list({ limit: input.limit, signal: input.signal });
1392
+ if (input.signal.aborted) return null;
1393
+ const summaries = [...page.items];
1394
+ const seen = new Set(summaries.map((thread) => thread.id));
1395
+ const missing = before.threads.filter((thread) => !seen.has(thread.id) && (thread.active_run || thread.id === before.activeThreadId)).map((thread) => thread.id);
1396
+ if (before.activeThreadId && !seen.has(before.activeThreadId) && !missing.includes(before.activeThreadId)) {
1397
+ missing.push(before.activeThreadId);
1398
+ }
1399
+ const deleted = /* @__PURE__ */ new Set();
1400
+ const errors = [];
1401
+ for (let offset = 0; offset < missing.length && !input.signal.aborted; offset += 4) {
1402
+ const ids = missing.slice(offset, offset + 4);
1403
+ const results = await Promise.allSettled(ids.map((threadId) => input.client.threads.get({ threadId, limit: 1, signal: input.signal })));
1404
+ results.forEach((result, index) => {
1405
+ if (result.status === "fulfilled") summaries.push(result.value);
1406
+ else if (result.reason?.name === "Agents24ClientError" && result.reason.kind === "not_found" && result.reason.status === 404) {
1407
+ deleted.add(ids[index]);
1408
+ } else errors.push(result.reason);
1409
+ });
1410
+ }
1411
+ if (input.signal.aborted) return null;
1412
+ const removed = [];
1413
+ input.update((current) => ({ ...current, threads: summaries.reduce((threads, thread) => {
1414
+ const previous = before.threads.find((item) => item.id === thread.id);
1415
+ const latest = current.threads.find((item) => item.id === thread.id);
1416
+ if (previous && !latest) return threads;
1417
+ if (latest && (previous?.last_run_id !== latest.last_run_id || previous?.active_run?.status !== latest.active_run?.status || previous?.last_run_status !== latest.last_run_status)) return threads;
1418
+ return applyThreadSummaryEvent(threads, {
1419
+ schema_version: "agents24.thread_summary_event.v1",
1420
+ event: "thread.summary.updated",
1421
+ cursor: 0,
1422
+ thread
1423
+ });
1424
+ }, current.threads.filter((thread) => {
1425
+ if (!deleted.has(thread.id) || before.threads.find((item) => item.id === thread.id) !== thread) return true;
1426
+ removed.push(thread.id);
1427
+ return false;
1428
+ })) }));
1429
+ if (before.activeThreadId && deleted.has(before.activeThreadId) && !input.current().threads.some((thread) => thread.id === before.activeThreadId) && !removed.includes(before.activeThreadId)) removed.push(before.activeThreadId);
1430
+ for (const id of removed) input.onDeleted?.(id);
1431
+ if (errors.length) throw errors[0];
1432
+ return input.current().threads.find((thread) => thread.id === before.activeThreadId) || null;
1433
+ }
1434
+
1474
1435
  // src/use-agent-chat-thread-list.ts
1475
1436
  import * as React4 from "react";
1476
1437
  function summaryToStored(thread) {
@@ -1670,7 +1631,7 @@ function shouldHydrateSelectedThread(selectedThreadId, activeThreadId, activeThr
1670
1631
  function activeRunIdFromThread(thread) {
1671
1632
  if (!thread) return null;
1672
1633
  const activity = getThreadActivity(thread);
1673
- return activity.isSpinning ? activity.runId : null;
1634
+ return thread.active_run ? activity.runId : null;
1674
1635
  }
1675
1636
  function useAgentChat(options = {}) {
1676
1637
  const runtime = useAgents24Runtime();
@@ -1746,7 +1707,16 @@ function useAgentChat(options = {}) {
1746
1707
  const onQueuePromotion = React7.useCallback((message, runId) => {
1747
1708
  if (stateRef.current.activeThreadId !== message.thread_id) return;
1748
1709
  const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1749
- update((current) => current.activeThreadId === message.thread_id ? { ...current, messages: promoteThreadMessage(current.messages, userMessage, runId) } : current);
1710
+ update((current) => current.activeThreadId === message.thread_id ? {
1711
+ ...current,
1712
+ messages: promoteThreadMessage(current.messages, userMessage, runId),
1713
+ threads: current.threads.map((thread) => thread.id === message.thread_id ? {
1714
+ ...thread,
1715
+ last_run_id: runId,
1716
+ last_run_status: "running",
1717
+ active_run: { run_id: runId, status: "running", started_at: null, created_at: message.created_at, is_cancellable: true }
1718
+ } : thread)
1719
+ } : current);
1750
1720
  attachPromotion(runId, message.thread_id);
1751
1721
  }, [update, attachPromotion]);
1752
1722
  const queue = useThreadMessageQueue({
@@ -1892,10 +1862,11 @@ function useAgentChat(options = {}) {
1892
1862
  ...context,
1893
1863
  contextCompressionRevision: 0,
1894
1864
  activeRunId: activeRunIdFromThread(stored),
1865
+ runState: getThreadActivity(stored).isSpinning ? "reconnecting" : stored.active_run?.status || "idle",
1895
1866
  threads: current.threads.some((thread) => thread.id === threadId) ? current.threads.map((thread) => thread.id === threadId ? stored : thread) : [stored, ...current.threads]
1896
1867
  } : current);
1897
1868
  const activeRunId = activeRunIdFromThread(stored);
1898
- if (activeRunId && isCurrent()) void attachRunRef.current(activeRunId).catch(() => void 0);
1869
+ if (activeRunId && getThreadActivity(stored).isSpinning && isCurrent()) void attachRunRef.current(activeRunId).catch(() => void 0);
1899
1870
  } catch (error) {
1900
1871
  if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
1901
1872
  failOperation("open_thread", error);
@@ -2254,6 +2225,8 @@ function useAgentChat(options = {}) {
2254
2225
  threadId: state.activeThreadId,
2255
2226
  running: Boolean(state.activeRunId) && !["idle", "completed", "failed", "cancelled"].includes(state.runState),
2256
2227
  waiting: ["paused", "waiting", "interrupted"].includes(state.runState),
2228
+ sidebar: true,
2229
+ background: state.threads.some((thread) => thread.id !== state.activeThreadId && Boolean(thread.active_run || thread.message_queue?.pending_count)),
2257
2230
  pending: pendingMessages.length > 0,
2258
2231
  paused: Boolean(messageQueue?.paused),
2259
2232
  degraded: state.runState === "reconnecting",
@@ -2264,22 +2237,36 @@ function useAgentChat(options = {}) {
2264
2237
  events: (input, handler) => client.threads.events(input, handler),
2265
2238
  async refresh(signal) {
2266
2239
  const target = stateRef.current.activeThreadId;
2267
- if (!target) return;
2240
+ const previous = stateRef.current.threads.find((thread) => thread.id === target);
2268
2241
  const isCurrent = ownOperation();
2269
- const detail = await client.threads.get({ threadId: target, limit: 1, signal });
2270
- if (!isCurrent() || signal.aborted) return;
2271
- const summary = summaryToStored(detail);
2272
- update((current) => ({ ...current, threads: applyThreadSummaryEvent(current.threads, {
2273
- schema_version: "agents24.thread_summary_event.v1",
2274
- event: "thread.summary.updated",
2275
- cursor: 0,
2276
- thread: detail
2277
- }) }));
2242
+ const summary = await refreshChatSummaries({
2243
+ client,
2244
+ limit: threadPageSize,
2245
+ signal,
2246
+ current: () => stateRef.current,
2247
+ update,
2248
+ onDeleted(threadId) {
2249
+ if (isCurrent() && stateRef.current.activeThreadId === threadId) startNewThread();
2250
+ }
2251
+ });
2252
+ if (!target || !summary || !isCurrent() || signal.aborted || threadOpenControllerRef.current) return;
2278
2253
  const runId = activeRunIdFromThread(summary);
2279
- if (runId && !streamControllerRef.current) {
2254
+ const changed = previous?.active_run?.run_id !== summary.active_run?.run_id || previous?.active_run?.status !== summary.active_run?.status;
2255
+ if (runId && getThreadActivity(summary).isSpinning && (!streamControllerRef.current || stateRef.current.activeRunId !== runId)) {
2280
2256
  void attachRunRef.current(runId, runId === stateRef.current.activeRunId ? stateRef.current.cursor ?? void 0 : 0).catch(() => void 0);
2257
+ } else if (!getThreadActivity(summary).isSpinning && changed) {
2258
+ streamControllerRef.current?.abort();
2259
+ streamControllerRef.current = null;
2260
+ update((current) => ({
2261
+ ...current,
2262
+ activeRunId: summary.active_run?.run_id || null,
2263
+ streamingMessageId: null,
2264
+ runState: summary.active_run?.status || summary.last_run_status || "idle"
2265
+ }));
2266
+ }
2267
+ if (changed || pendingMessages.length || stateRef.current.runState === "reconnecting" || !streamControllerRef.current) {
2268
+ await refreshMessages(target, runId || void 0, signal);
2281
2269
  }
2282
- await refreshMessages(target, runId || void 0, signal);
2283
2270
  },
2284
2271
  onEvent(event) {
2285
2272
  if (event.event === "thread.deleted" && event.thread_id === stateRef.current.activeThreadId) startNewThread();
@@ -2305,7 +2292,7 @@ function useAgentChat(options = {}) {
2305
2292
  onDetach: detach,
2306
2293
  onResume() {
2307
2294
  const current = stateRef.current;
2308
- if (current.activeRunId && !streamControllerRef.current && !["paused", "waiting", "interrupted", "completed", "idle"].includes(current.runState)) {
2295
+ if (current.activeRunId && !streamControllerRef.current && !["paused", "waiting", "interrupted", "completed", "failed", "cancelled", "idle"].includes(current.runState)) {
2309
2296
  void attachRunRef.current(current.activeRunId, current.cursor ?? void 0).catch(() => void 0);
2310
2297
  }
2311
2298
  }