@zihin/agent-client 0.6.0 → 0.7.0

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
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  classifyStreamErrorKind: () => classifyStreamErrorKind,
30
30
  createClient: () => createClient,
31
31
  errorFromDoneChunk: () => errorFromDoneChunk,
32
+ errorFromOutcome: () => errorFromOutcome,
32
33
  errorFromStreamChunk: () => errorFromStreamChunk,
33
34
  isCommercialBlock: () => isCommercialBlock,
34
35
  sanitizeApiError: () => sanitizeApiError,
@@ -89,6 +90,20 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
89
90
  function isTerminalStatus(status) {
90
91
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
91
92
  }
93
+ var _warnedDeprecations = /* @__PURE__ */ new Set();
94
+ function warnDeprecatedOnce(code, message) {
95
+ if (_warnedDeprecations.has(code)) return;
96
+ _warnedDeprecations.add(code);
97
+ try {
98
+ const proc = globalThis.process;
99
+ if (typeof proc?.emitWarning === "function") {
100
+ proc.emitWarning(message, { type: "ZihinDeprecationWarning", code });
101
+ return;
102
+ }
103
+ console.warn(`[${code}] ${message}`);
104
+ } catch {
105
+ }
106
+ }
92
107
 
93
108
  // src/security.ts
94
109
  var PRIVATE_IP_PATTERNS = [
@@ -408,6 +423,8 @@ function errorFromResponse(status, body, meta = {}) {
408
423
  retryable: false
409
424
  });
410
425
  }
426
+ if (code === "PLAN_LIMIT_REACHED")
427
+ return new ZihinAgentError("blocked", message || "This feature is not available on your Zihin plan.", { ...base, retryable: false });
411
428
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
412
429
  if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
413
430
  return new ZihinAgentError(
@@ -547,15 +564,15 @@ function buildHeaders(apiKey, extra = {}) {
547
564
  ...extra
548
565
  };
549
566
  }
