@agents24/react 0.2.0 → 0.2.1

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/index.js CHANGED
@@ -11,8 +11,34 @@ function isAbortLike(error) {
11
11
  }
12
12
 
13
13
  // src/model.ts
14
+ import {
15
+ compressionFromContextWindow,
16
+ mergeContextWindowUpdate,
17
+ normalizeContextCompression
18
+ } from "@agents24/client/protocol";
19
+ function chatAttachmentIds(attachments) {
20
+ return (attachments || []).flatMap((attachment) => typeof attachment.id === "string" && attachment.id.trim() ? [attachment.id.trim()] : []);
21
+ }
22
+ var chatNodeSkipReason = (value) => {
23
+ const normalized = text(value);
24
+ return normalized === "missing_required_input" || normalized === "upstream_input_unavailable" || normalized === "empty_optional_source" ? normalized : null;
25
+ };
26
+ function getThreadActivity(thread) {
27
+ const activeRun = thread.active_run || null;
28
+ const status = String(activeRun?.status || thread.last_run_status || "").trim().toLowerCase() || null;
29
+ return {
30
+ status,
31
+ runId: activeRun?.run_id || thread.last_run_id || null,
32
+ isSpinning: Boolean(activeRun),
33
+ isPaused: status === "paused",
34
+ isFailed: status === "failed",
35
+ isCompleted: status === "completed",
36
+ isCancelled: status === "cancelled" || status === "canceled"
37
+ };
38
+ }
14
39
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
15
40
  var text = (value) => typeof value === "string" && value.trim() ? value : null;
