@zihin/agent-client 0.4.0 → 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Aionz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -159,8 +159,35 @@ await zihin.cancelTask({ taskId: task.id, reason: 'mudou o plano' }); // 409 se
159
159
 
160
160
  Um `content` vazio com `contentSource: 'response'` é decisão do BE, não lacuna — o núcleo **não**
161
161
  preenche com tokens. E quando o BE fecha o stream com `event: error` (turno morto no deadline, por
162
- exemplo), o núcleo **lança** (`kind: 'timeout'` para `TURN_TIMEOUT`, com `code` e `requestId`) em vez
163
- de devolver o parcial.
162
+ exemplo), o núcleo **lança** (`kind: 'timeout'`, com `code` e `requestId`) em vez de devolver o
163
+ parcial. Se foi timeout ou não vem do campo **explícito `error.timed_out`** do evento (BE desde
164
+ 26/09/2026): quando presente ele é autoritativo — `true` é timeout seja qual for o `code`, `false`
165
+ é `server` mesmo com "TIMEOUT" no nome. Sem o campo (backend antigo), cai no fallback por nome
166
+ (`/TIMEOUT/i` no `code`), que será removido numa major futura. Não ramifique no **nome** do código
167
+ de timeout: o BE vai renomeá-los (zihin-agent-builder#561); use `kind`.
168
+
169
+ ### Turno encerrado por tempo (0.5)
170
+
171
+ Quando é o **motor** que encerra o turno por tempo, o BE não manda `error`: manda um `response` com a
172
+ mensagem de falha em `content` e fecha com `done { outcome: 'TIMEOUT', timeout_clock }`. O núcleo lê
173
+ o `done` antes do `response` e **lança** `ZihinAgentError` com `kind: 'timeout'`,
174
+ `code: 'TURN_TIMEOUT'`, `timeoutClock` (`turn` · `first_signal` · `idle` · `call`), `executionId` e
175
+ `sessionId` — não retentável, porque o turno pode ter executado tools. Quem consome `streamAgent`
176
+ direto aplica a mesma regra com `errorFromDoneChunk(chunk, { executionId, sessionId })`.
177
+
178
+ O mesmo vale para os outros desfechos em que o `content` é falha ou recusa do motor, e não
179
+ resposta: `ERROR`/`CANCELLED` (pelo servidor) → `kind: 'server'`, `QUOTA_EXCEEDED` → `quota_exceeded`,
180
+ `SUSPENDED`/`BUDGET_EXCEEDED`/`SESSION_BUSY`/`CONFLICT` e o `PAUSED` de agente pausado/arquivado →
181
+ `blocked` (o `code` é o `outcome`; só `SESSION_BUSY` é retentável). Um `ERROR` precedido de
182
+ `status.blocked` (gate da plataforma) também sai `blocked`, com o motivo em `code`
183
+ (`ownership_denied` → `not_found`).
184
+
185
+ Resposta **enlatada** de gate (atendimento humano, denylist, sessão suspensa) não lança: volta com
186
+ `AgentResult.gated: true` — nenhum modelo rodou. Num canal conversacional ela pode ir à pessoa; numa
187
+ automação, trate como "não respondido".
188
+
189
+ `AgentResult.outcome` traz o desfecho dos turnos que respondem: `COMPLETED`, `LENGTH` (com
190
+ `truncated`/`finishReason`), `PAUSED` de aprovação pendente e desfechos novos. `outcome` ausente = backend antigo.
164
191
 
165
192
  ## Dropdown de agentes
166
193
 
@@ -199,6 +226,10 @@ status, virariam "troque a key" ou "tente de novo". Têm `kind` próprio e **nun
199
226
  (`kind: 'auth'`) significa que o **papel** da key não permite a ação — e o papel exigido varia por
200
227
  rota (editor cria trigger, mas não delega task), então a mensagem não enumera papéis.
201
228
 
229
+ A exceção é o 403 com `details.reason: 'AGENT_ACCESS_DENIED'`: o agente não existe, está inativo ou
230
+ é de outro workspace (o BE não distingue, de propósito). Ele sai como `kind: 'not_found'` — o
231
+ problema é o `agentId`, não a key.
232
+
202
233
  `ZihinAgentError.executionId` vem preenchido quando o BE o entrega (sucesso e erro) — é o handle para
203
234
  correlacionar com o console.
204
235
 
@@ -209,7 +240,10 @@ correlacionar com o console.
209
240
  ACIMA do orçamento do BE para aquela rota (SSE 180s, turno async 240s, timeout global 30s), para
210
241
  que o desfecho do servidor chegue como erro estruturado em vez de abort cego do cliente.
211
242
  `timeoutMs` na config sobrepõe todos; `options.timeoutMs` por chamada vai ao BE como
212
- `timeout_ms` e o deadline local fica um pouco acima dele. Retentativas NÃO encolhem o deadline
243
+ `timeout_ms`, que o BE **honra só para encurtar** (`min(prazo da plataforma, pedido)`; o chat dá
244
+ 150s). O deadline local fica 30s acima (`DEADLINE_GRACE_MS`), porque o relógio do turno no BE só
245
+ começa depois da carga de tools/MCP. Em serverless (Lambda, Vercel, Zapier…), use `timeoutMs` ao menos 30s
246
+ abaixo do limite da plataforma, ou a plataforma mata a função antes do erro local. Retentativas NÃO encolhem o deadline
213
247
  da tentativa seguinte — o que fica limitado é o tempo gasto com retries.
214
248
  - **Retry sem duplicar efeito** — GET (idempotente) retenta em rede/429/5xx; POST com efeito
215
249
  (`invokeAgent`/`dispatchAgent`/`createTrigger`) retenta **só em 429/503** e **nunca em erro de
package/dist/index.d.mts CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * @module errors
6
6
  */
7
- type ZihinErrorKind = 'config' | 'auth' | 'trial_expired' | 'no_subscription' | 'quota_exceeded' | 'not_found' | 'rate_limit' | 'timeout' | 'aborted' | 'server' | 'network' | 'protocol' | 'unknown';
7
+
8
+ type ZihinErrorKind = 'config' | 'auth' | 'trial_expired' | 'no_subscription' | 'quota_exceeded' | 'not_found' | 'rate_limit' | 'timeout' | 'aborted' | 'server' | 'network' | 'protocol' | 'blocked' | 'unknown';
8
9
  /** Contexto opcional anexado a um erro para correlação/depuração. */
9
10
  interface ErrorMeta {
10
11
  /** Ex.: "POST llm.zihin.ai/api/v2/agents/abc/stream". */
@@ -23,6 +24,14 @@ interface ZihinErrorOptions extends ErrorMeta {
23
24
  * handoff) — é o handle que liga um erro daqui à execução no console/telemetria.
24
25
  */
25
26
  executionId?: string;
27
+ /** Sessão do turno — para o chamador retomar/inspecionar a conversa depois de um erro. */
28
+ sessionId?: string;
29
+ /**
30
+ * Só em `kind: 'timeout'` vindo do desfecho do turno (`done.timeout_clock`): qual relógio do BE
31
+ * encerrou o turno — `turn`, `first_signal`, `idle` ou `call`. Ausente num timeout de
32
+ * transporte/local, onde o turno pode nem ter terminado.
33
+ */
34
+ timeoutClock?: TimeoutClock;
26
35
  }
27
36
  declare class ZihinAgentError extends Error {
28
37
  readonly kind: ZihinErrorKind;
@@ -33,6 +42,8 @@ declare class ZihinAgentError extends Error {
33
42
  readonly attempts?: number;
34
43
  readonly endpoint?: string;
35
44
  readonly executionId?: string;
45
+ readonly sessionId?: string;
46
+ readonly timeoutClock?: TimeoutClock;
36
47
  constructor(kind: ZihinErrorKind, message: string, opts?: ZihinErrorOptions);
37
48
  }
38
49
  /**
@@ -109,7 +120,15 @@ interface Attachment {
109
120
  interface InvokeOptions {
110
121
  temperature?: number;
111
122
  maxTokens?: number;
112
- /** Timeout específico desta chamada (ms); sobrepõe o do cliente. */
123
+ /**
124
+ * Teto do TURNO desta chamada (ms); sobrepõe o do cliente. Vai ao BE como `options.timeout_ms`
125
+ * e o BE o honra (desde 30/09/2026, contrato de tempo §4), mas **só para encurtar**: o prazo
126
+ * efetivo é `min(prazo da plataforma, pedido)` — no `/stream` o canal chat dá 150s. Pedir mais
127
+ * não alonga. Um turno cortado por esse prazo sai `TIMEOUT` com `timeoutClock: 'turn'`.
128
+ *
129
+ * O relógio do turno no BE só começa DEPOIS da carga (config, tools, MCP frio). O deadline
130
+ * local soma `DEADLINE_GRACE_MS` a este valor para ainda receber o desfecho do servidor.
131
+ */
113
132
  timeoutMs?: number;
114
133
  /** Just-in-time tool loading (Sprint 33). */
115
134
  jitMode?: boolean;
@@ -175,9 +194,41 @@ interface AgentResult {
175
194
  }>;
176
195
  iterations?: number;
177
196
  executionTimeMs?: number;
197
+ /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
+ executionId?: string;
199
+ /**
200
+ * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
+ * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
202
+ * conversacional ela pode ser entregue à pessoa; numa automação, trate como "não respondido".
203
+ */
204
+ gated?: boolean;
205
+ /**
206
+ * Desfecho do turno, lido do `done` (`exitReason` do motor). `undefined` = backend antigo ou
207
+ * stream cortado antes do `done`. Só chegam aqui os desfechos cujo `content` é resposta
208
+ * (`COMPLETED`, `LENGTH`), o `PAUSED` (aprovação pendente ou agente pausado) e desfechos novos.
209
+ * `TIMEOUT`, `ERROR`, `CANCELLED`, `SUSPENDED` e os de bloqueio viram `ZihinAgentError` — o
210
+ * `content` desses turnos é a mensagem de falha/recusa do motor (`errorFromDoneChunk`).
211
+ */
212
+ outcome?: AgentOutcome;
213
+ /** `true` quando a resposta foi cortada pelo teto de saída do modelo (`done.truncated`). */
214
+ truncated?: boolean;
215
+ /** Motivo do corte quando `truncated` (ex.: `length`). */
216
+ finishReason?: string;
178
217
  /** Eventos SSE brutos coletados (para depuração / casos avançados). */
179
218
  raw: AgentStreamChunk[];
180
219
  }
220
+ /**
221
+ * Desfecho do turno (`done.outcome` = `resolveTurnOutcome` do motor). `COMPLETED` e `LENGTH`
222
+ * contam como sucesso no BE (`isSuccessfulOutcome`); os demais valores são repassados como vêm.
223
+ * Aberto (`string & {}`) porque o BE pode acrescentar desfechos (ex.: os de bloqueio).
224
+ */
225
+ type AgentOutcome = 'COMPLETED' | 'LENGTH' | 'TIMEOUT' | 'ERROR' | 'CANCELLED' | 'PAUSED' | 'SUSPENDED' | 'BUDGET_EXCEEDED' | 'QUOTA_EXCEEDED' | 'SESSION_BUSY' | 'CONFLICT' | (string & {});
226
+ /**
227
+ * Qual relógio encerrou o turno (contrato de tempo §2): `turn` (prazo do turno, inclusive o
228
+ * `options.timeoutMs` pedido), `first_signal` (o provedor não começou a responder), `idle`
229
+ * (silêncio entre pedaços do stream do provedor) ou `call` (chamada ao modelo sem stream).
230
+ */
231
+ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
181
232
  /**
182
233
  * Chunk normalizado emitido por `streamAgent`. Mapeia os tipos de evento SSE do BE:
183
234
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
@@ -189,9 +240,12 @@ type AgentStreamChunk = {
189
240
  agentId?: string;
190
241
  sessionId?: string;
191
242
  data: unknown;
192
- } | {
243
+ }
244
+ /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
245
+ | {
193
246
  type: 'status';
194
247
  phase?: string;
248
+ blocked?: string;
195
249
  data: unknown;
196
250
  } | {
197
251
  type: 'attachments';
@@ -223,16 +277,21 @@ type AgentStreamChunk = {
223
277
  data: unknown;
224
278
  }
225
279
  /**
226
- * O BE encerra o stream com `event: error` + `{ error: { type, code, message, request_id } }`
227
- * (`utils/sse.js:97`) — inclusive quando o TURNO morre no deadline. Antes isto caía em
228
- * `unknown` e o `invokeAgent` ignorava: se já tinha chegado token, devolvia a deliberação
229
- * crua como SUCESSO.
280
+ * O BE encerra o stream com `event: error` + `{ error: { type, code, message, request_id,
281
+ * timed_out? } }` (`utils/sse.js`) — inclusive quando o TURNO morre no deadline. Antes isto
282
+ * caía em `unknown` e o `invokeAgent` ignorava: se já tinha chegado token, devolvia a
283
+ * deliberação crua como SUCESSO.
284
+ *
285
+ * `timedOut` espelha `error.timed_out` (BE desde 26/09/2026, zihin-agent-builder#605): campo
286
+ * explícito que diz se o turno morreu por timeout, independente do NOME do código — os códigos
287
+ * vão ser renomeados (zihin-agent-builder#561). `undefined` = backend antigo, sem o campo.
230
288
  */
231
289
  | {
232
290
  type: 'error';
233
291
  code?: string;
234
292
  message?: string;
235
293
  requestId?: string;
294
+ timedOut?: boolean;
236
295
  data: unknown;
237
296
  }
238
297
  /**
@@ -245,9 +304,20 @@ type AgentStreamChunk = {
245
304
  sessionId?: string;
246
305
  data: unknown;
247
306
  }
248
- /** Fim LIMPO do stream (`utils/sse.js:92`). Sem ele, o stream foi cortado no meio. */
307
+ /**
308
+ * Fim LIMPO do stream (`utils/sse.js`). Sem ele, o stream foi cortado no meio. Carrega o
309
+ * desfecho do turno: `{ outcome, timed_out?, timeout_clock?, truncated?, finish_reason? }`
310
+ * (aditivo — backend antigo manda `{}`). Um turno que o motor encerrou por tempo chega aqui
311
+ * com `outcome: 'TIMEOUT'` (idem ERROR e os bloqueios), DEPOIS de um `response` cujo `content`
312
+ * é a mensagem de falha — use `errorFromDoneChunk` para não publicá-lo como resposta.
313
+ */
249
314
  | {
250
315
  type: 'done';
316
+ outcome?: AgentOutcome;
317
+ timedOut?: boolean;
318
+ timeoutClock?: TimeoutClock;
319
+ truncated?: boolean;
320
+ finishReason?: string;
251
321
  data: unknown;
252
322
  } | {
253
323
  type: 'response';
@@ -258,6 +328,8 @@ type AgentStreamChunk = {
258
328
  type: string;
259
329
  [k: string]: unknown;
260
330
  }>;
331
+ /** Resposta ENLATADA de um gate (handoff humano, denylist, sessão suspensa) — não do agente. */
332
+ gated?: boolean;
261
333
  data: unknown;
262
334
  } | {
263
335
  type: 'metrics';
@@ -265,6 +337,7 @@ type AgentStreamChunk = {
265
337
  model?: string;
266
338
  iterations?: number;
267
339
  executionTimeMs?: number;
340
+ executionId?: string;
268
341
  data: unknown;
269
342
  } | {
270
343
  type: 'unknown';
@@ -584,8 +657,13 @@ type TimeoutOperation = keyof typeof OPERATION_TIMEOUT_MS;
584
657
  * Folga somada ao `options.timeoutMs` do chamador para formar o deadline LOCAL. O mesmo número vai
585
658
  * ao BE como `options.timeout_ms` (o teto que ele deve respeitar); sem folga, os dois relógios
586
659
  * disparam juntos e o erro do servidor — que tem request_id — perde a corrida para o nosso abort.
660
+ *
661
+ * 30s, não 10s: o relógio do turno no BE só começa DEPOIS da carga (config, tools, gates — um MCP
662
+ * frio leva dezenas de segundos), então o tempo de parede visto daqui é `carga + prazo` (contrato
663
+ * de tempo §4). Com 10s, um `timeoutMs: 60000` num agente com MCP abortava aos 70s e trocava o
664
+ * `done { outcome: 'TIMEOUT' }` do servidor por um abort cego.
587
665
  */
588
- declare const DEADLINE_GRACE_MS = 10000;
666
+ declare const DEADLINE_GRACE_MS = 30000;
589
667
 
590
668
  /**
591
669
  * Listagem de agentes do tenant (GET /api/v2/agents). Alimenta o dropdown do nó n8n (#17)
@@ -664,6 +742,58 @@ declare class ZihinAgentClient {
664
742
  /** Açúcar: cria um cliente a partir da config. */
665
743
  declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
666
744
 
745
+ /**
746
+ * Caminho A — invocação direta de agente (POST /api/v2/agents/:id/stream).
747
+ * `streamAgent` emite deltas ao vivo; `invokeAgent` buffeia o stream e consolida
748
+ * o evento `response` (content/sources/session_id/blocks) com o `metrics` (usage/model).
749
+ *
750
+ * @module invoke
751
+ */
752
+
753
+ /**
754
+ * Decide se o `event: error` terminal do BE foi um timeout do turno (#75).
755
+ *
756
+ * Fonte primária é o campo EXPLÍCITO `error.timed_out` (BE `utils/sse.js`, em prod desde
757
+ * 26/09/2026 — zihin-agent-builder#605): quando presente, é autoritativo nos dois sentidos —
758
+ * `true` classifica como timeout seja qual for o `code`; `false` classifica como `server` mesmo
759
+ * que o nome do código contenha "TIMEOUT". O BE vai renomear esses códigos
760
+ * (zihin-agent-builder#561) e o nome deixa de ser um sinal confiável.
761
+ *
762
+ * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
+ * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
+ */
765
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
766
+ /**
767
+ * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
+ * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
769
+ * Paperclip, por exemplo) não reimplementar a classificação por conta e divergir dela.
770
+ */
771
+ declare function errorFromStreamChunk(chunk: Extract<AgentStreamChunk, {
772
+ type: 'error';
773
+ }>): ZihinAgentError;
774
+ /**
775
+ * Converte o `done` de um turno que NÃO produziu resposta num `ZihinAgentError`; devolve
776
+ * `undefined` quando o `content` é resposta. Mesma regra do `invokeAgent`, exportada para quem
777
+ * consome `streamAgent` direto (o Paperclip) não reimplementá-la.
778
+ *
779
+ * Por que erro e não um campo no resultado: nesses turnos o BE já emitiu um `response` com a
780
+ * mensagem de falha do motor em `content` (contrato de tempo §6). Como campo, cada shell teria
781
+ * de lembrar de checá-lo — e o que esquecesse publicaria a falha como resposta, que é o defeito
782
+ * que isto fecha (#81). Como erro, o caminho seguro é o default.
783
+ *
784
+ * TIMEOUT: `outcome: 'TIMEOUT'` é a fonte primária; `timed_out: true` sozinho também conta (o BE
785
+ * só o emite com TIMEOUT). Vira `kind: 'timeout'`, `code: 'TURN_TIMEOUT'`, com `timeoutClock`.
786
+ * Nenhum é retentável por default: o turno pode ter executado tools com efeito colateral
787
+ * (exceção: `SESSION_BUSY`, em que o turno nem começou).
788
+ */
789
+ declare function errorFromDoneChunk(chunk: Extract<AgentStreamChunk, {
790
+ type: 'done';
791
+ }>, ctx?: {
792
+ executionId?: string;
793
+ sessionId?: string;
794
+ blocked?: string;
795
+ }): ZihinAgentError | undefined;
796
+
667
797
  /**
668
798
  * Security utilities — portado de n8n-nodes-zihin/src/utils/security.ts (zero-dep).
669
799
  * SSRF prevention, payload limits, error sanitization, API-key format.
@@ -786,4 +916,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
786
916
  */
787
917
  declare const MAX_TASK_PAYLOAD_BYTES: number;
788
918
 
789
- export { type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type CreateTriggerParams, DEADLINE_GRACE_MS, DEFAULT_LIMITS, type DelegateTaskParams, type DeleteTriggerParams, type DispatchAgentParams, type DispatchResult, type ExecutionStatus, type FindOrCreateResult, type GetExecutionParams, type GetTaskParams, type GetTriggerParams, type InvokeAgentParams, type InvokeOptions, type ListTasksParams, type ListTriggersParams, MAX_TASK_PAYLOAD_BYTES, OPERATION_TIMEOUT_MS, type QueryExtraction, type ResponseTransformMode, type RetryInfo, type SignedRequestHeaders, type Source, type TaskDossier, type TaskExecution, type TaskInfo, type TaskListPage, type TimeoutOperation, type TriggerInfo, type TriggerMode, type TriggerRef, type UpdateTriggerParams, type Usage, ZihinAgentClient, ZihinAgentError, type ZihinClientConfig, type ZihinErrorKind, createClient, isCommercialBlock, sanitizeApiError, signRequest, validateApiKeyFormat, validateBaseUrl, validatePromptSize };
919
+ export { type AgentOutcome, type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type CreateTriggerParams, DEADLINE_GRACE_MS, DEFAULT_LIMITS, type DelegateTaskParams, type DeleteTriggerParams, type DispatchAgentParams, type DispatchResult, type ExecutionStatus, type FindOrCreateResult, type GetExecutionParams, type GetTaskParams, type GetTriggerParams, type InvokeAgentParams, type InvokeOptions, type ListTasksParams, type ListTriggersParams, MAX_TASK_PAYLOAD_BYTES, OPERATION_TIMEOUT_MS, type QueryExtraction, type ResponseTransformMode, type RetryInfo, type SignedRequestHeaders, type Source, type TaskDossier, type TaskExecution, type TaskInfo, type TaskListPage, type TimeoutClock, type TimeoutOperation, type TriggerInfo, type TriggerMode, type TriggerRef, type UpdateTriggerParams, type Usage, ZihinAgentClient, ZihinAgentError, type ZihinClientConfig, type ZihinErrorKind, classifyStreamErrorKind, createClient, errorFromDoneChunk, errorFromStreamChunk, isCommercialBlock, sanitizeApiError, signRequest, validateApiKeyFormat, validateBaseUrl, validatePromptSize };
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  *
5
5
  * @module errors
6
6
  */
7
- type ZihinErrorKind = 'config' | 'auth' | 'trial_expired' | 'no_subscription' | 'quota_exceeded' | 'not_found' | 'rate_limit' | 'timeout' | 'aborted' | 'server' | 'network' | 'protocol' | 'unknown';
7
+
8
+ type ZihinErrorKind = 'config' | 'auth' | 'trial_expired' | 'no_subscription' | 'quota_exceeded' | 'not_found' | 'rate_limit' | 'timeout' | 'aborted' | 'server' | 'network' | 'protocol' | 'blocked' | 'unknown';
8
9
  /** Contexto opcional anexado a um erro para correlação/depuração. */
9
10
  interface ErrorMeta {
10
11
  /** Ex.: "POST llm.zihin.ai/api/v2/agents/abc/stream". */
@@ -23,6 +24,14 @@ interface ZihinErrorOptions extends ErrorMeta {
23
24
  * handoff) — é o handle que liga um erro daqui à execução no console/telemetria.
24
25
  */
25
26
  executionId?: string;
27
+ /** Sessão do turno — para o chamador retomar/inspecionar a conversa depois de um erro. */
28
+ sessionId?: string;
29
+ /**
30
+ * Só em `kind: 'timeout'` vindo do desfecho do turno (`done.timeout_clock`): qual relógio do BE
31
+ * encerrou o turno — `turn`, `first_signal`, `idle` ou `call`. Ausente num timeout de
32
+ * transporte/local, onde o turno pode nem ter terminado.
33
+ */
34
+ timeoutClock?: TimeoutClock;
26
35
  }
27
36
  declare class ZihinAgentError extends Error {
28
37
  readonly kind: ZihinErrorKind;
@@ -33,6 +42,8 @@ declare class ZihinAgentError extends Error {
33
42
  readonly attempts?: number;
34
43
  readonly endpoint?: string;
35
44
  readonly executionId?: string;
45
+ readonly sessionId?: string;
46
+ readonly timeoutClock?: TimeoutClock;
36
47
  constructor(kind: ZihinErrorKind, message: string, opts?: ZihinErrorOptions);
37
48
  }
38
49
  /**
@@ -109,7 +120,15 @@ interface Attachment {
109
120
  interface InvokeOptions {
110
121
  temperature?: number;
111
122
  maxTokens?: number;
112
- /** Timeout específico desta chamada (ms); sobrepõe o do cliente. */
123
+ /**
124
+ * Teto do TURNO desta chamada (ms); sobrepõe o do cliente. Vai ao BE como `options.timeout_ms`
125
+ * e o BE o honra (desde 30/09/2026, contrato de tempo §4), mas **só para encurtar**: o prazo
126
+ * efetivo é `min(prazo da plataforma, pedido)` — no `/stream` o canal chat dá 150s. Pedir mais
127
+ * não alonga. Um turno cortado por esse prazo sai `TIMEOUT` com `timeoutClock: 'turn'`.
128
+ *
129
+ * O relógio do turno no BE só começa DEPOIS da carga (config, tools, MCP frio). O deadline
130
+ * local soma `DEADLINE_GRACE_MS` a este valor para ainda receber o desfecho do servidor.
131
+ */
113
132
  timeoutMs?: number;
114
133
  /** Just-in-time tool loading (Sprint 33). */
115
134
  jitMode?: boolean;
@@ -175,9 +194,41 @@ interface AgentResult {
175
194
  }>;
176
195
  iterations?: number;
177
196
  executionTimeMs?: number;
197
+ /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
+ executionId?: string;
199
+ /**
200
+ * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
+ * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
202
+ * conversacional ela pode ser entregue à pessoa; numa automação, trate como "não respondido".
203
+ */
204
+ gated?: boolean;
205
+ /**
206
+ * Desfecho do turno, lido do `done` (`exitReason` do motor). `undefined` = backend antigo ou
207
+ * stream cortado antes do `done`. Só chegam aqui os desfechos cujo `content` é resposta
208
+ * (`COMPLETED`, `LENGTH`), o `PAUSED` (aprovação pendente ou agente pausado) e desfechos novos.
209
+ * `TIMEOUT`, `ERROR`, `CANCELLED`, `SUSPENDED` e os de bloqueio viram `ZihinAgentError` — o
210
+ * `content` desses turnos é a mensagem de falha/recusa do motor (`errorFromDoneChunk`).
211
+ */
212
+ outcome?: AgentOutcome;
213
+ /** `true` quando a resposta foi cortada pelo teto de saída do modelo (`done.truncated`). */
214
+ truncated?: boolean;
215
+ /** Motivo do corte quando `truncated` (ex.: `length`). */
216
+ finishReason?: string;
178
217
  /** Eventos SSE brutos coletados (para depuração / casos avançados). */
179
218
  raw: AgentStreamChunk[];
180
219
  }
220
+ /**
221
+ * Desfecho do turno (`done.outcome` = `resolveTurnOutcome` do motor). `COMPLETED` e `LENGTH`
222
+ * contam como sucesso no BE (`isSuccessfulOutcome`); os demais valores são repassados como vêm.
223
+ * Aberto (`string & {}`) porque o BE pode acrescentar desfechos (ex.: os de bloqueio).
224
+ */
225
+ type AgentOutcome = 'COMPLETED' | 'LENGTH' | 'TIMEOUT' | 'ERROR' | 'CANCELLED' | 'PAUSED' | 'SUSPENDED' | 'BUDGET_EXCEEDED' | 'QUOTA_EXCEEDED' | 'SESSION_BUSY' | 'CONFLICT' | (string & {});
226
+ /**
227
+ * Qual relógio encerrou o turno (contrato de tempo §2): `turn` (prazo do turno, inclusive o
228
+ * `options.timeoutMs` pedido), `first_signal` (o provedor não começou a responder), `idle`
229
+ * (silêncio entre pedaços do stream do provedor) ou `call` (chamada ao modelo sem stream).
230
+ */
231
+ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
181
232
  /**
182
233
  * Chunk normalizado emitido por `streamAgent`. Mapeia os tipos de evento SSE do BE:
183
234
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
@@ -189,9 +240,12 @@ type AgentStreamChunk = {
189
240
  agentId?: string;
190
241
  sessionId?: string;
191
242
  data: unknown;
192
- } | {
243
+ }
244
+ /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
245
+ | {
193
246
  type: 'status';
194
247
  phase?: string;
248
+ blocked?: string;
195
249
  data: unknown;
196
250
  } | {
197
251
  type: 'attachments';
@@ -223,16 +277,21 @@ type AgentStreamChunk = {
223
277
  data: unknown;
224
278
  }
225
279
  /**
226
- * O BE encerra o stream com `event: error` + `{ error: { type, code, message, request_id } }`
227
- * (`utils/sse.js:97`) — inclusive quando o TURNO morre no deadline. Antes isto caía em
228
- * `unknown` e o `invokeAgent` ignorava: se já tinha chegado token, devolvia a deliberação
229
- * crua como SUCESSO.
280
+ * O BE encerra o stream com `event: error` + `{ error: { type, code, message, request_id,
281
+ * timed_out? } }` (`utils/sse.js`) — inclusive quando o TURNO morre no deadline. Antes isto
282
+ * caía em `unknown` e o `invokeAgent` ignorava: se já tinha chegado token, devolvia a
283
+ * deliberação crua como SUCESSO.
284
+ *
285
+ * `timedOut` espelha `error.timed_out` (BE desde 26/09/2026, zihin-agent-builder#605): campo
286
+ * explícito que diz se o turno morreu por timeout, independente do NOME do código — os códigos
287
+ * vão ser renomeados (zihin-agent-builder#561). `undefined` = backend antigo, sem o campo.
230
288
  */
231
289
  | {
232
290
  type: 'error';
233
291
  code?: string;
234
292
  message?: string;
235
293
  requestId?: string;
294
+ timedOut?: boolean;
236
295
  data: unknown;
237
296
  }
238
297
  /**
@@ -245,9 +304,20 @@ type AgentStreamChunk = {
245
304
  sessionId?: string;
246
305
  data: unknown;
247
306
  }
248
- /** Fim LIMPO do stream (`utils/sse.js:92`). Sem ele, o stream foi cortado no meio. */
307
+ /**
308
+ * Fim LIMPO do stream (`utils/sse.js`). Sem ele, o stream foi cortado no meio. Carrega o
309
+ * desfecho do turno: `{ outcome, timed_out?, timeout_clock?, truncated?, finish_reason? }`
310
+ * (aditivo — backend antigo manda `{}`). Um turno que o motor encerrou por tempo chega aqui
311
+ * com `outcome: 'TIMEOUT'` (idem ERROR e os bloqueios), DEPOIS de um `response` cujo `content`
312
+ * é a mensagem de falha — use `errorFromDoneChunk` para não publicá-lo como resposta.
313
+ */
249
314
  | {
250
315
  type: 'done';
316
+ outcome?: AgentOutcome;
317
+ timedOut?: boolean;
318
+ timeoutClock?: TimeoutClock;
319
+ truncated?: boolean;
320
+ finishReason?: string;
251
321
  data: unknown;
252
322
  } | {
253
323
  type: 'response';
@@ -258,6 +328,8 @@ type AgentStreamChunk = {
258
328
  type: string;
259
329
  [k: string]: unknown;
260
330
  }>;
331
+ /** Resposta ENLATADA de um gate (handoff humano, denylist, sessão suspensa) — não do agente. */
332
+ gated?: boolean;
261
333
  data: unknown;
262
334
  } | {
263
335
  type: 'metrics';
@@ -265,6 +337,7 @@ type AgentStreamChunk = {
265
337
  model?: string;
266
338
  iterations?: number;
267
339
  executionTimeMs?: number;
340
+ executionId?: string;
268
341
  data: unknown;
269
342
  } | {
270
343
  type: 'unknown';
@@ -584,8 +657,13 @@ type TimeoutOperation = keyof typeof OPERATION_TIMEOUT_MS;
584
657
  * Folga somada ao `options.timeoutMs` do chamador para formar o deadline LOCAL. O mesmo número vai
585
658
  * ao BE como `options.timeout_ms` (o teto que ele deve respeitar); sem folga, os dois relógios
586
659
  * disparam juntos e o erro do servidor — que tem request_id — perde a corrida para o nosso abort.
660
+ *
661
+ * 30s, não 10s: o relógio do turno no BE só começa DEPOIS da carga (config, tools, gates — um MCP
662
+ * frio leva dezenas de segundos), então o tempo de parede visto daqui é `carga + prazo` (contrato
663
+ * de tempo §4). Com 10s, um `timeoutMs: 60000` num agente com MCP abortava aos 70s e trocava o
664
+ * `done { outcome: 'TIMEOUT' }` do servidor por um abort cego.
587
665
  */
588
- declare const DEADLINE_GRACE_MS = 10000;
666
+ declare const DEADLINE_GRACE_MS = 30000;
589
667
 
590
668
  /**
591
669
  * Listagem de agentes do tenant (GET /api/v2/agents). Alimenta o dropdown do nó n8n (#17)
@@ -664,6 +742,58 @@ declare class ZihinAgentClient {
664
742
  /** Açúcar: cria um cliente a partir da config. */
665
743
  declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
666
744
 
745
+ /**
746
+ * Caminho A — invocação direta de agente (POST /api/v2/agents/:id/stream).
747
+ * `streamAgent` emite deltas ao vivo; `invokeAgent` buffeia o stream e consolida
748
+ * o evento `response` (content/sources/session_id/blocks) com o `metrics` (usage/model).
749
+ *
750
+ * @module invoke
751
+ */
752
+
753
+ /**
754
+ * Decide se o `event: error` terminal do BE foi um timeout do turno (#75).
755
+ *
756
+ * Fonte primária é o campo EXPLÍCITO `error.timed_out` (BE `utils/sse.js`, em prod desde
757
+ * 26/09/2026 — zihin-agent-builder#605): quando presente, é autoritativo nos dois sentidos —
758
+ * `true` classifica como timeout seja qual for o `code`; `false` classifica como `server` mesmo
759
+ * que o nome do código contenha "TIMEOUT". O BE vai renomear esses códigos
760
+ * (zihin-agent-builder#561) e o nome deixa de ser um sinal confiável.
761
+ *
762
+ * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
+ * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
+ */
765
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
766
+ /**
767
+ * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
+ * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
769
+ * Paperclip, por exemplo) não reimplementar a classificação por conta e divergir dela.
770
+ */
771
+ declare function errorFromStreamChunk(chunk: Extract<AgentStreamChunk, {
772
+ type: 'error';
773
+ }>): ZihinAgentError;
774
+ /**
775
+ * Converte o `done` de um turno que NÃO produziu resposta num `ZihinAgentError`; devolve
776
+ * `undefined` quando o `content` é resposta. Mesma regra do `invokeAgent`, exportada para quem
777
+ * consome `streamAgent` direto (o Paperclip) não reimplementá-la.
778
+ *
779
+ * Por que erro e não um campo no resultado: nesses turnos o BE já emitiu um `response` com a
780
+ * mensagem de falha do motor em `content` (contrato de tempo §6). Como campo, cada shell teria
781
+ * de lembrar de checá-lo — e o que esquecesse publicaria a falha como resposta, que é o defeito
782
+ * que isto fecha (#81). Como erro, o caminho seguro é o default.
783
+ *
784
+ * TIMEOUT: `outcome: 'TIMEOUT'` é a fonte primária; `timed_out: true` sozinho também conta (o BE
785
+ * só o emite com TIMEOUT). Vira `kind: 'timeout'`, `code: 'TURN_TIMEOUT'`, com `timeoutClock`.
786
+ * Nenhum é retentável por default: o turno pode ter executado tools com efeito colateral
787
+ * (exceção: `SESSION_BUSY`, em que o turno nem começou).
788
+ */
789
+ declare function errorFromDoneChunk(chunk: Extract<AgentStreamChunk, {
790
+ type: 'done';
791
+ }>, ctx?: {
792
+ executionId?: string;
793
+ sessionId?: string;
794
+ blocked?: string;
795
+ }): ZihinAgentError | undefined;
796
+
667
797
  /**
668
798
  * Security utilities — portado de n8n-nodes-zihin/src/utils/security.ts (zero-dep).
669
799
  * SSRF prevention, payload limits, error sanitization, API-key format.
@@ -786,4 +916,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
786
916
  */
787
917
  declare const MAX_TASK_PAYLOAD_BYTES: number;
788
918
 
789
- export { type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type CreateTriggerParams, DEADLINE_GRACE_MS, DEFAULT_LIMITS, type DelegateTaskParams, type DeleteTriggerParams, type DispatchAgentParams, type DispatchResult, type ExecutionStatus, type FindOrCreateResult, type GetExecutionParams, type GetTaskParams, type GetTriggerParams, type InvokeAgentParams, type InvokeOptions, type ListTasksParams, type ListTriggersParams, MAX_TASK_PAYLOAD_BYTES, OPERATION_TIMEOUT_MS, type QueryExtraction, type ResponseTransformMode, type RetryInfo, type SignedRequestHeaders, type Source, type TaskDossier, type TaskExecution, type TaskInfo, type TaskListPage, type TimeoutOperation, type TriggerInfo, type TriggerMode, type TriggerRef, type UpdateTriggerParams, type Usage, ZihinAgentClient, ZihinAgentError, type ZihinClientConfig, type ZihinErrorKind, createClient, isCommercialBlock, sanitizeApiError, signRequest, validateApiKeyFormat, validateBaseUrl, validatePromptSize };
919
+ export { type AgentOutcome, type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type CreateTriggerParams, DEADLINE_GRACE_MS, DEFAULT_LIMITS, type DelegateTaskParams, type DeleteTriggerParams, type DispatchAgentParams, type DispatchResult, type ExecutionStatus, type FindOrCreateResult, type GetExecutionParams, type GetTaskParams, type GetTriggerParams, type InvokeAgentParams, type InvokeOptions, type ListTasksParams, type ListTriggersParams, MAX_TASK_PAYLOAD_BYTES, OPERATION_TIMEOUT_MS, type QueryExtraction, type ResponseTransformMode, type RetryInfo, type SignedRequestHeaders, type Source, type TaskDossier, type TaskExecution, type TaskInfo, type TaskListPage, type TimeoutClock, type TimeoutOperation, type TriggerInfo, type TriggerMode, type TriggerRef, type UpdateTriggerParams, type Usage, ZihinAgentClient, ZihinAgentError, type ZihinClientConfig, type ZihinErrorKind, classifyStreamErrorKind, createClient, errorFromDoneChunk, errorFromStreamChunk, isCommercialBlock, sanitizeApiError, signRequest, validateApiKeyFormat, validateBaseUrl, validatePromptSize };
package/dist/index.js CHANGED
@@ -26,7 +26,10 @@ __export(index_exports, {
26
26
  OPERATION_TIMEOUT_MS: () => OPERATION_TIMEOUT_MS,
27
27
  ZihinAgentClient: () => ZihinAgentClient,
28
28
  ZihinAgentError: () => ZihinAgentError,
29
+ classifyStreamErrorKind: () => classifyStreamErrorKind,
29
30
  createClient: () => createClient,
31
+ errorFromDoneChunk: () => errorFromDoneChunk,
32
+ errorFromStreamChunk: () => errorFromStreamChunk,
30
33
  isCommercialBlock: () => isCommercialBlock,
31
34
  sanitizeApiError: () => sanitizeApiError,
32
35
  signRequest: () => signRequest,
@@ -44,6 +47,12 @@ function utf8Len(s) {
44
47
  function rec(x) {
45
48
  return x && typeof x === "object" ? x : {};
46
49
  }
50
+ function safeId(x) {
51
+ return typeof x === "string" && /^[\w.:-]{1,128}$/.test(x) ? x : void 0;
52
+ }
53
+ function safeLabel(x) {
54
+ return typeof x === "string" && /^[A-Za-z0-9_]{1,64}$/.test(x) ? x : void 0;
55
+ }
47
56
  function str(x) {
48
57
  return typeof x === "string" ? x : void 0;
49
58
  }
@@ -69,8 +78,13 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
69
78
  "waiting",
70
79
  "retrying",
71
80
  "scheduled",
72
- "awaiting_approval"
81
+ "awaiting_approval",
73
82
  // HITL: turno pausado esperando decisão humana não terminou
83
+ // Estados VIVOS reais do backend (zihin-agent-builder `TaskService.LIVE_STATUSES` e
84
+ // `agent_executions.status`): sem eles, uma task/execução pausada ou aguardando input
85
+ // era lida como TERMINAL e o polling parava com o trabalho ainda em andamento.
86
+ "paused",
87
+ "awaiting_input"
74
88
  ]);
75
89
  function isTerminalStatus(status) {
76
90
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
@@ -265,6 +279,9 @@ var ERROR_CODE_MAP = {
265
279
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
266
280
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
267
281
  AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
282
+ // Nome novo do `AGENT_NOT_FOUND` (zihin-agent-builder#561, fatia B). `AGENT_ERROR` virou
283
+ // `INTERNAL_ERROR`, que já está acima.
284
+ NOT_FOUND: "Resource not found \u2014 check the ID (agent, trigger or execution) and that it belongs to this workspace.",
268
285
  // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
269
286
  // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
270
287
  TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
@@ -335,6 +352,8 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
335
352
  this.code = opts.code;
336
353
  this.requestId = opts.requestId;
337
354
  this.executionId = opts.executionId;
355
+ this.sessionId = opts.sessionId;
356
+ this.timeoutClock = opts.timeoutClock;
338
357
  this.attempts = opts.attempts;
339
358
  this.endpoint = opts.endpoint;
340
359
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -369,7 +388,7 @@ function commercialKind(body) {
369
388
  }
370
389
  function extractRequestId(body) {
371
390
  const b = rec(body);
372
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
391
+ return safeId(b.request_id) ?? safeId(b.requestId) ?? safeId(rec(b.error).request_id) ?? safeId(rec(b.meta).request_id);
373
392
  }
374
393
  function errorFromResponse(status, body, meta = {}) {
375
394
  const sanitized = sanitizeApiError(body);
@@ -379,7 +398,7 @@ function errorFromResponse(status, body, meta = {}) {
379
398
  status,
380
399
  code,
381
400
  requestId: extractRequestId(body),
382
- executionId: str(rec(body).execution_id),
401
+ executionId: safeId(rec(body).execution_id),
383
402
  ...meta
384
403
  };
385
404
  const commercial = commercialKind(body);
@@ -390,6 +409,12 @@ function errorFromResponse(status, body, meta = {}) {
390
409
  });
391
410
  }
392
411
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
412
+ if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
413
+ return new ZihinAgentError(
414
+ "not_found",
415
+ "Agent not found, inactive, or not accessible with this API key \u2014 check the agent ID and that the key belongs to the same Zihin workspace.",
416
+ { ...base, retryable: false }
417
+ );
393
418
  if (status === 403)
394
419
  return new ZihinAgentError(
395
420
  "auth",
@@ -439,7 +464,7 @@ var OPERATION_TIMEOUT_MS = Object.freeze({
439
464
  dispatch: 26e4,
440
465
  short: 35e3
441
466
  });
442
- var DEADLINE_GRACE_MS = 1e4;
467
+ var DEADLINE_GRACE_MS = 3e4;
443
468
  var RETRY_OVERHEAD_MS = 6e4;
444
469
  var MIN_TIMEOUT_MS = 1e3;
445
470
  var MAX_TIMEOUT_MS = 9e5;
@@ -891,13 +916,13 @@ function mapEvent(e) {
891
916
  case "metadata":
892
917
  return {
893
918
  type: "metadata",
894
- requestId: str(d.request_id),
919
+ requestId: safeId(d.request_id),
895
920
  agentId: str(d.agent_id),
896
- sessionId: str(d.session_id),
921
+ sessionId: safeId(d.session_id),
897
922
  data: e.data
898
923
  };
899
924
  case "status":
900
- return { type: "status", phase: str(d.phase), data: e.data };
925
+ return { type: "status", phase: str(d.phase), blocked: safeLabel(d.blocked), data: e.data };
901
926
  case "attachments":
902
927
  return { type: "attachments", data: e.data };
903
928
  case "phase":
@@ -919,16 +944,25 @@ function mapEvent(e) {
919
944
  case "warning":
920
945
  return { type: "warning", message: str(d.message), data: e.data };
921
946
  case "session":
922
- return { type: "session", sessionId: str(d.session_id), data: e.data };
947
+ return { type: "session", sessionId: safeId(d.session_id), data: e.data };
923
948
  case "done":
924
- return { type: "done", data: e.data };
949
+ return {
950
+ type: "done",
951
+ outcome: safeLabel(d.outcome),
952
+ timedOut: typeof d.timed_out === "boolean" ? d.timed_out : void 0,
953
+ timeoutClock: safeLabel(d.timeout_clock),
954
+ truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
955
+ finishReason: str(d.finish_reason),
956
+ data: e.data
957
+ };
925
958
  case "error": {
926
959
  const err = rec(d.error);
927
960
  return {
928
961
  type: "error",
929
- code: str(err.code),
962
+ code: safeLabel(err.code),
930
963
  message: str(err.message),
931
- requestId: str(err.request_id),
964
+ requestId: safeId(err.request_id),
965
+ timedOut: typeof err.timed_out === "boolean" ? err.timed_out : void 0,
932
966
  data: e.data
933
967
  };
934
968
  }
@@ -936,7 +970,8 @@ function mapEvent(e) {
936
970
  return {
937
971
  type: "response",
938
972
  content: str(d.content) ?? "",
939
- sessionId: str(d.session_id),
973
+ sessionId: safeId(d.session_id),
974
+ gated: d.gated === true ? true : void 0,
940
975
  sources: Array.isArray(d.sources) ? d.sources : void 0,
941
976
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
942
977
  data: e.data
@@ -949,6 +984,7 @@ function mapEvent(e) {
949
984
  model: str(modelConfig.model) ?? str(d.model),
950
985
  iterations: num(d.iterations),
951
986
  executionTimeMs: num(d.execution_time_ms),
987
+ executionId: safeId(d.execution_id),
952
988
  data: e.data
953
989
  };
954
990
  }
@@ -1047,6 +1083,91 @@ async function* streamAgent(cfg, params) {
1047
1083
  deadline.dispose();
1048
1084
  }
1049
1085
  }
1086
+ function classifyStreamErrorKind(code, timedOut) {
1087
+ if (timedOut === true) return "timeout";
1088
+ if (timedOut === false) return "server";
1089
+ return /TIMEOUT/i.test(code) ? "timeout" : "server";
1090
+ }
1091
+ function errorFromStreamChunk(chunk) {
1092
+ const code = chunk.code ?? "AGENT_ERROR";
1093
+ const kind = classifyStreamErrorKind(code, chunk.timedOut);
1094
+ return new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1095
+ code,
1096
+ requestId: chunk.requestId,
1097
+ retryable: false
1098
+ });
1099
+ }
1100
+ var FAILED_OUTCOMES = {
1101
+ ERROR: { kind: "server", message: "The agent turn failed on the server; its text is the engine failure notice, not an answer." },
1102
+ // `server`, não `aborted`: `aborted` é o cancelamento do PRÓPRIO chamador, que `onError` manda
1103
+ // filtrar — um cancelamento feito pelo operador sumiria junto.
1104
+ CANCELLED: { kind: "server", message: "The agent turn was cancelled on the server (by an operator or a platform guard)." },
1105
+ // Modo processo drenando/encerrando; o motor retoma depois. Nenhum canal atual o produz.
1106
+ SUSPENDED: { kind: "blocked", message: "The agent turn was suspended by the server and did not answer; retry later." },
1107
+ BUDGET_EXCEEDED: { kind: "blocked", message: "The agent's spending cap was reached; raise the agent budget to keep invoking it." },
1108
+ QUOTA_EXCEEDED: { kind: "quota_exceeded", message: "Your Zihin token quota is exhausted. Upgrade the plan or wait for the next cycle." },
1109
+ SESSION_BUSY: {
1110
+ kind: "blocked",
1111
+ message: "Another turn is still running in this session; retry after it finishes.",
1112
+ retryable: true
1113
+ },
1114
+ // Hoje a rota manda CONFLICT como `event: error` (não chega ao `done`); mapeado por defesa.
1115
+ CONFLICT: {
1116
+ kind: "blocked",
1117
+ message: "The declared consumer identity does not match the one already bound to this session."
1118
+ }
1119
+ };
1120
+ var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1121
+ var ERROR_BLOCKS = {
1122
+ ownership_denied: {
1123
+ kind: "not_found",
1124
+ message: "Session not found for this API key \u2014 start a new conversation (omit sessionId) instead of reusing this one."
1125
+ },
1126
+ session_load_failed: { kind: "blocked", message: "The server could not load this conversation right now; retry shortly.", retryable: true },
1127
+ approval_resolve_failed: { kind: "blocked", message: "The server could not route the required approval right now; retry shortly.", retryable: true },
1128
+ approval_no_approver: { kind: "blocked", message: "This action requires approval, but the approval policy has no approver configured." },
1129
+ blocked_schedule: { kind: "blocked", message: "The agent is outside its allowed schedule (security policy)." },
1130
+ owner_inactive: { kind: "blocked", message: "The agent's owner is inactive; the agent cannot be invoked." }
1131
+ };
1132
+ function own(table, key) {
1133
+ return key !== void 0 && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
1134
+ }
1135
+ function errorFromDoneChunk(chunk, ctx = {}) {
1136
+ const meta = { executionId: ctx.executionId, sessionId: ctx.sessionId };
1137
+ const blockedReason = safeLabel(ctx.blocked);
1138
+ if (chunk.outcome === "TIMEOUT" || chunk.timedOut === true) {
1139
+ const clock = chunk.timeoutClock;
1140
+ const what = clock === "turn" ? "the turn deadline" : clock === "first_signal" ? "the model provider did not start responding in time" : clock === "idle" ? "the model provider stream went silent" : clock === "call" ? "the model call took too long" : "a server-side clock";
1141
+ return new ZihinAgentError("timeout", `The agent turn timed out (${what}).`, {
1142
+ ...meta,
1143
+ code: "TURN_TIMEOUT",
1144
+ retryable: false,
1145
+ timeoutClock: clock
1146
+ });
1147
+ }
1148
+ if (chunk.outcome === "PAUSED" && blockedReason && PAUSED_BLOCKS.has(blockedReason)) {
1149
+ return new ZihinAgentError("blocked", "The agent is paused or archived and did not answer.", {
1150
+ ...meta,
1151
+ code: "PAUSED",
1152
+ retryable: false
1153
+ });
1154
+ }
1155
+ if (chunk.outcome === "ERROR" && blockedReason) {
1156
+ const gate = own(ERROR_BLOCKS, blockedReason);
1157
+ return new ZihinAgentError(gate?.kind ?? "blocked", gate?.message ?? `The agent turn was blocked by the server (${blockedReason}).`, {
1158
+ ...meta,
1159
+ code: blockedReason.toUpperCase(),
1160
+ retryable: gate?.retryable ?? false
1161
+ });
1162
+ }
1163
+ const failed = own(FAILED_OUTCOMES, chunk.outcome);
1164
+ if (!failed) return void 0;
1165
+ return new ZihinAgentError(failed.kind, failed.message, {
1166
+ ...meta,
1167
+ code: chunk.outcome,
1168
+ retryable: failed.retryable ?? false
1169
+ });
1170
+ }
1050
1171
  async function invokeAgent(cfg, params) {
1051
1172
  const raw = [];
1052
1173
  const keepRaw = params.includeRaw === true;
@@ -1054,26 +1175,27 @@ async function invokeAgent(cfg, params) {
1054
1175
  let metricsChunk;
1055
1176
  let metadataSessionId;
1056
1177
  let mintedSessionId;
1057
- let cleanEnd = false;
1178
+ let doneChunk;
1179
+ let blocked;
1058
1180
  let tokenBuffer = "";
1059
1181
  for await (const chunk of streamAgent(cfg, params)) {
1060
1182
  if (keepRaw) raw.push(chunk);
1061
1183
  if (chunk.type === "error") {
1062
- const code = chunk.code ?? "AGENT_ERROR";
1063
- const kind = /TIMEOUT/i.test(code) ? "timeout" : "server";
1064
- throw new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1065
- code,
1066
- requestId: chunk.requestId,
1067
- retryable: false
1068
- });
1184
+ throw errorFromStreamChunk(chunk);
1069
1185
  }
1070
1186
  if (chunk.type === "response") responseChunk = chunk;
1071
1187
  else if (chunk.type === "metrics") metricsChunk = chunk;
1072
1188
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1073
1189
  else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1074
1190
  else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1075
- else if (chunk.type === "done") cleanEnd = true;
1076
- }
1191
+ else if (chunk.type === "done") doneChunk = chunk;
1192
+ else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1193
+ }
1194
+ const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1195
+ const executionId = metricsChunk?.executionId;
1196
+ const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1197
+ if (outcomeError) throw outcomeError;
1198
+ const cleanEnd = doneChunk !== void 0;
1077
1199
  if (!responseChunk) {
1078
1200
  if (cleanEnd) {
1079
1201
  throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
@@ -1082,11 +1204,12 @@ async function invokeAgent(cfg, params) {
1082
1204
  return {
1083
1205
  content: tokenBuffer,
1084
1206
  contentSource: "tokens",
1085
- sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
1207
+ sessionId,
1086
1208
  usage: metricsChunk?.usage,
1087
1209
  model: metricsChunk?.model,
1088
1210
  iterations: metricsChunk?.iterations,
1089
1211
  executionTimeMs: metricsChunk?.executionTimeMs,
1212
+ executionId,
1090
1213
  raw
1091
1214
  };
1092
1215
  }
@@ -1095,13 +1218,18 @@ async function invokeAgent(cfg, params) {
1095
1218
  return {
1096
1219
  content: responseChunk.content,
1097
1220
  contentSource: "response",
1098
- sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1221
+ sessionId,
1099
1222
  model: metricsChunk?.model,
1100
1223
  usage: metricsChunk?.usage,
1101
1224
  sources: responseChunk.sources,
1102
1225
  blocks: responseChunk.blocks,
1226
+ gated: responseChunk.gated,
1103
1227
  iterations: metricsChunk?.iterations,
1104
1228
  executionTimeMs: metricsChunk?.executionTimeMs,
1229
+ executionId,
1230
+ outcome: doneChunk?.outcome,
1231
+ truncated: doneChunk?.truncated,
1232
+ finishReason: doneChunk?.finishReason,
1105
1233
  raw
1106
1234
  };
1107
1235
  }
@@ -1706,7 +1834,10 @@ function signRequest(payload, secretKey, timestamp) {
1706
1834
  OPERATION_TIMEOUT_MS,
1707
1835
  ZihinAgentClient,
1708
1836
  ZihinAgentError,
1837
+ classifyStreamErrorKind,
1709
1838
  createClient,
1839
+ errorFromDoneChunk,
1840
+ errorFromStreamChunk,
1710
1841
  isCommercialBlock,
1711
1842
  sanitizeApiError,
1712
1843
  signRequest,
package/dist/index.mjs CHANGED
@@ -13,6 +13,12 @@ function utf8Len(s) {
13
13
  function rec(x) {
14
14
  return x && typeof x === "object" ? x : {};
15
15
  }
16
+ function safeId(x) {
17
+ return typeof x === "string" && /^[\w.:-]{1,128}$/.test(x) ? x : void 0;
18
+ }
19
+ function safeLabel(x) {
20
+ return typeof x === "string" && /^[A-Za-z0-9_]{1,64}$/.test(x) ? x : void 0;
21
+ }
16
22
  function str(x) {
17
23
  return typeof x === "string" ? x : void 0;
18
24
  }
@@ -38,8 +44,13 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
38
44
  "waiting",
39
45
  "retrying",
40
46
  "scheduled",
41
- "awaiting_approval"
47
+ "awaiting_approval",
42
48
  // HITL: turno pausado esperando decisão humana não terminou
49
+ // Estados VIVOS reais do backend (zihin-agent-builder `TaskService.LIVE_STATUSES` e
50
+ // `agent_executions.status`): sem eles, uma task/execução pausada ou aguardando input
51
+ // era lida como TERMINAL e o polling parava com o trabalho ainda em andamento.
52
+ "paused",
53
+ "awaiting_input"
43
54
  ]);
44
55
  function isTerminalStatus(status) {
45
56
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
@@ -234,6 +245,9 @@ var ERROR_CODE_MAP = {
234
245
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
235
246
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
236
247
  AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
248
+ // Nome novo do `AGENT_NOT_FOUND` (zihin-agent-builder#561, fatia B). `AGENT_ERROR` virou
249
+ // `INTERNAL_ERROR`, que já está acima.
250
+ NOT_FOUND: "Resource not found \u2014 check the ID (agent, trigger or execution) and that it belongs to this workspace.",
237
251
  // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
238
252
  // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
239
253
  TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
@@ -304,6 +318,8 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
304
318
  this.code = opts.code;
305
319
  this.requestId = opts.requestId;
306
320
  this.executionId = opts.executionId;
321
+ this.sessionId = opts.sessionId;
322
+ this.timeoutClock = opts.timeoutClock;
307
323
  this.attempts = opts.attempts;
308
324
  this.endpoint = opts.endpoint;
309
325
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -338,7 +354,7 @@ function commercialKind(body) {
338
354
  }
339
355
  function extractRequestId(body) {
340
356
  const b = rec(body);
341
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
357
+ return safeId(b.request_id) ?? safeId(b.requestId) ?? safeId(rec(b.error).request_id) ?? safeId(rec(b.meta).request_id);
342
358
  }
343
359
  function errorFromResponse(status, body, meta = {}) {
344
360
  const sanitized = sanitizeApiError(body);
@@ -348,7 +364,7 @@ function errorFromResponse(status, body, meta = {}) {
348
364
  status,
349
365
  code,
350
366
  requestId: extractRequestId(body),
351
- executionId: str(rec(body).execution_id),
367
+ executionId: safeId(rec(body).execution_id),
352
368
  ...meta
353
369
  };
354
370
  const commercial = commercialKind(body);
@@ -359,6 +375,12 @@ function errorFromResponse(status, body, meta = {}) {
359
375
  });
360
376
  }
361
377
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
378
+ if (status === 403 && str(rec(rec(rec(body).error).details).reason) === "AGENT_ACCESS_DENIED")
379
+ return new ZihinAgentError(
380
+ "not_found",
381
+ "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.",
382
+ { ...base, retryable: false }
383
+ );
362
384
  if (status === 403)
363
385
  return new ZihinAgentError(
364
386
  "auth",
@@ -408,7 +430,7 @@ var OPERATION_TIMEOUT_MS = Object.freeze({
408
430
  dispatch: 26e4,
409
431
  short: 35e3
410
432
  });
411
- var DEADLINE_GRACE_MS = 1e4;
433
+ var DEADLINE_GRACE_MS = 3e4;
412
434
  var RETRY_OVERHEAD_MS = 6e4;
413
435
  var MIN_TIMEOUT_MS = 1e3;
414
436
  var MAX_TIMEOUT_MS = 9e5;
@@ -860,13 +882,13 @@ function mapEvent(e) {
860
882
  case "metadata":
861
883
  return {
862
884
  type: "metadata",
863
- requestId: str(d.request_id),
885
+ requestId: safeId(d.request_id),
864
886
  agentId: str(d.agent_id),
865
- sessionId: str(d.session_id),
887
+ sessionId: safeId(d.session_id),
866
888
  data: e.data
867
889
  };
868
890
  case "status":
869
- return { type: "status", phase: str(d.phase), data: e.data };
891
+ return { type: "status", phase: str(d.phase), blocked: safeLabel(d.blocked), data: e.data };
870
892
  case "attachments":
871
893
  return { type: "attachments", data: e.data };
872
894
  case "phase":
@@ -888,16 +910,25 @@ function mapEvent(e) {
888
910
  case "warning":
889
911
  return { type: "warning", message: str(d.message), data: e.data };
890
912
  case "session":
891
- return { type: "session", sessionId: str(d.session_id), data: e.data };
913
+ return { type: "session", sessionId: safeId(d.session_id), data: e.data };
892
914
  case "done":
893
- return { type: "done", data: e.data };
915
+ return {
916
+ type: "done",
917
+ outcome: safeLabel(d.outcome),
918
+ timedOut: typeof d.timed_out === "boolean" ? d.timed_out : void 0,
919
+ timeoutClock: safeLabel(d.timeout_clock),
920
+ truncated: typeof d.truncated === "boolean" ? d.truncated : void 0,
921
+ finishReason: str(d.finish_reason),
922
+ data: e.data
923
+ };
894
924
  case "error": {
895
925
  const err = rec(d.error);
896
926
  return {
897
927
  type: "error",
898
- code: str(err.code),
928
+ code: safeLabel(err.code),
899
929
  message: str(err.message),
900
- requestId: str(err.request_id),
930
+ requestId: safeId(err.request_id),
931
+ timedOut: typeof err.timed_out === "boolean" ? err.timed_out : void 0,
901
932
  data: e.data
902
933
  };
903
934
  }
@@ -905,7 +936,8 @@ function mapEvent(e) {
905
936
  return {
906
937
  type: "response",
907
938
  content: str(d.content) ?? "",
908
- sessionId: str(d.session_id),
939
+ sessionId: safeId(d.session_id),
940
+ gated: d.gated === true ? true : void 0,
909
941
  sources: Array.isArray(d.sources) ? d.sources : void 0,
910
942
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
911
943
  data: e.data
@@ -918,6 +950,7 @@ function mapEvent(e) {
918
950
  model: str(modelConfig.model) ?? str(d.model),
919
951
  iterations: num(d.iterations),
920
952
  executionTimeMs: num(d.execution_time_ms),
953
+ executionId: safeId(d.execution_id),
921
954
  data: e.data
922
955
  };
923
956
  }
@@ -1016,6 +1049,91 @@ async function* streamAgent(cfg, params) {
1016
1049
  deadline.dispose();
1017
1050
  }
1018
1051
  }
1052
+ function classifyStreamErrorKind(code, timedOut) {
1053
+ if (timedOut === true) return "timeout";
1054
+ if (timedOut === false) return "server";
1055
+ return /TIMEOUT/i.test(code) ? "timeout" : "server";
1056
+ }
1057
+ function errorFromStreamChunk(chunk) {
1058
+ const code = chunk.code ?? "AGENT_ERROR";
1059
+ const kind = classifyStreamErrorKind(code, chunk.timedOut);
1060
+ return new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1061
+ code,
1062
+ requestId: chunk.requestId,
1063
+ retryable: false
1064
+ });
1065
+ }
1066
+ var FAILED_OUTCOMES = {
1067
+ ERROR: { kind: "server", message: "The agent turn failed on the server; its text is the engine failure notice, not an answer." },
1068
+ // `server`, não `aborted`: `aborted` é o cancelamento do PRÓPRIO chamador, que `onError` manda
1069
+ // filtrar — um cancelamento feito pelo operador sumiria junto.
1070
+ CANCELLED: { kind: "server", message: "The agent turn was cancelled on the server (by an operator or a platform guard)." },
1071
+ // Modo processo drenando/encerrando; o motor retoma depois. Nenhum canal atual o produz.
1072
+ SUSPENDED: { kind: "blocked", message: "The agent turn was suspended by the server and did not answer; retry later." },
1073
+ BUDGET_EXCEEDED: { kind: "blocked", message: "The agent's spending cap was reached; raise the agent budget to keep invoking it." },
1074
+ QUOTA_EXCEEDED: { kind: "quota_exceeded", message: "Your Zihin token quota is exhausted. Upgrade the plan or wait for the next cycle." },
1075
+ SESSION_BUSY: {
1076
+ kind: "blocked",
1077
+ message: "Another turn is still running in this session; retry after it finishes.",
1078
+ retryable: true
1079
+ },
1080
+ // Hoje a rota manda CONFLICT como `event: error` (não chega ao `done`); mapeado por defesa.
1081
+ CONFLICT: {
1082
+ kind: "blocked",
1083
+ message: "The declared consumer identity does not match the one already bound to this session."
1084
+ }
1085
+ };
1086
+ var PAUSED_BLOCKS = /* @__PURE__ */ new Set(["agent_paused", "agent_archived"]);
1087
+ var ERROR_BLOCKS = {
1088
+ ownership_denied: {
1089
+ kind: "not_found",
1090
+ message: "Session not found for this API key \u2014 start a new conversation (omit sessionId) instead of reusing this one."
1091
+ },
1092
+ session_load_failed: { kind: "blocked", message: "The server could not load this conversation right now; retry shortly.", retryable: true },
1093
+ approval_resolve_failed: { kind: "blocked", message: "The server could not route the required approval right now; retry shortly.", retryable: true },
1094
+ approval_no_approver: { kind: "blocked", message: "This action requires approval, but the approval policy has no approver configured." },
1095
+ blocked_schedule: { kind: "blocked", message: "The agent is outside its allowed schedule (security policy)." },
1096
+ owner_inactive: { kind: "blocked", message: "The agent's owner is inactive; the agent cannot be invoked." }
1097
+ };
1098
+ function own(table, key) {
1099
+ return key !== void 0 && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : void 0;
1100
+ }
1101
+ function errorFromDoneChunk(chunk, ctx = {}) {
1102
+ const meta = { executionId: ctx.executionId, sessionId: ctx.sessionId };
1103
+ const blockedReason = safeLabel(ctx.blocked);
1104
+ if (chunk.outcome === "TIMEOUT" || chunk.timedOut === true) {
1105
+ const clock = chunk.timeoutClock;
1106
+ const what = clock === "turn" ? "the turn deadline" : clock === "first_signal" ? "the model provider did not start responding in time" : clock === "idle" ? "the model provider stream went silent" : clock === "call" ? "the model call took too long" : "a server-side clock";
1107
+ return new ZihinAgentError("timeout", `The agent turn timed out (${what}).`, {
1108
+ ...meta,
1109
+ code: "TURN_TIMEOUT",
1110
+ retryable: false,
1111
+ timeoutClock: clock
1112
+ });
1113
+ }
1114
+ if (chunk.outcome === "PAUSED" && blockedReason && PAUSED_BLOCKS.has(blockedReason)) {
1115
+ return new ZihinAgentError("blocked", "The agent is paused or archived and did not answer.", {
1116
+ ...meta,
1117
+ code: "PAUSED",
1118
+ retryable: false
1119
+ });
1120
+ }
1121
+ if (chunk.outcome === "ERROR" && blockedReason) {
1122
+ const gate = own(ERROR_BLOCKS, blockedReason);
1123
+ return new ZihinAgentError(gate?.kind ?? "blocked", gate?.message ?? `The agent turn was blocked by the server (${blockedReason}).`, {
1124
+ ...meta,
1125
+ code: blockedReason.toUpperCase(),
1126
+ retryable: gate?.retryable ?? false
1127
+ });
1128
+ }
1129
+ const failed = own(FAILED_OUTCOMES, chunk.outcome);
1130
+ if (!failed) return void 0;
1131
+ return new ZihinAgentError(failed.kind, failed.message, {
1132
+ ...meta,
1133
+ code: chunk.outcome,
1134
+ retryable: failed.retryable ?? false
1135
+ });
1136
+ }
1019
1137
  async function invokeAgent(cfg, params) {
1020
1138
  const raw = [];
1021
1139
  const keepRaw = params.includeRaw === true;
@@ -1023,26 +1141,27 @@ async function invokeAgent(cfg, params) {
1023
1141
  let metricsChunk;
1024
1142
  let metadataSessionId;
1025
1143
  let mintedSessionId;
1026
- let cleanEnd = false;
1144
+ let doneChunk;
1145
+ let blocked;
1027
1146
  let tokenBuffer = "";
1028
1147
  for await (const chunk of streamAgent(cfg, params)) {
1029
1148
  if (keepRaw) raw.push(chunk);
1030
1149
  if (chunk.type === "error") {
1031
- const code = chunk.code ?? "AGENT_ERROR";
1032
- const kind = /TIMEOUT/i.test(code) ? "timeout" : "server";
1033
- throw new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1034
- code,
1035
- requestId: chunk.requestId,
1036
- retryable: false
1037
- });
1150
+ throw errorFromStreamChunk(chunk);
1038
1151
  }
1039
1152
  if (chunk.type === "response") responseChunk = chunk;
1040
1153
  else if (chunk.type === "metrics") metricsChunk = chunk;
1041
1154
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1042
1155
  else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1043
1156
  else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1044
- else if (chunk.type === "done") cleanEnd = true;
1045
- }
1157
+ else if (chunk.type === "done") doneChunk = chunk;
1158
+ else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1159
+ }
1160
+ const sessionId = mintedSessionId ?? responseChunk?.sessionId ?? metadataSessionId ?? params.sessionId;
1161
+ const executionId = metricsChunk?.executionId;
1162
+ const outcomeError = doneChunk && errorFromDoneChunk(doneChunk, { executionId, sessionId, blocked });
1163
+ if (outcomeError) throw outcomeError;
1164
+ const cleanEnd = doneChunk !== void 0;
1046
1165
  if (!responseChunk) {
1047
1166
  if (cleanEnd) {
1048
1167
  throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
@@ -1051,11 +1170,12 @@ async function invokeAgent(cfg, params) {
1051
1170
  return {
1052
1171
  content: tokenBuffer,
1053
1172
  contentSource: "tokens",
1054
- sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
1173
+ sessionId,
1055
1174
  usage: metricsChunk?.usage,
1056
1175
  model: metricsChunk?.model,
1057
1176
  iterations: metricsChunk?.iterations,
1058
1177
  executionTimeMs: metricsChunk?.executionTimeMs,
1178
+ executionId,
1059
1179
  raw
1060
1180
  };
1061
1181
  }
@@ -1064,13 +1184,18 @@ async function invokeAgent(cfg, params) {
1064
1184
  return {
1065
1185
  content: responseChunk.content,
1066
1186
  contentSource: "response",
1067
- sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1187
+ sessionId,
1068
1188
  model: metricsChunk?.model,
1069
1189
  usage: metricsChunk?.usage,
1070
1190
  sources: responseChunk.sources,
1071
1191
  blocks: responseChunk.blocks,
1192
+ gated: responseChunk.gated,
1072
1193
  iterations: metricsChunk?.iterations,
1073
1194
  executionTimeMs: metricsChunk?.executionTimeMs,
1195
+ executionId,
1196
+ outcome: doneChunk?.outcome,
1197
+ truncated: doneChunk?.truncated,
1198
+ finishReason: doneChunk?.finishReason,
1074
1199
  raw
1075
1200
  };
1076
1201
  }
@@ -1674,7 +1799,10 @@ export {
1674
1799
  OPERATION_TIMEOUT_MS,
1675
1800
  ZihinAgentClient,
1676
1801
  ZihinAgentError,
1802
+ classifyStreamErrorKind,
1677
1803
  createClient,
1804
+ errorFromDoneChunk,
1805
+ errorFromStreamChunk,
1678
1806
  isCommercialBlock,
1679
1807
  sanitizeApiError,
1680
1808
  signRequest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihin/agent-client",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",