550
- function delay(ms, signal) {
567
+ function delay(ms, signal, meta) {
551
568
  return new Promise((resolve, reject) => {
552
569
  if (signal?.aborted) {
553
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
570
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
554
571
  return;
555
572
  }
556
573
  const onAbort = () => {
557
574
  clearTimeout(timer);
558
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
575
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
559
576
  };
560
577
  const timer = setTimeout(() => {
561
578
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -645,7 +662,7 @@ async function requestBuffered(args) {
645
662
  }
646
663
  d.dispose();
647
664
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
648
- await delay(wait, args.callerSignal);
665
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
649
666
  continue;
650
667
  }
651
668
  const text = peeked ?? await res.text();
@@ -664,7 +681,7 @@ async function requestBuffered(args) {
664
681
  const wait = backoffMs(attempt);
665
682
  if (budget.overheadLeft() <= wait) throw lastError;
666
683
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
667
- await delay(wait, args.callerSignal);
684
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
668
685
  }
669
686
  }
670
687
  throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -673,6 +690,7 @@ async function openStream(args) {
673
690
  const endpoint = endpointLabel("POST", args.url);
674
691
  const doFetch = resolveFetch(args.fetchImpl);
675
692
  const budget = budgetTracker({ ...args, endpoint });
693
+ const policy = args.idempotent ? "idempotent" : "side-effecting";
676
694
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
677
695
  const d = createDeadline(args.timeoutMs, args.callerSignal);
678
696
  try {
@@ -684,7 +702,7 @@ async function openStream(args) {
684
702
  redirect: "error",
685
703
  cache: "no-store"
686
704
  });
687
- let wait = shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
705
+ let wait = shouldRetryStatus(response.status, policy) && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
688
706
  if (response.status === 429) {
689
707
  const text = await response.text().catch(() => "");
690
708
  const body = parseBody(text);
@@ -694,7 +712,7 @@ async function openStream(args) {
694
712
  }
695
713
  d.dispose();
696
714
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
697
- await delay(wait, args.callerSignal);
715
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
698
716
  continue;
699
717
  }
700
718
  if (wait !== null && budget.overheadLeft() > wait) {
@@ -710,7 +728,7 @@ async function openStream(args) {
710
728
  reason: "status",
711
729
  status: response.status
712
730
  });
713
- await delay(wait, args.callerSignal);
731
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
714
732
  continue;
715
733
  }
716
734
  return { response, deadline: d, endpoint };
@@ -721,7 +739,14 @@ async function openStream(args) {
721
739
  if (d.timedOut()) throw budget.expired(attempt + 1);
722
740
  if (d.abortedByCaller())
723
741
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
724
- throw errorFromThrow(err, budget.reported, meta);
742
+ const netErr = errorFromThrow(err, budget.reported, meta);
743
+ const wait = backoffMs(attempt);
744
+ if (policy === "idempotent" && netErr.retryable && attempt < args.maxRetries && budget.overheadLeft() > wait) {
745
+ args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
746
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
747
+ continue;
748
+ }
749
+ throw netErr;
725
750
  }
726
751
  }
727
752
  throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -923,6 +948,25 @@ function parseIgnoredFields(v) {
923
948
  }
924
949
  return out;
925
950
  }
951
+ var MAX_SUGGESTIONS = 10;
952
+ var MAX_SUGGESTION_CHARS = 200;
953
+ function suggestionsFromBlocks(blocks) {
954
+ if (!Array.isArray(blocks)) return void 0;
955
+ const qr = [...blocks].reverse().find((b) => rec(b).type === "quick_replies");
956
+ const labels = rec(rec(qr).data).labels;
957
+ if (!Array.isArray(labels)) return void 0;
958
+ const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
959
+ return out.length ? out : void 0;
960
+ }
961
+ function turnIds(d) {
962
+ return {
963
+ executionId: safeId(d.execution_id),
964
+ clientMessageId: safeId(d.client_message_id),
965
+ idempotentReplay: d.idempotent_replay === true ? true : void 0,
966
+ userMessageId: safeId(d.user_message_id),
967
+ assistantMessageId: safeId(d.assistant_message_id)
968
+ };
969
+ }
926
970
  function mapEvent(e) {
927
971
  const d = rec(e.data);
928
972
  switch (e.event) {
@@ -932,6 +976,7 @@ function mapEvent(e) {
932
976
  requestId: safeId(d.request_id),
933
977
  agentId: str(d.agent_id),
934
978
  sessionId: safeId(d.session_id),
979
+ ...turnIds(d),
935
980
  acceptedFields: parseAcceptedFields(d.accepted_fields),
936
981
  ignoredFields: parseIgnoredFields(d.ignored_fields),
937
982
  data: e.data
@@ -954,6 +999,8 @@ function mapEvent(e) {
954
999
  };
955
1000
  case "action_proposed":
956
1001
  return { type: "action_proposed", data: e.data };
1002
+ case "action_forwarded":
1003
+ return { type: "action_forwarded", actionId: safeId(d.action_id), data: e.data };
957
1004
  case "token":
958
1005
  return { type: "token", content: str(d.content) ?? "", data: e.data };
959
1006
  case "warning":
@@ -968,6 +1015,7 @@ function mapEvent(e) {
968
1015
  timeoutClock: safeLabel(d.timeout_clock),
969
1016
  truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
970
1017
  finishReason: str(d.finish_reason),
1018
+ ...turnIds(d),
971
1019
  data: e.data
972
1020
  };
973
1021
  case "error": {
@@ -989,6 +1037,8 @@ function mapEvent(e) {
989
1037
  gated: d.gated === true ? true : void 0,
990
1038
  sources: Array.isArray(d.sources) ? d.sources : void 0,
991
1039
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
1040
+ suggestions: suggestionsFromBlocks(d.blocks),
1041
+ ...turnIds(d),
992
1042
  data: e.data
993
1043
  };
994
1044
  case "metrics": {
@@ -1011,7 +1061,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
1011
1061
  const body = {};
1012
1062
  if (params.message !== void 0) body.message = params.message;
1013
1063
  if (params.sessionId) body.session_id = params.sessionId;
1064
+ if (params.clientMessageId) body.client_message_id = params.clientMessageId;
1014
1065
  if (params.consumerKey) body.consumer_key = params.consumerKey;
1066
+ if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
1067
+ if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
1015
1068
  if (params.context) body.context = { ...params.context };
1016
1069
  if (params.bindings) body.bindings = { ...params.bindings };
1017
1070
  if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
@@ -1054,6 +1107,7 @@ function validateParams(params) {
1054
1107
  }
1055
1108
  }
1056
1109
  validateStructuredInput(params);
1110
+ warnDeprecatedOptions(params);
1057
1111
  if (params.options?.timeoutMs !== void 0) {
1058
1112
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1059
1113
  }
@@ -1064,6 +1118,22 @@ function validateParams(params) {
1064
1118
  }
1065
1119
  }
1066
1120
  }
1121
+ function warnDeprecatedOptions(params) {
1122
+ const o = params.options;
1123
+ if (!o) return;
1124
+ if (o.jitMode !== void 0) {
1125
+ warnDeprecatedOnce(
1126
+ "ZIHIN_DEP001",
1127
+ "`options.jitMode` is deprecated and has no effect (the server discontinued it). Remove it; it will be dropped from the SDK in 1.0."
1128
+ );
1129
+ }
1130
+ if (o.temperature !== void 0 || o.maxTokens !== void 0) {
1131
+ warnDeprecatedOnce(
1132
+ "ZIHIN_DEP002",
1133
+ "`options.temperature`/`options.maxTokens` have no effect: the agent's own configuration (`llm_config`) decides them \u2014 change it in the Zihin console or with `update_agent`. They will be dropped from the SDK in 1.0."
1134
+ );
1135
+ }
1136
+ }
1067
1137
  var MAX_FIELDS = 50;
1068
1138
  var MAX_VALUE_CHARS = 2e3;
1069
1139
  var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
@@ -1071,13 +1141,37 @@ var MAX_SESSION_FROM_FIELDS = 5;
1071
1141
  var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1072
1142
  var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1073
1143
  var MAX_CONSUMER_KEY_CHARS = 200;
1144
+ var MAX_CONSUMER_NAME_CHARS = 255;
1145
+ var MAX_CLIENT_BLOCKS = 10;
1146
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1074
1147
  function snapshotStructuredInput(params) {
1075
1148
  const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1076
1149
  return {
1077
1150
  ...params,
1151
+ ...normalizeConsumer(params),
1078
1152
  context: snap(params.context),
1079
1153
  bindings: snap(params.bindings),
1080
- sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1154
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom,
1155
+ capabilities: isPlainObject(params.capabilities) ? {
1156
+ ...params.capabilities,
1157
+ blocks: Array.isArray(params.capabilities.blocks) ? [...params.capabilities.blocks] : params.capabilities.blocks
1158
+ } : params.capabilities
1159
+ };
1160
+ }
1161
+ function normalizeConsumer(params) {
1162
+ const c = params.consumer;
1163
+ if (c === void 0) return {};
1164
+ if (!isPlainObject(c)) {
1165
+ throw new ZihinAgentError("config", "`consumer` must be an object: `{ key, name? }`.");
1166
+ }
1167
+ const key = c.key;
1168
+ if (params.consumerKey !== void 0 && key !== void 0 && params.consumerKey !== key) {
1169
+ throw new ZihinAgentError("config", "`consumerKey` and `consumer.key` differ \u2014 send only one of them.");
1170
+ }
1171
+ const resolved = key ?? params.consumerKey;
1172
+ return {
1173
+ consumerKey: resolved,
1174
+ consumer: { key: resolved, ...c.name !== void 0 ? { name: c.name } : {} }
1081
1175
  };
1082
1176
  }
1083
1177
  function isPlainObject(v) {
@@ -1089,12 +1183,33 @@ function isScalar(v) {
1089
1183
  return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1090
1184
  }
1091
1185
  function validateStructuredInput(params) {
1186
+ if (params.clientMessageId !== void 0 && (typeof params.clientMessageId !== "string" || !UUID_RE.test(params.clientMessageId))) {
1187
+ throw new ZihinAgentError("config", "`clientMessageId` must be a UUID (e.g. from `crypto.randomUUID()`).");
1188
+ }
1092
1189
  if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1093
1190
  throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1094
1191
  }
1095
1192
  if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1096
1193
  throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1097
1194
  }
1195
+ if (params.consumer !== void 0) {
1196
+ if (params.consumerKey === void 0) {
1197
+ throw new ZihinAgentError("config", "`consumer.key` is required (the name has nothing to attach to without it).");
1198
+ }
1199
+ const n = params.consumer.name;
1200
+ if (n !== void 0 && (typeof n !== "string" || !n.trim() || n.length > MAX_CONSUMER_NAME_CHARS)) {
1201
+ throw new ZihinAgentError("config", `\`consumer.name\` must be a non-empty string of up to ${MAX_CONSUMER_NAME_CHARS} characters.`);
1202
+ }
1203
+ }
1204
+ if (params.capabilities !== void 0) {
1205
+ const blocks = isPlainObject(params.capabilities) ? params.capabilities.blocks : null;
1206
+ if (blocks === null || blocks !== void 0 && (!Array.isArray(blocks) || blocks.length > MAX_CLIENT_BLOCKS || blocks.some((b) => typeof b !== "string"))) {
1207
+ throw new ZihinAgentError(
1208
+ "config",
1209
+ `\`capabilities\` must be \`{ blocks: [...] }\` with up to ${MAX_CLIENT_BLOCKS} block names (e.g. \`['quick_replies']\`).`
1210
+ );
1211
+ }
1212
+ }
1098
1213
  for (const name of ["context", "bindings"]) {
1099
1214
  const v = params[name];
1100
1215
  if (v === void 0) continue;
@@ -1148,6 +1263,7 @@ function requestedInputFields(params) {
1148
1263
  if (params.context) out.push("context");
1149
1264
  if (params.bindings) out.push("bindings");
1150
1265
  if (params.sessionFrom?.length) out.push("session_from");
1266
+ if (params.clientMessageId) out.push("client_message_id");
1151
1267
  return out;
1152
1268
  }
1153
1269
  function checkInputCapability(params, chunk) {
@@ -1184,7 +1300,7 @@ function checkInputCapability(params, chunk) {
1184
1300
  );
1185
1301
  }
1186
1302
  }
1187
- var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1303
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "action_forwarded", "token", "response", "metrics", "done"]);
1188
1304
  async function* streamAgent(cfg, rawParams) {
1189
1305
  const params = snapshotStructuredInput(rawParams);
1190
1306
  validateParams(params);
@@ -1203,7 +1319,18 @@ async function* streamAgent(cfg, rawParams) {
1203
1319
  maxRetries: cfg.maxRetries,
1204
1320
  callerSignal: params.signal,
1205
1321
  fetchImpl: cfg.fetchImpl,
1206
- onRetry: cfg.onRetry
1322
+ onRetry: cfg.onRetry,
1323
+ // Retry do POST só ANTES dos headers chegarem. O que o torna seguro é a ordem do BE: o SSE faz
1324
+ // `flushHeaders()` antes de reservar o id e antes de qualquer trabalho do turno, então falhar
1325
+ // sem headers quase sempre significa que o handler nem começou. A chave cobre o resto: se o 1º
1326
+ // POST chegou a rodar, o reenvio dá SESSION_BUSY (em voo) ou replay (COMPLETED/LENGTH). A chave
1327
+ // NÃO protege turno cortado no meio: o BE cancela e libera o id, e o reenvio roda de novo com as
1328
+ // tools — por isso o erro mid-stream sai `retryable: false`. Liga antes de ver `accepted_fields`
1329
+ // (chega no `metadata`): contra BE antigo o retry pré-headers acontece sem a chave valer —
1330
+ // aceitável pela mesma ordem (headers antes do trabalho), mas documentado no README.
1331
+ // Resíduo não verificado: proxy que bufferiza headers e devolve 502/504 depois do BE já ter
1332
+ // começado (X-Accel-Buffering: no torna isso improvável).
1333
+ idempotent: !!params.clientMessageId
1207
1334
  });
1208
1335
  try {
1209
1336
  if (!response.ok) {
@@ -1214,7 +1341,7 @@ async function* streamAgent(cfg, rawParams) {
1214
1341
  }
1215
1342
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
1216
1343
  }
1217
- const onAbortError = () => deadline.timedOut() ? new ZihinAgentError("timeout", `Stream timed out after ${reportedMs}ms.`, { endpoint }) : deadline.abortedByCaller() ? new ZihinAgentError("aborted", "Stream aborted by the caller.", { endpoint }) : new ZihinAgentError("network", "Stream connection interrupted.", { endpoint });
1344
+ const onAbortError = () => deadline.timedOut() ? new ZihinAgentError("timeout", `Stream timed out after ${reportedMs}ms.`, { endpoint }) : deadline.abortedByCaller() ? new ZihinAgentError("aborted", "Stream aborted by the caller.", { endpoint }) : new ZihinAgentError("network", "Stream connection interrupted.", { endpoint, retryable: false });
1218
1345
  for await (const e of parseSse(response, {
1219
1346
  signal: params.signal,
1220
1347
  onAbortError,
@@ -1237,16 +1364,62 @@ async function* streamAgent(cfg, rawParams) {
1237
1364
  function classifyStreamErrorKind(code, timedOut) {
1238
1365
  if (timedOut === true) return "timeout";
1239
1366
  if (code === "VALIDATION_ERROR") return "config";
1367
+ const known = own(STREAM_ERROR_CODES, code);
1368
+ if (known) return known.kind;
1240
1369
  if (timedOut === false) return "server";
1241
1370
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1242
1371
  }
1243
- function errorFromStreamChunk(chunk) {
1372
+ var STREAM_ERROR_CODES = {
1373
+ SESSION_BUSY: {
1374
+ kind: "blocked",
1375
+ message: "Another turn is still running in this session (or the same clientMessageId is still in flight); retry after it finishes.",
1376
+ retryable: true
1377
+ },
1378
+ IDEMPOTENCY_CONFLICT: {
1379
+ kind: "config",
1380
+ message: "This `clientMessageId` was already used with a different message \u2014 use a new UUID per message."
1381
+ },
1382
+ // `keepServerMessage`: a frase do BE diz o que foi e o que NÃO foi feito (tools que rodaram) —
1383
+ // o integrador precisa dela para decidir se reenvia.
1384
+ CONTEXT_LIMIT: {
1385
+ kind: "blocked",
1386
+ message: "The conversation gathered more context than the model can process; send a smaller request or start a new session.",
1387
+ keepServerMessage: true
1388
+ },
1389
+ CONFLICT: {
1390
+ kind: "blocked",
1391
+ message: "The declared consumer identity does not match the one already bound to this session."
1392
+ },
1393
+ NOT_FOUND: {
1394
+ kind: "not_found",
1395
+ message: "Agent not found, inactive, or not accessible with this API key \u2014 check the agent ID and that the key belongs to the same Zihin workspace."
1396
+ },
1397
+ // Replay idempotente sem resposta recuperável. Pode persistir por 24 h (turno gravado sem
1398
+ // mensagem do assistente): retentar com o MESMO id bate no mesmo erro, por isso não é
1399
+ // retentável — o aviso diz a saída (novo `clientMessageId`).
1400
+ SERVICE_UNAVAILABLE: {
1401
+ kind: "server",
1402
+ message: "This message was already answered, but the stored answer could not be read; resend with a new clientMessageId.",
1403
+ retryable: false
1404
+ },
1405
+ // Defensivo: não há ponto de emissão in-stream observado no BE @ 9ec79cfc.
1406
+ RATE_LIMIT_EXCEEDED: { kind: "rate_limit", retryable: true },
1407
+ QUOTA_EXCEEDED: { kind: "quota_exceeded" },
1408
+ TRIAL_EXPIRED: { kind: "trial_expired" },
1409
+ NO_ACTIVE_SUBSCRIPTION: { kind: "no_subscription" }
1410
+ };
1411
+ function errorFromStreamChunk(chunk, ctx = {}) {
1244
1412
  const code = chunk.code ?? "AGENT_ERROR";
1245
1413
  const kind = classifyStreamErrorKind(code, chunk.timedOut);
1246
- return new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1414
+ const known = chunk.timedOut === true ? void 0 : own(STREAM_ERROR_CODES, code);
1415
+ const serverMsg = chunk.message ? sanitizeApiError(chunk.message).message.replace(/<[^>]*>/g, "").slice(0, 300) : "";
1416
+ const message = known?.message ? known.keepServerMessage && serverMsg ? `${known.message} Server: ${serverMsg}` : known.message : serverMsg || code;
1417
+ return new ZihinAgentError(kind, message, {
1247
1418
  code,
1248
1419
  requestId: chunk.requestId,
1249
- retryable: false
1420
+ executionId: safeId(ctx.executionId),
1421
+ sessionId: safeId(ctx.sessionId),
1422
+ retryable: known?.retryable ?? false
1250
1423
  });
1251
1424
  }
1252
1425
  var FAILED_OUTCOMES = {
@@ -1267,8 +1440,32 @@ var FAILED_OUTCOMES = {
1267
1440
  CONFLICT: {
1268
1441
  kind: "blocked",
1269
1442
  message: "The declared consumer identity does not match the one already bound to this session."
1443
+ },
1444
+ // O provedor do modelo recusou por política (BE `4cea8d53`, 08/10/2026). O `content` é um texto
1445
+ // neutro FIXO da plataforma ("Não consigo ajudar com esse pedido…"), não uma resposta: o BE
1446
+ // registra a execução como erro. Publicá-lo como resposta era a falha silenciosa da 0.6.
1447
+ REFUSED: {
1448
+ kind: "blocked",
1449
+ message: "The model provider refused this request under its content policy; the agent did not answer."
1450
+ },
1451
+ // No `/stream` chega como `event: error` (ver STREAM_ERROR_CODES); no webhook sync e em
1452
+ // backends futuros pode vir como desfecho.
1453
+ CONTEXT_LIMIT: {
1454
+ kind: "blocked",
1455
+ message: "The conversation gathered more context than the model can process; send a smaller request or start a new session."
1270
1456
  }
1271
1457
  };
1458
+ function errorFromOutcome(outcome, ctx = {}) {
1459
+ const label = safeLabel(outcome);
1460
+ if (!label) return void 0;
1461
+ ctx = { ...ctx, executionId: safeId(ctx.executionId), sessionId: safeId(ctx.sessionId) };
1462
+ if (label === "TIMEOUT") {
1463
+ return new ZihinAgentError("timeout", "The agent turn timed out on the server.", { ...ctx, code: "TURN_TIMEOUT", retryable: false });
1464
+ }
1465
+ const failed = own(FAILED_OUTCOMES, label);
1466
+ if (!failed) return void 0;
1467
+ return new ZihinAgentError(failed.kind, failed.message, { ...ctx, code: label, retryable: failed.retryable ?? false });
1468
+ }
1272
1469
  var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1273
1470
  var ERROR_BLOCKS = {
1274
1471
  ownership_denied: {
@@ -1325,6 +1522,7 @@ async function invokeAgent(cfg, params) {
1325
1522
  const keepRaw = params.includeRaw === true;
1326
1523
  let responseChunk;
1327
1524
  let metricsChunk;
1525
+ let metadataChunk;
1328
1526
  let metadataSessionId;
1329
1527
  let mintedSessionId;
1330
1528
  let doneChunk;
@@ -1335,12 +1533,16 @@ async function invokeAgent(cfg, params) {
1335
1533
  for await (const chunk of streamAgent(cfg, params)) {
1336
1534
  if (keepRaw) raw.push(chunk);
1337
1535
  if (chunk.type === "error") {
1338
- throw errorFromStreamChunk(chunk);
1536
+ throw errorFromStreamChunk(chunk, {
1537
+ executionId: metadataChunk?.executionId,
1538
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
1539
+ });
1339
1540
  }
1340
1541
  if (chunk.type === "response") responseChunk = chunk;
1341
1542
  else if (chunk.type === "metrics") metricsChunk = chunk;
1342
1543
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1343
1544
  else if (chunk.type === "metadata") {
1545
+ metadataChunk = chunk;
1344
1546
  if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1345
1547
  acceptedFields = chunk.acceptedFields;
1346
1548
  ignoredFields = chunk.ignoredFields;
@@ -1349,9 +1551,23 @@ async function invokeAgent(cfg, params) {
1349
1551
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1350
1552
  }
1351
1553
  const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1352
- const executionId = metricsChunk?.executionId;
1554
+ const executionId = metadataChunk?.executionId ?? doneChunk?.executionId ?? responseChunk?.executionId ?? metricsChunk?.executionId;
1555
+ const ids = {
1556
+ clientMessageId: doneChunk?.clientMessageId ?? responseChunk?.clientMessageId ?? metadataChunk?.clientMessageId,
1557
+ idempotentReplay: doneChunk?.idempotentReplay ?? responseChunk?.idempotentReplay ?? metadataChunk?.idempotentReplay,
1558
+ userMessageId: doneChunk?.userMessageId ?? responseChunk?.userMessageId,
1559
+ assistantMessageId: doneChunk?.assistantMessageId ?? responseChunk?.assistantMessageId
1560
+ };
1353
1561
  const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1354
1562
  if (outcomeError) throw outcomeError;
1563
+ const replaySession = responseChunk?.sessionId ?? metadataSessionId;
1564
+ if (ids.idempotentReplay && params.sessionId && replaySession && replaySession.toLowerCase() !== params.sessionId.toLowerCase()) {
1565
+ throw new ZihinAgentError(
1566
+ "config",
1567
+ "This `clientMessageId` was already used in a different session: the server replayed that session's answer. Use a new UUID per message (never a fixed value).",
1568
+ { code: "IDEMPOTENT_REPLAY_OTHER_SESSION", executionId, sessionId: params.sessionId, retryable: false }
1569
+ );
1570
+ }
1355
1571
  const cleanEnd = doneChunk !== void 0;
1356
1572
  if (!responseChunk) {
1357
1573
  if (cleanEnd) {
@@ -1367,6 +1583,7 @@ async function invokeAgent(cfg, params) {
1367
1583
  iterations: metricsChunk?.iterations,
1368
1584
  executionTimeMs: metricsChunk?.executionTimeMs,
1369
1585
  executionId,
1586
+ ...ids,
1370
1587
  acceptedFields,
1371
1588
  ignoredFields,
1372
1589
  raw
@@ -1382,10 +1599,12 @@ async function invokeAgent(cfg, params) {
1382
1599
  usage: metricsChunk?.usage,
1383
1600
  sources: responseChunk.sources,
1384
1601
  blocks: responseChunk.blocks,
1602
+ suggestions: responseChunk.suggestions,
1385
1603
  gated: responseChunk.gated,
1386
1604
  iterations: metricsChunk?.iterations,
1387
1605
  executionTimeMs: metricsChunk?.executionTimeMs,
1388
1606
  executionId,
1607
+ ...ids,
1389
1608
  outcome: doneChunk?.outcome,
1390
1609
  truncated: doneChunk?.truncated,
1391
1610
  finishReason: doneChunk?.finishReason,
@@ -1656,22 +1875,40 @@ async function dispatchAgent(cfg, params) {
1656
1875
  }
1657
1876
  const json = parseBody(res.text);
1658
1877
  const j = rec(json);
1878
+ const preTurnOutcome = safeLabel(j.outcome);
1879
+ if (j.success === false && preTurnOutcome && preTurnOutcome !== "ERROR") {
1880
+ const classified = errorFromOutcome(preTurnOutcome, {
1881
+ status: res.status,
1882
+ endpoint: res.endpoint,
1883
+ attempts: res.attempts,
1884
+ executionId: safeId(j.execution_id)
1885
+ });
1886
+ if (classified) throw classified;
1887
+ }
1659
1888
  if (j.success === false) {
1660
1889
  const detail = sanitizeApiError(str(j.error) ?? "").message;
1661
1890
  throw new ZihinAgentError("server", detail || "The agent run failed.", {
1662
1891
  status: res.status,
1663
1892
  endpoint: res.endpoint,
1664
1893
  attempts: res.attempts,
1665
- executionId: str(j.execution_id),
1894
+ executionId: safeId(j.execution_id),
1666
1895
  // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1667
1896
  retryable: false
1668
1897
  });
1669
1898
  }
1899
+ const outcomeError = errorFromOutcome(str(j.outcome), {
1900
+ status: res.status,
1901
+ endpoint: res.endpoint,
1902
+ attempts: res.attempts,
1903
+ executionId: safeId(j.execution_id)
1904
+ });
1905
+ if (outcomeError) throw outcomeError;
1670
1906
  return {
1671
1907
  accepted: true,
1908
+ outcome: safeLabel(j.outcome),
1672
1909
  triggerId: str(j.trigger_id) ?? params.triggerId,
1673
1910
  agentId: str(j.agent_id),
1674
- executionId: str(j.execution_id),
1911
+ executionId: safeId(j.execution_id),
1675
1912
  agentResponse: str(j.agent_response),
1676
1913
  usage: parseUsage(j.usage),
1677
1914
  iterations: num(j.iterations),
@@ -1998,6 +2235,7 @@ function signRequest(payload, secretKey, timestamp) {
1998
2235
  classifyStreamErrorKind,
1999
2236
  createClient,
2000
2237
  errorFromDoneChunk,
2238
+ errorFromOutcome,
2001
2239
  errorFromStreamChunk,
2002
2240
  isCommercialBlock,
2003
2241
  sanitizeApiError,