@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.mjs
CHANGED
|
@@ -388,6 +388,8 @@ function errorFromResponse(status, body, meta = {}) {
|
|
|
388
388
|
retryable: false
|
|
389
389
|
});
|
|
390
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 });
|
|
391
393
|
if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
|
|
392
394
|
if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
|
|
393
395
|
return new ZihinAgentError(
|
|
@@ -484,7 +486,7 @@ function resolveConfig(config) {
|
|
|
484
486
|
if (typeof (config.fetch ?? globalThis.fetch) !== "function") {
|
|
485
487
|
throw new ZihinAgentError(
|
|
486
488
|
"config",
|
|
487
|
-
"Global fetch is unavailable in this runtime. Use Node >=
|
|
489
|
+
"Global fetch is unavailable in this runtime. Use Node >=20.16 or pass a `fetch` implementation in the client config."
|
|
488
490
|
);
|
|
489
491
|
}
|
|
490
492
|
return {
|
|
@@ -527,15 +529,15 @@ function buildHeaders(apiKey, extra = {}) {
|
|
|
527
529
|
...extra
|
|
528
530
|
};
|
|
529
531
|
}
|
|
530
|
-
function delay(ms, signal) {
|
|
532
|
+
function delay(ms, signal, meta) {
|
|
531
533
|
return new Promise((resolve, reject) => {
|
|
532
534
|
if (signal?.aborted) {
|
|
533
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
535
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
534
536
|
return;
|
|
535
537
|
}
|
|
536
538
|
const onAbort = () => {
|
|
537
539
|
clearTimeout(timer);
|
|
538
|
-
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
540
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller.", meta));
|
|
539
541
|
};
|
|
540
542
|
const timer = setTimeout(() => {
|
|
541
543
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -625,7 +627,7 @@ async function requestBuffered(args) {
|
|
|
625
627
|
}
|
|
626
628
|
d.dispose();
|
|
627
629
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
|
|
628
|
-
await delay(wait, args.callerSignal);
|
|
630
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
629
631
|
continue;
|
|
630
632
|
}
|
|
631
633
|
const text = peeked ?? await res.text();
|
|
@@ -644,7 +646,7 @@ async function requestBuffered(args) {
|
|
|
644
646
|
const wait = backoffMs(attempt);
|
|
645
647
|
if (budget.overheadLeft() <= wait) throw lastError;
|
|
646
648
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
|
|
647
|
-
await delay(wait, args.callerSignal);
|
|
649
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
648
650
|
}
|
|
649
651
|
}
|
|
650
652
|
throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -653,6 +655,7 @@ async function openStream(args) {
|
|
|
653
655
|
const endpoint = endpointLabel("POST", args.url);
|
|
654
656
|
const doFetch = resolveFetch(args.fetchImpl);
|
|
655
657
|
const budget = budgetTracker({ ...args, endpoint });
|
|
658
|
+
const policy = args.idempotent ? "idempotent" : "side-effecting";
|
|
656
659
|
for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
|
|
657
660
|
const d = createDeadline(args.timeoutMs, args.callerSignal);
|
|
658
661
|
try {
|
|
@@ -664,7 +667,7 @@ async function openStream(args) {
|
|
|
664
667
|
redirect: "error",
|
|
665
668
|
cache: "no-store"
|
|
666
669
|
});
|
|
667
|
-
let wait = shouldRetryStatus(response.status,
|
|
670
|
+
let wait = shouldRetryStatus(response.status, policy) && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
|
|
668
671
|
if (response.status === 429) {
|
|
669
672
|
const text = await response.text().catch(() => "");
|
|
670
673
|
const body = parseBody(text);
|
|
@@ -674,7 +677,7 @@ async function openStream(args) {
|
|
|
674
677
|
}
|
|
675
678
|
d.dispose();
|
|
676
679
|
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
|
|
677
|
-
await delay(wait, args.callerSignal);
|
|
680
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
678
681
|
continue;
|
|
679
682
|
}
|
|
680
683
|
if (wait !== null && budget.overheadLeft() > wait) {
|
|
@@ -690,7 +693,7 @@ async function openStream(args) {
|
|
|
690
693
|
reason: "status",
|
|
691
694
|
status: response.status
|
|
692
695
|
});
|
|
693
|
-
await delay(wait, args.callerSignal);
|
|
696
|
+
await delay(wait, args.callerSignal, { endpoint, attempts: attempt + 1 });
|
|
694
697
|
continue;
|
|
695
698
|
}
|
|
696
699
|
return { response, deadline: d, endpoint };
|
|
@@ -701,7 +704,14 @@ async function openStream(args) {
|
|
|
701
704
|
if (d.timedOut()) throw budget.expired(attempt + 1);
|
|
702
705
|
if (d.abortedByCaller())
|
|
703
706
|
throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
|
|
704
|
-
|
|
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;
|
|
705
715
|
}
|
|
706
716
|
}
|
|
707
717
|
throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
@@ -913,6 +923,15 @@ function suggestionsFromBlocks(blocks) {
|
|
|
913
923
|
const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
|
|
914
924
|
return out.length ? out : void 0;
|
|
915
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
|
+
}
|
|
916
935
|
function mapEvent(e) {
|
|
917
936
|
const d = rec(e.data);
|
|
918
937
|
switch (e.event) {
|
|
@@ -922,6 +941,7 @@ function mapEvent(e) {
|
|
|
922
941
|
requestId: safeId(d.request_id),
|
|
923
942
|
agentId: str(d.agent_id),
|
|
924
943
|
sessionId: safeId(d.session_id),
|
|
944
|
+
...turnIds(d),
|
|
925
945
|
acceptedFields: parseAcceptedFields(d.accepted_fields),
|
|
926
946
|
ignoredFields: parseIgnoredFields(d.ignored_fields),
|
|
927
947
|
data: e.data
|
|
@@ -944,6 +964,8 @@ function mapEvent(e) {
|
|
|
944
964
|
};
|
|
945
965
|
case "action_proposed":
|
|
946
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 };
|
|
947
969
|
case "token":
|
|
948
970
|
return { type: "token", content: str(d.content) ?? "", data: e.data };
|
|
949
971
|
case "warning":
|
|
@@ -958,6 +980,7 @@ function mapEvent(e) {
|
|
|
958
980
|
timeoutClock: safeLabel(d.timeout_clock),
|
|
959
981
|
truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
|
|
960
982
|
finishReason: str(d.finish_reason),
|
|
983
|
+
...turnIds(d),
|
|
961
984
|
data: e.data
|
|
962
985
|
};
|
|
963
986
|
case "error": {
|
|
@@ -980,6 +1003,7 @@ function mapEvent(e) {
|
|
|
980
1003
|
sources: Array.isArray(d.sources) ? d.sources : void 0,
|
|
981
1004
|
blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
|
|
982
1005
|
suggestions: suggestionsFromBlocks(d.blocks),
|
|
1006
|
+
...turnIds(d),
|
|
983
1007
|
data: e.data
|
|
984
1008
|
};
|
|
985
1009
|
case "metrics": {
|
|
@@ -1002,6 +1026,7 @@ function buildRequestBody(params, normalizedTimeoutMs) {
|
|
|
1002
1026
|
const body = {};
|
|
1003
1027
|
if (params.message !== void 0) body.message = params.message;
|
|
1004
1028
|
if (params.sessionId) body.session_id = params.sessionId;
|
|
1029
|
+
if (params.clientMessageId) body.client_message_id = params.clientMessageId;
|
|
1005
1030
|
if (params.consumerKey) body.consumer_key = params.consumerKey;
|
|
1006
1031
|
if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
|
|
1007
1032
|
if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
|
|
@@ -1083,6 +1108,7 @@ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "proto
|
|
|
1083
1108
|
var MAX_CONSUMER_KEY_CHARS = 200;
|
|
1084
1109
|
var MAX_CONSUMER_NAME_CHARS = 255;
|
|
1085
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;
|
|
1086
1112
|
function snapshotStructuredInput(params) {
|
|
1087
1113
|
const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
|
|
1088
1114
|
return {
|
|
@@ -1122,6 +1148,9 @@ function isScalar(v) {
|
|
|
1122
1148
|
return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
|
|
1123
1149
|
}
|
|
1124
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
|
+
}
|
|
1125
1154
|
if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
|
|
1126
1155
|
throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
|
|
1127
1156
|
}
|
|
@@ -1199,6 +1228,7 @@ function requestedInputFields(params) {
|
|
|
1199
1228
|
if (params.context) out.push("context");
|
|
1200
1229
|
if (params.bindings) out.push("bindings");
|
|
1201
1230
|
if (params.sessionFrom?.length) out.push("session_from");
|
|
1231
|
+
if (params.clientMessageId) out.push("client_message_id");
|
|
1202
1232
|
return out;
|
|
1203
1233
|
}
|
|
1204
1234
|
function checkInputCapability(params, chunk) {
|
|
@@ -1235,7 +1265,7 @@ function checkInputCapability(params, chunk) {
|
|
|
1235
1265
|
);
|
|
1236
1266
|
}
|
|
1237
1267
|
}
|
|
1238
|
-
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"]);
|
|
1239
1269
|
async function* streamAgent(cfg, rawParams) {
|
|
1240
1270
|
const params = snapshotStructuredInput(rawParams);
|
|
1241
1271
|
validateParams(params);
|
|
@@ -1254,7 +1284,18 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1254
1284
|
maxRetries: cfg.maxRetries,
|
|
1255
1285
|
callerSignal: params.signal,
|
|
1256
1286
|
fetchImpl: cfg.fetchImpl,
|
|
1257
|
-
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
|
|
1258
1299
|
});
|
|
1259
1300
|
try {
|
|
1260
1301
|
if (!response.ok) {
|
|
@@ -1265,7 +1306,7 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1265
1306
|
}
|
|
1266
1307
|
throw errorFromResponse(response.status, parseBody(text), { endpoint });
|
|
1267
1308
|
}
|
|
1268
|
-
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 });
|
|
1269
1310
|
for await (const e of parseSse(response, {
|
|
1270
1311
|
signal: params.signal,
|
|
1271
1312
|
onAbortError,
|
|
@@ -1288,16 +1329,62 @@ async function* streamAgent(cfg, rawParams) {
|
|
|
1288
1329
|
function classifyStreamErrorKind(code, timedOut) {
|
|
1289
1330
|
if (timedOut === true) return "timeout";
|
|
1290
1331
|
if (code === "VALIDATION_ERROR") return "config";
|
|
1332
|
+
const known = own(STREAM_ERROR_CODES, code);
|
|
1333
|
+
if (known) return known.kind;
|
|
1291
1334
|
if (timedOut === false) return "server";
|
|
1292
1335
|
return /TIMEOUT/i.test(code) ? "timeout" : "server";
|
|
1293
1336
|
}
|
|
1294
|
-
|
|
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 = {}) {
|
|
1295
1377
|
const code = chunk.code ?? "AGENT_ERROR";
|
|
1296
1378
|
const kind = classifyStreamErrorKind(code, chunk.timedOut);
|
|
1297
|
-
|
|
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, {
|
|
1298
1383
|
code,
|
|
1299
1384
|
requestId: chunk.requestId,
|
|
1300
|
-
|
|
1385
|
+
executionId: safeId(ctx.executionId),
|
|
1386
|
+
sessionId: safeId(ctx.sessionId),
|
|
1387
|
+
retryable: known?.retryable ?? false
|
|
1301
1388
|
});
|
|
1302
1389
|
}
|
|
1303
1390
|
var FAILED_OUTCOMES = {
|
|
@@ -1318,8 +1405,32 @@ var FAILED_OUTCOMES = {
|
|
|
1318
1405
|
CONFLICT: {
|
|
1319
1406
|
kind: "blocked",
|
|
1320
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."
|
|
1321
1421
|
}
|
|
1322
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
|
+
}
|
|
1323
1434
|
var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
|
|
1324
1435
|
var ERROR_BLOCKS = {
|
|
1325
1436
|
ownership_denied: {
|
|
@@ -1376,6 +1487,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1376
1487
|
const keepRaw = params.includeRaw === true;
|
|
1377
1488
|
let responseChunk;
|
|
1378
1489
|
let metricsChunk;
|
|
1490
|
+
let metadataChunk;
|
|
1379
1491
|
let metadataSessionId;
|
|
1380
1492
|
let mintedSessionId;
|
|
1381
1493
|
let doneChunk;
|
|
@@ -1386,12 +1498,16 @@ async function invokeAgent(cfg, params) {
|
|
|
1386
1498
|
for await (const chunk of streamAgent(cfg, params)) {
|
|
1387
1499
|
if (keepRaw) raw.push(chunk);
|
|
1388
1500
|
if (chunk.type === "error") {
|
|
1389
|
-
throw errorFromStreamChunk(chunk
|
|
1501
|
+
throw errorFromStreamChunk(chunk, {
|
|
1502
|
+
executionId: metadataChunk?.executionId,
|
|
1503
|
+
sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId
|
|
1504
|
+
});
|
|
1390
1505
|
}
|
|
1391
1506
|
if (chunk.type === "response") responseChunk = chunk;
|
|
1392
1507
|
else if (chunk.type === "metrics") metricsChunk = chunk;
|
|
1393
1508
|
else if (chunk.type === "token") tokenBuffer += chunk.content;
|
|
1394
1509
|
else if (chunk.type === "metadata") {
|
|
1510
|
+
metadataChunk = chunk;
|
|
1395
1511
|
if (chunk.sessionId) metadataSessionId = chunk.sessionId;
|
|
1396
1512
|
acceptedFields = chunk.acceptedFields;
|
|
1397
1513
|
ignoredFields = chunk.ignoredFields;
|
|
@@ -1400,9 +1516,23 @@ async function invokeAgent(cfg, params) {
|
|
|
1400
1516
|
else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
|
|
1401
1517
|
}
|
|
1402
1518
|
const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
|
|
1403
|
-
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
|
+
};
|
|
1404
1526
|
const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
|
|
1405
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
|
+
}
|
|
1406
1536
|
const cleanEnd = doneChunk !== void 0;
|
|
1407
1537
|
if (!responseChunk) {
|
|
1408
1538
|
if (cleanEnd) {
|
|
@@ -1418,6 +1548,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1418
1548
|
iterations: metricsChunk?.iterations,
|
|
1419
1549
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1420
1550
|
executionId,
|
|
1551
|
+
...ids,
|
|
1421
1552
|
acceptedFields,
|
|
1422
1553
|
ignoredFields,
|
|
1423
1554
|
raw
|
|
@@ -1438,6 +1569,7 @@ async function invokeAgent(cfg, params) {
|
|
|
1438
1569
|
iterations: metricsChunk?.iterations,
|
|
1439
1570
|
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
1440
1571
|
executionId,
|
|
1572
|
+
...ids,
|
|
1441
1573
|
outcome: doneChunk?.outcome,
|
|
1442
1574
|
truncated: doneChunk?.truncated,
|
|
1443
1575
|
finishReason: doneChunk?.finishReason,
|
|
@@ -1708,22 +1840,40 @@ async function dispatchAgent(cfg, params) {
|
|
|
1708
1840
|
}
|
|
1709
1841
|
const json = parseBody(res.text);
|
|
1710
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
|
+
}
|
|
1711
1853
|
if (j.success === false) {
|
|
1712
1854
|
const detail = sanitizeApiError(str(j.error) ?? "").message;
|
|
1713
1855
|
throw new ZihinAgentError("server", detail || "The agent run failed.", {
|
|
1714
1856
|
status: res.status,
|
|
1715
1857
|
endpoint: res.endpoint,
|
|
1716
1858
|
attempts: res.attempts,
|
|
1717
|
-
executionId:
|
|
1859
|
+
executionId: safeId(j.execution_id),
|
|
1718
1860
|
// Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
|
|
1719
1861
|
retryable: false
|
|
1720
1862
|
});
|
|
1721
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;
|
|
1722
1871
|
return {
|
|
1723
1872
|
accepted: true,
|
|
1873
|
+
outcome: safeLabel(j.outcome),
|
|
1724
1874
|
triggerId: str(j.trigger_id) ?? params.triggerId,
|
|
1725
1875
|
agentId: str(j.agent_id),
|
|
1726
|
-
executionId:
|
|
1876
|
+
executionId: safeId(j.execution_id),
|
|
1727
1877
|
agentResponse: str(j.agent_response),
|
|
1728
1878
|
usage: parseUsage(j.usage),
|
|
1729
1879
|
iterations: num(j.iterations),
|
|
@@ -2049,6 +2199,7 @@ export {
|
|
|
2049
2199
|
classifyStreamErrorKind,
|
|
2050
2200
|
createClient,
|
|
2051
2201
|
errorFromDoneChunk,
|
|
2202
|
+
errorFromOutcome,
|
|
2052
2203
|
errorFromStreamChunk,
|
|
2053
2204
|
isCommercialBlock,
|
|
2054
2205
|
sanitizeApiError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zihin/agent-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dist"
|
|
41
41
|
],
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
43
|
+
"node": ">=20.16"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"prepublishOnly": "npm run build && npm test"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@types/node": ">=
|
|
57
|
+
"@types/node": ">=20",
|
|
58
58
|
"tsup": "^8.0.0",
|
|
59
59
|
"typescript": "^5.4.0"
|
|
60
60
|
}
|