@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.mjs CHANGED
@@ -55,6 +55,20 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
55
55
  function isTerminalStatus(status) {
56
56
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
57
57
  }
58
+ var _warnedDeprecations = /* @__PURE__ */ new Set();
59
+ function warnDeprecatedOnce(code, message) {
60
+ if (_warnedDeprecations.has(code)) return;
61
+ _warnedDeprecations.add(code);
62
+ try {
63
+ const proc = globalThis.process;
64
+ if (typeof proc?.emitWarning === "function") {
65
+ proc.emitWarning(message, { type: "ZihinDeprecationWarning", code });
66
+ return;
67
+ }
68
+ console.warn(`[${code}] ${message}`);
69
+ } catch {
70
+ }
71
+ }
58
72
 
59
73
  // src/security.ts
60
74
  var PRIVATE_IP_PATTERNS = [
@@ -374,6 +388,8 @@ function errorFromResponse(status, body, meta = {}) {
374
388
  retryable: false
375
389
  });
376
390
  }
391
+ if (code === "PLAN_LIMIT_REACHED")
392
+ return new ZihinAgentError("blocked", message || "This feature is not available on your Zihin plan.", { ...base, retryable: false });
377
393
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
378
394
  if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
379
395
  return new ZihinAgentError(
@@ -513,15 +529,15 @@ function buildHeaders(apiKey, extra = {}) {
513
529
  ...extra
514
530
  };
515
531
  }