41
+ var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
16
42
  var statusToToolState = (value) => {
17
43
  const normalized = String(value || "").toLowerCase();
18
44
  if (normalized === "failed" || normalized === "error") return "output-error";
@@ -20,42 +46,167 @@ var statusToToolState = (value) => {
20
46
  if (["complete", "completed", "done", "success"].includes(normalized)) return "output-available";
21
47
  return normalized === "streaming" ? "input-streaming" : "input-available";
22
48
  };
49
+ function failureView(value) {
50
+ const candidate = record(value);
51
+ return candidate?.schema_version === "agents24.failure.v1" && text(candidate.failure_id) && text(candidate.code) && text(candidate.message) && typeof candidate.retryable === "boolean" && record(candidate.details) ? candidate : null;
52
+ }
23
53
  var allowedActions = {
24
54
  tool_review: ["approve", "reject"],
25
55
  mcp_auth: ["connect", "skip"],
26
56
  user_approval: ["approve", "reject"],
27
- app_data_permission: ["approve", "reject"]
57
+ app_data_permission: ["approve", "reject"],
58
+ user_input: ["respond"]
28
59
  };
29
- function partsFromResponseBlocks(blocks, fallbackText = "") {
60
+ function hitlQuestions(presentation) {
61
+ return (Array.isArray(presentation.questions) ? presentation.questions : []).flatMap((value) => {
62
+ const question = record(value);
63
+ if (!question || !Array.isArray(question.options)) return [];
64
+ const options = question.options.flatMap((rawOption) => {
65
+ const option = record(rawOption);
66
+ return option && text(option.id) && text(option.label) ? [{ id: String(option.id), label: String(option.label) }] : [];
67
+ });
68
+ return text(question.id) && text(question.header) && text(question.question) && options.length >= 2 ? [{
69
+ id: String(question.id),
70
+ header: String(question.header),
71
+ question: String(question.question),
72
+ options,
73
+ multiSelect: question.multi_select === true
74
+ }] : [];
75
+ });
76
+ }
77
+ function hitlResolution(value) {
78
+ const resolution = record(value);
79
+ if (!resolution) return null;
80
+ const answers = (Array.isArray(resolution.answers) ? resolution.answers : []).flatMap((value2) => {
81
+ const answer = record(value2);
82
+ if (!answer || !text(answer.question_id) || !Array.isArray(answer.selected_option_ids)) return [];
83
+ return [{
84
+ questionId: String(answer.question_id),
85
+ selectedOptionIds: answer.selected_option_ids.map(String),
86
+ ...text(answer.custom_text) ? { customText: String(answer.custom_text) } : {}
87
+ }];
88
+ });
89
+ return { ...resolution, ...answers.length ? { answers } : {} };
90
+ }
91
+ function partMetadata(block, id) {
92
+ return {
93
+ id,
94
+ runId: text(block.runId) || text(block.run_id),
95
+ sequence: finiteNumber(block.seq),
96
+ timestamp: text(block.ts),
97
+ raw: block
98
+ };
99
+ }
100
+ function toolPresentation(tool) {
101
+ const nested = record(tool.presentation) || {};
102
+ const childRunValue = record(tool.childRun) || record(tool.child_run);
103
+ const childRun = childRunValue && (text(childRunValue.runId || childRunValue.run_id) || text(childRunValue.agentName || childRunValue.agent_name) || text(childRunValue.status)) ? {
104
+ runId: text(childRunValue.runId) || text(childRunValue.run_id),
105
+ agentId: text(childRunValue.agentId) || text(childRunValue.agent_id),
106
+ agentName: text(childRunValue.agentName) || text(childRunValue.agent_name),
107
+ status: text(childRunValue.status),
108
+ error: text(childRunValue.error)
109
+ } : null;
110
+ const childTools = (Array.isArray(tool.childTools) ? tool.childTools : Array.isArray(tool.child_tools) ? tool.child_tools : []).flatMap((value) => {
111
+ const child = record(value);
112
+ if (!child || !text(child.id) || !text(child.title)) return [];
113
+ return [{
114
+ id: String(child.id),
115
+ runId: text(child.runId) || text(child.run_id),
116
+ toolCallId: text(child.toolCallId) || text(child.tool_call_id),
117
+ toolName: String(child.toolName || child.tool_name || child.title),
118
+ title: String(child.title),
119
+ status: String(child.status || ""),
120
+ detail: text(child.detail),
121
+ sequence: finiteNumber(child.seq),
122
+ timestamp: text(child.ts)
123
+ }];
124
+ });
125
+ return {
126
+ ...nested,
127
+ title: text(tool.title) || text(nested.title),
128
+ activity: text(tool.activity) || text(nested.activity),
129
+ detail: text(tool.detail) || text(nested.detail),
130
+ category: text(tool.category) || text(nested.category),
131
+ groupKey: text(tool.groupKey) || text(tool.group_key) || text(nested.groupKey) || text(nested.group_key),
132
+ groupLabel: text(tool.groupLabel) || text(tool.group_label) || text(nested.groupLabel) || text(nested.group_label),
133
+ path: text(tool.path),
134
+ threadId: text(tool.threadId) || text(tool.thread_id),
135
+ isExploration: tool.isExploration === true || tool.is_exploration === true,
136
+ childRun,
137
+ childTools,
138
+ activeChildToolId: text(tool.activeChildToolId) || text(tool.active_child_tool_id)
139
+ };
140
+ }
141
+ function partsFromResponseBlocks(blocks, fallbackText = "", fallbackPartId = "assistant-text") {
30
142
  const input = Array.isArray(blocks) ? blocks : [];
31
143
  const parts = input.flatMap((item, index) => {
32
144
  const block = record(item);
33
145
  if (!block) return [];
34
146
  const kind = String(block.kind || "data");
35
147
  const id = String(block.id || `${kind}-${index}`);
148
+ const metadata = partMetadata(block, id);
36
149
  if (kind === "assistant_text") {
37
150
  const value = text(block.text);
38
- return value ? [{ id, type: "text", kind: "text", text: value, raw: block }] : [];
151
+ return value ? [{ ...metadata, type: "text", kind: "text", text: value }] : [];
152
+ }
153
+ if (kind === "context_compression") {
154
+ const compression = normalizeContextCompression(block.compression);
155
+ if (!compression?.active) return [];
156
+ const status = compression.status === "started" || compression.status === "completed" || compression.status === "failed" ? compression.status : block.status === "complete" ? "completed" : block.status === "error" ? "failed" : "started";
157
+ return [{
158
+ ...metadata,
159
+ type: "context-compression",
160
+ kind: "context-compression",
161
+ status,
162
+ compression: { ...compression, status }
163
+ }];
164
+ }
165
+ if (kind === "node_activity") {
166
+ const node = record(block.node);
167
+ const activityId = text(node?.activityId) || text(node?.activity_id) || id;
168
+ const statusValue = String(node?.status || "").toLowerCase();
169
+ const status = ["completed", "failed", "cancelled", "skipped"].includes(statusValue) ? statusValue : "running";
170
+ const label = text(node?.label) || "Working";
171
+ const nodeKind = text(node?.kind) || "node";
172
+ return [{
173
+ ...metadata,
174
+ type: "node-activity",
175
+ kind: "node-activity",
176
+ activityId,
177
+ traceStepId: text(node?.traceStepId) || text(node?.trace_step_id) || null,
178
+ label,
179
+ nodeKind,
180
+ status,
181
+ defaultChatVisible: node?.defaultChatVisible === true,
182
+ reason: chatNodeSkipReason(node?.reason),
183
+ failure: failureView(node?.failure)
184
+ }];
39
185
  }
40
186
  if (kind === "tool" || kind === "tool_call" || kind === "tool_result") {
41
187
  const tool = record(block.tool) || block;
42
- const toolName = String(tool.action || tool.title || "tool");
188
+ const toolName = String(tool.toolName || tool.tool_name || tool.action || tool.title || "tool");
43
189
  return [{
44
- id,
190
+ ...metadata,
45
191
  type: `tool-${toolName}`,
46
192
  kind: "tool",
47
193
  toolName,
48
- toolCallId: text(tool.toolCallId),
194
+ toolKey: text(tool.toolKey) || text(tool.tool_key),
195
+ action: text(tool.action),
196
+ displayName: text(tool.displayName) || text(tool.display_name),
197
+ summary: text(tool.summary),
198
+ toolCallId: text(tool.toolCallId) || text(tool.tool_call_id),
199
+ sourceKind: text(tool.sourceKind) || text(tool.source_kind),
49
200
  state: statusToToolState(block.status),
50
201
  input: tool.input,
51
202
  output: tool.output,
52
203
  errorText: text(tool.error || block.error),
53
- presentation: record(tool.presentation),
54
- raw: block
204
+ failure: failureView(tool.failure),
205
+ presentation: toolPresentation(tool)
55
206
  }];
56
207
  }
57
- if (kind === "reasoning_note") return [{ id, type: "reasoning", kind: "reasoning", label: text(block.label), text: text(block.description), status: text(block.status), raw: block }];
58
- if (kind === "ui_blocks") return [{ id, type: "ui-blocks", kind: "ui-blocks", state: statusToToolState(block.status), toolCallId: text(block.toolCallId), contractVersion: text(block.contractVersion), bundle: record(block.bundle), errorText: text(block.error), raw: block }];
208
+ if (kind === "reasoning_note") return [{ ...metadata, type: "reasoning", kind: "reasoning", label: text(block.label), text: text(block.description), status: text(block.status) }];
209
+ if (kind === "ui_blocks") return [{ ...metadata, type: "ui-blocks", kind: "ui-blocks", state: statusToToolState(block.status), toolCallId: text(block.toolCallId), contractVersion: text(block.contractVersion), bundle: record(block.bundle), errorText: text(block.error) }];
59
210
  if (kind === "hitl_request") {
60
211
  const hitl = record(block.hitl) || {};
61
212
  const hitlKind = String(block.hitlKind || hitl.kind || "");
@@ -64,7 +215,7 @@ function partsFromResponseBlocks(blocks, fallbackText = "") {
64
215
  const expected = allowedActions[hitlKind];
65
216
  if (actions.length !== expected.length || actions.some((action, actionIndex) => action !== expected[actionIndex])) return [];
66
217
  return [{
67
- id,
218
+ ...metadata,
68
219
  type: "hitl",
69
220
  kind: "hitl",
70
221
  interruptId: String(block.interruptId || hitl.interrupt_id || ""),
@@ -73,63 +224,93 @@ function partsFromResponseBlocks(blocks, fallbackText = "") {
73
224
  allowedActions: actions,
74
225
  status: String(block.status || "pending"),
75
226
  presentation: record(hitl.presentation) || hitl,
76
- resolution: record(block.resolution),
77
- raw: block
227
+ questions: hitlQuestions(record(hitl.presentation) || {}),
228
+ resolution: hitlResolution(block.resolution)
78
229
  }];
79
230
  }
80
- if (kind === "error") return [{ id, type: "error", kind: "error", errorText: String(record(block.error)?.message || block.text || "The run failed."), raw: block }];
81
- if (kind === "source") return [{ id, type: "source", kind: "source", title: String(block.title || "Source"), url: text(block.url), description: text(block.description), raw: block }];
82
- if (kind === "citation") return [{ id, type: "citation", kind: "citation", sourceId: text(block.sourceId), label: text(block.label), url: text(block.url), raw: block }];
231
+ if (kind === "error") {
232
+ const failure = failureView(block.failure) || failureView(block.error);
233
+ return [{
234
+ ...metadata,
235
+ type: "error",
236
+ kind: "error",
237
+ errorText: failure?.message || String(block.text || "The run failed."),
238
+ failure
239
+ }];
240
+ }
241
+ if (kind === "source") return [{ ...metadata, type: "source", kind: "source", title: String(block.title || "Source"), url: text(block.url), description: text(block.description) }];
242
+ if (kind === "citation") return [{ ...metadata, type: "citation", kind: "citation", sourceId: text(block.sourceId), label: text(block.label), url: text(block.url) }];
83
243
  if (kind === "attachment") {
84
244
  const attachment = record(block.attachment) || {};
85
- return [{ id, type: "attachment", kind: "attachment", attachment: { ...attachment, id: text(attachment.id) || void 0, filename: text(attachment.name) || "Attachment", mediaType: text(attachment.mime_type) || void 0 }, raw: block }];
245
+ return [{ ...metadata, type: "attachment", kind: "attachment", attachment: { ...attachment, id: text(attachment.id) || void 0, filename: text(attachment.name) || "Attachment", mediaType: text(attachment.mime_type) || void 0 } }];
86
246
  }
87
- return [{ id, type: "data", kind: "data", name: kind, data: block, raw: block }];
247
+ return [{ ...metadata, type: "data", kind: "data", name: kind, data: block }];
88
248
  });
89
249
  if (!parts.some((part) => part.kind === "text") && fallbackText.trim()) {
90
- parts.push({ id: "assistant-text", type: "text", kind: "text", text: fallbackText.trim() });
250
+ parts.push({ id: fallbackPartId, type: "text", kind: "text", text: fallbackText.trim() });
91
251
  }
92
252
  return parts;
93
253
  }
94
254
  function assistantProjectionFromEvent(event) {
95
255
  const payload = record(event.payload) || {};
96
256
  const content = text(payload.assistant_output_text) || "";
97
- const fromBlocks = partsFromResponseBlocks(payload.response_blocks, content);
98
- if (fromBlocks.length) return { content, parts: fromBlocks };
99
- if (event.event === "reasoning.update") return {
257
+ const fromBlocks = partsFromResponseBlocks(
258
+ payload.response_blocks,
100
259
  content,
101
- parts: [{ id: `reasoning-${event.seq}`, type: "reasoning", kind: "reasoning", label: text(payload.label), text: text(payload.description), status: text(payload.status), raw: payload }]
260
+ `${event.run_id}:assistant-text`
261
+ );
262
+ if (fromBlocks.length) return {
263
+ content: content || fromBlocks.filter((part) => part.kind === "text").map((part) => part.text).join(""),
264
+ parts: fromBlocks
102
265
  };
103
- if (event.event === "ui_blocks.updated") return {
104
- content,
105
- parts: [{ id: `ui-blocks-${event.seq}`, type: "ui-blocks", kind: "ui-blocks", state: "input-available", bundle: record(payload.bundle), raw: payload }]
266
+ const fromPatch = partsFromResponseBlocks(
267
+ payload.response_block === void 0 ? void 0 : [payload.response_block]
268
+ );
269
+ if (fromPatch.length) return { content, parts: fromPatch };
270
+ return { content, parts: [] };
271
+ }
272
+ function modelTurnPartFromEvent(event) {
273
+ const eventName = String(event.event || "");
274
+ const payload = record(event.payload) || {};
275
+ const source = eventName === "run.snapshot" ? record(payload.active_model_turn) : payload;
276
+ if (!source || eventName !== "model.turn.started" && eventName !== "run.snapshot" || source.status !== "running" || !text(source.model_turn_id)) return null;
277
+ const modelTurnId = String(source.model_turn_id);
278
+ const iteration = finiteNumber(source.iteration);
279
+ return {
280
+ id: `model-turn:${modelTurnId}`,
281
+ type: "model-turn",
282
+ kind: "model-turn",
283
+ modelTurnId,
284
+ turnKind: source.kind === "agent" ? "agent" : "model",
285
+ status: "running",
286
+ runId: event.run_id || null,
287
+ sequence: finiteNumber(event.seq),
288
+ timestamp: text(event.ts),
289
+ ...iteration === null ? {} : { iteration },
290
+ raw: source
106
291
  };
107
- if (event.event === "runtime.error") return {
108
- content,
109
- parts: [{ id: `error-${event.seq}`, type: "error", kind: "error", errorText: String(record(payload.error)?.message || "The run failed."), raw: payload }]
292
+ }
293
+ function contextCompressionPartFromEvent(event, existingParts = []) {
294
+ if (String(event.event || "") !== "context.compression.updated") return null;
295
+ const payload = record(event.payload) || {};
296
+ const compression = normalizeContextCompression(payload.context_compression);
297
+ if (!compression?.active) return null;
298
+ const status = compression.status === "completed" || compression.status === "failed" ? compression.status : "started";
299
+ const activePart = [...existingParts].reverse().find(
300
+ (part) => part.kind === "context-compression" && part.status === "started"
301
+ );
302
+ const sequence = finiteNumber(event.seq);
303
+ const id = status === "started" ? `context-compression:${event.run_id || "run"}:${sequence ?? "active"}` : activePart?.id || `context-compression:${event.run_id || "run"}:${sequence ?? "terminal"}`;
304
+ return {
305
+ id,
306
+ type: "context-compression",
307
+ kind: "context-compression",
308
+ runId: event.run_id || null,
309
+ sequence,
310
+ timestamp: text(event.ts),
311
+ status,
312
+ compression
110
313
  };
111
- if ((event.event === "source.added" || event.event === "citation.added") && record(payload.source)) {
112
- return { content, parts: partsFromResponseBlocks([payload.source]) };
113
- }
114
- if (event.event === "tool.started") {
115
- const toolName = String(payload.display_name || "Tool");
116
- return {
117
- content,
118
- parts: [{ id: String(payload.tool_call_id || `tool-${event.seq}`), type: `tool-${toolName}`, kind: "tool", toolName, toolCallId: text(payload.tool_call_id), state: "input-available", presentation: { title: toolName }, raw: payload }]
119
- };
120
- }
121
- if (event.event === "hitl.requested") {
122
- const hitl = record(payload.hitl) || {};
123
- const kind = String(hitl.kind || "");
124
- const actions = Array.isArray(hitl.allowed_actions) ? hitl.allowed_actions.map(String) : [];
125
- if (kind in allowedActions && actions.length === allowedActions[kind].length && actions.every((action, index) => action === allowedActions[kind][index])) {
126
- return {
127
- content,
128
- parts: [{ id: `hitl-${event.seq}`, type: "hitl", kind: "hitl", interruptId: String(hitl.interrupt_id || ""), hitlKind: kind, message: String(hitl.message || ""), allowedActions: actions, status: "pending", presentation: record(hitl.presentation) || hitl, raw: payload }]
129
- };
130
- }
131
- }
132
- return { content, parts: [] };
133
314
  }
134
315
  function threadDetailToMessages(detail) {
135
316
  return (detail.turns || []).flatMap((item, index) => {
@@ -138,7 +319,25 @@ function threadDetailToMessages(detail) {
138
319
  const assistantText = text(turn.assistant_output_text) || "";
139
320
  const runId = text(turn.run_id);
140
321
  const createdAt = text(turn.created_at) || (/* @__PURE__ */ new Date(0)).toISOString();
141
- const attachments = Array.isArray(turn.attachments) ? turn.attachments.flatMap((attachment) => record(attachment) ? [record(attachment)] : []) : [];
322
+ const rawFeedback = record(turn.feedback);
323
+ const feedback = rawFeedback && (rawFeedback.rating === "like" || rawFeedback.rating === "dislike") && text(rawFeedback.created_at) && text(rawFeedback.updated_at) ? {
324
+ rating: rawFeedback.rating === "like" ? "like" : "dislike",
325
+ reason: [
326
+ "incorrect",
327
+ "irrelevant",
328
+ "incomplete",
329
+ "unsafe",
330
+ "citation_issue",
331
+ "other"
332
+ ].includes(String(rawFeedback.reason || "")) ? rawFeedback.reason : null,
333
+ comment: text(rawFeedback.comment),
334
+ created_at: String(rawFeedback.created_at),
335
+ updated_at: String(rawFeedback.updated_at)
336
+ } : null;
337
+ const attachments = Array.isArray(turn.attachments) ? turn.attachments.flatMap((attachment) => {
338
+ const value = record(attachment);
339
+ return value ? [attachmentResultToChatAttachment(value)] : [];
340
+ }) : [];
142
341
  const output = [];
143
342
  if (userText || attachments.length) output.push({
144
343
  id: String(turn.id || `${runId || index}-user`),
@@ -149,21 +348,45 @@ function threadDetailToMessages(detail) {
149
348
  attachments,
150
349
  messageIndex: typeof turn.turn_index === "number" ? turn.turn_index : index
151
350
  });
152
- if (assistantText || Array.isArray(turn.response_blocks)) output.push({
153
- id: `${runId || turn.id || index}-assistant`,
154
- role: "assistant",
155
- runId,
156
- content: assistantText,
157
- createdAt: text(turn.completed_at) || createdAt,
158
- isFinal: !["queued", "running", "paused", "cancelling"].includes(String(turn.status || "").toLowerCase()),
159
- parts: partsFromResponseBlocks(turn.response_blocks, assistantText),
160
- messageIndex: typeof turn.turn_index === "number" ? turn.turn_index : index
161
- });
351
+ if (assistantText || Array.isArray(turn.response_blocks)) {
352
+ const assistantParts = partsFromResponseBlocks(
353
+ turn.response_blocks,
354
+ assistantText,
355
+ `${runId || turn.id || index}-assistant-text`
356
+ );
357
+ output.push({
358
+ id: `${runId || turn.id || index}-assistant`,
359
+ role: "assistant",
360
+ runId,
361
+ content: assistantText,
362
+ createdAt: text(turn.completed_at) || createdAt,
363
+ isFinal: !["queued", "running", "paused", "cancelling"].includes(String(turn.status || "").toLowerCase()),
364
+ feedback,
365
+ parts: assistantParts,
366
+ messageIndex: typeof turn.turn_index === "number" ? turn.turn_index : index
367
+ });
368
+ }
162
369
  return output;
163
370
  });
164
371
  }
372
+ function latestThreadContext(detail) {
373
+ let contextWindow = null;
374
+ for (const turn of detail.turns || []) {
375
+ contextWindow = mergeContextWindowUpdate(contextWindow, turn.context_window);
376
+ }
377
+ return {
378
+ contextWindow,
379
+ contextCompression: compressionFromContextWindow(contextWindow)
380
+ };
381
+ }
165
382
  function attachmentResultToChatAttachment(result) {
166
- return { ...result, id: result.id, filename: result.filename, mediaType: result.mime_type, status: result.status };
383
+ return {
384
+ ...result,
385
+ id: result.id,
386
+ filename: result.filename,
387
+ mediaType: result.mime_type,
388
+ status: result.processing_status
389
+ };
167
390
  }
168
391
 
169
392
  // src/provider.tsx
@@ -193,6 +416,11 @@ function useAgentClient() {
193
416
  }
194
417
 
195
418
  // src/state.ts
419
+ import {
420
+ compressionFromContextWindow as compressionFromContextWindow2,
421
+ mergeContextWindowUpdate as mergeContextWindowUpdate2,
422
+ normalizeContextCompression as normalizeContextCompression2
423
+ } from "@agents24/client/protocol";
196
424
  var initialAgentChatState = {
197
425
  threads: [],
198
426
  activeThreadId: null,
@@ -200,9 +428,62 @@ var initialAgentChatState = {
200
428
  activeRunId: null,
201
429
  cursor: null,
202
430
  streamingMessageId: null,
431
+ contextWindow: null,
432
+ contextCompression: null,
433
+ contextCompressionRevision: 0,
203
434
  runState: "idle",
204
- error: null
435
+ error: null,
436
+ operationError: null,
437
+ backgroundError: null
205
438
  };
439
+ function captureAgentChatThreadSelection(state) {
440
+ return {
441
+ activeRunId: state.activeRunId,
442
+ activeThreadId: state.activeThreadId,
443
+ cursor: state.cursor,
444
+ messages: state.messages,
445
+ runState: state.runState,
446
+ streamingMessageId: state.streamingMessageId,
447
+ contextWindow: state.contextWindow,
448
+ contextCompression: state.contextCompression,
449
+ contextCompressionRevision: state.contextCompressionRevision
450
+ };
451
+ }
452
+ function restoreAgentChatThreadSelection(state, snapshot) {
453
+ return {
454
+ ...state,
455
+ ...snapshot,
456
+ runState: snapshot.activeRunId ? "reconnecting" : snapshot.runState
457
+ };
458
+ }
459
+ function withAgentChatOperationError(state, operation, cause) {
460
+ return {
461
+ ...state,
462
+ operationError: { kind: "operation", operation, cause }
463
+ };
464
+ }
465
+ function clearAgentChatOperationError(state, operation) {
466
+ if (!state.operationError || operation && state.operationError.operation !== operation) {
467
+ return state;
468
+ }
469
+ return { ...state, operationError: null };
470
+ }
471
+ function withAgentChatBackgroundError(state, cause) {
472
+ return {
473
+ ...state,
474
+ backgroundError: { kind: "background", source: "thread_events", cause }
475
+ };
476
+ }
477
+ function clearAgentChatBackgroundError(state) {
478
+ return state.backgroundError ? { ...state, backgroundError: null } : state;
479
+ }
480
+ function reconcileRefreshedThreads(current, incoming, activeThreadId) {
481
+ if (!activeThreadId || incoming.some((thread) => thread.id === activeThreadId)) {
482
+ return incoming;
483
+ }
484
+ const active = current.find((thread) => thread.id === activeThreadId);
485
+ return active?.isHydrated ? [active, ...incoming] : incoming;
486
+ }
206
487
  function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
207
488
  const next = [...messages];
208
489
  if (!next.some((message) => message.id === userMessage.id)) next.push(userMessage);
@@ -222,6 +503,43 @@ function upsertStableAssistantMessage(messages, input) {
222
503
  else next[index] = input.update(next[index]);
223
504
  return next;
224
505
  }
506
+ function appendAssistantDelta(parts, delta, fallbackPartId) {
507
+ const next = [...parts];
508
+ const trailing = next[next.length - 1];
509
+ if (trailing?.kind === "text") {
510
+ next[next.length - 1] = { ...trailing, text: `${trailing.text}${delta}` };
511
+ return next;
512
+ }
513
+ next.push({ id: fallbackPartId, type: "text", kind: "text", text: delta });
514
+ return next;
515
+ }
516
+ function upsertResponseBlockParts(parts, incoming) {
517
+ const next = [...parts];
518
+ for (const part of incoming) {
519
+ const index = next.findIndex((current) => current.id === part.id);
520
+ if (index === -1) next.push(part);
521
+ else next[index] = part;
522
+ }
523
+ return next;
524
+ }
525
+ function reconcileModelTurnParts(parts, event) {
526
+ const eventName = String(event.event || "");
527
+ const payload = event.payload || {};
528
+ const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
529
+ const modelTurnId = typeof payload.model_turn_id === "string" ? payload.model_turn_id : null;
530
+ let next = parts;
531
+ if (eventName === "run.snapshot" || terminal) {
532
+ next = next.filter((part) => part.kind !== "model-turn");
533
+ } else if (eventName === "assistant.delta" && typeof payload.content === "string" && payload.content) {
534
+ next = next.filter((part) => part.kind !== "model-turn" || part.turnKind !== "agent");
535
+ } else if (eventName.startsWith("tool.") || eventName === "node.status.updated") {
536
+ next = next.filter((part) => part.kind !== "model-turn" || part.turnKind !== "agent");
537
+ } else if ((eventName === "model.turn.failed" || eventName === "model.turn.completed" && payload.outcome !== "tool_calls") && modelTurnId) {
538
+ next = next.filter((part) => part.kind !== "model-turn" || part.modelTurnId !== modelTurnId);
539
+ }
540
+ const active = modelTurnPartFromEvent(event);
541
+ return active ? upsertResponseBlockParts(next, [active]) : next;
542
+ }
225
543
  function applyRuntimeEvent(state, event) {
226
544
  const projection = assistantProjectionFromEvent(event);
227
545
  const eventName = String(event.event || "");
@@ -229,41 +547,84 @@ function applyRuntimeEvent(state, event) {
229
547
  const messageId = state.streamingMessageId || `${runId || "run"}-assistant`;
230
548
  const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
231
549
  const paused = eventName === "run.paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
232
- const existing = state.messages.find((message) => message.id === messageId || Boolean(runId && message.runId === runId));
550
+ const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
551
+ const existing = state.messages.find((message) => message.role === "assistant" && (message.id === messageId || Boolean(runId && message.runId === runId)));
233
552
  const payload = event.payload || {};
553
+ const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
554
+ const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
555
+ const contextWindow = contextApplies ? mergeContextWindowUpdate2(state.contextWindow, payload.context_window) : state.contextWindow;
556
+ const explicitCompression = contextApplies ? normalizeContextCompression2(payload.context_compression) : null;
557
+ const contextCompression = contextApplies ? explicitCompression || compressionFromContextWindow2(contextWindow) || state.contextCompression : state.contextCompression;
558
+ const hasLiveCompressionUpdate = Boolean(
559
+ contextApplies && (explicitCompression?.active || compressionFromContextWindow2(
560
+ mergeContextWindowUpdate2(null, payload.context_window)
561
+ )?.active)
562
+ );
234
563
  const delta = eventName === "assistant.delta" && typeof payload.content === "string" ? payload.content : "";
235
- const content = delta ? `${existing?.content || ""}${delta}` : projection.content || existing?.content || "";
236
- const deltaParts = delta ? [{ id: `${runId || "run"}-assistant-text`, type: "text", kind: "text", text: content }] : [];
237
- const incomingParts = projection.parts.length ? projection.parts : deltaParts;
238
- const parts = incomingParts.length ? [...(existing?.parts || []).filter((part) => !incomingParts.some((incoming) => incoming.id === part.id || incoming.kind === "text" && part.kind === "text")), ...incomingParts] : existing?.parts || [];
564
+ const hasCanonicalResponseBlocks = Array.isArray(payload.response_blocks);
565
+ const content = hasCanonicalResponseBlocks ? projection.content : delta ? `${existing?.content || ""}${delta}` : projection.content || existing?.content || "";
566
+ const projectedParts = hasCanonicalResponseBlocks ? projection.parts : delta ? appendAssistantDelta(
567
+ existing?.parts || [],
568
+ delta,
569
+ `${runId || "run"}-assistant-text-${event.seq}`
570
+ ) : projection.parts.length ? upsertResponseBlockParts(existing?.parts || [], projection.parts) : existing?.parts || [];
571
+ const lifecycleParts = reconcileModelTurnParts(projectedParts, event);
572
+ const compressionPart = contextApplies && explicitCompression?.active ? contextCompressionPartFromEvent(event, lifecycleParts) : null;
573
+ const parts = compressionPart ? upsertResponseBlockParts(lifecycleParts, [compressionPart]) : lifecycleParts;
574
+ const activeThreadId = eventThreadId || state.activeThreadId;
575
+ const messages = upsertStableAssistantMessage(state.messages, {
576
+ messageId,
577
+ runId,
578
+ create: () => ({
579
+ id: messageId,
580
+ role: "assistant",
581
+ runId,
582
+ content,
583
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
584
+ isFinal: terminal,
585
+ parts
586
+ }),
587
+ update: (message) => ({
588
+ ...message,
589
+ runId: runId ?? message.runId,
590
+ content,
591
+ parts,
592
+ isFinal: terminal || message.isFinal
593
+ })
594
+ });
595
+ const threads = activeThreadId ? state.threads.some((thread) => thread.id === activeThreadId) ? state.threads.map((thread) => thread.id === activeThreadId ? {
596
+ ...thread,
597
+ messages,
598
+ isHydrated: true,
599
+ last_run_id: runId ?? thread.last_run_id,
600
+ contextWindow,
601
+ contextCompression
602
+ } : thread) : [{
603
+ id: activeThreadId,
604
+ title: null,
605
+ messages,
606
+ isHydrated: true,
607
+ last_run_id: runId,
608
+ contextWindow,
609
+ contextCompression
610
+ }, ...state.threads] : state.threads;
239
611
  return {
240
612
  ...state,
613
+ activeThreadId,
241
614
  activeRunId: terminal ? null : runId,
242
615
  cursor: typeof event.seq === "number" ? event.seq : state.cursor,
243
- messages: upsertStableAssistantMessage(state.messages, {
244
- messageId,
245
- runId,
246
- create: () => ({
247
- id: messageId,
248
- role: "assistant",
249
- runId,
250
- content,
251
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
252
- isFinal: terminal,
253
- parts
254
- }),
255
- update: (message) => ({
256
- ...message,
257
- runId: runId ?? message.runId,
258
- content,
259
- parts,
260
- isFinal: terminal || message.isFinal
261
- })
262
- }),
263
- runState: paused ? "paused" : terminal ? eventName === "run.completed" ? "completed" : "failed" : "streaming",
616
+ messages,
617
+ threads,
618
+ contextWindow,
619
+ contextCompression,
620
+ contextCompressionRevision: hasLiveCompressionUpdate ? state.contextCompressionRevision + 1 : state.contextCompressionRevision,
621
+ runState: paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming",
264
622
  streamingMessageId: terminal || paused ? null : messageId
265
623
  };
266
624
  }
625
+ function applyRuntimeEvents(state, events) {
626
+ return events.reduce(applyRuntimeEvent, state);
627
+ }
267
628
  function applyThreadSummaryEvent(threads, event) {
268
629
  if (event.event === "snapshot_required") return threads;
269
630
  if (event.event === "thread.deleted") return threads.filter((thread) => thread.id !== event.thread_id);
@@ -278,15 +639,74 @@ function applyThreadSummaryEvent(threads, event) {
278
639
 
279
640
  // src/use-agent-chat.ts
280
641
  import * as React2 from "react";
642
+
643
+ // src/runtime-event-buffer.ts
644
+ function runtimeEventFrameScheduler() {
645
+ const runtime = globalThis;
646
+ if (typeof runtime.requestAnimationFrame !== "function") return void 0;
647
+ return (callback) => {
648
+ const handle = runtime.requestAnimationFrame?.(callback);
649
+ return () => {
650
+ if (handle !== void 0) runtime.cancelAnimationFrame?.(handle);
651
+ };
652
+ };
653
+ }
654
+ function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
655
+ let pending = [];
656
+ let cancelFrame = null;
657
+ const flush = () => {
658
+ cancelFrame?.();
659
+ cancelFrame = null;
660
+ if (!pending.length) return;
661
+ const events = pending;
662
+ pending = [];
663
+ apply(events);
664
+ };
665
+ return {
666
+ dispose() {
667
+ cancelFrame?.();
668
+ cancelFrame = null;
669
+ pending = [];
670
+ },
671
+ flush,
672
+ push(event) {
673
+ if (event.event !== "assistant.delta" || !scheduleFrame) {
674
+ pending.push(event);
675
+ flush();
676
+ return;
677
+ }
678
+ pending.push(event);
679
+ if (cancelFrame) return;
680
+ cancelFrame = scheduleFrame(() => {
681
+ cancelFrame = null;
682
+ if (!pending.length) return;
683
+ const events = pending;
684
+ pending = [];
685
+ apply(events);
686
+ });
687
+ }
688
+ };
689
+ }
690
+
691
+ // src/initial-thread.ts
692
+ function resolveInitialActiveThreadId(options, storedThreadId) {
693
+ return Object.prototype.hasOwnProperty.call(options, "activeThreadId") ? options.activeThreadId ?? null : storedThreadId;
694
+ }
695
+ function shouldHydrateSelectedThread(selectedThreadId, activeThreadId, activeThreadIsHydrated) {
696
+ return Boolean(selectedThreadId) && (selectedThreadId !== activeThreadId || !activeThreadIsHydrated);
697
+ }
698
+ function isCurrentThreadOpenRequest(input) {
699
+ return !input.signalAborted && input.requestGeneration === input.currentGeneration && input.requestedThreadId === input.activeThreadId;
700
+ }
701
+
702
+ // src/use-agent-chat.ts
281
703
  function summaryToStored(thread) {
282
704
  return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
283
705
  }
284
706
  function activeRunIdFromThread(thread) {
285
707
  if (!thread) return null;
286
- const active = thread.active_run;
287
- const status = String(active?.status || thread.last_run_status || "").toLowerCase();
288
- if (!["queued", "running", "cancelling"].includes(status)) return null;
289
- return String(active?.run_id || thread.last_run_id || "") || null;
708
+ const activity = getThreadActivity(thread);
709
+ return activity.isSpinning ? activity.runId : null;
290
710
  }
291
711
  function useAgentChat(options = {}) {
292
712
  const runtime = useAgents24Runtime();
@@ -295,7 +715,10 @@ function useAgentChat(options = {}) {
295
715
  const threadPageSize = options.threadPageSize ?? 50;
296
716
  const [state, setState] = React2.useState(() => ({
297
717
  ...initialAgentChatState,
298
- activeThreadId: options.activeThreadId ?? runtime.storage?.loadActiveThreadId?.() ?? null,
718
+ activeThreadId: resolveInitialActiveThreadId(
719
+ options,
720
+ runtime.storage?.loadActiveThreadId?.() ?? null
721
+ ),
299
722
  threads: runtime.storage?.loadThreads() ?? []
300
723
  }));
301
724
  const [isLoadingThreads, setIsLoadingThreads] = React2.useState(false);
@@ -305,6 +728,9 @@ function useAgentChat(options = {}) {
305
728
  const [isLoadingOlder, setIsLoadingOlder] = React2.useState(false);
306
729
  const [isSubmitting, setIsSubmitting] = React2.useState(false);
307
730
  const streamControllerRef = React2.useRef(null);
731
+ const threadOpenControllerRef = React2.useRef(null);
732
+ const threadOpenGenerationRef = React2.useRef(0);
733
+ const refreshThreadsPromiseRef = React2.useRef(null);
308
734
  const attachRunRef = React2.useRef(async () => void 0);
309
735
  const stateRef = React2.useRef(state);
310
736
  stateRef.current = state;
@@ -320,33 +746,87 @@ function useAgentChat(options = {}) {
320
746
  return next;
321
747
  });
322
748
  }, [persist]);
749
+ const eventBuffer = React2.useMemo(
750
+ () => createRuntimeEventFrameBuffer(
751
+ (events) => update((current) => applyRuntimeEvents(current, events)),
752
+ runtimeEventFrameScheduler()
753
+ ),
754
+ [update]
755
+ );
756
+ const clearOperationError = React2.useCallback((operation) => {
757
+ update((current) => clearAgentChatOperationError(current, operation));
758
+ }, [update]);
759
+ const clearBackgroundError = React2.useCallback(() => {
760
+ update(clearAgentChatBackgroundError);
761
+ }, [update]);
762
+ const failOperation = React2.useCallback((operation, cause) => {
763
+ update((current) => withAgentChatOperationError(current, operation, cause));
764
+ }, [update]);
765
+ const invalidateThreadOpen = React2.useCallback(() => {
766
+ threadOpenGenerationRef.current += 1;
767
+ threadOpenControllerRef.current?.abort("Thread selection changed.");
768
+ threadOpenControllerRef.current = null;
769
+ setIsLoadingHistory(false);
770
+ }, []);
323
771
  const detach = React2.useCallback(() => {
772
+ eventBuffer.flush();
324
773
  streamControllerRef.current?.abort();
325
774
  streamControllerRef.current = null;
326
775
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
327
- }, [update]);
328
- React2.useEffect(() => () => streamControllerRef.current?.abort(), []);
329
- const refreshThreads = React2.useCallback(async () => {
776
+ }, [eventBuffer, update]);
777
+ React2.useEffect(() => () => {
778
+ eventBuffer.dispose();
779
+ streamControllerRef.current?.abort();
780
+ threadOpenControllerRef.current?.abort();
781
+ }, [eventBuffer]);
782
+ const refreshThreads = React2.useCallback(() => {
783
+ if (refreshThreadsPromiseRef.current) return refreshThreadsPromiseRef.current;
784
+ clearOperationError();
330
785
  setIsLoadingThreads(true);
331
- try {
332
- const result = await client.threads.list({ limit: threadPageSize });
333
- setThreadTotal(result.total);
334
- update((current) => {
335
- const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
336
- const nextThreads = result.items.map((item) => {
337
- const existing = byId.get(item.id);
338
- return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
786
+ const request = (async () => {
787
+ try {
788
+ const result = await client.threads.list({ limit: threadPageSize });
789
+ setThreadTotal(result.total);
790
+ update((current) => {
791
+ const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
792
+ const nextThreads = result.items.map((item) => {
793
+ const existing = byId.get(item.id);
794
+ return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
795
+ });
796
+ return {
797
+ ...current,
798
+ threads: reconcileRefreshedThreads(
799
+ current.threads,
800
+ nextThreads,
801
+ current.activeThreadId
802
+ )
803
+ };
339
804
  });
340
- return { ...current, threads: nextThreads };
341
- });
342
- } finally {
343
- setIsLoadingThreads(false);
344
- }
345
- }, [client, threadPageSize, update]);
805
+ } catch (error) {
806
+ if (!isAbortLike(error)) {
807
+ failOperation("refresh_threads", error);
808
+ throw error;
809
+ }
810
+ } finally {
811
+ setIsLoadingThreads(false);
812
+ }
813
+ })();
814
+ refreshThreadsPromiseRef.current = request;
815
+ void request.then(
816
+ () => {
817
+ if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
818
+ },
819
+ () => {
820
+ if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
821
+ }
822
+ );
823
+ return request;
824
+ }, [clearOperationError, client, failOperation, threadPageSize, update]);
346
825
  const loadMoreThreads = React2.useCallback(async () => {
347
826
  if (isLoadingMoreThreads) return;
348
827
  const skip = stateRef.current.threads.length;
349
828
  if (threadTotal !== void 0 && skip >= threadTotal) return;
829
+ clearOperationError();
350
830
  setIsLoadingMoreThreads(true);
351
831
  try {
352
832
  const result = await client.threads.list({ skip, limit: threadPageSize });
@@ -364,64 +844,126 @@ function useAgentChat(options = {}) {
364
844
  }
365
845
  return { ...current, threads: [...byId.values()] };
366
846
  });
847
+ } catch (error) {
848
+ if (!isAbortLike(error)) {
849
+ failOperation("load_more_threads", error);
850
+ throw error;
851
+ }
367
852
  } finally {
368
853
  setIsLoadingMoreThreads(false);
369
854
  }
370
- }, [client, isLoadingMoreThreads, threadPageSize, threadTotal, update]);
855
+ }, [clearOperationError, client, failOperation, isLoadingMoreThreads, threadPageSize, threadTotal, update]);
371
856
  const openThread = React2.useCallback(async (threadId) => {
857
+ const previousSelection = captureAgentChatThreadSelection(stateRef.current);
858
+ threadOpenControllerRef.current?.abort("A newer thread was selected.");
859
+ const requestController = runtime.createAbortController();
860
+ threadOpenControllerRef.current = requestController;
861
+ const requestGeneration = threadOpenGenerationRef.current + 1;
862
+ threadOpenGenerationRef.current = requestGeneration;
372
863
  detach();
373
- update((current) => ({ ...current, activeThreadId: threadId, error: null, messages: current.threads.find((thread) => thread.id === threadId)?.messages || [] }));
864
+ clearOperationError();
865
+ update((current) => ({
866
+ ...current,
867
+ activeThreadId: threadId,
868
+ activeRunId: null,
869
+ cursor: null,
870
+ error: null,
871
+ messages: current.threads.find((thread) => thread.id === threadId)?.messages || [],
872
+ contextWindow: current.threads.find((thread) => thread.id === threadId)?.contextWindow || null,
873
+ contextCompression: current.threads.find((thread) => thread.id === threadId)?.contextCompression || null,
874
+ contextCompressionRevision: 0,
875
+ runState: "idle",
876
+ streamingMessageId: null
877
+ }));
374
878
  setIsLoadingHistory(true);
375
879
  try {
376
- const detail = await client.threads.get({ threadId, limit: pageSize, includeRunEvents: true });
880
+ const detail = await client.threads.get({
881
+ threadId,
882
+ limit: pageSize,
883
+ signal: requestController.signal
884
+ });
885
+ if (!isCurrentThreadOpenRequest({
886
+ activeThreadId: stateRef.current.activeThreadId,
887
+ currentGeneration: threadOpenGenerationRef.current,
888
+ requestGeneration,
889
+ requestedThreadId: threadId,
890
+ signalAborted: requestController.signal.aborted
891
+ })) return;
377
892
  const messages = threadDetailToMessages(detail);
893
+ const context = latestThreadContext(detail);
378
894
  const stored = {
379
895
  ...detail,
380
896
  messages,
381
897
  isHydrated: true,
382
898
  hasOlderTurns: Boolean(detail.paging?.has_more),
383
- nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null
899
+ nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null,
900
+ ...context
384
901
  };
385
- update((current) => ({
902
+ update((current) => isCurrentThreadOpenRequest({
903
+ activeThreadId: current.activeThreadId,
904
+ currentGeneration: threadOpenGenerationRef.current,
905
+ requestGeneration,
906
+ requestedThreadId: threadId,
907
+ signalAborted: requestController.signal.aborted
908
+ }) ? {
386
909
  ...current,
387
910
  activeThreadId: threadId,
388
911
  messages,
912
+ ...context,
913
+ contextCompressionRevision: 0,
389
914
  activeRunId: activeRunIdFromThread(stored),
390
915
  threads: current.threads.some((thread) => thread.id === threadId) ? current.threads.map((thread) => thread.id === threadId ? stored : thread) : [stored, ...current.threads]
391
- }));
916
+ } : current);
392
917
  const activeRunId = activeRunIdFromThread(stored);
393
- if (activeRunId) await attachRunRef.current(activeRunId);
918
+ if (activeRunId && isCurrentThreadOpenRequest({
919
+ activeThreadId: stateRef.current.activeThreadId,
920
+ currentGeneration: threadOpenGenerationRef.current,
921
+ requestGeneration,
922
+ requestedThreadId: threadId,
923
+ signalAborted: requestController.signal.aborted
924
+ })) await attachRunRef.current(activeRunId).catch(() => void 0);
925
+ } catch (error) {
926
+ if (!isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
927
+ failOperation("open_thread", error);
928
+ update((current) => restoreAgentChatThreadSelection(current, previousSelection));
929
+ if (previousSelection.activeRunId) {
930
+ void attachRunRef.current(previousSelection.activeRunId, previousSelection.cursor ?? void 0).catch(() => void 0);
931
+ }
932
+ throw error;
933
+ }
394
934
  } finally {
395
- setIsLoadingHistory(false);
935
+ if (requestGeneration === threadOpenGenerationRef.current) {
936
+ if (threadOpenControllerRef.current === requestController) threadOpenControllerRef.current = null;
937
+ setIsLoadingHistory(false);
938
+ }
396
939
  }
397
- }, [client, detach, pageSize, update]);
940
+ }, [clearOperationError, client, detach, failOperation, pageSize, runtime, update]);
398
941
  const consumeEvent = React2.useCallback((event) => {
399
- update((current) => {
400
- const next = applyRuntimeEvent(current, event);
401
- if (!next.activeThreadId) return next;
402
- return {
403
- ...next,
404
- threads: next.threads.map((thread) => thread.id === next.activeThreadId ? { ...thread, messages: next.messages, isHydrated: true, last_run_id: next.activeRunId } : thread)
405
- };
406
- });
407
- }, [update]);
942
+ eventBuffer.push(event);
943
+ }, [eventBuffer]);
408
944
  const attachRun = React2.useCallback(async (runId, cursor) => {
409
945
  streamControllerRef.current?.abort();
410
946
  const controller = runtime.createAbortController();
411
947
  streamControllerRef.current = controller;
948
+ clearOperationError();
412
949
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
413
950
  try {
414
951
  const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
415
952
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
416
953
  } catch (error) {
417
- if (!isAbortLike(error)) update((current) => ({ ...current, error, runState: "failed", streamingMessageId: null }));
954
+ if (!isAbortLike(error)) {
955
+ update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
956
+ failOperation("attach_run", error);
957
+ throw error;
958
+ }
418
959
  } finally {
419
960
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
420
961
  }
421
- }, [client, consumeEvent, runtime, update]);
962
+ }, [clearOperationError, client, consumeEvent, failOperation, runtime, update]);
422
963
  attachRunRef.current = attachRun;
423
964
  const submit = React2.useCallback(async (input) => {
424
965
  if (!input.text.trim() && !input.attachmentIds?.length) return;
966
+ invalidateThreadOpen();
425
967
  detach();
426
968
  const controller = runtime.createAbortController();
427
969
  streamControllerRef.current = controller;
@@ -430,76 +972,208 @@ function useAgentChat(options = {}) {
430
972
  const idempotencyKey = runtime.createId();
431
973
  const userMessage = { id: userId, role: "user", content: input.text.trim(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), parts: input.text.trim() ? [{ id: `${userId}-text`, type: "text", kind: "text", text: input.text.trim() }] : [], attachments: input.attachments };
432
974
  const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
975
+ clearOperationError();
433
976
  update((current) => ({ ...current, messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage), streamingMessageId: assistantId, runState: "streaming", error: null }));
434
977
  setIsSubmitting(true);
435
978
  try {
436
979
  const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds: input.attachmentIds, requestedModelId: input.requestedModelId, idempotencyKey, signal: controller.signal }, consumeEvent);
437
980
  update((current) => ({ ...current, activeThreadId: result.threadId || current.activeThreadId, activeRunId: result.detached ? result.runId : current.activeRunId, cursor: result.cursor, runState: result.detached ? "reconnecting" : current.runState }));
438
- if (result.threadId) await refreshThreads();
981
+ if (result.threadId) await refreshThreads().catch(() => void 0);
439
982
  } catch (error) {
440
- if (!isAbortLike(error)) update((current) => ({ ...current, error, runState: "failed", streamingMessageId: null }));
983
+ if (!isAbortLike(error)) {
984
+ update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
985
+ failOperation("submit", error);
986
+ throw error;
987
+ }
441
988
  } finally {
442
989
  setIsSubmitting(false);
443
990
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
444
991
  }
445
- }, [client, consumeEvent, detach, refreshThreads, runtime, update]);
992
+ }, [clearOperationError, client, consumeEvent, detach, failOperation, invalidateThreadOpen, refreshThreads, runtime, update]);
446
993
  const startNewThread = React2.useCallback(() => {
994
+ invalidateThreadOpen();
447
995
  detach();
448
996
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
449
- }, [detach, update]);
997
+ }, [detach, invalidateThreadOpen, update]);
450
998
  const deleteThread = React2.useCallback(async (threadId) => {
451
- await client.threads.delete({ threadId, idempotencyKey: runtime.createId() });
452
- update((current) => ({ ...current, activeThreadId: current.activeThreadId === threadId ? null : current.activeThreadId, messages: current.activeThreadId === threadId ? [] : current.messages, threads: current.threads.filter((thread) => thread.id !== threadId) }));
453
- }, [client, runtime, update]);
999
+ if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
1000
+ clearOperationError();
1001
+ try {
1002
+ await client.threads.delete({ threadId, idempotencyKey: runtime.createId() });
1003
+ update((current) => ({
1004
+ ...current,
1005
+ activeThreadId: current.activeThreadId === threadId ? null : current.activeThreadId,
1006
+ messages: current.activeThreadId === threadId ? [] : current.messages,
1007
+ contextWindow: current.activeThreadId === threadId ? null : current.contextWindow,
1008
+ contextCompression: current.activeThreadId === threadId ? null : current.contextCompression,
1009
+ contextCompressionRevision: current.activeThreadId === threadId ? 0 : current.contextCompressionRevision,
1010
+ threads: current.threads.filter((thread) => thread.id !== threadId)
1011
+ }));
1012
+ } catch (error) {
1013
+ if (!isAbortLike(error)) {
1014
+ failOperation("delete_thread", error);
1015
+ throw error;
1016
+ }
1017
+ }
1018
+ }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
454
1019
  const cancelRun = React2.useCallback(async () => {
455
1020
  const runId = stateRef.current.activeRunId;
456
- if (!runId) return;
457
- await client.runs.cancel({ runId, idempotencyKey: runtime.createId() });
458
- detach();
459
- update((current) => ({ ...current, activeRunId: null, runState: "completed", streamingMessageId: null }));
460
- }, [client, detach, runtime, update]);
461
- const resumeHitl = React2.useCallback(async (part, action, comment) => {
1021
+ if (!runId || stateRef.current.runState === "cancelling") return;
1022
+ const previousRunState = stateRef.current.runState;
1023
+ clearOperationError();
1024
+ update((current) => current.activeRunId === runId ? { ...current, runState: "cancelling" } : current);
1025
+ try {
1026
+ const result = await client.runs.cancel({ runId, idempotencyKey: runtime.createId() });
1027
+ if (result.status === "cancelled") {
1028
+ detach();
1029
+ update((current) => ({
1030
+ ...current,
1031
+ activeRunId: null,
1032
+ runState: "cancelled",
1033
+ streamingMessageId: null
1034
+ }));
1035
+ } else if (result.status === "cancelling") {
1036
+ update((current) => ({
1037
+ ...current,
1038
+ activeRunId: runId,
1039
+ runState: "cancelling"
1040
+ }));
1041
+ } else {
1042
+ detach();
1043
+ update((current) => ({
1044
+ ...current,
1045
+ activeRunId: null,
1046
+ runState: result.status === "completed" ? "completed" : "failed",
1047
+ streamingMessageId: null
1048
+ }));
1049
+ }
1050
+ } catch (error) {
1051
+ if (!isAbortLike(error)) {
1052
+ update((current) => current.activeRunId === runId ? { ...current, runState: previousRunState } : current);
1053
+ failOperation("cancel_run", error);
1054
+ throw error;
1055
+ }
1056
+ }
1057
+ }, [clearOperationError, client, detach, failOperation, runtime, update]);
1058
+ const resumeHitl = React2.useCallback(async (part, response) => {
462
1059
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
463
- if (!runId) throw new Error("No paused run is available to resume.");
464
- const result = await client.hitl.resume({ runId, interruptId: part.interruptId, action, comment, idempotencyKey: runtime.createId() });
465
- await attachRun(result.run_id || runId);
466
- }, [attachRun, client, runtime]);
1060
+ clearOperationError();
1061
+ if (!runId) {
1062
+ const error = new Error("No paused run is available to resume.");
1063
+ failOperation("resume_hitl", error);
1064
+ throw error;
1065
+ }
1066
+ try {
1067
+ const result = await client.hitl.resume({
1068
+ runId,
1069
+ interruptId: part.interruptId,
1070
+ ...response,
1071
+ idempotencyKey: runtime.createId()
1072
+ });
1073
+ await attachRun(result.run_id || runId);
1074
+ } catch (error) {
1075
+ if (!isAbortLike(error)) failOperation("resume_hitl", error);
1076
+ throw error;
1077
+ }
1078
+ }, [attachRun, clearOperationError, client, failOperation, runtime]);
1079
+ const setFeedback = React2.useCallback(async (message, input) => {
1080
+ const runId = message.runId;
1081
+ if (!runId || message.role !== "assistant" || message.isFinal === false) {
1082
+ const error = new Error("Only persisted final responses can receive feedback.");
1083
+ failOperation("set_feedback", error);
1084
+ throw error;
1085
+ }
1086
+ const previous = message.feedback ?? null;
1087
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1088
+ const optimistic = input.rating ? {
1089
+ rating: input.rating,
1090
+ reason: input.rating === "dislike" ? input.reason ?? null : null,
1091
+ comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
1092
+ created_at: previous?.created_at || timestamp,
1093
+ updated_at: timestamp
1094
+ } : null;
1095
+ const replaceFeedback = (current, feedback) => {
1096
+ const messages = current.messages.map(
1097
+ (item) => item.id === message.id ? { ...item, feedback } : item
1098
+ );
1099
+ return {
1100
+ ...current,
1101
+ messages,
1102
+ threads: current.threads.map((thread) => ({
1103
+ ...thread,
1104
+ messages: thread.messages.map(
1105
+ (item) => item.id === message.id ? { ...item, feedback } : item
1106
+ )
1107
+ }))
1108
+ };
1109
+ };
1110
+ clearOperationError();
1111
+ update((current) => replaceFeedback(current, optimistic));
1112
+ try {
1113
+ const result = await client.runs.setFeedback({ runId, ...input });
1114
+ update((current) => replaceFeedback(current, result.feedback));
1115
+ } catch (error) {
1116
+ update(
1117
+ (current) => withAgentChatOperationError(
1118
+ replaceFeedback(current, previous),
1119
+ "set_feedback",
1120
+ error
1121
+ )
1122
+ );
1123
+ throw error;
1124
+ }
1125
+ }, [clearOperationError, client, failOperation, update]);
467
1126
  const loadOlder = React2.useCallback(async () => {
468
1127
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
469
1128
  if (!thread?.nextBeforeTurnIndex) return;
1129
+ clearOperationError();
470
1130
  setIsLoadingOlder(true);
471
1131
  try {
472
- const detail = await client.threads.get({ threadId: thread.id, limit: pageSize, beforeTurnIndex: thread.nextBeforeTurnIndex, includeRunEvents: true });
1132
+ const detail = await client.threads.get({ threadId: thread.id, limit: pageSize, beforeTurnIndex: thread.nextBeforeTurnIndex });
473
1133
  const older = threadDetailToMessages(detail);
474
1134
  update((current) => ({ ...current, messages: [...older, ...current.messages.filter((message) => !older.some((item) => item.id === message.id))], threads: current.threads.map((item) => item.id === thread.id ? { ...item, hasOlderTurns: Boolean(detail.paging?.has_more), nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null } : item) }));
1135
+ } catch (error) {
1136
+ if (!isAbortLike(error)) {
1137
+ failOperation("load_older_turns", error);
1138
+ throw error;
1139
+ }
475
1140
  } finally {
476
1141
  setIsLoadingOlder(false);
477
1142
  }
478
- }, [client, pageSize, update]);
1143
+ }, [clearOperationError, client, failOperation, pageSize, update]);
479
1144
  const regenerate = React2.useCallback(async (message) => {
480
1145
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
481
1146
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
482
- if (user) await submit({ text: user.content, attachments: user.attachments });
1147
+ if (user) await submit({
1148
+ text: user.content,
1149
+ attachmentIds: chatAttachmentIds(user.attachments),
1150
+ attachments: user.attachments
1151
+ });
483
1152
  }, [submit]);
