@zihin/agent-client 0.6.1 → 0.8.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
@@ -388,6 +388,8 @@ function errorFromResponse(status, body, meta = {}) {
388
388
  retryable: false
389
389
  });
390
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 });
391
393
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
392
394
  if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
393
395
  return new ZihinAgentError(
@@ -484,7 +486,7 @@ function resolveConfig(config) {
484
486
  if (typeof (config.fetch ?? globalThis.fetch) !== "function") {
485
487
  throw new ZihinAgentError(
486
488
  "config",
487
- "Global fetch is unavailable in this runtime. Use Node >=18 or pass a `fetch` implementation in the client config."
489
+ "Global fetch is unavailable in this runtime. Use Node >=20.16 or pass a `fetch` implementation in the client config."
488
490
  );
489
491
  }
490
492
  return {
@@ -527,15 +529,15 @@ function buildHeaders(apiKey, extra = {}) {
527
529
  ...extra
528
530
  };
529
531
  }
530
- function delay(ms, signal) {
532
+ function delay(ms, signal, meta) {
531
533
  return new Promise((resolve, reject) => {
532
534
  if (signal?.aborted) {
533
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
535
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
534
536
  return;
535
537
  }
536
538
  const onAbort = () => {
537
539
  clearTimeout(timer);
538
- reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
540
+ reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
539
541
  };
540
542
  const timer = setTimeout(() => {
541
543
  if (signal) signal.removeEventListener("abort", onAbort);
@@ -625,7 +627,7 @@ async function requestBuffered(args) {
625
627
  }
626
628
  d.dispose();
627
629
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
628
- await delay(wait, args.callerSignal);
630
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
629
631
  continue;
630
632
  }
631
633
  const text = peeked ?? await res.text();
@@ -644,7 +646,7 @@ async function requestBuffered(args) {
644
646
  const wait = backoffMs(attempt);
645
647
  if (budget.overheadLeft() <= wait) throw lastError;
646
648
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
647
- await delay(wait, args.callerSignal);
649
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
648
650
  }
649
651
  }
650
652
  throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -653,6 +655,7 @@ async function openStream(args) {
653
655
  const endpoint = endpointLabel("POST", args.url);
654
656
  const doFetch = resolveFetch(args.fetchImpl);
655
657
  const budget = budgetTracker({ ...args, endpoint });
658
+ const policy = args.idempotent ? "idempotent" : "side-effecting";
656
659
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
657
660
  const d = createDeadline(args.timeoutMs, args.callerSignal);
658
661
  try {
@@ -664,7 +667,7 @@ async function openStream(args) {
664
667
  redirect: "error",
665
668
  cache: "no-store"
666
669
  });
667
- 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;
668
671
  if (response.status === 429) {
669
672
  const text = await response.text().catch(() => "");
670
673
  const body = parseBody(text);
@@ -674,7 +677,7 @@ async function openStream(args) {
674
677
  }
675
678
  d.dispose();
676
679
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
677
- await delay(wait, args.callerSignal);
680
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
678
681
  continue;
679
682
  }
680
683
  if (wait !== null && budget.overheadLeft() > wait) {
@@ -690,7 +693,7 @@ async function openStream(args) {
690
693
  reason: "status",
691
694
  status: response.status
692
695
  });
693
- await delay(wait, args.callerSignal);
696
+ await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
694
697
  continue;
695
698
  }
696
699
  return { response, deadline: d, endpoint };
@@ -701,7 +704,14 @@ async function openStream(args) {
701
704
  if (d.timedOut()) throw budget.expired(attempt + 1);
702
705
  if (d.abortedByCaller())
703
706
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
704
- 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;
705
715
  }
706
716
  }
707
717
  throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
@@ -913,6 +923,15 @@ function suggestionsFromBlocks(blocks) {
913
923
  const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
914
924
  return out.length ? out : void 0;
915
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
+ }
916
935
  function mapEvent(e) {
917
936
  const d = rec(e.data);
918
937
  switch (e.event) {
@@ -922,6 +941,7 @@ function mapEvent(e) {
922
941
  requestId: safeId(d.request_id),
923
942
  agentId: str(d.agent_id),
924
943
  sessionId: safeId(d.session_id),
944
+ ...turnIds(d),
925
945
  acceptedFields: parseAcceptedFields(d.accepted_fields),
926
946
  ignoredFields: parseIgnoredFields(d.ignored_fields),
927
947
  data: e.data
@@ -944,6 +964,8 @@ function mapEvent(e) {
944
964
  };
945
965
  case "action_proposed":
946
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 };
947
969
  case "token":
948
970
  return { type: "token", content: str(d.content) ?? "", data: e.data };
949
971
  case "warning":
@@ -958,6 +980,7 @@ function mapEvent(e) {
958
980
  timeoutClock: safeLabel(d.timeout_clock),
959
981
  truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
960
982
  finishReason: str(d.finish_reason),
983
+ ...turnIds(d),
961
984
  data: e.data
962
985
  };
963
986
  case "error": {
@@ -980,6 +1003,7 @@ function mapEvent(e) {
980
1003
  sources: Array.isArray(d.sources) ? d.sources : void 0,
981
1004
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
982
1005
  suggestions: suggestionsFromBlocks(d.blocks),
1006
+ ...turnIds(d),
983
1007
  data: e.data
984
1008
  };
985
1009
  case "metrics": {
@@ -1002,6 +1026,7 @@ function buildRequestBody(params, normalizedTimeoutMs) {
1002
1026
  const body = {};
1003
1027
  if (params.message !== void 0) body.message = params.message;
1004
1028
  if (params.sessionId) body.session_id = params.sessionId;
1029
+ if (params.clientMessageId) body.client_message_id = params.clientMessageId;
1005
1030
  if (params.consumerKey) body.consumer_key = params.consumerKey;
1006
1031
  if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
1007
1032
  if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
@@ -1083,6 +1108,7 @@ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "proto
1083
1108
  var MAX_CONSUMER_KEY_CHARS = 200;
1084
1109
  var MAX_CONSUMER_NAME_CHARS = 255;
1085
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;
1086
1112
  function snapshotStructuredInput(params) {
1087
1113
  const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1088
1114
  return {
@@ -1122,6 +1148,9 @@ function isScalar(v) {
1122
1148
  return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1123
1149
  }
1124
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
+ }
1125
1154
  if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1126
1155
  throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1127
1156
  }
@@ -1199,6 +1228,7 @@ function requestedInputFields(params) {
1199
1228
  if (params.context) out.push("context");
1200
1229
  if (params.bindings) out.push("bindings");
1201
1230
  if (params.sessionFrom?.length) out.push("session_from");
1231
+ if (params.clientMessageId) out.push("client_message_id");
1202
1232
  return out;
1203
1233
  }
1204
1234
  function checkInputCapability(params, chunk) {
@@ -1235,7 +1265,7 @@ function checkInputCapability(params, chunk) {
1235
1265
  );
1236
1266
  }
1237
1267
  }
1238
- 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"]);
1239
1269
  async function* streamAgent(cfg, rawParams) {
1240
1270
  const params = snapshotStructuredInput(rawParams);
1241
1271
  validateParams(params);
@@ -1254,7 +1284,18 @@ async function* streamAgent(cfg, rawParams) {
1254
1284
  maxRetries: cfg.maxRetries,
1255
1285
  callerSignal: params.signal,
1256
1286
  fetchImpl: cfg.fetchImpl,
1257
- 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
1258
1299
  });
1259
1300
  try {
1260
1301
  if (!response.ok) {
@@ -1265,7 +1306,7 @@ async function* streamAgent(cfg, rawParams) {
1265
1306
  }
1266
1307
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
1267
1308
  }
1268
- 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 });
1269
1310
  for await (const e of parseSse(response, {
1270
1311
  signal: params.signal,
1271
1312
  onAbortError,
@@ -1288,16 +1329,62 @@ async function* streamAgent(cfg, rawParams) {
1288
1329
  function classifyStreamErrorKind(code, timedOut) {
1289
1330
  if (timedOut === true) return "timeout";
1290
1331
  if (code === "VALIDATION_ERROR") return "config";
1332
+ const known = own(STREAM_ERROR_CODES, code);
1333
+ if (known) return known.kind;
1291
1334
  if (timedOut === false) return "server";
1292
1335
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1293
1336
  }
1294
- 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 = {}) {
1295
1377
  const code = chunk.code ?? "AGENT_ERROR";
1296
1378
  const kind = classifyStreamErrorKind(code, chunk.timedOut);
1297
- 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, {
1298
1383
  code,
1299
1384
  requestId: chunk.requestId,
1300
- retryable: false
1385
+ executionId: safeId(ctx.executionId),
1386
+ sessionId: safeId(ctx.sessionId),
1387
+ retryable: known?.retryable ?? false
1301
1388
  });
1302
1389
  }
1303
1390
  var FAILED_OUTCOMES = {
@@ -1318,8 +1405,32 @@ var FAILED_OUTCOMES = {
1318
1405
  CONFLICT: {
1319
1406
  kind: "blocked",
1320
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."
1321
1421
  }
1322
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
+ }
1323
1434
  var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1324
1435
  var ERROR_BLOCKS = {
1325
1436
  ownership_denied: {
@@ -1376,6 +1487,7 @@ async function invokeAgent(cfg, params) {
1376
1487
  const keepRaw = params.includeRaw === true;
1377
1488
  let responseChunk;
1378
1489
  let metricsChunk;
1490
+ let metadataChunk;
1379
1491
  let metadataSessionId;
1380
1492
  let mintedSessionId;
1381
1493
  let doneChunk;
@@ -1386,12 +1498,16 @@ async function invokeAgent(cfg, params) {
1386
1498
  for await (const chunk of streamAgent(cfg, params)) {
1387
1499
  if (keepRaw) raw.push(chunk);
1388
1500
  if (chunk.type === "error") {
1389
- throw errorFromStreamChunk(chunk);
1501
+ throw errorFromStreamChunk(chunk, {
1502
+ executionId: metadataChunk?.executionId,
1503
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
1504
+ });
1390
1505
  }
1391
1506
  if (chunk.type === "response") responseChunk = chunk;
1392
1507
  else if (chunk.type === "metrics") metricsChunk = chunk;
1393
1508
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1394
1509
  else if (chunk.type === "metadata") {
1510
+ metadataChunk = chunk;
1395
1511
  if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1396
1512
  acceptedFields = chunk.acceptedFields;
1397
1513
  ignoredFields = chunk.ignoredFields;
@@ -1400,9 +1516,23 @@ async function invokeAgent(cfg, params) {
1400
1516
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1401
1517
  }
1402
1518
  const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1403
- 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
+ };
1404
1526
  const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1405
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
+ }
1406
1536
  const cleanEnd = doneChunk !== void 0;
1407
1537
  if (!responseChunk) {
1408
1538
  if (cleanEnd) {
@@ -1418,6 +1548,7 @@ async function invokeAgent(cfg, params) {
1418
1548
  iterations: metricsChunk?.iterations,
1419
1549
  executionTimeMs: metricsChunk?.executionTimeMs,
1420
1550
  executionId,
1551
+ ...ids,
1421
1552
  acceptedFields,
1422
1553
  ignoredFields,
1423
1554
  raw
@@ -1438,6 +1569,7 @@ async function invokeAgent(cfg, params) {
1438
1569
  iterations: metricsChunk?.iterations,
1439
1570
  executionTimeMs: metricsChunk?.executionTimeMs,
1440
1571
  executionId,
1572
+ ...ids,
1441
1573
  outcome: doneChunk?.outcome,
1442
1574
  truncated: doneChunk?.truncated,
1443
1575
  finishReason: doneChunk?.finishReason,
@@ -1708,22 +1840,40 @@ async function dispatchAgent(cfg, params) {
1708
1840
  }
1709
1841
  const json = parseBody(res.text);
1710
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
+ }
1711
1853
  if (j.success === false) {
1712
1854
  const detail = sanitizeApiError(str(j.error) ?? "").message;
1713
1855
  throw new ZihinAgentError("server", detail || "The agent run failed.", {
1714
1856
  status: res.status,
1715
1857
  endpoint: res.endpoint,
1716
1858
  attempts: res.attempts,
1717
- executionId: str(j.execution_id),
1859
+ executionId: safeId(j.execution_id),
1718
1860
  // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1719
1861
  retryable: false
1720
1862
  });
1721
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;
1722
1871
  return {
1723
1872
  accepted: true,
1873
+ outcome: safeLabel(j.outcome),
1724
1874
  triggerId: str(j.trigger_id) ?? params.triggerId,
1725
1875
  agentId: str(j.agent_id),
1726
- executionId: str(j.execution_id),
1876
+ executionId: safeId(j.execution_id),
1727
1877
  agentResponse: str(j.agent_response),
1728
1878
  usage: parseUsage(j.usage),
1729
1879
  iterations: num(j.iterations),
@@ -2049,6 +2199,7 @@ export {
2049
2199
  classifyStreamErrorKind,
2050
2200
  createClient,
2051
2201
  errorFromDoneChunk,
2202
+ errorFromOutcome,
2052
2203
  errorFromStreamChunk,
2053
2204
  isCommercialBlock,
2054
2205
  sanitizeApiError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihin/agent-client",
3
- "version": "0.6.1",
3
+ "version": "0.8.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",
@@ -40,7 +40,7 @@
40
40
  "dist"
41
41
  ],
42
42
  "engines": {
43
- "node": ">=18"
43
+ "node": ">=20.16"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
@@ -54,7 +54,7 @@
54
54
  "prepublishOnly": "npm run build && npm test"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": ">=18",
57
+ "@types/node": ">=20",
58
58
  "tsup": "^8.0.0",
59
59
  "typescript": "^5.4.0"
60
60
  }