@zihin/agent-client 0.4.1 → 0.6.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
@@ -13,6 +13,12 @@ function utf8Len(s) {
13
13
  function rec(x) {
14
14
  return x && typeof x === "object" ? x : {};
15
15
  }
16
+ function safeId(x) {
17
+ return typeof x === "string" && /^[\w.:-]{1,128}$/.test(x) ? x : void 0;
18
+ }
19
+ function safeLabel(x) {
20
+ return typeof x === "string" && /^[A-Za-z0-9_]{1,64}$/.test(x) ? x : void 0;
21
+ }
16
22
  function str(x) {
17
23
  return typeof x === "string" ? x : void 0;
18
24
  }
@@ -38,8 +44,13 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
38
44
  "waiting",
39
45
  "retrying",
40
46
  "scheduled",
41
- "awaiting_approval"
47
+ "awaiting_approval",
42
48
  // HITL: turno pausado esperando decisão humana não terminou
49
+ // Estados VIVOS reais do backend (zihin-agent-builder `TaskService.LIVE_STATUSES` e
50
+ // `agent_executions.status`): sem eles, uma task/execução pausada ou aguardando input
51
+ // era lida como TERMINAL e o polling parava com o trabalho ainda em andamento.
52
+ "paused",
53
+ "awaiting_input"
43
54
  ]);