484
- React2.useEffect(() => {
485
- void refreshThreads();
486
- }, [refreshThreads]);
487
1153
  React2.useEffect(() => {
488
1154
  const controller = runtime.createAbortController();
1155
+ clearBackgroundError();
489
1156
  void client.threads.events({ signal: controller.signal }, async (event) => {
490
1157
  if (event.event === "snapshot_required") {
1158
+ clearBackgroundError();
491
1159
  await refreshThreads();
492
1160
  return;
493
1161
  }
494
- update((current) => ({ ...current, threads: applyThreadSummaryEvent(current.threads, event) }));
1162
+ update((current) => ({
1163
+ ...clearAgentChatBackgroundError(current),
1164
+ threads: applyThreadSummaryEvent(current.threads, event)
1165
+ }));
495
1166
  }).catch((error) => {
496
- if (!isAbortLike(error)) update((current) => ({ ...current, error }));
1167
+ if (!isAbortLike(error)) update((current) => withAgentChatBackgroundError(current, error));
497
1168
  });
498
1169
  return () => controller.abort();
499
- }, [client, refreshThreads, runtime, update]);
1170
+ }, [clearBackgroundError, client, refreshThreads, runtime.createAbortController, update]);
500
1171
  React2.useEffect(() => {
501
1172
  const threadId = options.activeThreadId;
502
- if (threadId && threadId !== stateRef.current.activeThreadId) void openThread(threadId);
1173
+ const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1174
+ if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
1175
+ void openThread(threadId).catch(() => void 0);
1176
+ }
503
1177
  }, [openThread, options.activeThreadId]);
