@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/README.md +100 -5
- package/dist/index.d.mts +167 -10
- package/dist/index.d.ts +167 -10
- package/dist/index.js +258 -20
- package/dist/index.mjs +257 -20
- package/package.json +1 -1
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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",
|