44
55
  function isTerminalStatus(status) {
45
56
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
@@ -234,6 +245,9 @@ var ERROR_CODE_MAP = {
234
245
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
235
246
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
236
247
  AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
248
+ // Nome novo do `AGENT_NOT_FOUND` (zihin-agent-builder#561, fatia B). `AGENT_ERROR` virou
249
+ // `INTERNAL_ERROR`, que já está acima.
250
+ NOT_FOUND: "Resource not found \u2014 check the ID (agent, trigger or execution) and that it belongs to this workspace.",
237
251
  // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
238
252
  // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
239
253
  TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
@@ -304,6 +318,8 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
304
318
  this.code = opts.code;
305
319
  this.requestId = opts.requestId;
306
320
  this.executionId = opts.executionId;
321
+ this.sessionId = opts.sessionId;
322
+ this.timeoutClock = opts.timeoutClock;
307
323
  this.attempts = opts.attempts;
308
324
  this.endpoint = opts.endpoint;
309
325
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -338,7 +354,7 @@ function commercialKind(body) {
338
354
  }
339
355
  function extractRequestId(body) {
340
356
  const b = rec(body);
341
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
357
+ return safeId(b.request_id) ?? safeId(b.requestId) ?? safeId(rec(b.error).request_id) ?? safeId(rec(b.meta).request_id);
342
358
  }
343
359
  function errorFromResponse(status, body, meta = {}) {
344
360
  const sanitized = sanitizeApiError(body);
@@ -348,7 +364,7 @@ function errorFromResponse(status, body, meta = {}) {
348
364
  status,
349
365
  code,
350
366
  requestId: extractRequestId(body),
351
- executionId: str(rec(body).execution_id),
367
+ executionId: safeId(rec(body).execution_id),
352
368
  ...meta
353
369
  };
354
370
  const commercial = commercialKind(body);
@@ -359,6 +375,12 @@ function errorFromResponse(status, body, meta = {}) {
359
375
  });
360
376
  }
361
377
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
378
+ if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
379
+ return new ZihinAgentError(
380
+ "not_found",
381
+ "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.",
382
+ { ...base, retryable: false }
383
+ );
362
384
  if (status === 403)
363
385
  return new ZihinAgentError(
364
386
  "auth",
@@ -408,7 +430,7 @@ var OPERATION_TIMEOUT_MS = Object.freeze({
408
430
  dispatch: 26e4,
409
431
  short: 35e3
410
432
  });
411
- var DEADLINE_GRACE_MS = 1e4;
433
+ var DEADLINE_GRACE_MS = 3e4;
412
434
  var RETRY_OVERHEAD_MS = 6e4;
413
435
  var MIN_TIMEOUT_MS = 1e3;
414
436
  var MAX_TIMEOUT_MS = 9e5;
@@ -854,19 +876,34 @@ async function* parseSse(response, opts = {}) {
854
876
  }
855
877
 
856
878
  // src/invoke.ts
879
+ function parseAcceptedFields(v) {
880
+ if (!Array.isArray(v)) return void 0;
881
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
882
+ }
883
+ function parseIgnoredFields(v) {
884
+ if (!Array.isArray(v)) return void 0;
885
+ const out = [];
886
+ for (const x of v) {
887
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
888
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
889
+ }
890
+ return out;
891
+ }
857
892
  function mapEvent(e) {
858
893
  const d = rec(e.data);
859
894
  switch (e.event) {
860
895
  case "metadata":
861
896
  return {
862
897
  type: "metadata",
863
- requestId: str(d.request_id),
898
+ requestId: safeId(d.request_id),
864
899
  agentId: str(d.agent_id),
865
- sessionId: str(d.session_id),
900
+ sessionId: safeId(d.session_id),
901
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
902
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
866
903
  data: e.data
867
904
  };
868
905
  case "status":
869
- return { type: "status", phase: str(d.phase), data: e.data };
906
+ return { type: "status", phase: str(d.phase), blocked: safeLabel(d.blocked), data: e.data };
870
907
  case "attachments":
871
908
  return { type: "attachments", data: e.data };
872
909
  case "phase":
@@ -888,16 +925,24 @@ function mapEvent(e) {
888
925
  case "warning":
889
926
  return { type: "warning", message: str(d.message), data: e.data };
890
927
  case "session":
891
- return { type: "session", sessionId: str(d.session_id), data: e.data };
928
+ return { type: "session", sessionId: safeId(d.session_id), data: e.data };
892
929
  case "done":
893
- return { type: "done", data: e.data };
930
+ return {
931
+ type: "done",
932
+ outcome: safeLabel(d.outcome),
933
+ timedOut: typeof d.timed_out === "boolean" ? d.timed_out : void 0,
934
+ timeoutClock: safeLabel(d.timeout_clock),
935
+ truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
936
+ finishReason: str(d.finish_reason),
937
+ data: e.data
938
+ };
894
939
  case "error": {
895
940
  const err = rec(d.error);
896
941
  return {
897
942
  type: "error",
898
- code: str(err.code),
943
+ code: safeLabel(err.code),
899
944
  message: str(err.message),
900
- requestId: str(err.request_id),
945
+ requestId: safeId(err.request_id),
901
946
  timedOut: typeof err.timed_out === "boolean" ? err.timed_out : void 0,
902
947
  data: e.data
903
948
  };
@@ -906,7 +951,8 @@ function mapEvent(e) {
906
951
  return {
907
952
  type: "response",
908
953
  content: str(d.content) ?? "",
909
- sessionId: str(d.session_id),
954
+ sessionId: safeId(d.session_id),
955
+ gated: d.gated === true ? true : void 0,
910
956
  sources: Array.isArray(d.sources) ? d.sources : void 0,
911
957
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
912
958
  data: e.data
@@ -919,6 +965,7 @@ function mapEvent(e) {
919
965
  model: str(modelConfig.model) ?? str(d.model),
920
966
  iterations: num(d.iterations),
921
967
  executionTimeMs: num(d.execution_time_ms),
968
+ executionId: safeId(d.execution_id),
922
969
  data: e.data
923
970
  };
924
971
  }
@@ -930,6 +977,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
930
977
  const body = {};
931
978
  if (params.message !== void 0) body.message = params.message;
932
979
  if (params.sessionId) body.session_id = params.sessionId;
980
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
981
+ if (params.context) body.context = { ...params.context };
982
+ if (params.bindings) body.bindings = { ...params.bindings };
983
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
933
984
  if (params.attachments?.length) {
934
985
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
935
986
  }
@@ -968,6 +1019,7 @@ function validateParams(params) {
968
1019
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
969
1020
  }
970
1021
  }
1022
+ validateStructuredInput(params);
971
1023
  if (params.options?.timeoutMs !== void 0) {
972
1024
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
973
1025
  }
@@ -978,12 +1030,136 @@ function validateParams(params) {
978
1030
  }
979
1031
  }
980
1032
  }
981
- async function* streamAgent(cfg, params) {
1033
+ var MAX_FIELDS = 50;
1034
+ var MAX_VALUE_CHARS = 2e3;
1035
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1036
+ var MAX_SESSION_FROM_FIELDS = 5;
1037
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1038
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1039
+ var MAX_CONSUMER_KEY_CHARS = 200;
1040
+ function snapshotStructuredInput(params) {
1041
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1042
+ return {
1043
+ ...params,
1044
+ context: snap(params.context),
1045
+ bindings: snap(params.bindings),
1046
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1047
+ };
1048
+ }
1049
+ function isPlainObject(v) {
1050
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1051
+ const proto = Object.getPrototypeOf(v);
1052
+ return proto === Object.prototype || proto === null;
1053
+ }
1054
+ function isScalar(v) {
1055
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1056
+ }
1057
+ function validateStructuredInput(params) {
1058
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1059
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1060
+ }
1061
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1062
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1063
+ }
1064
+ for (const name of ["context", "bindings"]) {
1065
+ const v = params[name];
1066
+ if (v === void 0) continue;
1067
+ if (!isPlainObject(v)) {
1068
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1069
+ }
1070
+ const entries = Object.entries(v);
1071
+ if (entries.length > MAX_FIELDS) {
1072
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1073
+ }
1074
+ for (const [k, val] of entries) {
1075
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1076
+ throw new ZihinAgentError(
1077
+ "config",
1078
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1079
+ );
1080
+ }
1081
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1082
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1083
+ }
1084
+ if (!isScalar(val)) {
1085
+ throw new ZihinAgentError(
1086
+ "config",
1087
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1088
+ );
1089
+ }
1090
+ }
1091
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1092
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1093
+ }
1094
+ }
1095
+ if (params.sessionFrom !== void 0) {
1096
+ if (!Array.isArray(params.sessionFrom) || params.sessionFrom.length > MAX_SESSION_FROM_FIELDS || params.sessionFrom.some((f) => typeof f !== "string" || !FIELD_NAME_RE.test(f) || PROTOTYPE_KEYS.has(f))) {
1097
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1098
+ }
1099
+ if (params.sessionFrom.length && params.sessionId) {
1100
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1101
+ }
1102
+ const available = { ...params.context, ...params.bindings };
1103
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1104
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1105
+ throw new ZihinAgentError(
1106
+ "config",
1107
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1108
+ );
1109
+ }
1110
+ }
1111
+ }
1112
+ function requestedInputFields(params) {
1113
+ const out = [];
1114
+ if (params.context) out.push("context");
1115
+ if (params.bindings) out.push("bindings");
1116
+ if (params.sessionFrom?.length) out.push("session_from");
1117
+ return out;
1118
+ }
1119
+ function checkInputCapability(params, chunk) {
1120
+ const requested = requestedInputFields(params);
1121
+ const ignored = chunk?.ignoredFields ?? [];
1122
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1123
+ const blocking = ignored.filter(
1124
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1125
+ );
1126
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1127
+ if (blocking.length) {
1128
+ const shown = blocking.slice(0, 5);
1129
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1130
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1131
+ const hint = blocking.some((f) => f.reason === "reserved_key") ? " Reserved keys are names the agent engine uses internally; rename them." : blocking.some((f) => f.reason === "requires_admin_api_key") ? " These fields only apply with an admin/owner API key." : "";
1132
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1133
+ requestId: chunk?.requestId
1134
+ });
1135
+ }
1136
+ if (requested.length && chunk?.acceptedFields === void 0) {
1137
+ throw new ZihinAgentError(
1138
+ "config",
1139
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1140
+ { requestId: chunk?.requestId }
1141
+ );
1142
+ }
1143
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1144
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1145
+ if (notAccepted.length) {
1146
+ throw new ZihinAgentError(
1147
+ "config",
1148
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1149
+ { requestId: chunk?.requestId }
1150
+ );
1151
+ }
1152
+ }
1153
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1154
+ async function* streamAgent(cfg, rawParams) {
1155
+ const params = snapshotStructuredInput(rawParams);
982
1156
  validateParams(params);
983
1157
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
984
1158
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
985
1159
  const reportedMs = callerTimeoutMs ?? timeoutMs;
986
1160
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1161
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1162
+ let gated = false;
987
1163
  const { response, deadline, endpoint } = await openStream({
988
1164
  url,
989
1165
  headers: buildHeaders(cfg.apiKey),
@@ -1011,7 +1187,14 @@ async function* streamAgent(cfg, params) {
1011
1187
  maxEventBytes: cfg.maxStreamEventBytes,
1012
1188
  maxTotalBytes: cfg.maxStreamTotalBytes
1013
1189
  })) {
1014
- yield mapEvent(e);
1190
+ const chunk = mapEvent(e);
1191
+ if (needsCapability) {
1192
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1193
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1194
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1195
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1196
+ }
1197
+ yield chunk;
1015
1198
  }
1016
1199
  } finally {
1017
1200
  deadline.dispose();
@@ -1019,6 +1202,7 @@ async function* streamAgent(cfg, params) {
1019
1202
  }
1020
1203
  function classifyStreamErrorKind(code, timedOut) {
1021
1204
  if (timedOut === true) return "timeout";
1205
+ if (code === "VALIDATION_ERROR") return "config";
1022
1206
  if (timedOut === false) return "server";
1023
1207
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1024
1208
  }
@@ -1031,6 +1215,77 @@ function errorFromStreamChunk(chunk) {
1031
1215
  retryable: false
1032
1216
  });