504
1178
  const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId) || null;
505
1179
  const hasMoreThreads = threadTotal === void 0 ? state.threads.length >= threadPageSize : state.threads.length < threadTotal;
@@ -515,6 +1189,8 @@ function useAgentChat(options = {}) {
515
1189
  isSubmitting,
516
1190
  attachRun,
517
1191
  cancelRun,
1192
+ clearBackgroundError,
1193
+ clearOperationError,
518
1194
  deleteThread,
519
1195
  detach,
520
1196
  loadOlder,
@@ -523,6 +1199,7 @@ function useAgentChat(options = {}) {
523
1199
  refreshThreads,
524
1200
  regenerate,
525
1201
  resumeHitl,
1202
+ setFeedback,
526
1203
  startNewThread,
527
1204
  submit
528
1205
  };
@@ -594,7 +1271,7 @@ function useAgentThread(threadId, options = {}) {
594
1271
  setIsLoading(false);
595
1272
  }
596
1273
  }
597
- }, [client, createAbortController, options.beforeTurnIndex, options.includeRunEvents, options.limit, threadId]);
1274
+ }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
598
1275
  React3.useEffect(() => {
599
1276
  void load();
600
1277
  return () => controllerRef.current?.abort();
@@ -639,17 +1316,24 @@ function useAgentAttachments() {
639
1316
  const { client, createId } = useAgents24Runtime();
640
1317
  const [uploads, setUploads] = React3.useState({});
641
1318
  const [isUploading, setIsUploading] = React3.useState(false);
642
- const upload = React3.useCallback(async (input) => {
1319
+ const uploadBatch = React3.useCallback(async (input) => {
643
1320
  setIsUploading(true);
644
1321
  try {
645
- const result = await client.attachments.upload({ upload: input, threadId: input.threadId, idempotencyKey: createId(), signal: input.signal });
646
- setUploads((current) => ({ ...current, [result.id]: result }));
647
- return attachmentResultToChatAttachment(result);
1322
+ const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, idempotencyKey: createId(), signal: input.signal });
1323
+ setUploads((current) => Object.fromEntries([
1324
+ ...Object.entries(current),
1325
+ ...results.map((result) => [result.id, result])
1326
+ ]));
1327
+ return results.map(attachmentResultToChatAttachment);
648
1328
  } finally {
649
1329
  setIsUploading(false);
650
1330
  }
651
1331
  }, [client, createId]);
652
- return { isUploading, upload, uploads };
1332
+ const createContentAccess = React3.useCallback(
1333
+ (input) => client.attachments.createContentAccess(input),
1334
+ [client]
1335
+ );
1336
+ return { createContentAccess, isUploading, uploadBatch, uploads };
653
1337
  }
654
1338
  function useAgentHitl() {
655
1339
  const { client, createId } = useAgents24Runtime();
@@ -685,13 +1369,24 @@ export {
685
1369
  Agents24Provider,
686
1370
  appendStableStreamingTurn,
687
1371
  applyRuntimeEvent,
1372
+ applyRuntimeEvents,
688
1373
  applyThreadSummaryEvent,
689
1374
  assistantProjectionFromEvent,
690
1375
  attachmentResultToChatAttachment,
1376
+ captureAgentChatThreadSelection,
1377
+ chatAttachmentIds,
1378
+ clearAgentChatBackgroundError,
1379
+ clearAgentChatOperationError,
1380
+ contextCompressionPartFromEvent,
691
1381
  createPortableId,
1382
+ getThreadActivity,
692
1383
  initialAgentChatState,
693
1384
  isAbortLike,
1385
+ latestThreadContext,
1386
+ modelTurnPartFromEvent,
694
1387
  partsFromResponseBlocks,
1388
+ reconcileRefreshedThreads,
1389
+ restoreAgentChatThreadSelection,
695
1390
  threadDetailToMessages,
696
1391
  upsertStableAssistantMessage,
697
1392
  useAgentAttachments,
@@ -702,6 +1397,8 @@ export {
702
1397
  useAgentRun,
703
1398
  useAgentThread,
704
1399
  useAgentThreads,
705
- useAgents24Runtime
1400
+ useAgents24Runtime,
1401
+ withAgentChatBackgroundError,
1402
+ withAgentChatOperationError
706
1403
  };
707
1404
  //# sourceMappingURL=index.js.map