516
- function delay(ms, signal) {
532
+ function delay(ms, signal, meta) {
517
533
  return new Promise((resolve, reject) => {
518
534
  if (signal?.aborted) {
519
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
535
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
520
536
  return;
521
537
  }
522
538
  const onAbort = () => {
523
539
  clearTimeout(timer);
524
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
540
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
525
541
  };
526
542
  const timer = setTimeout(() => {
527
543
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -611,7 +627,7 @@ async function requestBuffered(args) {
611
627
  }
612
628
  d.dispose();
613
629
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
614
- await delay(wait, args.callerSignal);
630
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
615
631
  continue;
616
632
  }
617
633
  const text = peeked ?? await res.text();
@@ -630,7 +646,7 @@ async function requestBuffered(args) {
630
646
  const wait = backoffMs(attempt);
631
647
  if (budget.overheadLeft() <= wait) throw lastError;
632
648
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
633
- await delay(wait, args.callerSignal);
649
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
634
650
  }
635
651
  }
636
652
  throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -639,6 +655,7 @@ async function openStream(args) {
639
655
  const endpoint = endpointLabel("POST", args.url);
640
656
  const doFetch = resolveFetch(args.fetchImpl);
641
657
  const budget = budgetTracker({ ...args, endpoint });
658
+ const policy = args.idempotent ? "idempotent" : "side-effecting";
642
659
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
643
660
  const d = createDeadline(args.timeoutMs, args.callerSignal);
644
661
  try {
@@ -650,7 +667,7 @@ async function openStream(args) {
650
667
  redirect: "error",
651
668
  cache: "no-store"
652
669
  });
653
- let wait = shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
670
+ let wait = shouldRetryStatus(response.status, policy) && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
654
671
  if (response.status === 429) {
655
672
  const text = await response.text().catch(() => "");
656
673
  const body = parseBody(text);
@@ -660,7 +677,7 @@ async function openStream(args) {
660
677
  }
661
678
  d.dispose();
662
679
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
663
- await delay(wait, args.callerSignal);
680
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
664
681
  continue;
665
682
  }
666
683
  if (wait !== null && budget.overheadLeft() > wait) {
@@ -676,7 +693,7 @@ async function openStream(args) {
676
693
  reason: "status",
677
694
  status: response.status
678
695
  });
679
- await delay(wait, args.callerSignal);
696
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
680
697
  continue;
681
698
  }
682
699
  return { response, deadline: d, endpoint };
@@ -687,7 +704,14 @@ async function openStream(args) {
687
704
  if (d.timedOut()) throw budget.expired(attempt + 1);
688
705
  if (d.abortedByCaller())
689
706
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
690
- throw errorFromThrow(err, budget.reported, meta);
707
+ const netErr = errorFromThrow(err, budget.reported, meta);
708
+ const wait = backoffMs(attempt);
709
+ if (policy === "idempotent" && netErr.retryable && attempt < args.maxRetries && budget.overheadLeft() > wait) {
710
+ args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
711
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
712
+ continue;
713
+ }
714
+ throw netErr;
691
715
  }
692
716
  }
693
717
  throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -889,6 +913,25 @@ function parseIgnoredFields(v) {
889
913
  }
890
914
  return out;
891
915
  }
916
+ var MAX_SUGGESTIONS = 10;
917
+ var MAX_SUGGESTION_CHARS = 200;
918
+ function suggestionsFromBlocks(blocks) {
919
+ if (!Array.isArray(blocks)) return void 0;
920
+ const qr = [...blocks].reverse().find((b) => rec(b).type === "quick_replies");
921
+ const labels = rec(rec(qr).data).labels;
922
+ if (!Array.isArray(labels)) return void 0;
923
+ const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
924
+ return out.length ? out : void 0;
925
+ }
926
+ function turnIds(d) {
927
+ return {
928
+ executionId: safeId(d.execution_id),
929
+ clientMessageId: safeId(d.client_message_id),
930
+ idempotentReplay: d.idempotent_replay === true ? true : void 0,
931
+ userMessageId: safeId(d.user_message_id),
932
+ assistantMessageId: safeId(d.assistant_message_id)
933
+ };
934
+ }
892
935
  function mapEvent(e) {
893
936
  const d = rec(e.data);
894
937
  switch (e.event) {
@@ -898,6 +941,7 @@ function mapEvent(e) {
898
941
  requestId: safeId(d.request_id),
899
942
  agentId: str(d.agent_id),
900
943
  sessionId: safeId(d.session_id),
944
+ ...turnIds(d),
901
945
  acceptedFields: parseAcceptedFields(d.accepted_fields),
902
946
  ignoredFields: parseIgnoredFields(d.ignored_fields),
903
947
  data: e.data
@@ -920,6 +964,8 @@ function mapEvent(e) {
920
964
  };
921
965
  case "action_proposed":
922
966
  return { type: "action_proposed", data: e.data };
967
+ case "action_forwarded":
968
+ return { type: "action_forwarded", actionId: safeId(d.action_id), data: e.data };
923
969
  case "token":
924
970
  return { type: "token", content: str(d.content) ?? "", data: e.data };
925
971
  case "warning":
@@ -934,6 +980,7 @@ function mapEvent(e) {
934
980
  timeoutClock: safeLabel(d.timeout_clock),
935
981
  truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
936
982
  finishReason: str(d.finish_reason),
983
+ ...turnIds(d),
937
984
  data: e.data
938
985
  };
939
986
  case "error": {
@@ -955,6 +1002,8 @@ function mapEvent(e) {
955
1002
  gated: d.gated === true ? true : void 0,
956
1003
  sources: Array.isArray(d.sources) ? d.sources : void 0,
957
1004
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
1005
+ suggestions: suggestionsFromBlocks(d.blocks),
1006
+ ...turnIds(d),
958
1007
  data: e.data
959
1008
  };
960
1009
  case "metrics": {
@@ -977,7 +1026,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
977
1026
  const body = {};
978
1027
  if (params.message !== void 0) body.message = params.message;
979
1028
  if (params.sessionId) body.session_id = params.sessionId;
1029
+ if (params.clientMessageId) body.client_message_id = params.clientMessageId;
980
1030
  if (params.consumerKey) body.consumer_key = params.consumerKey;
1031
+ if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
1032
+ if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
981
1033
  if (params.context) body.context = { ...params.context };
982
1034
  if (params.bindings) body.bindings = { ...params.bindings };
983
1035
  if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
@@ -1020,6 +1072,7 @@ function validateParams(params) {
1020
1072
  }
1021
1073
  }
1022
1074
  validateStructuredInput(params);
1075
+ warnDeprecatedOptions(params);
1023
1076
  if (params.options?.timeoutMs !== void 0) {
1024
1077
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1025
1078
  }
@@ -1030,6 +1083,22 @@ function validateParams(params) {
1030
1083
  }
1031
1084
  }
1032
1085
  }
1086
+ function warnDeprecatedOptions(params) {
1087
+ const o = params.options;
1088
+ if (!o) return;
1089
+ if (o.jitMode !== void 0) {
1090
+ warnDeprecatedOnce(
1091
+ "ZIHIN_DEP001",
1092
+ "`options.jitMode` is deprecated and has no effect (the server discontinued it). Remove it; it will be dropped from the SDK in 1.0."
1093
+ );
1094
+ }
1095
+ if (o.temperature !== void 0 || o.maxTokens !== void 0) {
1096
+ warnDeprecatedOnce(
1097
+ "ZIHIN_DEP002",
1098
+ "`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."
1099
+ );
1100
+ }
1101
+ }
1033
1102
  var MAX_FIELDS = 50;
1034
1103
  var MAX_VALUE_CHARS = 2e3;
1035
1104
  var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
@@ -1037,13 +1106,37 @@ var MAX_SESSION_FROM_FIELDS = 5;
1037
1106
  var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1038
1107
  var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1039
1108
  var MAX_CONSUMER_KEY_CHARS = 200;
1109
+ var MAX_CONSUMER_NAME_CHARS = 255;
1110
+ var MAX_CLIENT_BLOCKS = 10;
1111
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1040
1112
  function snapshotStructuredInput(params) {
1041
1113
  const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1042
1114
  return {
1043
1115
  ...params,
1116
+ ...normalizeConsumer(params),
1044
1117
  context: snap(params.context),
1045
1118
  bindings: snap(params.bindings),
1046
- sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1119
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom,
1120
+ capabilities: isPlainObject(params.capabilities) ? {
1121
+ ...params.capabilities,
1122
+ blocks: Array.isArray(params.capabilities.blocks) ? [...params.capabilities.blocks] : params.capabilities.blocks
1123
+ } : params.capabilities
1124
+ };
1125
+ }
1126
+ function normalizeConsumer(params) {
1127
+ const c = params.consumer;
1128
+ if (c === void 0) return {};
1129
+ if (!isPlainObject(c)) {
1130
+ throw new ZihinAgentError("config", "`consumer` must be an object: `{ key, name? }`.");
1131
+ }
1132
+ const key = c.key;
1133
+ if (params.consumerKey !== void 0 && key !== void 0 && params.consumerKey !== key) {
1134
+ throw new ZihinAgentError("config", "`consumerKey` and `consumer.key` differ \u2014 send only one of them.");
1135
+ }
1136
+ const resolved = key ?? params.consumerKey;
1137
+ return {
1138
+ consumerKey: resolved,
1139
+ consumer: { key: resolved, ...c.name !== void 0 ? { name: c.name } : {} }
1047
1140
  };
1048
1141
  }
1049
1142
  function isPlainObject(v) {
@@ -1055,12 +1148,33 @@ function isScalar(v) {
1055
1148
  return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1056
1149
  }
1057
1150
  function validateStructuredInput(params) {
1151
+ if (params.clientMessageId !== void 0 && (typeof params.clientMessageId !== "string" || !UUID_RE.test(params.clientMessageId))) {
1152
+ throw new ZihinAgentError("config", "`clientMessageId` must be a UUID (e.g. from `crypto.randomUUID()`).");
1153
+ }
1058
1154
  if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1059
1155
  throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1060
1156
  }
1061
1157
  if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1062
1158
  throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1063
1159
  }
1160
+ if (params.consumer !== void 0) {
1161
+ if (params.consumerKey === void 0) {
1162
+ throw new ZihinAgentError("config", "`consumer.key` is required (the name has nothing to attach to without it).");
1163
+ }
1164
+ const n = params.consumer.name;
1165
+ if (n !== void 0 && (typeof n !== "string" || !n.trim() || n.length > MAX_CONSUMER_NAME_CHARS)) {
1166
+ throw new ZihinAgentError("config", `\`consumer.name\` must be a non-empty string of up to ${MAX_CONSUMER_NAME_CHARS} characters.`);
1167
+ }
1168
+ }
1169
+ if (params.capabilities !== void 0) {
1170
+ const blocks = isPlainObject(params.capabilities) ? params.capabilities.blocks : null;
1171
+ if (blocks === null || blocks !== void 0 && (!Array.isArray(blocks) || blocks.length > MAX_CLIENT_BLOCKS || blocks.some((b) => typeof b !== "string"))) {
1172
+ throw new ZihinAgentError(
1173
+ "config",
1174
+ `\`capabilities\` must be \`{ blocks: [...] }\` with up to ${MAX_CLIENT_BLOCKS} block names (e.g. \`['quick_replies']\`).`
1175
+ );
1176
+ }
1177
+ }
1064
1178
  for (const name of ["context", "bindings"]) {
1065
1179
  const v = params[name];
1066
1180
  if (v === void 0) continue;
@@ -1114,6 +1228,7 @@ function requestedInputFields(params) {
1114
1228
  if (params.context) out.push("context");
1115
1229
  if (params.bindings) out.push("bindings");
1116
1230
  if (params.sessionFrom?.length) out.push("session_from");
1231
+ if (params.clientMessageId) out.push("client_message_id");
1117
1232
  return out;
1118
1233
  }
1119
1234
  function checkInputCapability(params, chunk) {
@@ -1150,7 +1265,7 @@ function checkInputCapability(params, chunk) {
1150
1265
  );
1151
1266
  }
1152
1267
  }
1153
- var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1268
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "action_forwarded", "token", "response", "metrics", "done"]);
1154
1269
  async function* streamAgent(cfg, rawParams) {
1155
1270
  const params = snapshotStructuredInput(rawParams);
1156
1271
  validateParams(params);
@@ -1169,7 +1284,18 @@ async function* streamAgent(cfg, rawParams) {
1169
1284
  maxRetries: cfg.maxRetries,
1170
1285
  callerSignal: params.signal,
1171
1286
  fetchImpl: cfg.fetchImpl,
1172
- onRetry: cfg.onRetry
1287
+ onRetry: cfg.onRetry,
1288
+ // Retry do POST só ANTES dos headers chegarem. O que o torna seguro é a ordem do BE: o SSE faz
1289
+ // `flushHeaders()` antes de reservar o id e antes de qualquer trabalho do turno, então falhar
1290
+ // sem headers quase sempre significa que o handler nem começou. A chave cobre o resto: se o 1º
1291
+ // POST chegou a rodar, o reenvio dá SESSION_BUSY (em voo) ou replay (COMPLETED/LENGTH). A chave
1292
+ // NÃO protege turno cortado no meio: o BE cancela e libera o id, e o reenvio roda de novo com as
1293
+ // tools — por isso o erro mid-stream sai `retryable: false`. Liga antes de ver `accepted_fields`
1294
+ // (chega no `metadata`): contra BE antigo o retry pré-headers acontece sem a chave valer —
1295
+ // aceitável pela mesma ordem (headers antes do trabalho), mas documentado no README.
1296
+ // Resíduo não verificado: proxy que bufferiza headers e devolve 502/504 depois do BE já ter
1297
+ // começado (X-Accel-Buffering: no torna isso improvável).
1298
+ idempotent: !!params.clientMessageId
1173
1299
  });
1174
1300
  try {
1175
1301
  if (!response.ok) {
@@ -1180,7 +1306,7 @@ async function* streamAgent(cfg, rawParams) {
1180
1306
  }
1181
1307
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
1182
1308
  }
1183
- 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 });
1309
+ 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 });
1184
1310
  for await (const e of parseSse(response, {
1185
1311
  signal: params.signal,
1186
1312
  onAbortError,
@@ -1203,16 +1329,62 @@ async function* streamAgent(cfg, rawParams) {
1203
1329
  function classifyStreamErrorKind(code, timedOut) {
1204
1330
  if (timedOut === true) return "timeout";
1205
1331
  if (code === "VALIDATION_ERROR") return "config";
1332
+ const known = own(STREAM_ERROR_CODES, code);
1333
+ if (known) return known.kind;
1206
1334
  if (timedOut === false) return "server";
1207
1335
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1208
1336
  }
1209
- function errorFromStreamChunk(chunk) {
1337
+ var STREAM_ERROR_CODES = {
1338
+ SESSION_BUSY: {
1339
+ kind: "blocked",
1340
+ message: "Another turn is still running in this session (or the same clientMessageId is still in flight); retry after it finishes.",
1341
+ retryable: true
1342
+ },
1343
+ IDEMPOTENCY_CONFLICT: {
1344
+ kind: "config",
1345
+ message: "This `clientMessageId` was already used with a different message \u2014 use a new UUID per message."
1346
+ },
1347
+ // `keepServerMessage`: a frase do BE diz o que foi e o que NÃO foi feito (tools que rodaram) —
1348
+ // o integrador precisa dela para decidir se reenvia.
1349
+ CONTEXT_LIMIT: {
1350
+ kind: "blocked",
1351
+ message: "The conversation gathered more context than the model can process; send a smaller request or start a new session.",
1352
+ keepServerMessage: true
1353
+ },
1354
+ CONFLICT: {
1355
+ kind: "blocked",
1356
+ message: "The declared consumer identity does not match the one already bound to this session."
1357
+ },
1358
+ NOT_FOUND: {
1359
+ kind: "not_found",
1360
+ 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."
1361
+ },
1362
+ // Replay idempotente sem resposta recuperável. Pode persistir por 24 h (turno gravado sem
1363
+ // mensagem do assistente): retentar com o MESMO id bate no mesmo erro, por isso não é
1364
+ // retentável — o aviso diz a saída (novo `clientMessageId`).
1365
+ SERVICE_UNAVAILABLE: {
1366
+ kind: "server",
1367
+ message: "This message was already answered, but the stored answer could not be read; resend with a new clientMessageId.",
1368
+ retryable: false
1369
+ },
1370
+ // Defensivo: não há ponto de emissão in-stream observado no BE @ 9ec79cfc.
1371
+ RATE_LIMIT_EXCEEDED: { kind: "rate_limit", retryable: true },
1372
+ QUOTA_EXCEEDED: { kind: "quota_exceeded" },
1373
+ TRIAL_EXPIRED: { kind: "trial_expired" },
1374
+ NO_ACTIVE_SUBSCRIPTION: { kind: "no_subscription" }
1375
+ };
1376
+ function errorFromStreamChunk(chunk, ctx = {}) {
1210
1377
  const code = chunk.code ?? "AGENT_ERROR";
1211
1378
  const kind = classifyStreamErrorKind(code, chunk.timedOut);
1212
- return new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1379
+ const known = chunk.timedOut === true ? void 0 : own(STREAM_ERROR_CODES, code);
1380
+ const serverMsg = chunk.message ? sanitizeApiError(chunk.message).message.replace(/<[^>]*>/g, "").slice(0, 300) : "";
1381
+ const message = known?.message ? known.keepServerMessage && serverMsg ? `${known.message} Server: ${serverMsg}` : known.message : serverMsg || code;
1382
+ return new ZihinAgentError(kind, message, {
1213
1383
  code,
1214
1384
  requestId: chunk.requestId,
1215
- retryable: false
1385
+ executionId: safeId(ctx.executionId),
1386
+ sessionId: safeId(ctx.sessionId),
1387
+ retryable: known?.retryable ?? false
1216
1388
  });
1217
1389
  }
1218
1390
  var FAILED_OUTCOMES = {
@@ -1233,8 +1405,32 @@ var FAILED_OUTCOMES = {
1233
1405
  CONFLICT: {
1234
1406
  kind: "blocked",
1235
1407
  message: "The declared consumer identity does not match the one already bound to this session."
1408
+ },
1409
+ // O provedor do modelo recusou por política (BE `4cea8d53`, 08/10/2026). O `content` é um texto
1410
+ // neutro FIXO da plataforma ("Não consigo ajudar com esse pedido…"), não uma resposta: o BE
1411
+ // registra a execução como erro. Publicá-lo como resposta era a falha silenciosa da 0.6.
1412
+ REFUSED: {
1413
+ kind: "blocked",
1414
+ message: "The model provider refused this request under its content policy; the agent did not answer."
1415
+ },
1416
+ // No `/stream` chega como `event: error` (ver STREAM_ERROR_CODES); no webhook sync e em
1417
+ // backends futuros pode vir como desfecho.
1418
+ CONTEXT_LIMIT: {
1419
+ kind: "blocked",
1420
+ message: "The conversation gathered more context than the model can process; send a smaller request or start a new session."
1236
1421
  }
1237
1422
  };
1423
+ function errorFromOutcome(outcome, ctx = {}) {
1424
+ const label = safeLabel(outcome);
1425
+ if (!label) return void 0;
1426
+ ctx = { ...ctx, executionId: safeId(ctx.executionId), sessionId: safeId(ctx.sessionId) };
1427
+ if (label === "TIMEOUT") {
1428
+ return new ZihinAgentError("timeout", "The agent turn timed out on the server.", { ...ctx, code: "TURN_TIMEOUT", retryable: false });
1429
+ }
1430
+ const failed = own(FAILED_OUTCOMES, label);
1431
+ if (!failed) return void 0;
1432
+ return new ZihinAgentError(failed.kind, failed.message, { ...ctx, code: label, retryable: failed.retryable ?? false });
1433
+ }
1238
1434
  var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1239
1435
  var ERROR_BLOCKS = {
1240
1436
  ownership_denied: {
@@ -1291,6 +1487,7 @@ async function invokeAgent(cfg, params) {
1291
1487
  const keepRaw = params.includeRaw === true;
1292
1488
  let responseChunk;
1293
1489
  let metricsChunk;
1490
+ let metadataChunk;
1294
1491
  let metadataSessionId;
1295
1492
  let mintedSessionId;
1296
1493
  let doneChunk;
@@ -1301,12 +1498,16 @@ async function invokeAgent(cfg, params) {
1301
1498
  for await (const chunk of streamAgent(cfg, params)) {
1302
1499
  if (keepRaw) raw.push(chunk);
1303
1500
  if (chunk.type === "error") {
1304
- throw errorFromStreamChunk(chunk);
1501
+ throw errorFromStreamChunk(chunk, {
1502
+ executionId: metadataChunk?.executionId,
1503
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
1504
+ });
1305
1505
  }
1306
1506
  if (chunk.type === "response") responseChunk = chunk;
1307
1507
  else if (chunk.type === "metrics") metricsChunk = chunk;
1308
1508
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1309
1509
  else if (chunk.type === "metadata") {
1510
+ metadataChunk = chunk;
1310
1511
  if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1311
1512
  acceptedFields = chunk.acceptedFields;
1312
1513
  ignoredFields = chunk.ignoredFields;
@@ -1315,9 +1516,23 @@ async function invokeAgent(cfg, params) {
1315
1516
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1316
1517
  }
1317
1518
  const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1318
- const executionId = metricsChunk?.executionId;
1519
+ const executionId = metadataChunk?.executionId ?? doneChunk?.executionId ?? responseChunk?.executionId ?? metricsChunk?.executionId;
1520
+ const ids = {
1521
+ clientMessageId: doneChunk?.clientMessageId ?? responseChunk?.clientMessageId ?? metadataChunk?.clientMessageId,
1522
+ idempotentReplay: doneChunk?.idempotentReplay ?? responseChunk?.idempotentReplay ?? metadataChunk?.idempotentReplay,
1523
+ userMessageId: doneChunk?.userMessageId ?? responseChunk?.userMessageId,
1524
+ assistantMessageId: doneChunk?.assistantMessageId ?? responseChunk?.assistantMessageId
1525
+ };
1319
1526
  const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1320
1527
  if (outcomeError) throw outcomeError;
1528
+ const replaySession = responseChunk?.sessionId ?? metadataSessionId;
1529
+ if (ids.idempotentReplay && params.sessionId && replaySession && replaySession.toLowerCase() !== params.sessionId.toLowerCase()) {
1530
+ throw new ZihinAgentError(
1531
+ "config",
1532
+ "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).",
1533
+ { code: "IDEMPOTENT_REPLAY_OTHER_SESSION", executionId, sessionId: params.sessionId, retryable: false }
1534
+ );
1535
+ }
1321
1536
  const cleanEnd = doneChunk !== void 0;
1322
1537
  if (!responseChunk) {
1323
1538
  if (cleanEnd) {
@@ -1333,6 +1548,7 @@ async function invokeAgent(cfg, params) {
1333
1548
  iterations: metricsChunk?.iterations,
1334
1549
  executionTimeMs: metricsChunk?.executionTimeMs,
1335
1550
  executionId,
1551
+ ...ids,
1336
1552
  acceptedFields,
1337
1553
  ignoredFields,
1338
1554
  raw
@@ -1348,10 +1564,12 @@ async function invokeAgent(cfg, params) {
1348
1564
  usage: metricsChunk?.usage,
1349
1565
  sources: responseChunk.sources,
1350
1566
  blocks: responseChunk.blocks,
1567
+ suggestions: responseChunk.suggestions,
1351
1568
  gated: responseChunk.gated,
1352
1569
  iterations: metricsChunk?.iterations,
1353
1570
  executionTimeMs: metricsChunk?.executionTimeMs,
1354
1571
  executionId,
1572
+ ...ids,
1355
1573
  outcome: doneChunk?.outcome,
1356
1574
  truncated: doneChunk?.truncated,
1357
1575
  finishReason: doneChunk?.finishReason,
@@ -1622,22 +1840,40 @@ async function dispatchAgent(cfg, params) {
1622
1840
  }
1623
1841
  const json = parseBody(res.text);
1624
1842
  const j = rec(json);
1843
+ const preTurnOutcome = safeLabel(j.outcome);
1844
+ if (j.success === false && preTurnOutcome && preTurnOutcome !== "ERROR") {
1845
+ const classified = errorFromOutcome(preTurnOutcome, {
1846
+ status: res.status,
1847
+ endpoint: res.endpoint,
1848
+ attempts: res.attempts,
1849
+ executionId: safeId(j.execution_id)
1850
+ });
1851
+ if (classified) throw classified;
1852
+ }
1625
1853
  if (j.success === false) {
1626
1854
  const detail = sanitizeApiError(str(j.error) ?? "").message;
1627
1855
  throw new ZihinAgentError("server", detail || "The agent run failed.", {
1628
1856
  status: res.status,
1629
1857
  endpoint: res.endpoint,
1630
1858
  attempts: res.attempts,
1631
- executionId: str(j.execution_id),
1859
+ executionId: safeId(j.execution_id),
1632
1860
  // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1633
1861
  retryable: false
1634
1862
  });
1635
1863
  }
1864
+ const outcomeError = errorFromOutcome(str(j.outcome), {
1865
+ status: res.status,
1866
+ endpoint: res.endpoint,
1867
+ attempts: res.attempts,
1868
+ executionId: safeId(j.execution_id)
1869
+ });
1870
+ if (outcomeError) throw outcomeError;
1636
1871
  return {
1637
1872
  accepted: true,
1873
+ outcome: safeLabel(j.outcome),
1638
1874
  triggerId: str(j.trigger_id) ?? params.triggerId,
1639
1875
  agentId: str(j.agent_id),
1640
- executionId: str(j.execution_id),
1876
+ executionId: safeId(j.execution_id),
1641
1877
  agentResponse: str(j.agent_response),
1642
1878
  usage: parseUsage(j.usage),
1643
1879
  iterations: num(j.iterations),
@@ -1963,6 +2199,7 @@ export {
1963
2199
  classifyStreamErrorKind,
1964
2200
  createClient,
1965
2201
  errorFromDoneChunk,
2202
+ errorFromOutcome,
1966
2203
  errorFromStreamChunk,
1967
2204
  isCommercialBlock,
1968
2205
  sanitizeApiError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihin/agent-client",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Núcleo de invocação de agentes Zihin.ai — padrão único para n8n, Paperclip e integrações futuras. Stream direto (SSE bufferizado) + triggers assíncronos com callback.",
5
5
  "keywords": [
6
6
  "zihin",