@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.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
classifyStreamErrorKind: () => classifyStreamErrorKind,
|
|
30
30
|
createClient: () => createClient,
|
|
31
31
|
errorFromDoneChunk: () => errorFromDoneChunk,
|
|
32
|
+
errorFromOutcome: () => errorFromOutcome,
|
|
32
33
|
errorFromStreamChunk: () => errorFromStreamChunk,
|
|
33
34
|
isCommercialBlock: () => isCommercialBlock,
|
|
34
35
|
sanitizeApiError: () => sanitizeApiError,
|
|
@@ -89,6 +90,20 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
|
|
|
89
90
|
function isTerminalStatus(status) {
|
|
90
91
|
return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
|
|
91
92
|
}
|
|
93
|
+
var _warnedDeprecations = /* @__PURE__ */ new Set();
|
|
94
|
+
function warnDeprecatedOnce(code, message) {
|
|
95
|
+
if (_warnedDeprecations.has(code)) return;
|
|
96
|
+
_warnedDeprecations.add(code);
|
|
97
|
+
try {
|
|
98
|
+
const proc = globalThis.process;
|
|
99
|
+
if (typeof proc?.emitWarning === "function") {
|
|
100
|
+
proc.emitWarning(message, { type: "ZihinDeprecationWarning", code });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.warn(`[${code}] ${message}`);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
|
|
93
108
|
// src/security.ts
|
|
94
109
|
var PRIVATE_IP_PATTERNS = [
|
|
@@ -408,6 +423,8 @@ function errorFromResponse(status, body, meta = {}) {
|
|
|
408
423
|
retryable: false
|
|
409
424
|
});
|
|
410
425
|
}
|
|
426
|
+
if (code === "PLAN_LIMIT_REACHED")
|
|
427
|
+
return new ZihinAgentError("blocked", message || "This feature is not available on your Zihin plan.", { ...base, retryable: false });
|
|
411
428
|
if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
|
|
412
429
|
if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
|
|
413
430
|
return new ZihinAgentError(
|
|
@@ -547,15 +564,15 @@ function buildHeaders(apiKey, extra = {}) {
|
|
|
547
564
|
...extra
|
|
548
565
|
};
|
|
549
566
|
}
|
|
550
|
-
function delay(ms, signal) {
|
|
567
|
+
function delay(ms, signal, meta) {
|
|
551
568
|
return new Promise((resolve, reject) => {
|
|
552
569
|
if (signal?.aborted) {
|
|
553
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
570
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
554
571
|
return;
|
|
555
572
|
}
|
|
556
573
|
const onAbort = () => {
|
|
557
574
|
clearTimeout(timer);
|
|
558
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
575
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
559
576
|
};
|
|
560
577
|
const timer = setTimeout(() => {
|
|
561
578
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -645,7 +662,7 @@ async function requestBuffered(args) {
|
|
|
645
662
|
}
|
|
646
663
|
d.dispose();
|
|
647
664
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
|
|
648
|
-
await delay(wait, args.callerSignal);
|
|
665
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
649
666
|
continue;
|
|
650
667
|
}
|
|
651
668
|
const text = peeked ?? await res.text();
|
|
@@ -664,7 +681,7 @@ async function requestBuffered(args) {
|
|
|
664
681
|
const wait = backoffMs(attempt);
|
|
665
682
|
if (budget.overheadLeft() <= wait) throw lastError;
|
|
666
683
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
|
|
667
|
-
await delay(wait, args.callerSignal);
|
|
684
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
668
685
|
}
|
|
669
686
|
}
|
|
670
687
|
throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -673,6 +690,7 @@ async function openStream(args) {
|
|
|
673
690
|
const endpoint = endpointLabel("POST", args.url);
|
|
674
691
|
const doFetch = resolveFetch(args.fetchImpl);
|
|
675
692
|
const budget = budgetTracker({ ...args, endpoint });
|
|
693
|
+
const policy = args.idempotent ? "idempotent" : "side-effecting";
|
|
676
694
|
for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
|
|
677
695
|
const d = createDeadline(args.timeoutMs, args.callerSignal);
|
|
678
696
|
try {
|
|
@@ -684,7 +702,7 @@ async function openStream(args) {
|
|
|
684
702
|
redirect: "error",
|
|
685
703
|
cache: "no-store"
|
|
686
704
|
});
|
|
687
|
-
let wait = shouldRetryStatus(response.status,
|
|
705
|
+
let wait = shouldRetryStatus(response.status, policy) && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
|
|
688
706
|
if (response.status === 429) {
|
|
689
707
|
const text = await response.text().catch(() => "");
|
|
690
708
|
const body = parseBody(text);
|
|
@@ -694,7 +712,7 @@ async function openStream(args) {
|
|
|
694
712
|
}
|
|
695
713
|
d.dispose();
|
|
696
714
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
|
|
697
|
-
await delay(wait, args.callerSignal);
|
|
715
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
698
716
|
continue;
|
|
699
717
|
}
|
|
700
718
|
if (wait !== null && budget.overheadLeft() > wait) {
|
|
@@ -710,7 +728,7 @@ async function openStream(args) {
|
|
|
710
728
|
reason: "status",
|
|
711
729
|
status: response.status
|
|
712
730
|
});
|
|
713
|
-
await delay(wait, args.callerSignal);
|
|
731
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
714
732
|
continue;
|
|
715
733
|
}
|
|
716
734
|
return { response, deadline: d, endpoint };
|
|
@@ -721,7 +739,14 @@ async function openStream(args) {
|
|
|
721
739
|
if (d.timedOut()) throw budget.expired(attempt + 1);
|
|
722
740
|
if (d.abortedByCaller())
|
|
723
741
|
throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
|
|
724
|
-
|
|
742
|
+
const netErr = errorFromThrow(err, budget.reported, meta);
|
|
743
|
+
const wait = backoffMs(attempt);
|
|
744
|
+
if (policy === "idempotent" && netErr.retryable && attempt < args.maxRetries && budget.overheadLeft() > wait) {
|
|
745
|
+
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
|
|
746
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
throw netErr;
|
|
725
750
|
}
|
|
726
751
|
}
|
|
727
752
|
throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -923,6 +948,25 @@ function parseIgnoredFields(v) {
|
|
|
923
948
|
}
|
|
924
949
|
return out;
|
|
925
950
|
}
|
|
951
|
+
var MAX_SUGGESTIONS = 10;
|
|
952
|
+
var MAX_SUGGESTION_CHARS = 200;
|
|
953
|
+
function suggestionsFromBlocks(blocks) {
|
|
954
|
+
if (!Array.isArray(blocks)) return void 0;
|
|
955
|
+
const qr = [...blocks].reverse().find((b) => rec(b).type === "quick_replies");
|
|
956
|
+
const labels = rec(rec(qr).data).labels;
|
|
957
|
+
if (!Array.isArray(labels)) return void 0;
|
|
958
|
+
const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
|
|
959
|
+
return out.length ? out : void 0;
|
|
960
|
+
}
|
|
961
|
+
function turnIds(d) {
|
|
962
|
+
return {
|
|
963
|
+
executionId: safeId(d.execution_id),
|
|
964
|
+
clientMessageId: safeId(d.client_message_id),
|
|
965
|
+
idempotentReplay: d.idempotent_replay === true ? true : void 0,
|
|
966
|
+
userMessageId: safeId(d.user_message_id),
|
|
967
|
+
assistantMessageId: safeId(d.assistant_message_id)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
926
970
|
function mapEvent(e) {
|
|
927
971
|
const d = rec(e.data);
|
|
928
972
|
switch (e.event) {
|
|
@@ -932,6 +976,7 @@ function mapEvent(e) {
|
|
|
932
976
|
requestId: safeId(d.request_id),
|
|
933
977
|
agentId: str(d.agent_id),
|
|
934
978
|
sessionId: safeId(d.session_id),
|
|
979
|
+
...turnIds(d),
|
|
935
980
|
acceptedFields: parseAcceptedFields(d.accepted_fields),
|
|
936
981
|
ignoredFields: parseIgnoredFields(d.ignored_fields),
|
|
937
982
|
data: e.data
|
|
@@ -954,6 +999,8 @@ function mapEvent(e) {
|
|
|
954
999
|
};
|
|
955
1000
|
case "action_proposed":
|
|
956
1001
|
return { type: "action_proposed", data: e.data };
|
|
1002
|
+
case "action_forwarded":
|
|
1003
|
+
return { type: "action_forwarded", actionId: safeId(d.action_id), data: e.data };
|
|
957
1004
|
case "token":
|
|
958
1005
|
return { type: "token", content: str(d.content) ?? "", data: e.data };
|
|
959
1006
|
case "warning":
|
|
@@ -968,6 +1015,7 @@ function mapEvent(e) {
|
|
|
968
1015
|
timeoutClock: safeLabel(d.timeout_clock),
|
|
969
1016
|
truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
|
|
970
1017
|
finishReason: str(d.finish_reason),
|
|
1018
|
+
...turnIds(d),
|
|
971
1019
|
data: e.data
|
|
972
1020
|
};
|
|
973
1021
|
case "error": {
|
|
@@ -989,6 +1037,8 @@ function mapEvent(e) {
|
|
|
989
1037
|
gated: d.gated === true ? true : void 0,
|
|
990
1038
|
sources: Array.isArray(d.sources) ? d.sources : void 0,
|
|
991
1039
|
blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
|
|
1040
|
+
suggestions: suggestionsFromBlocks(d.blocks),
|
|
1041
|
+
...turnIds(d),
|
|
992
1042
|
data: e.data
|
|
993
1043
|
};
|
|
994
1044
|
case "metrics": {
|
|
@@ -1011,7 +1061,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
|
|
|
1011
1061
|
const body = {};
|
|
1012
1062
|
if (params.message !== void 0) body.message = params.message;
|
|
1013
1063
|
if (params.sessionId) body.session_id = params.sessionId;
|
|
1064
|
+
if (params.clientMessageId) body.client_message_id = params.clientMessageId;
|
|
1014
1065
|
if (params.consumerKey) body.consumer_key = params.consumerKey;
|
|
1066
|
+
if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
|
|
1067
|
+
if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
|
|
1015
1068
|
if (params.context) body.context = { ...params.context };
|
|
1016
1069
|
if (params.bindings) body.bindings = { ...params.bindings };
|
|
1017
1070
|
if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
|
|
@@ -1054,6 +1107,7 @@ function validateParams(params) {
|
|
|
1054
1107
|
}
|
|
1055
1108
|
}
|
|
1056
1109
|
validateStructuredInput(params);
|
|
1110
|
+
warnDeprecatedOptions(params);
|
|
1057
1111
|
if (params.options?.timeoutMs !== void 0) {
|
|
1058
1112
|
normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
|
|
1059
1113
|
}
|
|
@@ -1064,6 +1118,22 @@ function validateParams(params) {
|
|
|
1064
1118
|
}
|
|
1065
1119
|
}
|
|
1066
1120
|
}
|
|
1121
|
+
function warnDeprecatedOptions(params) {
|
|
1122
|
+
const o = params.options;
|
|
1123
|
+
if (!o) return;
|
|
1124
|
+
if (o.jitMode !== void 0) {
|
|
1125
|
+
warnDeprecatedOnce(
|
|
1126
|
+
"ZIHIN_DEP001",
|
|
1127
|
+
"`options.jitMode` is deprecated and has no effect (the server discontinued it). Remove it; it will be dropped from the SDK in 1.0."
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (o.temperature !== void 0 || o.maxTokens !== void 0) {
|
|
1131
|
+
warnDeprecatedOnce(
|
|
1132
|
+
"ZIHIN_DEP002",
|
|
1133
|
+
"`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."
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1067
1137
|
var MAX_FIELDS = 50;
|
|
1068
1138
|
var MAX_VALUE_CHARS = 2e3;
|
|
1069
1139
|
var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
|
|
@@ -1071,13 +1141,37 @@ var MAX_SESSION_FROM_FIELDS = 5;
|
|
|
1071
1141
|
var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
1072
1142
|
var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1073
1143
|
var MAX_CONSUMER_KEY_CHARS = 200;
|
|
1144
|
+
var MAX_CONSUMER_NAME_CHARS = 255;
|
|
1145
|
+
var MAX_CLIENT_BLOCKS = 10;
|
|
1146
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1074
1147
|
function snapshotStructuredInput(params) {
|
|
1075
1148
|
const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
|
|
1076
1149
|
return {
|
|
1077
1150
|
...params,
|
|
1151
|
+
...normalizeConsumer(params),
|
|
1078
1152
|
context: snap(params.context),
|
|
1079
1153
|
bindings: snap(params.bindings),
|
|
1080
|
-
sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
|
|
1154
|
+
sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom,
|
|
1155
|
+
capabilities: isPlainObject(params.capabilities) ? {
|
|
1156
|
+
...params.capabilities,
|
|
1157
|
+
blocks: Array.isArray(params.capabilities.blocks) ? [...params.capabilities.blocks] : params.capabilities.blocks
|
|
1158
|
+
} : params.capabilities
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function normalizeConsumer(params) {
|
|
1162
|
+
const c = params.consumer;
|
|
1163
|
+
if (c === void 0) return {};
|
|
1164
|
+
if (!isPlainObject(c)) {
|
|
1165
|
+
throw new ZihinAgentError("config", "`consumer` must be an object: `{ key, name? }`.");
|
|
1166
|
+
}
|
|
1167
|
+
const key = c.key;
|
|
1168
|
+
if (params.consumerKey !== void 0 && key !== void 0 && params.consumerKey !== key) {
|
|
1169
|
+
throw new ZihinAgentError("config", "`consumerKey` and `consumer.key` differ \u2014 send only one of them.");
|
|
1170
|
+
}
|
|
1171
|
+
const resolved = key ?? params.consumerKey;
|
|
1172
|
+
return {
|
|
1173
|
+
consumerKey: resolved,
|
|
1174
|
+
consumer: { key: resolved, ...c.name !== void 0 ? { name: c.name } : {} }
|
|
1081
1175
|
};
|
|
1082
1176
|
}
|
|
1083
1177
|
function isPlainObject(v) {
|
|
@@ -1089,12 +1183,33 @@ function isScalar(v) {
|
|
|
1089
1183
|
return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
|
|
1090
1184
|
}
|
|
1091
1185
|
function validateStructuredInput(params) {
|
|
1186
|
+
if (params.clientMessageId !== void 0 && (typeof params.clientMessageId !== "string" || !UUID_RE.test(params.clientMessageId))) {
|
|
1187
|
+
throw new ZihinAgentError("config", "`clientMessageId` must be a UUID (e.g. from `crypto.randomUUID()`).");
|
|
1188
|
+
}
|
|
1092
1189
|
if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
|
|
1093
1190
|
throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
|
|
1094
1191
|
}
|
|
1095
1192
|
if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
|
|
1096
1193
|
throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
|
|
1097
1194
|
}
|
|
1195
|
+
if (params.consumer !== void 0) {
|
|
1196
|
+
if (params.consumerKey === void 0) {
|
|
1197
|
+
throw new ZihinAgentError("config", "`consumer.key` is required (the name has nothing to attach to without it).");
|
|
1198
|
+
}
|
|
1199
|
+
const n = params.consumer.name;
|
|
1200
|
+
if (n !== void 0 && (typeof n !== "string" || !n.trim() || n.length > MAX_CONSUMER_NAME_CHARS)) {
|
|
1201
|
+
throw new ZihinAgentError("config", `\`consumer.name\` must be a non-empty string of up to ${MAX_CONSUMER_NAME_CHARS} characters.`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (params.capabilities !== void 0) {
|
|
1205
|
+
const blocks = isPlainObject(params.capabilities) ? params.capabilities.blocks : null;
|
|
1206
|
+
if (blocks === null || blocks !== void 0 && (!Array.isArray(blocks) || blocks.length > MAX_CLIENT_BLOCKS || blocks.some((b) => typeof b !== "string"))) {
|
|
1207
|
+
throw new ZihinAgentError(
|
|
1208
|
+
"config",
|
|
1209
|
+
`\`capabilities\` must be \`{ blocks: [...] }\` with up to ${MAX_CLIENT_BLOCKS} block names (e.g. \`['quick_replies']\`).`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1098
1213
|
for (const name of ["context", "bindings"]) {
|
|
1099
1214
|
const v = params[name];
|
|
1100
1215
|
if (v === void 0) continue;
|
|
@@ -1148,6 +1263,7 @@ function requestedInputFields(params) {
|
|
|
1148
1263
|
if (params.context) out.push("context");
|
|
1149
1264
|
if (params.bindings) out.push("bindings");
|
|
1150
1265
|
if (params.sessionFrom?.length) out.push("session_from");
|
|
1266
|
+
if (params.clientMessageId) out.push("client_message_id");
|
|
1151
1267
|
return out;
|
|
1152
1268
|
}
|
|
1153
1269
|
function checkInputCapability(params, chunk) {
|
|
@@ -1184,7 +1300,7 @@ function checkInputCapability(params, chunk) {
|
|
|
1184
1300
|
);
|
|
1185
1301
|
}
|
|
1186
1302
|
}
|
|
1187
|
-
var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
|
|
1303
|
+
var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "action_forwarded", "token", "response", "metrics", "done"]);
|
|
1188
1304
|
async function* streamAgent(cfg, rawParams) {
|
|
1189
1305
|
const params = snapshotStructuredInput(rawParams);
|
|
1190
1306
|
validateParams(params);
|
|
@@ -1203,7 +1319,18 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1203
1319
|
maxRetries: cfg.maxRetries,
|
|
1204
1320
|
callerSignal: params.signal,
|
|
1205
1321
|
fetchImpl: cfg.fetchImpl,
|
|
1206
|
-
onRetry: cfg.onRetry
|
|
1322
|
+
onRetry: cfg.onRetry,
|
|
1323
|
+
// Retry do POST só ANTES dos headers chegarem. O que o torna seguro é a ordem do BE: o SSE faz
|
|
1324
|
+
// `flushHeaders()` antes de reservar o id e antes de qualquer trabalho do turno, então falhar
|
|
1325
|
+
// sem headers quase sempre significa que o handler nem começou. A chave cobre o resto: se o 1º
|
|
1326
|
+
// POST chegou a rodar, o reenvio dá SESSION_BUSY (em voo) ou replay (COMPLETED/LENGTH). A chave
|
|
1327
|
+
// NÃO protege turno cortado no meio: o BE cancela e libera o id, e o reenvio roda de novo com as
|
|
1328
|
+
// tools — por isso o erro mid-stream sai `retryable: false`. Liga antes de ver `accepted_fields`
|
|
1329
|
+
// (chega no `metadata`): contra BE antigo o retry pré-headers acontece sem a chave valer —
|
|
1330
|
+
// aceitável pela mesma ordem (headers antes do trabalho), mas documentado no README.
|
|
1331
|
+
// Resíduo não verificado: proxy que bufferiza headers e devolve 502/504 depois do BE já ter
|
|
1332
|
+
// começado (X-Accel-Buffering: no torna isso improvável).
|
|
1333
|
+
idempotent: !!params.clientMessageId
|
|
1207
1334
|
});
|
|
1208
1335
|
try {
|
|
1209
1336
|
if (!response.ok) {
|
|
@@ -1214,7 +1341,7 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1214
1341
|
}
|
|
1215
1342
|
throw errorFromResponse(response.status, parseBody(text), { endpoint });
|
|
1216
1343
|
}
|
|
1217
|
-
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 });
|
|
1344
|
+
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 });
|
|
1218
1345
|
for await (const e of parseSse(response, {
|
|
1219
1346
|
signal: params.signal,
|
|
1220
1347
|
onAbortError,
|
|
@@ -1237,16 +1364,62 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1237
1364
|
function classifyStreamErrorKind(code, timedOut) {
|
|
1238
1365
|
if (timedOut === true) return "timeout";
|
|
1239
1366
|
if (code === "VALIDATION_ERROR") return "config";
|
|
1367
|
+
const known = own(STREAM_ERROR_CODES, code);
|
|
1368
|
+
if (known) return known.kind;
|
|
1240
1369
|
if (timedOut === false) return "server";
|
|
1241
1370
|
return /TIMEOUT/i.test(code) ? "timeout" : "server";
|
|
1242
1371
|
}
|
|
1243
|
-
|
|
1372
|
+
var STREAM_ERROR_CODES = {
|
|
1373
|
+
SESSION_BUSY: {
|
|
1374
|
+
kind: "blocked",
|
|
1375
|
+
message: "Another turn is still running in this session (or the same clientMessageId is still in flight); retry after it finishes.",
|
|
1376
|
+
retryable: true
|
|
1377
|
+
},
|
|
1378
|
+
IDEMPOTENCY_CONFLICT: {
|
|
1379
|
+
kind: "config",
|
|
1380
|
+
message: "This `clientMessageId` was already used with a different message \u2014 use a new UUID per message."
|
|
1381
|
+
},
|
|
1382
|
+
// `keepServerMessage`: a frase do BE diz o que foi e o que NÃO foi feito (tools que rodaram) —
|
|
1383
|
+
// o integrador precisa dela para decidir se reenvia.
|
|
1384
|
+
CONTEXT_LIMIT: {
|
|
1385
|
+
kind: "blocked",
|
|
1386
|
+
message: "The conversation gathered more context than the model can process; send a smaller request or start a new session.",
|
|
1387
|
+
keepServerMessage: true
|
|
1388
|
+
},
|
|
1389
|
+
CONFLICT: {
|
|
1390
|
+
kind: "blocked",
|
|
1391
|
+
message: "The declared consumer identity does not match the one already bound to this session."
|
|
1392
|
+
},
|
|
1393
|
+
NOT_FOUND: {
|
|
1394
|
+
kind: "not_found",
|
|
1395
|
+
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."
|
|
1396
|
+
},
|
|
1397
|
+
// Replay idempotente sem resposta recuperável. Pode persistir por 24 h (turno gravado sem
|
|
1398
|
+
// mensagem do assistente): retentar com o MESMO id bate no mesmo erro, por isso não é
|
|
1399
|
+
// retentável — o aviso diz a saída (novo `clientMessageId`).
|
|
1400
|
+
SERVICE_UNAVAILABLE: {
|
|
1401
|
+
kind: "server",
|
|
1402
|
+
message: "This message was already answered, but the stored answer could not be read; resend with a new clientMessageId.",
|
|
1403
|
+
retryable: false
|
|
1404
|
+
},
|
|
1405
|
+
// Defensivo: não há ponto de emissão in-stream observado no BE @ 9ec79cfc.
|
|
1406
|
+
RATE_LIMIT_EXCEEDED: { kind: "rate_limit", retryable: true },
|
|
1407
|
+
QUOTA_EXCEEDED: { kind: "quota_exceeded" },
|
|
1408
|
+
TRIAL_EXPIRED: { kind: "trial_expired" },
|
|
1409
|
+
NO_ACTIVE_SUBSCRIPTION: { kind: "no_subscription" }
|
|
1410
|
+
};
|
|
1411
|
+
function errorFromStreamChunk(chunk, ctx = {}) {
|
|
1244
1412
|
const code = chunk.code ?? "AGENT_ERROR";
|
|
1245
1413
|
const kind = classifyStreamErrorKind(code, chunk.timedOut);
|
|
1246
|
-
|
|
1414
|
+
const known = chunk.timedOut === true ? void 0 : own(STREAM_ERROR_CODES, code);
|
|
1415
|
+
const serverMsg = chunk.message ? sanitizeApiError(chunk.message).message.replace(/<[^>]*>/g, "").slice(0, 300) : "";
|
|
1416
|
+
const message = known?.message ? known.keepServerMessage && serverMsg ? `${known.message} Server: ${serverMsg}` : known.message : serverMsg || code;
|
|
1417
|
+
return new ZihinAgentError(kind, message, {
|
|
1247
1418
|
code,
|
|
1248
1419
|
requestId: chunk.requestId,
|
|
1249
|
-
|
|
1420
|
+
executionId: safeId(ctx.executionId),
|
|
1421
|
+
sessionId: safeId(ctx.sessionId),
|
|
1422
|
+
retryable: known?.retryable ?? false
|
|
1250
1423
|
});
|
|
1251
1424
|
}
|
|
1252
1425
|
var FAILED_OUTCOMES = {
|
|
@@ -1267,8 +1440,32 @@ var FAILED_OUTCOMES = {
|
|
|
1267
1440
|
CONFLICT: {
|
|
1268
1441
|
kind: "blocked",
|
|
1269
1442
|
message: "The declared consumer identity does not match the one already bound to this session."
|
|
1443
|
+
},
|
|
1444
|
+
// O provedor do modelo recusou por política (BE `4cea8d53`, 08/10/2026). O `content` é um texto
|
|
1445
|
+
// neutro FIXO da plataforma ("Não consigo ajudar com esse pedido…"), não uma resposta: o BE
|
|
1446
|
+
// registra a execução como erro. Publicá-lo como resposta era a falha silenciosa da 0.6.
|
|
1447
|
+
REFUSED: {
|
|
1448
|
+
kind: "blocked",
|
|
1449
|
+
message: "The model provider refused this request under its content policy; the agent did not answer."
|
|
1450
|
+
},
|
|
1451
|
+
// No `/stream` chega como `event: error` (ver STREAM_ERROR_CODES); no webhook sync e em
|
|
1452
|
+
// backends futuros pode vir como desfecho.
|
|
1453
|
+
CONTEXT_LIMIT: {
|
|
1454
|
+
kind: "blocked",
|
|
1455
|
+
message: "The conversation gathered more context than the model can process; send a smaller request or start a new session."
|
|
1270
1456
|
}
|
|
1271
1457
|
};
|
|
1458
|
+
function errorFromOutcome(outcome, ctx = {}) {
|
|
1459
|
+
const label = safeLabel(outcome);
|
|
1460
|
+
if (!label) return void 0;
|
|
1461
|
+
ctx = { ...ctx, executionId: safeId(ctx.executionId), sessionId: safeId(ctx.sessionId) };
|
|
1462
|
+
if (label === "TIMEOUT") {
|
|
1463
|
+
return new ZihinAgentError("timeout", "The agent turn timed out on the server.", { ...ctx, code: "TURN_TIMEOUT", retryable: false });
|
|
1464
|
+
}
|
|
1465
|
+
const failed = own(FAILED_OUTCOMES, label);
|
|
1466
|
+
if (!failed) return void 0;
|
|
1467
|
+
return new ZihinAgentError(failed.kind, failed.message, { ...ctx, code: label, retryable: failed.retryable ?? false });
|
|
1468
|
+
}
|
|
1272
1469
|
var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
|
|
1273
1470
|
var ERROR_BLOCKS = {
|
|
1274
1471
|
ownership_denied: {
|
|
@@ -1325,6 +1522,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1325
1522
|
const keepRaw = params.includeRaw === true;
|
|
1326
1523
|
let responseChunk;
|
|
1327
1524
|
let metricsChunk;
|
|
1525
|
+
let metadataChunk;
|
|
1328
1526
|
let metadataSessionId;
|
|
1329
1527
|
let mintedSessionId;
|
|
1330
1528
|
let doneChunk;
|
|
@@ -1335,12 +1533,16 @@ async function invokeAgent(cfg, params) {
|
|
|
1335
1533
|
for await (const chunk of streamAgent(cfg, params)) {
|
|
1336
1534
|
if (keepRaw) raw.push(chunk);
|
|
1337
1535
|
if (chunk.type === "error") {
|
|
1338
|
-
throw errorFromStreamChunk(chunk
|
|
1536
|
+
throw errorFromStreamChunk(chunk, {
|
|
1537
|
+
executionId: metadataChunk?.executionId,
|
|
1538
|
+
sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
|
|
1539
|
+
});
|
|
1339
1540
|
}
|
|
1340
1541
|
if (chunk.type === "response") responseChunk = chunk;
|
|
1341
1542
|
else if (chunk.type === "metrics") metricsChunk = chunk;
|
|
1342
1543
|
else if (chunk.type === "token") tokenBuffer += chunk.content;
|
|
1343
1544
|
else if (chunk.type === "metadata") {
|
|
1545
|
+
metadataChunk = chunk;
|
|
1344
1546
|
if (chunk.sessionId) metadataSessionId = chunk.sessionId;
|
|
1345
1547
|
acceptedFields = chunk.acceptedFields;
|
|
1346
1548
|
ignoredFields = chunk.ignoredFields;
|
|
@@ -1349,9 +1551,23 @@ async function invokeAgent(cfg, params) {
|
|
|
1349
1551
|
else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
|
|
1350
1552
|
}
|
|
1351
1553
|
const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
|
|
1352
|
-
const executionId = metricsChunk?.executionId;
|
|
1554
|
+
const executionId = metadataChunk?.executionId ?? doneChunk?.executionId ?? responseChunk?.executionId ?? metricsChunk?.executionId;
|
|
1555
|
+
const ids = {
|
|
1556
|
+
clientMessageId: doneChunk?.clientMessageId ?? responseChunk?.clientMessageId ?? metadataChunk?.clientMessageId,
|
|
1557
|
+
idempotentReplay: doneChunk?.idempotentReplay ?? responseChunk?.idempotentReplay ?? metadataChunk?.idempotentReplay,
|
|
1558
|
+
userMessageId: doneChunk?.userMessageId ?? responseChunk?.userMessageId,
|
|
1559
|
+
assistantMessageId: doneChunk?.assistantMessageId ?? responseChunk?.assistantMessageId
|
|
1560
|
+
};
|
|
1353
1561
|
const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
|
|
1354
1562
|
if (outcomeError) throw outcomeError;
|
|
1563
|
+
const replaySession = responseChunk?.sessionId ?? metadataSessionId;
|
|
1564
|
+
if (ids.idempotentReplay && params.sessionId && replaySession && replaySession.toLowerCase() !== params.sessionId.toLowerCase()) {
|
|
1565
|
+
throw new ZihinAgentError(
|
|
1566
|
+
"config",
|
|
1567
|
+
"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).",
|
|
1568
|
+
{ code: "IDEMPOTENT_REPLAY_OTHER_SESSION", executionId, sessionId: params.sessionId, retryable: false }
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1355
1571
|
const cleanEnd = doneChunk !== void 0;
|
|
1356
1572
|
if (!responseChunk) {
|
|
1357
1573
|
if (cleanEnd) {
|
|
@@ -1367,6 +1583,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1367
1583
|
iterations: metricsChunk?.iterations,
|
|
1368
1584
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1369
1585
|
executionId,
|
|
1586
|
+
...ids,
|
|
1370
1587
|
acceptedFields,
|
|
1371
1588
|
ignoredFields,
|
|
1372
1589
|
raw
|
|
@@ -1382,10 +1599,12 @@ async function invokeAgent(cfg, params) {
|
|
|
1382
1599
|
usage: metricsChunk?.usage,
|
|
1383
1600
|
sources: responseChunk.sources,
|
|
1384
1601
|
blocks: responseChunk.blocks,
|
|
1602
|
+
suggestions: responseChunk.suggestions,
|
|
1385
1603
|
gated: responseChunk.gated,
|
|
1386
1604
|
iterations: metricsChunk?.iterations,
|
|
1387
1605
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1388
1606
|
executionId,
|
|
1607
|
+
...ids,
|
|
1389
1608
|
outcome: doneChunk?.outcome,
|
|
1390
1609
|
truncated: doneChunk?.truncated,
|
|
1391
1610
|
finishReason: doneChunk?.finishReason,
|
|
@@ -1656,22 +1875,40 @@ async function dispatchAgent(cfg, params) {
|
|
|
1656
1875
|
}
|
|
1657
1876
|
const json = parseBody(res.text);
|
|
1658
1877
|
const j = rec(json);
|
|
1878
|
+
const preTurnOutcome = safeLabel(j.outcome);
|
|
1879
|
+
if (j.success === false && preTurnOutcome && preTurnOutcome !== "ERROR") {
|
|
1880
|
+
const classified = errorFromOutcome(preTurnOutcome, {
|
|
1881
|
+
status: res.status,
|
|
1882
|
+
endpoint: res.endpoint,
|
|
1883
|
+
attempts: res.attempts,
|
|
1884
|
+
executionId: safeId(j.execution_id)
|
|
1885
|
+
});
|
|
1886
|
+
if (classified) throw classified;
|
|
1887
|
+
}
|
|
1659
1888
|
if (j.success === false) {
|
|
1660
1889
|
const detail = sanitizeApiError(str(j.error) ?? "").message;
|
|
1661
1890
|
throw new ZihinAgentError("server", detail || "The agent run failed.", {
|
|
1662
1891
|
status: res.status,
|
|
1663
1892
|
endpoint: res.endpoint,
|
|
1664
1893
|
attempts: res.attempts,
|
|
1665
|
-
executionId:
|
|
1894
|
+
executionId: safeId(j.execution_id),
|
|
1666
1895
|
// Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
|
|
1667
1896
|
retryable: false
|
|
1668
1897
|
});
|
|
1669
1898
|
}
|
|
1899
|
+
const outcomeError = errorFromOutcome(str(j.outcome), {
|
|
1900
|
+
status: res.status,
|
|
1901
|
+
endpoint: res.endpoint,
|
|
1902
|
+
attempts: res.attempts,
|
|
1903
|
+
executionId: safeId(j.execution_id)
|
|
1904
|
+
});
|
|
1905
|
+
if (outcomeError) throw outcomeError;
|
|
1670
1906
|
return {
|
|
1671
1907
|
accepted: true,
|
|
1908
|
+
outcome: safeLabel(j.outcome),
|
|
1672
1909
|
triggerId: str(j.trigger_id) ?? params.triggerId,
|
|
1673
1910
|
agentId: str(j.agent_id),
|
|
1674
|
-
executionId:
|
|
1911
|
+
executionId: safeId(j.execution_id),
|
|
1675
1912
|
agentResponse: str(j.agent_response),
|
|
1676
1913
|
usage: parseUsage(j.usage),
|
|
1677
1914
|
iterations: num(j.iterations),
|
|
@@ -1998,6 +2235,7 @@ function signRequest(payload, secretKey, timestamp) {
|
|
|
1998
2235
|
classifyStreamErrorKind,
|
|
1999
2236
|
createClient,
|
|
2000
2237
|
errorFromDoneChunk,
|
|
2238
|
+
errorFromOutcome,
|
|
2001
2239
|
errorFromStreamChunk,
|
|
2002
2240
|
isCommercialBlock,
|
|
2003
2241
|
sanitizeApiError,
|