@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.js CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  ZihinAgentError: () => ZihinAgentError,
29
29
  classifyStreamErrorKind: () => classifyStreamErrorKind,
30
30
  createClient: () => createClient,
31
+ errorFromDoneChunk: () => errorFromDoneChunk,
31
32
  errorFromStreamChunk: () => errorFromStreamChunk,
32
33
  isCommercialBlock: () => isCommercialBlock,
33
34
  sanitizeApiError: () => sanitizeApiError,
@@ -46,6 +47,12 @@ function utf8Len(s) {
46
47
  function rec(x) {
47
48
  return x && typeof x === "object" ? x : {};
48
49
  }
50
+ function safeId(x) {
51
+ return typeof x === "string" && /^[\w.:-]{1,128}$/.test(x) ? x : void 0;
52
+ }
53
+ function safeLabel(x) {
54
+ return typeof x === "string" && /^[A-Za-z0-9_]{1,64}$/.test(x) ? x : void 0;
55
+ }
49
56
  function str(x) {
50
57
  return typeof x === "string" ? x : void 0;
51
58
  }
@@ -71,8 +78,13 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
71
78
  "waiting",
72
79
  "retrying",
73
80
  "scheduled",
74
- "awaiting_approval"
81
+ "awaiting_approval",
75
82
  // HITL: turno pausado esperando decisão humana não terminou
83
+ // Estados VIVOS reais do backend (zihin-agent-builder `TaskService.LIVE_STATUSES` e
84
+ // `agent_executions.status`): sem eles, uma task/execução pausada ou aguardando input
85
+ // era lida como TERMINAL e o polling parava com o trabalho ainda em andamento.
86
+ "paused",
87
+ "awaiting_input"
76
88
  ]);