1033
1217
  }
1218
+ var FAILED_OUTCOMES = {
1219
+ ERROR: { kind: "server", message: "The agent turn failed on the server; its text is the engine failure notice, not an answer." },
1220
+ // `server`, não `aborted`: `aborted` é o cancelamento do PRÓPRIO chamador, que `onError` manda
1221
+ // filtrar — um cancelamento feito pelo operador sumiria junto.
1222
+ CANCELLED: { kind: "server", message: "The agent turn was cancelled on the server (by an operator or a platform guard)." },
1223
+ // Modo processo drenando/encerrando; o motor retoma depois. Nenhum canal atual o produz.
1224
+ SUSPENDED: { kind: "blocked", message: "The agent turn was suspended by the server and did not answer; retry later." },
1225
+ BUDGET_EXCEEDED: { kind: "blocked", message: "The agent's spending cap was reached; raise the agent budget to keep invoking it." },
1226
+ QUOTA_EXCEEDED: { kind: "quota_exceeded", message: "Your Zihin token quota is exhausted. Upgrade the plan or wait for the next cycle." },
1227
+ SESSION_BUSY: {
1228
+ kind: "blocked",
1229
+ message: "Another turn is still running in this session; retry after it finishes.",
1230
+ retryable: true
1231
+ },
1232
+ // Hoje a rota manda CONFLICT como `event: error` (não chega ao `done`); mapeado por defesa.
1233
+ CONFLICT: {
1234
+ kind: "blocked",
1235
+ message: "The declared consumer identity does not match the one already bound to this session."
1236
+ }
1237
+ };
1238
+ var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1239
+ var ERROR_BLOCKS = {
1240
+ ownership_denied: {
1241
+ kind: "not_found",
1242
+ message: "Session not found for this API key \u2014 start a new conversation (omit sessionId) instead of reusing this one."
1243
+ },
1244
+ session_load_failed: { kind: "blocked", message: "The server could not load this conversation right now; retry shortly.", retryable: true },
1245
+ approval_resolve_failed: { kind: "blocked", message: "The server could not route the required approval right now; retry shortly.", retryable: true },
1246
+ approval_no_approver: { kind: "blocked", message: "This action requires approval, but the approval policy has no approver configured." },
1247
+ blocked_schedule: { kind: "blocked", message: "The agent is outside its allowed schedule (security policy)." },
1248
+ owner_inactive: { kind: "blocked", message: "The agent's owner is inactive; the agent cannot be invoked." }
1249
+ };
1250
+ function own(table, key) {
1251
+ return key !== void 0 && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
1252
+ }
1253
+ function errorFromDoneChunk(chunk, ctx = {}) {
1254
+ const meta = { executionId: ctx.executionId, sessionId: ctx.sessionId };
1255
+ const blockedReason = safeLabel(ctx.blocked);
1256
+ if (chunk.outcome === "TIMEOUT" || chunk.timedOut === true) {
1257
+ const clock = chunk.timeoutClock;
1258
+ const what = clock === "turn" ? "the turn deadline" : clock === "first_signal" ? "the model provider did not start responding in time" : clock === "idle" ? "the model provider stream went silent" : clock === "call" ? "the model call took too long" : "a server-side clock";
1259
+ return new ZihinAgentError("timeout", `The agent turn timed out (${what}).`, {
1260
+ ...meta,
1261
+ code: "TURN_TIMEOUT",
1262
+ retryable: false,
1263
+ timeoutClock: clock
1264
+ });
1265
+ }
1266
+ if (chunk.outcome === "PAUSED" && blockedReason && PAUSED_BLOCKS.has(blockedReason)) {
1267
+ return new ZihinAgentError("blocked", "The agent is paused or archived and did not answer.", {
1268
+ ...meta,
1269
+ code: "PAUSED",
1270
+ retryable: false
1271
+ });
1272
+ }
1273
+ if (chunk.outcome === "ERROR" && blockedReason) {
1274
+ const gate = own(ERROR_BLOCKS, blockedReason);
1275
+ return new ZihinAgentError(gate?.kind ?? "blocked", gate?.message ?? `The agent turn was blocked by the server (${blockedReason}).`, {
1276
+ ...meta,
1277
+ code: blockedReason.toUpperCase(),
1278
+ retryable: gate?.retryable ?? false
1279
+ });
1280
+ }
1281
+ const failed = own(FAILED_OUTCOMES, chunk.outcome);
1282
+ if (!failed) return void 0;
1283
+ return new ZihinAgentError(failed.kind, failed.message, {
1284
+ ...meta,
1285
+ code: chunk.outcome,
1286
+ retryable: failed.retryable ?? false
1287
+ });
1288
+ }
1034
1289
  async function invokeAgent(cfg, params) {
1035
1290
  const raw = [];
1036
1291
  const keepRaw = params.includeRaw === true;
@@ -1038,7 +1293,10 @@ async function invokeAgent(cfg, params) {
1038
1293
  let metricsChunk;
1039
1294
  let metadataSessionId;
1040
1295
  let mintedSessionId;
1041
- let cleanEnd = false;
1296
+ let doneChunk;
1297
+ let blocked;
1298
+ let acceptedFields;
1299
+ let ignoredFields;
1042
1300
  let tokenBuffer = "";
1043
1301
  for await (const chunk of streamAgent(cfg, params)) {
1044
1302
  if (keepRaw) raw.push(chunk);
@@ -1048,10 +1306,19 @@ async function invokeAgent(cfg, params) {
1048
1306
  if (chunk.type === "response") responseChunk = chunk;
1049
1307
  else if (chunk.type === "metrics") metricsChunk = chunk;
1050
1308
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1051
- else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1052
- else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1053
- else if (chunk.type === "done") cleanEnd = true;
1054
- }
1309
+ else if (chunk.type === "metadata") {
1310
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1311
+ acceptedFields = chunk.acceptedFields;
1312
+ ignoredFields = chunk.ignoredFields;
1313
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1314
+ else if (chunk.type === "done") doneChunk = chunk;
1315
+ else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1316
+ }
1317
+ const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1318
+ const executionId = metricsChunk?.executionId;
1319
+ const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1320
+ if (outcomeError) throw outcomeError;
1321
+ const cleanEnd = doneChunk !== void 0;
1055
1322
  if (!responseChunk) {
1056
1323
  if (cleanEnd) {
1057
1324
  throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
@@ -1060,11 +1327,14 @@ async function invokeAgent(cfg, params) {
1060
1327
  return {
1061
1328
  content: tokenBuffer,
1062
1329
  contentSource: "tokens",
1063
- sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
1330
+ sessionId,
1064
1331
  usage: metricsChunk?.usage,
1065
1332
  model: metricsChunk?.model,
1066
1333
  iterations: metricsChunk?.iterations,
1067
1334
  executionTimeMs: metricsChunk?.executionTimeMs,
1335
+ executionId,
1336
+ acceptedFields,
1337
+ ignoredFields,
1068
1338
  raw
1069
1339
  };
1070
1340
  }
@@ -1073,13 +1343,20 @@ async function invokeAgent(cfg, params) {
1073
1343
  return {
1074
1344
  content: responseChunk.content,
1075
1345
  contentSource: "response",
1076
- sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1346
+ sessionId,
1077
1347
  model: metricsChunk?.model,
1078
1348
  usage: metricsChunk?.usage,
1079
1349
  sources: responseChunk.sources,
1080
1350
  blocks: responseChunk.blocks,
1351
+ gated: responseChunk.gated,
1081
1352
  iterations: metricsChunk?.iterations,
1082
1353
  executionTimeMs: metricsChunk?.executionTimeMs,
1354
+ executionId,
1355
+ outcome: doneChunk?.outcome,
1356
+ truncated: doneChunk?.truncated,
1357
+ finishReason: doneChunk?.finishReason,
1358
+ acceptedFields,
1359
+ ignoredFields,
1083
1360
  raw
1084
1361
  };
1085
1362
  }
@@ -1685,6 +1962,7 @@ export {
1685
1962
  ZihinAgentError,
1686
1963
  classifyStreamErrorKind,
1687
1964
  createClient,
1965
+ errorFromDoneChunk,
1688
1966
  errorFromStreamChunk,
1689
1967
  isCommercialBlock,
1690
1968
  sanitizeApiError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihin/agent-client",
3
- "version": "0.4.1",
3
+ "version": "0.6.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",