@cadraos/sdk 0.3.14-p46.3 → 0.3.14

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.
Files changed (52) hide show
  1. package/README.md +104 -53
  2. package/dist/ai-functions/react/index.cjs +176 -0
  3. package/dist/ai-functions/react/index.cjs.map +1 -0
  4. package/dist/ai-functions/react/index.d.cts +129 -0
  5. package/dist/ai-functions/react/index.d.ts +129 -0
  6. package/dist/ai-functions/react/index.js +153 -0
  7. package/dist/ai-functions/react/index.js.map +1 -0
  8. package/dist/ai-functions/server/index.cjs +296 -0
  9. package/dist/ai-functions/server/index.cjs.map +1 -0
  10. package/dist/ai-functions/server/index.d.cts +334 -0
  11. package/dist/ai-functions/server/index.d.ts +334 -0
  12. package/dist/ai-functions/server/index.js +284 -0
  13. package/dist/ai-functions/server/index.js.map +1 -0
  14. package/dist/board-host/index.cjs +217 -129
  15. package/dist/board-host/index.cjs.map +1 -1
  16. package/dist/board-host/index.d.cts +96 -1
  17. package/dist/board-host/index.d.ts +96 -1
  18. package/dist/board-host/index.js +218 -131
  19. package/dist/board-host/index.js.map +1 -1
  20. package/dist/board-host/server/index.d.cts +44 -0
  21. package/dist/board-host/server/index.d.ts +44 -0
  22. package/dist/chat/index.cjs +299 -17
  23. package/dist/chat/index.cjs.map +1 -1
  24. package/dist/chat/index.d.cts +225 -0
  25. package/dist/chat/index.d.ts +225 -0
  26. package/dist/chat/index.js +299 -17
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/index.cjs +221 -3
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +286 -1
  31. package/dist/index.d.ts +286 -1
  32. package/dist/index.js +215 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/kanban/engine/index.cjs +774 -683
  35. package/dist/kanban/engine/index.cjs.map +1 -1
  36. package/dist/kanban/engine/index.d.cts +90 -1
  37. package/dist/kanban/engine/index.d.ts +90 -1
  38. package/dist/kanban/engine/index.js +774 -684
  39. package/dist/kanban/engine/index.js.map +1 -1
  40. package/dist/kanban/index.cjs +214 -3
  41. package/dist/kanban/index.cjs.map +1 -1
  42. package/dist/kanban/index.d.cts +252 -0
  43. package/dist/kanban/index.d.ts +252 -0
  44. package/dist/kanban/index.js +214 -3
  45. package/dist/kanban/index.js.map +1 -1
  46. package/dist/teams/index.cjs +214 -3
  47. package/dist/teams/index.cjs.map +1 -1
  48. package/dist/teams/index.d.cts +225 -0
  49. package/dist/teams/index.d.ts +225 -0
  50. package/dist/teams/index.js +214 -3
  51. package/dist/teams/index.js.map +1 -1
  52. package/package.json +21 -1