77
89
  function isTerminalStatus(status) {
78
90
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
@@ -267,6 +279,9 @@ var ERROR_CODE_MAP = {
267
279
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
268
280
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
269
281
  AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
282
+ // Nome novo do `AGENT_NOT_FOUND` (zihin-agent-builder#561, fatia B). `AGENT_ERROR` virou
283
+ // `INTERNAL_ERROR`, que já está acima.
284
+ NOT_FOUND: "Resource not found \u2014 check the ID (agent, trigger or execution) and that it belongs to this workspace.",
270
285
  // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
271
286
  // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
272
287
  TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
@@ -337,6 +352,8 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
337
352
  this.code = opts.code;
338
353
  this.requestId = opts.requestId;
339
354
  this.executionId = opts.executionId;
355
+ this.sessionId = opts.sessionId;
356
+ this.timeoutClock = opts.timeoutClock;
340
357
  this.attempts = opts.attempts;
341
358
  this.endpoint = opts.endpoint;
342
359
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -371,7 +388,7 @@ function commercialKind(body) {
371
388
  }
372
389
  function extractRequestId(body) {
373
390
  const b = rec(body);
374
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
391
+ return safeId(b.request_id) ?? safeId(b.requestId) ?? safeId(rec(b.error).request_id) ?? safeId(rec(b.meta).request_id);
375
392
  }
376
393
  function errorFromResponse(status, body, meta = {}) {
377
394
  const sanitized = sanitizeApiError(body);
@@ -381,7 +398,7 @@ function errorFromResponse(status, body, meta = {}) {
381
398
  status,
382
399
  code,
383
400
  requestId: extractRequestId(body),
384
- executionId: str(rec(body).execution_id),
401
+ executionId: safeId(rec(body).execution_id),
385
402
  ...meta
386
403
  };
387
404
  const commercial = commercialKind(body);
@@ -392,6 +409,12 @@ function errorFromResponse(status, body, meta = {}) {
392
409
  });
393
410
  }
394
411
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
412
+ if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
413
+ return new ZihinAgentError(
414
+ "not_found",
415
+ "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.",
416
+ { ...base, retryable: false }
417
+ );
395
418
  if (status === 403)
396
419
  return new ZihinAgentError(
397
420
  "auth",
@@ -441,7 +464,7 @@ var OPERATION_TIMEOUT_MS = Object.freeze({
441
464
  dispatch: 26e4,
442
465
  short: 35e3
443
466
  });
444
- var DEADLINE_GRACE_MS = 1e4;
467
+ var DEADLINE_GRACE_MS = 3e4;
445
468
  var RETRY_OVERHEAD_MS = 6e4;
446
469
  var MIN_TIMEOUT_MS = 1e3;
447
470
  var MAX_TIMEOUT_MS = 9e5;
@@ -887,19 +910,34 @@ async function* parseSse(response, opts = {}) {
887
910
  }
888
911
 
889
912
  // src/invoke.ts
913
+ function parseAcceptedFields(v) {
914
+ if (!Array.isArray(v)) return void 0;
915
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
916
+ }
917
+ function parseIgnoredFields(v) {
918
+ if (!Array.isArray(v)) return void 0;
919
+ const out = [];
920
+ for (const x of v) {
921
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
922
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
923
+ }
924
+ return out;
925
+ }
890
926
  function mapEvent(e) {
891
927
  const d = rec(e.data);
892
928
  switch (e.event) {
893
929
  case "metadata":
894
930
  return {
895
931
  type: "metadata",
896
- requestId: str(d.request_id),
932
+ requestId: safeId(d.request_id),
897
933
  agentId: str(d.agent_id),
898
- sessionId: str(d.session_id),
934
+ sessionId: safeId(d.session_id),
935
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
936
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
899
937
  data: e.data
900
938
  };
901
939
  case "status":
902
- return { type: "status", phase: str(d.phase), data: e.data };
940
+ return { type: "status", phase: str(d.phase), blocked: safeLabel(d.blocked), data: e.data };
903
941
  case "attachments":
904
942
  return { type: "attachments", data: e.data };
905
943
  case "phase":
@@ -921,16 +959,24 @@ function mapEvent(e) {
921
959
  case "warning":
922
960
  return { type: "warning", message: str(d.message), data: e.data };
923
961
  case "session":
924
- return { type: "session", sessionId: str(d.session_id), data: e.data };
962
+ return { type: "session", sessionId: safeId(d.session_id), data: e.data };
925
963
  case "done":
926
- return { type: "done", data: e.data };
964
+ return {
965
+ type: "done",
966
+ outcome: safeLabel(d.outcome),
967
+ timedOut: typeof d.timed_out === "boolean" ? d.timed_out : void 0,
968
+ timeoutClock: safeLabel(d.timeout_clock),
969
+ truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
970
+ finishReason: str(d.finish_reason),
971
+ data: e.data
972
+ };
927
973
  case "error": {
928
974
  const err = rec(d.error);
929
975
  return {
930
976
  type: "error",
931
- code: str(err.code),
977
+ code: safeLabel(err.code),
932
978
  message: str(err.message),
933
- requestId: str(err.request_id),
979
+ requestId: safeId(err.request_id),
934
980
  timedOut: typeof err.timed_out === "boolean" ? err.timed_out : void 0,
935
981
  data: e.data
936
982
  };
@@ -939,7 +985,8 @@ function mapEvent(e) {
939
985
  return {
940
986
  type: "response",
941
987
  content: str(d.content) ?? "",
942
- sessionId: str(d.session_id),
988
+ sessionId: safeId(d.session_id),
989
+ gated: d.gated === true ? true : void 0,
943
990
  sources: Array.isArray(d.sources) ? d.sources : void 0,
944
991
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
945
992
  data: e.data
@@ -952,6 +999,7 @@ function mapEvent(e) {
952
999
  model: str(modelConfig.model) ?? str(d.model),
953
1000
  iterations: num(d.iterations),
954
1001
  executionTimeMs: num(d.execution_time_ms),
1002
+ executionId: safeId(d.execution_id),
955
1003
  data: e.data
956
1004
  };
957
1005
  }
@@ -963,6 +1011,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
963
1011
  const body = {};
964
1012
  if (params.message !== void 0) body.message = params.message;
965
1013
  if (params.sessionId) body.session_id = params.sessionId;
1014
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
1015
+ if (params.context) body.context = { ...params.context };
1016
+ if (params.bindings) body.bindings = { ...params.bindings };
1017
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
966
1018
  if (params.attachments?.length) {
967
1019
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
968
1020
  }
@@ -1001,6 +1053,7 @@ function validateParams(params) {
1001
1053
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
1002
1054
  }
1003
1055
  }
1056
+ validateStructuredInput(params);
1004
1057
  if (params.options?.timeoutMs !== void 0) {
1005
1058
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1006
1059
  }
@@ -1011,12 +1064,136 @@ function validateParams(params) {
1011
1064
  }
1012
1065
  }
1013
1066
  }
1014
- async function* streamAgent(cfg, params) {
1067
+ var MAX_FIELDS = 50;
1068
+ var MAX_VALUE_CHARS = 2e3;
1069
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1070
+ var MAX_SESSION_FROM_FIELDS = 5;
1071
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1072
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1073
+ var MAX_CONSUMER_KEY_CHARS = 200;
1074
+ function snapshotStructuredInput(params) {
1075
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1076
+ return {
1077
+ ...params,
1078
+ context: snap(params.context),
1079
+ bindings: snap(params.bindings),
1080
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1081
+ };
1082
+ }
1083
+ function isPlainObject(v) {
1084
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1085
+ const proto = Object.getPrototypeOf(v);
1086
+ return proto === Object.prototype || proto === null;
1087
+ }
1088
+ function isScalar(v) {
1089
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1090
+ }
1091
+ function validateStructuredInput(params) {
1092
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1093
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1094
+ }
1095
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1096
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1097
+ }
1098
+ for (const name of ["context", "bindings"]) {
1099
+ const v = params[name];
1100
+ if (v === void 0) continue;
1101
+ if (!isPlainObject(v)) {
1102
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1103
+ }
1104
+ const entries = Object.entries(v);
1105
+ if (entries.length > MAX_FIELDS) {
1106
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1107
+ }
1108
+ for (const [k, val] of entries) {
1109
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1110
+ throw new ZihinAgentError(
1111
+ "config",
1112
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1113
+ );
1114
+ }
1115
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1116
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1117
+ }
1118
+ if (!isScalar(val)) {
1119
+ throw new ZihinAgentError(
1120
+ "config",
1121
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1122
+ );
1123
+ }
1124
+ }
1125
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1126
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1127
+ }
1128
+ }
1129
+ if (params.sessionFrom !== void 0) {
1130
+ 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))) {
1131
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1132
+ }
1133
+ if (params.sessionFrom.length && params.sessionId) {
1134
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1135
+ }
1136
+ const available = { ...params.context, ...params.bindings };
1137
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1138
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1139
+ throw new ZihinAgentError(
1140
+ "config",
1141
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1142
+ );
1143
+ }
1144
+ }
1145
+ }
1146
+ function requestedInputFields(params) {
1147
+ const out = [];
1148
+ if (params.context) out.push("context");
1149
+ if (params.bindings) out.push("bindings");
1150
+ if (params.sessionFrom?.length) out.push("session_from");
1151
+ return out;
1152
+ }
1153
+ function checkInputCapability(params, chunk) {
1154
+ const requested = requestedInputFields(params);
1155
+ const ignored = chunk?.ignoredFields ?? [];
1156
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1157
+ const blocking = ignored.filter(
1158
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1159
+ );
1160
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1161
+ if (blocking.length) {
1162
+ const shown = blocking.slice(0, 5);
1163
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1164
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1165
+ 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." : "";
1166
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1167
+ requestId: chunk?.requestId
1168
+ });
1169
+ }
1170
+ if (requested.length && chunk?.acceptedFields === void 0) {
1171
+ throw new ZihinAgentError(
1172
+ "config",
1173
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1174
+ { requestId: chunk?.requestId }
1175
+ );
1176
+ }
1177
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1178
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1179
+ if (notAccepted.length) {
1180
+ throw new ZihinAgentError(
1181
+ "config",
1182
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1183
+ { requestId: chunk?.requestId }
1184
+ );
1185
+ }
1186
+ }
1187
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1188
+ async function* streamAgent(cfg, rawParams) {
1189
+ const params = snapshotStructuredInput(rawParams);
1015
1190
  validateParams(params);
1016
1191
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1017
1192
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1018
1193
  const reportedMs = callerTimeoutMs ?? timeoutMs;
1019
1194
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1195
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1196
+ let gated = false;
1020
1197
  const { response, deadline, endpoint } = await openStream({
1021
1198
  url,
1022
1199
  headers: buildHeaders(cfg.apiKey),
@@ -1044,7 +1221,14 @@ async function* streamAgent(cfg, params) {
1044
1221
  maxEventBytes: cfg.maxStreamEventBytes,
1045
1222
  maxTotalBytes: cfg.maxStreamTotalBytes
1046
1223
  })) {
1047
- yield mapEvent(e);
1224
+ const chunk = mapEvent(e);
1225
+ if (needsCapability) {
1226
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1227
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1228
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1229
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1230
+ }
1231
+ yield chunk;
1048
1232
  }
1049
1233
  } finally {
1050
1234
  deadline.dispose();
@@ -1052,6 +1236,7 @@ async function* streamAgent(cfg, params) {
1052
1236
  }
1053
1237
  function classifyStreamErrorKind(code, timedOut) {
1054
1238
  if (timedOut === true) return "timeout";
1239
+ if (code === "VALIDATION_ERROR") return "config";
1055
1240
  if (timedOut === false) return "server";
1056
1241
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1057
1242
  }
@@ -1064,6 +1249,77 @@ function errorFromStreamChunk(chunk) {
1064
1249
  retryable: false
1065
1250
  });
