@zihin/agent-client 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -7
- package/dist/index.d.mts +112 -12
- package/dist/index.d.ts +112 -12
- package/dist/index.js +172 -20
- package/dist/index.mjs +171 -20
- package/package.json +3 -3
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,
|
|
@@ -422,6 +423,8 @@ function errorFromResponse(status, body, meta = {}) {
|
|
|
422
423
|
retryable: false
|
|
423
424
|
});
|
|
424
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 });
|
|
425
428
|
if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
|
|
426
429
|
if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
|
|
427
430
|
return new ZihinAgentError(
|
|
@@ -518,7 +521,7 @@ function resolveConfig(config) {
|
|
|
518
521
|
if (typeof (config.fetch ?? globalThis.fetch) !== "function") {
|
|
519
522
|
throw new ZihinAgentError(
|
|
520
523
|
"config",
|
|
521
|
-
"Global fetch is unavailable in this runtime. Use Node >=
|
|
524
|
+
"Global fetch is unavailable in this runtime. Use Node >=20.16 or pass a `fetch` implementation in the client config."
|
|
522
525
|
);
|
|
523
526
|
}
|
|
524
527
|
return {
|
|
@@ -561,15 +564,15 @@ function buildHeaders(apiKey, extra = {}) {
|
|
|
561
564
|
...extra
|
|
562
565
|
};
|
|
563
566
|
}
|
|
564
|
-
function delay(ms, signal) {
|
|
567
|
+
function delay(ms, signal, meta) {
|
|
565
568
|
return new Promise((resolve, reject) => {
|
|
566
569
|
if (signal?.aborted) {
|
|
567
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
570
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
568
571
|
return;
|
|
569
572
|
}
|
|
570
573
|
const onAbort = () => {
|
|
571
574
|
clearTimeout(timer);
|
|
572
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
575
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
573
576
|
};
|
|
574
577
|
const timer = setTimeout(() => {
|
|
575
578
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -659,7 +662,7 @@ async function requestBuffered(args) {
|
|
|
659
662
|
}
|
|
660
663
|
d.dispose();
|
|
661
664
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
|
|
662
|
-
await delay(wait, args.callerSignal);
|
|
665
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
663
666
|
continue;
|
|
664
667
|
}
|
|
665
668
|
const text = peeked ?? await res.text();
|
|
@@ -678,7 +681,7 @@ async function requestBuffered(args) {
|
|
|
678
681
|
const wait = backoffMs(attempt);
|
|
679
682
|
if (budget.overheadLeft() <= wait) throw lastError;
|
|
680
683
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
|
|
681
|
-
await delay(wait, args.callerSignal);
|
|
684
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
682
685
|
}
|
|
683
686
|
}
|
|
684
687
|
throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -687,6 +690,7 @@ async function openStream(args) {
|
|
|
687
690
|
const endpoint = endpointLabel("POST", args.url);
|
|
688
691
|
const doFetch = resolveFetch(args.fetchImpl);
|
|
689
692
|
const budget = budgetTracker({ ...args, endpoint });
|
|
693
|
+
const policy = args.idempotent ? "idempotent" : "side-effecting";
|
|
690
694
|
for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
|
|
691
695
|
const d = createDeadline(args.timeoutMs, args.callerSignal);
|
|
692
696
|
try {
|
|
@@ -698,7 +702,7 @@ async function openStream(args) {
|
|
|
698
702
|
redirect: "error",
|
|
699
703
|
cache: "no-store"
|
|
700
704
|
});
|
|
701
|
-
let wait = shouldRetryStatus(response.status,
|
|
705
|
+
let wait = shouldRetryStatus(response.status, policy) && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
|
|
702
706
|
if (response.status === 429) {
|
|
703
707
|
const text = await response.text().catch(() => "");
|
|
704
708
|
const body = parseBody(text);
|
|
@@ -708,7 +712,7 @@ async function openStream(args) {
|
|
|
708
712
|
}
|
|
709
713
|
d.dispose();
|
|
710
714
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
|
|
711
|
-
await delay(wait, args.callerSignal);
|
|
715
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
712
716
|
continue;
|
|
713
717
|
}
|
|
714
718
|
if (wait !== null && budget.overheadLeft() > wait) {
|
|
@@ -724,7 +728,7 @@ async function openStream(args) {
|
|
|
724
728
|
reason: "status",
|
|
725
729
|
status: response.status
|
|
726
730
|
});
|
|
727
|
-
await delay(wait, args.callerSignal);
|
|
731
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
728
732
|
continue;
|
|
729
733
|
}
|
|
730
734
|
return { response, deadline: d, endpoint };
|
|
@@ -735,7 +739,14 @@ async function openStream(args) {
|
|
|
735
739
|
if (d.timedOut()) throw budget.expired(attempt + 1);
|
|
736
740
|
if (d.abortedByCaller())
|
|
737
741
|
throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
|
|
738
|
-
|
|
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;
|
|
739
750
|
}
|
|
740
751
|
}
|
|
741
752
|
throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -947,6 +958,15 @@ function suggestionsFromBlocks(blocks) {
|
|
|
947
958
|
const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
|
|
948
959
|
return out.length ? out : void 0;
|
|
949
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
|
+
}
|
|
950
970
|
function mapEvent(e) {
|
|
951
971
|
const d = rec(e.data);
|
|
952
972
|
switch (e.event) {
|
|
@@ -956,6 +976,7 @@ function mapEvent(e) {
|
|
|
956
976
|
requestId: safeId(d.request_id),
|
|
957
977
|
agentId: str(d.agent_id),
|
|
958
978
|
sessionId: safeId(d.session_id),
|
|
979
|
+
...turnIds(d),
|
|
959
980
|
acceptedFields: parseAcceptedFields(d.accepted_fields),
|
|
960
981
|
ignoredFields: parseIgnoredFields(d.ignored_fields),
|
|
961
982
|
data: e.data
|
|
@@ -978,6 +999,8 @@ function mapEvent(e) {
|
|
|
978
999
|
};
|
|
979
1000
|
case "action_proposed":
|
|
980
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 };
|
|
981
1004
|
case "token":
|
|
982
1005
|
return { type: "token", content: str(d.content) ?? "", data: e.data };
|
|
983
1006
|
case "warning":
|
|
@@ -992,6 +1015,7 @@ function mapEvent(e) {
|
|
|
992
1015
|
timeoutClock: safeLabel(d.timeout_clock),
|
|
993
1016
|
truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
|
|
994
1017
|
finishReason: str(d.finish_reason),
|
|
1018
|
+
...turnIds(d),
|
|
995
1019
|
data: e.data
|
|
996
1020
|
};
|
|
997
1021
|
case "error": {
|
|
@@ -1014,6 +1038,7 @@ function mapEvent(e) {
|
|
|
1014
1038
|
sources: Array.isArray(d.sources) ? d.sources : void 0,
|
|
1015
1039
|
blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
|
|
1016
1040
|
suggestions: suggestionsFromBlocks(d.blocks),
|
|
1041
|
+
...turnIds(d),
|
|
1017
1042
|
data: e.data
|
|
1018
1043
|
};
|
|
1019
1044
|
case "metrics": {
|
|
@@ -1036,6 +1061,7 @@ function buildRequestBody(params, normalizedTimeoutMs) {
|
|
|
1036
1061
|
const body = {};
|
|
1037
1062
|
if (params.message !== void 0) body.message = params.message;
|
|
1038
1063
|
if (params.sessionId) body.session_id = params.sessionId;
|
|
1064
|
+
if (params.clientMessageId) body.client_message_id = params.clientMessageId;
|
|
1039
1065
|
if (params.consumerKey) body.consumer_key = params.consumerKey;
|
|
1040
1066
|
if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
|
|
1041
1067
|
if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
|
|
@@ -1117,6 +1143,7 @@ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "proto
|
|
|
1117
1143
|
var MAX_CONSUMER_KEY_CHARS = 200;
|
|
1118
1144
|
var MAX_CONSUMER_NAME_CHARS = 255;
|
|
1119
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;
|
|
1120
1147
|
function snapshotStructuredInput(params) {
|
|
1121
1148
|
const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
|
|
1122
1149
|
return {
|
|
@@ -1156,6 +1183,9 @@ function isScalar(v) {
|
|
|
1156
1183
|
return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
|
|
1157
1184
|
}
|
|
1158
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
|
+
}
|
|
1159
1189
|
if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
|
|
1160
1190
|
throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
|
|
1161
1191
|
}
|
|
@@ -1233,6 +1263,7 @@ function requestedInputFields(params) {
|
|
|
1233
1263
|
if (params.context) out.push("context");
|
|
1234
1264
|
if (params.bindings) out.push("bindings");
|
|
1235
1265
|
if (params.sessionFrom?.length) out.push("session_from");
|
|
1266
|
+
if (params.clientMessageId) out.push("client_message_id");
|
|
1236
1267
|
return out;
|
|
1237
1268
|
}
|
|
1238
1269
|
function checkInputCapability(params, chunk) {
|
|
@@ -1269,7 +1300,7 @@ function checkInputCapability(params, chunk) {
|
|
|
1269
1300
|
);
|
|
1270
1301
|
}
|
|
1271
1302
|
}
|
|
1272
|
-
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"]);
|
|
1273
1304
|
async function* streamAgent(cfg, rawParams) {
|
|
1274
1305
|
const params = snapshotStructuredInput(rawParams);
|
|
1275
1306
|
validateParams(params);
|
|
@@ -1288,7 +1319,18 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1288
1319
|
maxRetries: cfg.maxRetries,
|
|
1289
1320
|
callerSignal: params.signal,
|
|
1290
1321
|
fetchImpl: cfg.fetchImpl,
|
|
1291
|
-
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
|
|
1292
1334
|
});
|
|
1293
1335
|
try {
|
|
1294
1336
|
if (!response.ok) {
|
|
@@ -1299,7 +1341,7 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1299
1341
|
}
|
|
1300
1342
|
throw errorFromResponse(response.status, parseBody(text), { endpoint });
|
|
1301
1343
|
}
|
|
1302
|
-
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 });
|
|
1303
1345
|
for await (const e of parseSse(response, {
|
|
1304
1346
|
signal: params.signal,
|
|
1305
1347
|
onAbortError,
|
|
@@ -1322,16 +1364,62 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1322
1364
|
function classifyStreamErrorKind(code, timedOut) {
|
|
1323
1365
|
if (timedOut === true) return "timeout";
|
|
1324
1366
|
if (code === "VALIDATION_ERROR") return "config";
|
|
1367
|
+
const known = own(STREAM_ERROR_CODES, code);
|
|
1368
|
+
if (known) return known.kind;
|
|
1325
1369
|
if (timedOut === false) return "server";
|
|
1326
1370
|
return /TIMEOUT/i.test(code) ? "timeout" : "server";
|
|
1327
1371
|
}
|
|
1328
|
-
|
|
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 = {}) {
|
|
1329
1412
|
const code = chunk.code ?? "AGENT_ERROR";
|
|
1330
1413
|
const kind = classifyStreamErrorKind(code, chunk.timedOut);
|
|
1331
|
-
|
|
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, {
|
|
1332
1418
|
code,
|
|
1333
1419
|
requestId: chunk.requestId,
|
|
1334
|
-
|
|
1420
|
+
executionId: safeId(ctx.executionId),
|
|
1421
|
+
sessionId: safeId(ctx.sessionId),
|
|
1422
|
+
retryable: known?.retryable ?? false
|
|
1335
1423
|
});
|
|
1336
1424
|
}
|
|
1337
1425
|
var FAILED_OUTCOMES = {
|
|
@@ -1352,8 +1440,32 @@ var FAILED_OUTCOMES = {
|
|
|
1352
1440
|
CONFLICT: {
|
|
1353
1441
|
kind: "blocked",
|
|
1354
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."
|
|
1355
1456
|
}
|
|
1356
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
|
+
}
|
|
1357
1469
|
var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
|
|
1358
1470
|
var ERROR_BLOCKS = {
|
|
1359
1471
|
ownership_denied: {
|
|
@@ -1410,6 +1522,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1410
1522
|
const keepRaw = params.includeRaw === true;
|
|
1411
1523
|
let responseChunk;
|
|
1412
1524
|
let metricsChunk;
|
|
1525
|
+
let metadataChunk;
|
|
1413
1526
|
let metadataSessionId;
|
|
1414
1527
|
let mintedSessionId;
|
|
1415
1528
|
let doneChunk;
|
|
@@ -1420,12 +1533,16 @@ async function invokeAgent(cfg, params) {
|
|
|
1420
1533
|
for await (const chunk of streamAgent(cfg, params)) {
|
|
1421
1534
|
if (keepRaw) raw.push(chunk);
|
|
1422
1535
|
if (chunk.type === "error") {
|
|
1423
|
-
throw errorFromStreamChunk(chunk
|
|
1536
|
+
throw errorFromStreamChunk(chunk, {
|
|
1537
|
+
executionId: metadataChunk?.executionId,
|
|
1538
|
+
sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
|
|
1539
|
+
});
|
|
1424
1540
|
}
|
|
1425
1541
|
if (chunk.type === "response") responseChunk = chunk;
|
|
1426
1542
|
else if (chunk.type === "metrics") metricsChunk = chunk;
|
|
1427
1543
|
else if (chunk.type === "token") tokenBuffer += chunk.content;
|
|
1428
1544
|
else if (chunk.type === "metadata") {
|
|
1545
|
+
metadataChunk = chunk;
|
|
1429
1546
|
if (chunk.sessionId) metadataSessionId = chunk.sessionId;
|
|
1430
1547
|
acceptedFields = chunk.acceptedFields;
|
|
1431
1548
|
ignoredFields = chunk.ignoredFields;
|
|
@@ -1434,9 +1551,23 @@ async function invokeAgent(cfg, params) {
|
|
|
1434
1551
|
else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
|
|
1435
1552
|
}
|
|
1436
1553
|
const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
|
|
1437
|
-
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
|
+
};
|
|
1438
1561
|
const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
|
|
1439
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
|
+
}
|
|
1440
1571
|
const cleanEnd = doneChunk !== void 0;
|
|
1441
1572
|
if (!responseChunk) {
|
|
1442
1573
|
if (cleanEnd) {
|
|
@@ -1452,6 +1583,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1452
1583
|
iterations: metricsChunk?.iterations,
|
|
1453
1584
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1454
1585
|
executionId,
|
|
1586
|
+
...ids,
|
|
1455
1587
|
acceptedFields,
|
|
1456
1588
|
ignoredFields,
|
|
1457
1589
|
raw
|
|
@@ -1472,6 +1604,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1472
1604
|
iterations: metricsChunk?.iterations,
|
|
1473
1605
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1474
1606
|
executionId,
|
|
1607
|
+
...ids,
|
|
1475
1608
|
outcome: doneChunk?.outcome,
|
|
1476
1609
|
truncated: doneChunk?.truncated,
|
|
1477
1610
|
finishReason: doneChunk?.finishReason,
|
|
@@ -1742,22 +1875,40 @@ async function dispatchAgent(cfg, params) {
|
|
|
1742
1875
|
}
|
|
1743
1876
|
const json = parseBody(res.text);
|
|
1744
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
|
+
}
|
|
1745
1888
|
if (j.success === false) {
|
|
1746
1889
|
const detail = sanitizeApiError(str(j.error) ?? "").message;
|
|
1747
1890
|
throw new ZihinAgentError("server", detail || "The agent run failed.", {
|
|
1748
1891
|
status: res.status,
|
|
1749
1892
|
endpoint: res.endpoint,
|
|
1750
1893
|
attempts: res.attempts,
|
|
1751
|
-
executionId:
|
|
1894
|
+
executionId: safeId(j.execution_id),
|
|
1752
1895
|
// Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
|
|
1753
1896
|
retryable: false
|
|
1754
1897
|
});
|
|
1755
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;
|
|
1756
1906
|
return {
|
|
1757
1907
|
accepted: true,
|
|
1908
|
+
outcome: safeLabel(j.outcome),
|
|
1758
1909
|
triggerId: str(j.trigger_id) ?? params.triggerId,
|
|
1759
1910
|
agentId: str(j.agent_id),
|
|
1760
|
-
executionId:
|
|
1911
|
+
executionId: safeId(j.execution_id),
|
|
1761
1912
|
agentResponse: str(j.agent_response),
|
|
1762
1913
|
usage: parseUsage(j.usage),
|
|
1763
1914
|
iterations: num(j.iterations),
|
|
@@ -2084,6 +2235,7 @@ function signRequest(payload, secretKey, timestamp) {
|
|
|
2084
2235
|
classifyStreamErrorKind,
|
|
2085
2236
|
createClient,
|
|
2086
2237
|
errorFromDoneChunk,
|
|
2238
|
+
errorFromOutcome,
|
|
2087
2239
|
errorFromStreamChunk,
|
|
2088
2240
|
isCommercialBlock,
|
|
2089
2241
|
sanitizeApiError,
|