@@ -75,6 +75,14 @@ var CadraError = class extends Error {
75
75
  details;
76
76
  /** Request ID from server (for debugging) */
77
77
  requestId;
78
+ /**
79
+ * The server's own `error.code` exactly as sent on the wire (e.g.
80
+ * `FUNCTION_NOT_PUBLISHED`, `DUPLICATE_REQUEST`, `VARIABLE_TYPE`).
81
+ * Populated by `parseApiError` for EVERY status. `code` keeps its historic
82
+ * per-class values (`conflict`, `server_error`, `validation_error`, …) for
83
+ * compatibility; branch on `serverCode` when you need the precise reason.
84
+ */
85
+ serverCode;
78
86
  constructor(message, code, status, details, requestId) {
79
87
  super(message);
80
88
  this.name = "CadraError";
@@ -152,6 +160,12 @@ function parseApiError(status, body, requestId) {
152
160
  const message = errorBody?.error?.message ?? "An error occurred";
153
161
  const code = errorBody?.error?.code ?? "unknown_error";
154
162
  const details = errorBody?.error?.details;
163
+ const serverCode = typeof errorBody?.error?.code === "string" ? errorBody.error.code : void 0;
164
+ const error = buildApiError(status, message, code, details, requestId);
165
+ if (serverCode !== void 0) error.serverCode = serverCode;
166
+ return error;
167
+ }
168
+ function buildApiError(status, message, code, details, requestId) {
155
169
  switch (status) {
156
170
  case 400:
157
171
  case 422: {
@@ -187,6 +201,42 @@ function parseApiError(status, body, requestId) {
187
201
  }
188
202
  }
189
203
 
204
+ // src/errors/ai-function-errors.ts
205
+ var AiFunctionDuplicateError = class extends ConflictError {
206
+ /** The existing run's execution id. */
207
+ executionId;
208
+ /** The existing run's status (`running`, a terminal status, or `abandoned`). */
209
+ runStatus;
210
+ constructor(message, details, requestId) {
211
+ super(message, details, requestId);
212
+ this.name = "AiFunctionDuplicateError";
213
+ this.serverCode = "DUPLICATE_REQUEST";
214
+ this.executionId = typeof details?.executionId === "string" ? details.executionId : void 0;
215
+ this.runStatus = typeof details?.status === "string" ? details.status : void 0;
216
+ }
217
+ };
218
+ var AiFunctionChargedError = class extends CadraError {
219
+ constructor(source) {
220
+ super(source.message, source.code, source.status, source.details, source.requestId);
221
+ this.name = "AiFunctionChargedError";
222
+ this.serverCode = source.serverCode;
223
+ }
224
+ };
225
+ var AiFunctionParameterError = class extends ValidationError {
226
+ functionKey;
227
+ issues;
228
+ constructor(functionKey, issues) {
229
+ super(
230
+ `Invalid parameters for AI function "${functionKey}": ` + issues.map((i) => `${i.name} (${i.code})`).join(", "),
231
+ issues.map((i) => ({ field: i.name, message: i.code })),
232
+ { functionKey, issues }
233
+ );
234
+ this.name = "AiFunctionParameterError";
235
+ this.functionKey = functionKey;
236
+ this.issues = issues;
237
+ }
238
+ };
239
+
190
240
  // src/utils/retry.ts
191
241
  var DEFAULT_RETRY_CONFIG = {
192
242
  maxRetries: 3,
@@ -305,7 +355,8 @@ var HttpClient = class {
305
355
  headers["Idempotency-Key"] = options.idempotencyKey;
306
356
  }
307
357
  const controller = new AbortController();
308
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
358
+ const timeoutMs = options.timeoutMs ?? this.config.timeout;
359
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
309
360
  try {
310
361
  const doFetch = this.config.fetch ?? fetch;
311
362
  const response = await doFetch(url, {
@@ -341,7 +392,7 @@ var HttpClient = class {
341
392
  } catch (error) {
342
393
  clearTimeout(timeoutId);
343
394
  if (error instanceof Error && error.name === "AbortError") {
344
- throw new TimeoutError(`Request timed out after ${this.config.timeout}ms`);
395
+ throw new TimeoutError(`Request timed out after ${options.timeoutMs ?? this.config.timeout}ms`);
345
396
  }
346
397
  if (error instanceof TypeError && error.message.includes("fetch")) {
347
398
  throw new NetworkError(`Network error: ${error.message}`);
@@ -373,7 +424,8 @@ var HttpClient = class {
373
424
  path,
374
425
  body,
375
426
  idempotencyKey: options?.idempotencyKey,
376
- skipRetry: options?.skipRetry
427
+ skipRetry: options?.skipRetry,
428
+ timeoutMs: options?.timeoutMs
377
429
  });
378
430
  }
379
431
  /**
@@ -1132,6 +1184,10 @@ var ExecutionStreamControllerImpl = class {
1132
1184
  toolName,
1133
1185
  result: data.result,
1134
1186
  durationMs: data.durationMs ?? 0,
1187
+ // CAD-243: pass the producer's failure verdict through.
1188
+ ...data.isError === true ? { isError: true } : {},
1189
+ ...typeof data.status === "string" ? { status: data.status } : {},
1190
+ ...typeof data.error === "string" ? { error: data.error } : {},
1135
1191
  forwardedFrom: event.data._forwardedFrom,
1136
1192
  // CAD-87 S1-001 EventIdentity → (execId, seq) reducer dedup (S1-003).
1137
1193
  sourceExecutionId: data.sourceExecutionId ?? data.executionId,
@@ -3370,6 +3426,156 @@ var RemoteRunnersAPI = class extends BaseResourceAPI {
3370
3426
  }
3371
3427
  };
3372
3428
 
3429
+ // src/resources/ai-functions.ts
3430
+ var AI_FUNCTION_RUN_TIMEOUT_MS = 6e4;
3431
+ var AI_FUNCTION_DESCRIPTOR_TTL_MS = 6e4;
3432
+ function checkAiFunctionParameters(parameters, variables) {
3433
+ const issues = [];
3434
+ const declared = /* @__PURE__ */ new Set();
3435
+ for (const p of parameters) {
3436
+ declared.add(p.name);
3437
+ const has = Object.prototype.hasOwnProperty.call(variables, p.name);
3438
+ const value = has ? variables[p.name] : void 0;
3439
+ if (value === void 0 || value === null) {
3440
+ if (p.required) issues.push({ name: p.name, code: "missing" });
3441
+ continue;
3442
+ }
3443
+ switch (p.type) {
3444
+ case "number":
3445
+ if (typeof value !== "number" || !Number.isFinite(value)) {
3446
+ issues.push({ name: p.name, code: "type" });
3447
+ }
3448
+ break;
3449
+ case "boolean":
3450
+ if (typeof value !== "boolean") issues.push({ name: p.name, code: "type" });
3451
+ break;
3452
+ case "enum":
3453
+ if (typeof value !== "string") issues.push({ name: p.name, code: "type" });
3454
+ else if (p.options && !p.options.includes(value)) {
3455
+ issues.push({ name: p.name, code: "option" });
3456
+ }
3457
+ break;
3458
+ case "string":
3459
+ case "text":
3460
+ if (typeof value !== "string") issues.push({ name: p.name, code: "type" });
3461
+ else if (typeof p.maxLength === "number" && value.length > p.maxLength) {
3462
+ issues.push({ name: p.name, code: "too_long" });
3463
+ }
3464
+ break;
3465
+ case "image":
3466
+ if (typeof value !== "string") issues.push({ name: p.name, code: "type" });
3467
+ break;
3468
+ }
3469
+ }
3470
+ for (const name of Object.keys(variables)) {
3471
+ if (!declared.has(name)) issues.push({ name, code: "unknown" });
3472
+ }
3473
+ return issues;
3474
+ }
3475
+ var AiFunctionsAPI = class extends BaseResourceAPI {
3476
+ descriptorCache = /* @__PURE__ */ new Map();
3477
+ constructor(http) {
3478
+ super(http, "/api/v1/ai-functions");
3479
+ }
3480
+ /**
3481
+ * List the functions this org can call — the index (D27a). Each entry carries
3482
+ * the live version's typed parameters; never prompt text.
3483
+ */
3484
+ async list() {
3485
+ const data = await this.httpGet("");
3486
+ const functions = Array.isArray(data) ? data : data?.functions ?? [];
3487
+ for (const d of functions) this.cacheDescriptor(d);
3488
+ return functions;
3489
+ }
3490
+ /**
3491
+ * Get one function's descriptor by key (D27a).
3492
+ * `NotFoundError` (`FUNCTION_NOT_FOUND`) / `ConflictError` (`FUNCTION_NOT_PUBLISHED`).
3493
+ */
3494
+ async get(functionKey) {
3495
+ const data = await this.httpGet(
3496
+ `/${encodeURIComponent(functionKey)}`
3497
+ );
3498
+ const descriptor = data && typeof data === "object" && "function" in data && !("functionKey" in data) ? data.function : data;
3499
+ this.cacheDescriptor(descriptor, functionKey);
3500
+ return descriptor;
3501
+ }
3502
+ /**
3503
+ * Read one of this org's own runs (safe projection, §6.5).
3504
+ */
3505
+ async getRun(executionId) {
3506
+ return this.httpGet(`/runs/${encodeURIComponent(executionId)}`);
3507
+ }
3508
+ /** Forget cached descriptors (one key, or all). */
3509
+ clearDescriptorCache(functionKey) {
3510
+ if (functionKey) this.descriptorCache.delete(functionKey);
3511
+ else this.descriptorCache.clear();
3512
+ }
3513
+ async run(refOrKey, parametersOrOptions, shorthandOpts) {
3514
+ let ref;
3515
+ let options;
3516
+ if (typeof refOrKey === "string") {
3517
+ ref = { functionKey: refOrKey };
3518
+ options = { ...shorthandOpts ?? {}, variables: parametersOrOptions };
3519
+ } else {
3520
+ ref = refOrKey;
3521
+ options = parametersOrOptions;
3522
+ }
3523
+ const variables = options.variables ?? {};
3524
+ const functionKey = "functionKey" in ref ? ref.functionKey : void 0;
3525
+ const shouldCheck = functionKey !== void 0 && options.version === void 0 && options.validate !== false;
3526
+ if (shouldCheck) {
3527
+ const descriptor = await this.getCachedDescriptor(functionKey);
3528
+ const issues = checkAiFunctionParameters(descriptor.parameters, variables);
3529
+ if (issues.length > 0) {
3530
+ throw new AiFunctionParameterError(functionKey, issues);
3531
+ }
3532
+ }
3533
+ const body = {
3534
+ ref,
3535
+ variables,
3536
+ version: options.version,
3537
+ idempotencyKey: options.idempotencyKey ?? generateExecutionId(),
3538
+ params: options.params
3539
+ };
3540
+ try {
3541
+ return await this.httpPost("/run", body, {
3542
+ skipRetry: true,
3543
+ timeoutMs: AI_FUNCTION_RUN_TIMEOUT_MS
3544
+ });
3545
+ } catch (error) {
3546
+ throw this.mapRunError(error, functionKey);
3547
+ }
3548
+ }
3549
+ // ── internals ──────────────────────────────────────────────────────────────
3550
+ cacheDescriptor(descriptor, key) {
3551
+ if (!descriptor || typeof descriptor !== "object") return;
3552
+ const cacheKey = key ?? descriptor.functionKey;
3553
+ if (!cacheKey) return;
3554
+ this.descriptorCache.set(cacheKey, {
3555
+ descriptor,
3556
+ expiresAt: Date.now() + AI_FUNCTION_DESCRIPTOR_TTL_MS
3557
+ });
3558
+ }
3559
+ async getCachedDescriptor(functionKey) {
3560
+ const hit = this.descriptorCache.get(functionKey);
3561
+ if (hit && hit.expiresAt > Date.now()) return hit.descriptor;
3562
+ return this.get(functionKey);
3563
+ }
3564
+ mapRunError(error, functionKey) {
3565
+ if (!(error instanceof CadraError)) return error;
3566
+ if (error.serverCode === "DUPLICATE_REQUEST") {
3567
+ return new AiFunctionDuplicateError(error.message, error.details, error.requestId);
3568
+ }
3569
+ if (error.details?.charged === true) {
3570
+ return new AiFunctionChargedError(error);
3571
+ }
3572
+ if (functionKey && error.serverCode?.startsWith("VARIABLE_")) {
3573
+ this.descriptorCache.delete(functionKey);
3574
+ }
3575
+ return error;
3576
+ }
3577
+ };
3578
+
3373
3579
  // src/client.ts
3374
3580
  var DEFAULT_BASE_URL = "https://api.cadraos.com";
3375
3581
  var DEFAULT_TIMEOUT = 3e4;
@@ -3416,6 +3622,10 @@ var CadraClient = class {
3416
3622
  * Remote runner operations (list)
3417
3623
  */
3418
3624
  remoteRunners;
3625
+ /**
3626
+ * AI function operations (list, get, run, getRun) — p93
3627
+ */
3628
+ aiFunctions;
3419
3629
  /**
3420
3630
  * Create a new AI SaaS client
3421
3631
  *
@@ -3505,6 +3715,7 @@ var CadraClient = class {
3505
3715
  this.kanban = new KanbanAPI(this.httpClient, getStreamConfig);
3506
3716
  this.remoteAgents = new RemoteAgentsAPI(this.httpClient, getStreamConfig);
3507
3717
  this.remoteRunners = new RemoteRunnersAPI(this.httpClient);
3718
+ this.aiFunctions = new AiFunctionsAPI(this.httpClient);
3508
3719
  }
3509
3720
  /**
3510
3721
  * Validate client options
@@ -4194,6 +4405,32 @@ function useExecutionStream(options) {
4194
4405
  };
4195
4406
  }
4196
4407
 
4408
+ // src/chat/utils/toolOutcome.ts
4409
+ var MAX_ERROR_CHARS = 500;
4410
+ function asRecord(value) {
4411
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
4412
+ }
4413
+ function errorFrom(value) {
4414
+ if (typeof value === "string" && value.trim()) return value;
4415
+ const rec = asRecord(value);
4416
+ if (rec && typeof rec.message === "string" && rec.message.trim()) return rec.message;
4417
+ return void 0;
4418
+ }
4419
+ function isFailurePayload(result) {
4420
+ const rec = asRecord(result);
4421
+ if (!rec) return false;
4422
+ return rec.success === false || rec.isError === true;
4423
+ }
4424
+ function cap(text) {
4425
+ return text.length > MAX_ERROR_CHARS ? `${text.slice(0, MAX_ERROR_CHARS)}\u2026` : text;
4426
+ }
4427
+ function resolveToolOutcome(input) {
4428
+ const failed = input.isError === true || input.status === "failed" || input.success === false || input.payloadFailure === true || isFailurePayload(input.result);
4429
+ if (!failed) return { status: "completed" };
4430
+ const error = errorFrom(input.error) ?? errorFrom(asRecord(input.result)?.error) ?? "Tool call failed";
4431
+ return { status: "failed", error: cap(error) };
4432
+ }
4433
+
4197
4434
  // src/chat/hooks/history-cache.ts
4198
4435
  var DEFAULT_HISTORY_STALE_MS = 1e4;
4199
4436
  var cache = /* @__PURE__ */ new Map();
@@ -4369,9 +4606,10 @@ function applyEventInner(state, event) {
4369
4606
  if (tc) {
4370
4607
  toolCalls.set(event.id, {
4371
4608
  ...tc,
4372
- status: "completed",
4609
+ status: event.status ?? "completed",
4373
4610
  result: event.result,
4374
- durationMs: event.durationMs
4611
+ durationMs: event.durationMs,
4612
+ ...event.error !== void 0 ? { error: event.error } : {}
4375
4613
  });
4376
4614
  return { ...state, toolCalls };
4377
4615
  }
@@ -4380,8 +4618,9 @@ function applyEventInner(state, event) {
4380
4618
  name: event.toolName ?? "unknown",
4381
4619
  args: {},
4382
4620
  result: event.result,
4383
- status: "completed",
4384
- durationMs: event.durationMs
4621
+ status: event.status ?? "completed",
4622
+ durationMs: event.durationMs,
4623
+ ...event.error !== void 0 ? { error: event.error } : {}
4385
4624
  });
4386
4625
  if (state.seenToolBlockIds.has(event.id)) {
4387
4626
  return { ...state, toolCalls };
@@ -4631,8 +4870,16 @@ function useAgentChat(options) {
4631
4870
  const streamMsgId = streamingMessageIdRef.current;
4632
4871
  setMessages((prev) => {
4633
4872
  const resolvedContent = streamingContentRef.current || finalOutput || "";
4873
+ const streamIdx = prev.findIndex((msg) => msg.id === streamMsgId);
4874
+ let turnStart = 0;
4875
+ for (let i = streamIdx - 1; i >= 0; i--) {
4876
+ if (prev[i].role === "user") {
4877
+ turnStart = i + 1;
4878
+ break;
4879
+ }
4880
+ }
4634
4881
  const duplicateExists = resolvedContent && prev.some(
4635
- (msg) => msg.id !== streamMsgId && msg.role === "assistant" && !msg.isStreaming && msg.content === resolvedContent
4882
+ (msg, idx) => idx >= turnStart && msg.id !== streamMsgId && msg.role === "assistant" && !msg.isStreaming && msg.content === resolvedContent
4636
4883
  );
4637
4884
  if (duplicateExists) {
4638
4885
  return prev.filter((msg) => msg.id !== streamMsgId);
@@ -4753,6 +5000,17 @@ function useAgentChat(options) {
4753
5000
  const resultToolCallId = log.metadata?.toolCallId;
4754
5001
  const result = log.metadata?.output;
4755
5002
  const durationMs = log.durationMs;
5003
+ const outcome = resolveToolOutcome({
5004
+ result,
5005
+ success: log.metadata?.success,
5006
+ payloadFailure: log.metadata?.payloadFailure,
5007
+ isError: log.metadata?.isError,
5008
+ error: log.metadata?.error
5009
+ });
5010
+ const outcomeFields = {
5011
+ status: outcome.status,
5012
+ ...outcome.error ? { error: outcome.error } : {}
5013
+ };
4756
5014
  let matchedToolCall = resultToolCallId ? pendingToolCallsRef.current.get(resultToolCallId) : void 0;
4757
5015
  if (!matchedToolCall) {
4758
5016
  for (const [, tc] of pendingToolCallsRef.current) {
@@ -4766,7 +5024,7 @@ function useAgentChat(options) {
4766
5024
  const completedToolCall = {
4767
5025
  ...matchedToolCall,
4768
5026
  result,
4769
- status: "completed",
5027
+ ...outcomeFields,
4770
5028
  durationMs
4771
5029
  };
4772
5030
  updateToolCall(completedToolCall);
@@ -4777,7 +5035,7 @@ function useAgentChat(options) {
4777
5035
  name: resultToolName || "unknown",
4778
5036
  args: {},
4779
5037
  result,
4780
- status: "completed",
5038
+ ...outcomeFields,
4781
5039
  durationMs
4782
5040
  };
4783
5041
  updateToolCall(lateToolCall);
@@ -4788,7 +5046,8 @@ function useAgentChat(options) {
4788
5046
  id: resultToolCallId,
4789
5047
  toolName: resultToolName,
4790
5048
  result,
4791
- durationMs
5049
+ durationMs,
5050
+ ...outcomeFields
4792
5051
  });
4793
5052
  firedToolResultsRef.current.add(resultToolCallId);
4794
5053
  }
@@ -4898,6 +5157,17 @@ function useAgentChat(options) {
4898
5157
  const resultToolCallId = log.metadata?.toolCallId;
4899
5158
  const result = log.metadata?.output;
4900
5159
  const durationMs = log.durationMs;
5160
+ const outcome = resolveToolOutcome({
5161
+ result,
5162
+ success: log.metadata?.success,
5163
+ payloadFailure: log.metadata?.payloadFailure,
5164
+ isError: log.metadata?.isError,
5165
+ error: log.metadata?.error
5166
+ });
5167
+ const outcomeFields = {
5168
+ status: outcome.status,
5169
+ ...outcome.error ? { error: outcome.error } : {}
5170
+ };
4901
5171
  let matchedToolCall = resultToolCallId ? pendingToolCallsRef.current.get(resultToolCallId) : void 0;
4902
5172
  if (!matchedToolCall) {
4903
5173
  for (const [, tc] of pendingToolCallsRef.current) {
@@ -4911,7 +5181,7 @@ function useAgentChat(options) {
4911
5181
  const completedToolCall = {
4912
5182
  ...matchedToolCall,
4913
5183
  result,
4914
- status: "completed",
5184
+ ...outcomeFields,
4915
5185
  durationMs
4916
5186
  };
4917
5187
  updateToolCall(completedToolCall);
@@ -4922,7 +5192,7 @@ function useAgentChat(options) {
4922
5192
  name: resultToolName || "unknown",
4923
5193
  args: {},
4924
5194
  result,
4925
- status: "completed",
5195
+ ...outcomeFields,
4926
5196
  durationMs
4927
5197
  };
4928
5198
  updateToolCall(lateToolCall);
@@ -4933,7 +5203,8 @@ function useAgentChat(options) {
4933
5203
  id: resultToolCallId,
4934
5204
  toolName: resultToolName,
4935
5205
  result,
4936
- durationMs
5206
+ durationMs,
5207
+ ...outcomeFields
4937
5208
  });
4938
5209
  }
4939
5210
  if (resultToolCallId) {
@@ -5182,12 +5453,22 @@ function useAgentChat(options) {
5182
5453
  if (SILENT_TOOL_NAMES.has(toolName) || isUuidLikeToolName(toolName)) return;
5183
5454
  if (firedToolResultsRef.current.has(toolCallId)) return;
5184
5455
  const existing = pendingToolCallsRef.current.get(toolCallId);
5185
- if (existing && existing.status === "completed") return;
5456
+ if (existing && (existing.status === "completed" || existing.status === "failed")) return;
5457
+ const outcome = resolveToolOutcome({
5458
+ result: data.result,
5459
+ isError: data.isError,
5460
+ status: data.status,
5461
+ error: data.error
5462
+ });
5463
+ const outcomeFields = {
5464
+ status: outcome.status,
5465
+ ...outcome.error ? { error: outcome.error } : {}
5466
+ };
5186
5467
  if (existing) {
5187
5468
  const completedToolCall = {
5188
5469
  ...existing,
5189
5470
  result: data.result,
5190
- status: "completed",
5471
+ ...outcomeFields,
5191
5472
  durationMs: data.durationMs
5192
5473
  };
5193
5474
  updateToolCall(completedToolCall);
@@ -5198,7 +5479,7 @@ function useAgentChat(options) {
5198
5479
  name: toolName,
5199
5480
  args: {},
5200
5481
  result: data.result,
5201
- status: "completed",
5482
+ ...outcomeFields,
5202
5483
  durationMs: data.durationMs
5203
5484
  };
5204
5485
  updateToolCall(lateToolCall);
@@ -5209,6 +5490,7 @@ function useAgentChat(options) {
5209
5490
  toolName,
5210
5491
  result: data.result,
5211
5492
  durationMs: data.durationMs,
5493
+ ...outcomeFields,
5212
5494
  // CAD-87 S1-003: (execId, seq) reducer dedup for replayed events.
5213
5495
  execId: data.sourceExecutionId,
5214
5496
  seq: data.seq