1066
1251
  }
1252
+ var FAILED_OUTCOMES = {
1253
+ ERROR: { kind: "server", message: "The agent turn failed on the server; its text is the engine failure notice, not an answer." },
1254
+ // `server`, não `aborted`: `aborted` é o cancelamento do PRÓPRIO chamador, que `onError` manda
1255
+ // filtrar — um cancelamento feito pelo operador sumiria junto.
1256
+ CANCELLED: { kind: "server", message: "The agent turn was cancelled on the server (by an operator or a platform guard)." },
1257
+ // Modo processo drenando/encerrando; o motor retoma depois. Nenhum canal atual o produz.
1258
+ SUSPENDED: { kind: "blocked", message: "The agent turn was suspended by the server and did not answer; retry later." },
1259
+ BUDGET_EXCEEDED: { kind: "blocked", message: "The agent's spending cap was reached; raise the agent budget to keep invoking it." },
1260
+ QUOTA_EXCEEDED: { kind: "quota_exceeded", message: "Your Zihin token quota is exhausted. Upgrade the plan or wait for the next cycle." },
1261
+ SESSION_BUSY: {
1262
+ kind: "blocked",
1263
+ message: "Another turn is still running in this session; retry after it finishes.",
1264
+ retryable: true
1265
+ },
1266
+ // Hoje a rota manda CONFLICT como `event: error` (não chega ao `done`); mapeado por defesa.
1267
+ CONFLICT: {
1268
+ kind: "blocked",
1269
+ message: "The declared consumer identity does not match the one already bound to this session."
1270
+ }
1271
+ };
1272
+ var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1273
+ var ERROR_BLOCKS = {
1274
+ ownership_denied: {
1275
+ kind: "not_found",
1276
+ message: "Session not found for this API key \u2014 start a new conversation (omit sessionId) instead of reusing this one."
1277
+ },
1278
+ session_load_failed: { kind: "blocked", message: "The server could not load this conversation right now; retry shortly.", retryable: true },
1279
+ approval_resolve_failed: { kind: "blocked", message: "The server could not route the required approval right now; retry shortly.", retryable: true },
1280
+ approval_no_approver: { kind: "blocked", message: "This action requires approval, but the approval policy has no approver configured." },
1281
+ blocked_schedule: { kind: "blocked", message: "The agent is outside its allowed schedule (security policy)." },
1282
+ owner_inactive: { kind: "blocked", message: "The agent's owner is inactive; the agent cannot be invoked." }
1283
+ };
1284
+ function own(table, key) {
1285
+ return key !== void 0 && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
1286
+ }
1287
+ function errorFromDoneChunk(chunk, ctx = {}) {
1288
+ const meta = { executionId: ctx.executionId, sessionId: ctx.sessionId };
1289
+ const blockedReason = safeLabel(ctx.blocked);
1290
+ if (chunk.outcome === "TIMEOUT" || chunk.timedOut === true) {
1291
+ const clock = chunk.timeoutClock;
1292
+ 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";
1293
+ return new ZihinAgentError("timeout", `The agent turn timed out (${what}).`, {
1294
+ ...meta,
1295
+ code: "TURN_TIMEOUT",
1296
+ retryable: false,
1297
+ timeoutClock: clock
1298
+ });
1299
+ }
1300
+ if (chunk.outcome === "PAUSED" && blockedReason && PAUSED_BLOCKS.has(blockedReason)) {
1301
+ return new ZihinAgentError("blocked", "The agent is paused or archived and did not answer.", {
1302
+ ...meta,
1303
+ code: "PAUSED",
1304
+ retryable: false
1305
+ });
1306
+ }
1307
+ if (chunk.outcome === "ERROR" && blockedReason) {
1308
+ const gate = own(ERROR_BLOCKS, blockedReason);
1309
+ return new ZihinAgentError(gate?.kind ?? "blocked", gate?.message ?? `The agent turn was blocked by the server (${blockedReason}).`, {
1310
+ ...meta,
1311
+ code: blockedReason.toUpperCase(),
1312
+ retryable: gate?.retryable ?? false
1313
+ });
1314
+ }
1315
+ const failed = own(FAILED_OUTCOMES, chunk.outcome);
1316
+ if (!failed) return void 0;
1317
+ return new ZihinAgentError(failed.kind, failed.message, {
1318
+ ...meta,
1319
+ code: chunk.outcome,
1320
+ retryable: failed.retryable ?? false
1321
+ });
1322
+ }
1067
1323
  async function invokeAgent(cfg, params) {
1068
1324
  const raw = [];
1069
1325
  const keepRaw = params.includeRaw === true;
@@ -1071,7 +1327,10 @@ async function invokeAgent(cfg, params) {
1071
1327
  let metricsChunk;
1072
1328
  let metadataSessionId;
1073
1329
  let mintedSessionId;
1074
- let cleanEnd = false;
1330
+ let doneChunk;
1331
+ let blocked;
1332
+ let acceptedFields;
1333
+ let ignoredFields;
1075
1334
  let tokenBuffer = "";
1076
1335
  for await (const chunk of streamAgent(cfg, params)) {
1077
1336
  if (keepRaw) raw.push(chunk);
@@ -1081,10 +1340,19 @@ async function invokeAgent(cfg, params) {
1081
1340
  if (chunk.type === "response") responseChunk = chunk;
1082
1341
  else if (chunk.type === "metrics") metricsChunk = chunk;
1083
1342
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1084
- else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1085
- else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1086
- else if (chunk.type === "done") cleanEnd = true;
1087
- }
1343
+ else if (chunk.type === "metadata") {
1344
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1345
+ acceptedFields = chunk.acceptedFields;
1346
+ ignoredFields = chunk.ignoredFields;
1347
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1348
+ else if (chunk.type === "done") doneChunk = chunk;
1349
+ else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1350
+ }
1351
+ const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1352
+ const executionId = metricsChunk?.executionId;
1353
+ const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1354
+ if (outcomeError) throw outcomeError;
1355
+ const cleanEnd = doneChunk !== void 0;
1088
1356
  if (!responseChunk) {
1089
1357
  if (cleanEnd) {
1090
1358
  throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
@@ -1093,11 +1361,14 @@ async function invokeAgent(cfg, params) {
1093
1361
  return {
1094
1362
  content: tokenBuffer,
1095
1363
  contentSource: "tokens",
1096
- sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
1364
+ sessionId,
1097
1365
  usage: metricsChunk?.usage,
1098
1366
  model: metricsChunk?.model,
1099
1367
  iterations: metricsChunk?.iterations,
1100
1368
  executionTimeMs: metricsChunk?.executionTimeMs,
1369
+ executionId,
1370
+ acceptedFields,
1371
+ ignoredFields,
1101
1372
  raw
1102
1373
  };
1103
1374
  }
@@ -1106,13 +1377,20 @@ async function invokeAgent(cfg, params) {
1106
1377
  return {
1107
1378
  content: responseChunk.content,
1108
1379
  contentSource: "response",
1109
- sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1380
+ sessionId,
1110
1381
  model: metricsChunk?.model,
1111
1382
  usage: metricsChunk?.usage,
1112
1383
  sources: responseChunk.sources,
1113
1384
  blocks: responseChunk.blocks,
1385
+ gated: responseChunk.gated,
1114
1386
  iterations: metricsChunk?.iterations,
1115
1387
  executionTimeMs: metricsChunk?.executionTimeMs,
1388
+ executionId,
1389
+ outcome: doneChunk?.outcome,
1390
+ truncated: doneChunk?.truncated,
1391
+ finishReason: doneChunk?.finishReason,
1392
+ acceptedFields,
1393
+ ignoredFields,
1116
1394
  raw
1117
1395
  };
1118
1396
  }
@@ -1719,6 +1997,7 @@ function signRequest(payload, secretKey, timestamp) {
1719
1997
  ZihinAgentError,
1720
1998
  classifyStreamErrorKind,
1721
1999
  createClient,
2000
+ errorFromDoneChunk,
1722
2001
  errorFromStreamChunk,
1723
2002
  isCommercialBlock,
1724
2003
  sanitizeApiError,