@zihin/agent-client 0.4.1 → 0.6.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
@@ -50,6 +50,67 @@ for await (const chunk of zihin.streamAgent({ agentId, message: 'Olá' })) {
50
50
  }
51
51
  ```
52
52
 
53
+ ### Contexto estruturado, identidade e sessão derivada (0.6)
54
+
55
+ > Backend: zihin-agent-builder#669 + #676, em produção desde 01/10/2026. Contra um backend
56
+ > anterior, o núcleo **recusa** o turno em vez de rodá-lo sem os campos (ver abaixo).
57
+
58
+ ```ts
59
+ await zihin.invokeAgent({
60
+ agentId,
61
+ message: 'Qual o status do meu pedido?',
62
+ consumerKey: '5511999990000', // quem é a pessoa — só dígitos, como o webhook grava o telefone
63
+ context: { cliente: 'Ana', plano: 'pro' }, // o MODELO lê (bloco junto da mensagem do usuário)
64
+ bindings: { hashFornecedor: 'f-81c2' }, // só as TOOLS recebem; nunca vai ao prompt
65
+ sessionFrom: ['hashFornecedor'], // deriva o session_id destes campos
66
+ });
67
+ ```
68
+
69
+ | Campo | Quem vê | Para quê |
70
+ |---|---|---|
71
+ | `context` | modelo + tools | dados que o agente deve considerar na resposta |
72
+ | `bindings` | só tools | ids internos, chaves de busca — nada que o modelo precise ler |
73
+ | `metadata` | ninguém no turno | rastreio opaco (`traceId`, `correlationId`) |
74
+
75
+ - **Credencial não vai em nenhum deles.** Use um secret do tenant.
76
+ - `context` **fica gravado** junto da mensagem (`declared_context`) e o bloco se repete em toda
77
+ mensagem do usuário, para o prompt cache ver os mesmos bytes a cada turno. O BE redige segredos
78
+ no `context` por nome de chave e por formato de valor, e avisa: `ignoredFields` traz
79
+ `{ field: 'context.<campo>', reason: 'redacted' }`. O campo segue aceito, com o valor alterado; o
80
+ núcleo **não** recusa o turno, só repassa o aviso. `bindings` **não é gravado nem redigido**: só
81
+ existe no contexto das tools durante o turno.
82
+ > Backends anteriores ao zihin-agent-builder#678 redigiam sem aviso e casavam `sk-…` dentro de
83
+ > valores comuns (`task-1234` → `ta[REDACTED]`). Contra eles, mande ids de tool e de sessão em
84
+ > `bindings`.
85
+ - `context` e `bindings` são **planos** (só string/number/boolean/null). Os tetos são os do BE,
86
+ checados no cliente antes de gastar turno: até 50 campos; nome `^[A-Za-z_][A-Za-z0-9_-]{0,63}$`;
87
+ valor de texto até 2.000 caracteres; total (JSON) até **2.000** no `context` e 16.000 no
88
+ `bindings`. `sessionFrom` aceita de 1 a 5 nomes.
89
+ - `context`, `bindings`, `sessionFrom` e `consumerKey` só valem sob API key **admin/owner**.
90
+ `metadata` vale com qualquer key.
91
+ - **Campo descartado não passa em silêncio**: se você mandou `context`/`bindings`/`sessionFrom` e o evento
92
+ `metadata` não trouxe `accepted_fields` (backend antigo) ou trouxe o campo em `ignored_fields`, o
93
+ núcleo lança `ZihinAgentError('config')` nomeando o campo e o motivo, e fecha o stream para
94
+ cancelar o turno. A resposta não é usada, mas a mensagem pode já estar gravada: o turno barrado
95
+ por gate (handoff, denylist) grava a troca mesmo recusado, então **reenviar depois de corrigir gera
96
+ duas linhas no histórico** da conversa. Motivos que recusam: `requires_admin_api_key` (JWT ou key
97
+ sem papel) e `reserved_key` (por chave, ex. `context.branch`: nome reservado do motor, renomeie).
98
+ `redacted` só avisa (ver acima). Todo campo
99
+ pedido precisa estar em `accepted_fields` (`consumer_key` também, quando o BE publica a lista).
100
+ Erro de validação do BE (`VALIDATION_ERROR` no stream) também sai como `config`. `AgentResult.acceptedFields`/`ignoredFields` mostram o que
101
+ o BE fez; `options.temperature`/`maxTokens` aparecem em `ignoredFields` sem bloquear (o `/stream`
102
+ não os aplica).
103
+ - `consumerKey` precisa ser o **mesmo valor** que o webhook gravava, para continuar as conversas dele.
104
+
105
+ **Migrando de webhook `derive`**: um trigger com `session: { derive: ['hashFornecedor'] }` e
106
+ payload `{ hashFornecedor: 'f-81c2', ... }` vira `sessionFrom: ['hashFornecedor']` com o valor em
107
+ `bindings` (ou em `context`, se o modelo deve lê-lo). O BE usa a mesma função de derivação, então o
108
+ mesmo valor cai no **mesmo** `session_id`. `sessionFrom` não combina com `sessionId`. Como no
109
+ webhook: basta **um** campo presente (`0`, `false` e `''` contam como ausentes), um conjunto parcial
110
+ gera **outra** sessão, e a ordem dos campos entra no hash. Chave repetida nos dois: vale o valor de
111
+ `bindings`. Campo do `sessionFrom` que a redação alterou (no `context`) é recusado com
112
+ `VALIDATION_ERROR` (sai como `config`): mande-o em `bindings`.
113
+
53
114
  ## Uso — caminho assíncrono (triggers)
54
115
 
55
116
  ```ts
@@ -166,6 +227,29 @@ parcial. Se foi timeout ou não vem do campo **explícito `error.timed_out`** do
166
227
  (`/TIMEOUT/i` no `code`), que será removido numa major futura. Não ramifique no **nome** do código
167
228
  de timeout: o BE vai renomeá-los (zihin-agent-builder#561); use `kind`.
168
229
 
230
+ ### Turno encerrado por tempo (0.5)
231
+
232
+ Quando é o **motor** que encerra o turno por tempo, o BE não manda `error`: manda um `response` com a
233
+ mensagem de falha em `content` e fecha com `done { outcome: 'TIMEOUT', timeout_clock }`. O núcleo lê
234
+ o `done` antes do `response` e **lança** `ZihinAgentError` com `kind: 'timeout'`,
235
+ `code: 'TURN_TIMEOUT'`, `timeoutClock` (`turn` · `first_signal` · `idle` · `call`), `executionId` e
236
+ `sessionId` — não retentável, porque o turno pode ter executado tools. Quem consome `streamAgent`
237
+ direto aplica a mesma regra com `errorFromDoneChunk(chunk, { executionId, sessionId })`.
238
+
239
+ O mesmo vale para os outros desfechos em que o `content` é falha ou recusa do motor, e não
240
+ resposta: `ERROR`/`CANCELLED` (pelo servidor) → `kind: 'server'`, `QUOTA_EXCEEDED` → `quota_exceeded`,
241
+ `SUSPENDED`/`BUDGET_EXCEEDED`/`SESSION_BUSY`/`CONFLICT` e o `PAUSED` de agente pausado/arquivado →
242
+ `blocked` (o `code` é o `outcome`; só `SESSION_BUSY` é retentável). Um `ERROR` precedido de
243
+ `status.blocked` (gate da plataforma) também sai `blocked`, com o motivo em `code`
244
+ (`ownership_denied` → `not_found`).
245
+
246
+ Resposta **enlatada** de gate (atendimento humano, denylist, sessão suspensa) não lança: volta com
247
+ `AgentResult.gated: true` — nenhum modelo rodou. Num canal conversacional ela pode ir à pessoa; numa
248
+ automação, trate como "não respondido".
249
+
250
+ `AgentResult.outcome` traz o desfecho dos turnos que respondem: `COMPLETED`, `LENGTH` (com
251
+ `truncated`/`finishReason`), `PAUSED` de aprovação pendente e desfechos novos. `outcome` ausente = backend antigo.
252
+
169
253
  ## Dropdown de agentes
170
254
 
171
255
  ```ts
@@ -203,6 +287,10 @@ status, virariam "troque a key" ou "tente de novo". Têm `kind` próprio e **nun
203
287
  (`kind: 'auth'`) significa que o **papel** da key não permite a ação — e o papel exigido varia por
204
288
  rota (editor cria trigger, mas não delega task), então a mensagem não enumera papéis.
205
289
 
290
+ A exceção é o 403 com `details.reason: 'AGENT_ACCESS_DENIED'`: o agente não existe, está inativo ou
291
+ é de outro workspace (o BE não distingue, de propósito). Ele sai como `kind: 'not_found'` — o
292
+ problema é o `agentId`, não a key.
293
+
206
294
  `ZihinAgentError.executionId` vem preenchido quando o BE o entrega (sucesso e erro) — é o handle para
207
295
  correlacionar com o console.
208
296
 
@@ -213,7 +301,10 @@ correlacionar com o console.
213
301
  ACIMA do orçamento do BE para aquela rota (SSE 180s, turno async 240s, timeout global 30s), para
214
302
  que o desfecho do servidor chegue como erro estruturado em vez de abort cego do cliente.
215
303
  `timeoutMs` na config sobrepõe todos; `options.timeoutMs` por chamada vai ao BE como
216
- `timeout_ms` e o deadline local fica um pouco acima dele. Retentativas NÃO encolhem o deadline
304
+ `timeout_ms`, que o BE **honra só para encurtar** (`min(prazo da plataforma, pedido)`; o chat dá
305
+ 150s). O deadline local fica 30s acima (`DEADLINE_GRACE_MS`), porque o relógio do turno no BE só
306
+ começa depois da carga de tools/MCP. Em serverless (Lambda, Vercel, Zapier…), use `timeoutMs` ao menos 30s
307
+ abaixo do limite da plataforma, ou a plataforma mata a função antes do erro local. Retentativas NÃO encolhem o deadline
217
308
  da tentativa seguinte — o que fica limitado é o tempo gasto com retries.
218
309
  - **Retry sem duplicar efeito** — GET (idempotente) retenta em rede/429/5xx; POST com efeito
219
310
  (`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;
@@ -121,8 +140,35 @@ interface InvokeAgentParams {
121
140
  message?: string;
122
141
  /** Anexos multimodais. `message` OU `attachments` é obrigatório. */
123
142
  attachments?: Attachment[];
124
- /** Continuidade de conversa (Supabase agent_sessions). */
143
+ /** Continuidade de conversa (Supabase agent_sessions). Não combina com `sessionFrom`. */
125
144
  sessionId?: string;
145
+ /**
146
+ * Identidade da pessoa do outro lado (memória por pessoa, denylist, gate de engajamento). Só vale
147
+ * sob API key **admin/owner**. Para continuar conversas que um webhook já gravava, use o MESMO
148
+ * valor que o webhook usava. Enviado como `consumer_key`.
149
+ */
150
+ consumerKey?: string;
151
+ /**
152
+ * Contexto estruturado que o **modelo lê** — bloco de plataforma junto da mensagem do usuário
153
+ * (não no system prompt). **Fica gravado** com a mensagem (`declared_context`) e se repete em
154
+ * todo turno; o BE redige segredos por nome de chave E por formato de valor (ex.: `sk-…`), o
155
+ * que pode alterar valores legítimos — ids que vão a tools ou ao `sessionFrom` ficam melhor em
156
+ * `bindings`. Plano, só escalares. Tools também o recebem. Só vale sob key admin/owner.
157
+ * ⚠️ Credencial não vai aqui nem em `bindings`: use um secret do tenant.
158
+ */
159
+ context?: Record<string, ContextValue>;
160
+ /**
161
+ * Valores que **só as tools** recebem — nunca vão ao prompt (ex.: id interno de fornecedor).
162
+ * Plano, só escalares; **nunca gravado** nem redigido (vive só no contexto das tools durante o
163
+ * turno). Só vale sob key admin/owner.
164
+ */
165
+ bindings?: Record<string, ContextValue>;
166
+ /**
167
+ * Deriva o `session_id` dos valores destes campos de `context ∪ bindings`, com a MESMA função
168
+ * do webhook (`derive: [campo]`) — o mesmo valor cai na mesma sessão pelos dois caminhos. Não
169
+ * combina com `sessionId`. O id derivado volta em `AgentResult.sessionId`.
170
+ */
171
+ sessionFrom?: string[];
126
172
  options?: InvokeOptions;
127
173
  metadata?: {
128
174
  traceId?: string;
@@ -137,6 +183,15 @@ interface InvokeAgentParams {
137
183
  */
138
184
  includeRaw?: boolean;
139
185
  }
186
+ /** Valor aceito em `context`/`bindings`: plano e escalar (regra 8 do contrato de entrada). */
187
+ type ContextValue = string | number | boolean | null;
188
+ /** Campo que o BE recebeu e NÃO aplicou (evento `metadata.ignored_fields`). */
189
+ interface IgnoredField {
190
+ /** Nome no fio (`context`, `bindings`, `session_from`, `consumer_key`, `options.temperature`…). */
191
+ field: string;
192
+ /** Motivo reportado pelo BE (ex.: papel da key insuficiente), quando houver. */
193
+ reason?: string;
194
+ }
140
195
  interface Usage {
141
196
  inputTokens?: number;
142
197
  outputTokens?: number;
@@ -175,23 +230,71 @@ interface AgentResult {
175
230
  }>;
176
231
  iterations?: number;
177
232
  executionTimeMs?: number;
233
+ /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
234
+ executionId?: string;
235
+ /** Campos do pedido que o BE aplicou (`metadata.accepted_fields`). Ausente = backend antigo. */
236
+ acceptedFields?: string[];
237
+ /** Campos do pedido que o BE recebeu e ignorou (`metadata.ignored_fields`). */
238
+ ignoredFields?: IgnoredField[];
239
+ /**
240
+ * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
241
+ * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
242
+ * conversacional ela pode ser entregue à pessoa; numa automação, trate como "não respondido".
243
+ */
244
+ gated?: boolean;
245
+ /**
246
+ * Desfecho do turno, lido do `done` (`exitReason` do motor). `undefined` = backend antigo ou
247
+ * stream cortado antes do `done`. Só chegam aqui os desfechos cujo `content` é resposta
248
+ * (`COMPLETED`, `LENGTH`), o `PAUSED` (aprovação pendente ou agente pausado) e desfechos novos.
249
+ * `TIMEOUT`, `ERROR`, `CANCELLED`, `SUSPENDED` e os de bloqueio viram `ZihinAgentError` — o
250
+ * `content` desses turnos é a mensagem de falha/recusa do motor (`errorFromDoneChunk`).
251
+ */
252
+ outcome?: AgentOutcome;
253
+ /** `true` quando a resposta foi cortada pelo teto de saída do modelo (`done.truncated`). */
254
+ truncated?: boolean;
255
+ /** Motivo do corte quando `truncated` (ex.: `length`). */
256
+ finishReason?: string;
178
257
  /** Eventos SSE brutos coletados (para depuração / casos avançados). */
179
258
  raw: AgentStreamChunk[];
180
259
  }
260
+ /**
261
+ * Desfecho do turno (`done.outcome` = `resolveTurnOutcome` do motor). `COMPLETED` e `LENGTH`
262
+ * contam como sucesso no BE (`isSuccessfulOutcome`); os demais valores são repassados como vêm.
263
+ * Aberto (`string & {}`) porque o BE pode acrescentar desfechos (ex.: os de bloqueio).
264
+ */
265
+ type AgentOutcome = 'COMPLETED' | 'LENGTH' | 'TIMEOUT' | 'ERROR' | 'CANCELLED' | 'PAUSED' | 'SUSPENDED' | 'BUDGET_EXCEEDED' | 'QUOTA_EXCEEDED' | 'SESSION_BUSY' | 'CONFLICT' | (string & {});
266
+ /**
267
+ * Qual relógio encerrou o turno (contrato de tempo §2): `turn` (prazo do turno, inclusive o
268
+ * `options.timeoutMs` pedido), `first_signal` (o provedor não começou a responder), `idle`
269
+ * (silêncio entre pedaços do stream do provedor) ou `call` (chamada ao modelo sem stream).
270
+ */
271
+ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
181
272
  /**
182
273
  * Chunk normalizado emitido por `streamAgent`. Mapeia os tipos de evento SSE do BE:
183
274
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
184
275
  * warning, response, metrics.
185
276
  */
186
- type AgentStreamChunk = {
277
+ type AgentStreamChunk =
278
+ /**
279
+ * Abertura do stream. `acceptedFields`/`ignoredFields` dizem o que o BE fez com cada campo do
280
+ * pedido (ausentes = backend anterior ao contrato de entrada). Se o chamador mandou
281
+ * `context`/`bindings`/`sessionFrom` e o BE não os aceitou, o `streamAgent` lança
282
+ * `ZihinAgentError('config')` ANTES de entregar este chunk — nunca segue em silêncio.
283
+ */
284
+ {
187
285
  type: 'metadata';
188
286
  requestId?: string;
189
287
  agentId?: string;
190
288
  sessionId?: string;
289
+ acceptedFields?: string[];
290
+ ignoredFields?: IgnoredField[];
191
291
  data: unknown;
192
- } | {
292
+ }
293
+ /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
294
+ | {
193
295
  type: 'status';
194
296
  phase?: string;
297
+ blocked?: string;
195
298
  data: unknown;
196
299
  } | {
197
300
  type: 'attachments';
@@ -250,9 +353,20 @@ type AgentStreamChunk = {
250
353
  sessionId?: string;
251
354
  data: unknown;
252
355
  }
253
- /** Fim LIMPO do stream (`utils/sse.js:92`). Sem ele, o stream foi cortado no meio. */
356
+ /**
357
+ * Fim LIMPO do stream (`utils/sse.js`). Sem ele, o stream foi cortado no meio. Carrega o
358
+ * desfecho do turno: `{ outcome, timed_out?, timeout_clock?, truncated?, finish_reason? }`
359
+ * (aditivo — backend antigo manda `{}`). Um turno que o motor encerrou por tempo chega aqui
360
+ * com `outcome: 'TIMEOUT'` (idem ERROR e os bloqueios), DEPOIS de um `response` cujo `content`
361
+ * é a mensagem de falha — use `errorFromDoneChunk` para não publicá-lo como resposta.
362
+ */
254
363
  | {
255
364
  type: 'done';
365
+ outcome?: AgentOutcome;
366
+ timedOut?: boolean;
367
+ timeoutClock?: TimeoutClock;
368
+ truncated?: boolean;
369
+ finishReason?: string;
256
370
  data: unknown;
257
371
  } | {
258
372
  type: 'response';
@@ -263,6 +377,8 @@ type AgentStreamChunk = {
263
377
  type: string;
264
378
  [k: string]: unknown;
265
379
  }>;
380
+ /** Resposta ENLATADA de um gate (handoff humano, denylist, sessão suspensa) — não do agente. */
381
+ gated?: boolean;
266
382
  data: unknown;
267
383
  } | {
268
384
  type: 'metrics';
@@ -270,6 +386,7 @@ type AgentStreamChunk = {
270
386
  model?: string;
271
387
  iterations?: number;
272
388
  executionTimeMs?: number;
389
+ executionId?: string;
273
390
  data: unknown;
274
391
  } | {
275
392
  type: 'unknown';
@@ -589,8 +706,13 @@ type TimeoutOperation = keyof typeof OPERATION_TIMEOUT_MS;
589
706
  * Folga somada ao `options.timeoutMs` do chamador para formar o deadline LOCAL. O mesmo número vai
590
707
  * ao BE como `options.timeout_ms` (o teto que ele deve respeitar); sem folga, os dois relógios
591
708
  * disparam juntos e o erro do servidor — que tem request_id — perde a corrida para o nosso abort.
709
+ *
710
+ * 30s, não 10s: o relógio do turno no BE só começa DEPOIS da carga (config, tools, gates — um MCP
711
+ * frio leva dezenas de segundos), então o tempo de parede visto daqui é `carga + prazo` (contrato
712
+ * de tempo §4). Com 10s, um `timeoutMs: 60000` num agente com MCP abortava aos 70s e trocava o
713
+ * `done { outcome: 'TIMEOUT' }` do servidor por um abort cego.
592
714
  */
593
- declare const DEADLINE_GRACE_MS = 10000;
715
+ declare const DEADLINE_GRACE_MS = 30000;
594
716
 
595
717
  /**
596
718
  * Listagem de agentes do tenant (GET /api/v2/agents). Alimenta o dropdown do nó n8n (#17)
@@ -689,7 +811,7 @@ declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
689
811
  * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
690
812
  * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
691
813
  */
692
- declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
814
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server' | 'config';
693
815
  /**
694
816
  * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
695
817
  * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
@@ -698,6 +820,28 @@ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'tim
698
820
  declare function errorFromStreamChunk(chunk: Extract<AgentStreamChunk, {
699
821
  type: 'error';
700
822
  }>): ZihinAgentError;
823
+ /**
824
+ * Converte o `done` de um turno que NÃO produziu resposta num `ZihinAgentError`; devolve
825
+ * `undefined` quando o `content` é resposta. Mesma regra do `invokeAgent`, exportada para quem
826
+ * consome `streamAgent` direto (o Paperclip) não reimplementá-la.
827
+ *
828
+ * Por que erro e não um campo no resultado: nesses turnos o BE já emitiu um `response` com a
829
+ * mensagem de falha do motor em `content` (contrato de tempo §6). Como campo, cada shell teria
830
+ * de lembrar de checá-lo — e o que esquecesse publicaria a falha como resposta, que é o defeito
831
+ * que isto fecha (#81). Como erro, o caminho seguro é o default.
832
+ *
833
+ * TIMEOUT: `outcome: 'TIMEOUT'` é a fonte primária; `timed_out: true` sozinho também conta (o BE
834
+ * só o emite com TIMEOUT). Vira `kind: 'timeout'`, `code: 'TURN_TIMEOUT'`, com `timeoutClock`.
835
+ * Nenhum é retentável por default: o turno pode ter executado tools com efeito colateral
836
+ * (exceção: `SESSION_BUSY`, em que o turno nem começou).
837
+ */
838
+ declare function errorFromDoneChunk(chunk: Extract<AgentStreamChunk, {
839
+ type: 'done';
840
+ }>, ctx?: {
841
+ executionId?: string;
842
+ sessionId?: string;
843
+ blocked?: string;
844
+ }): ZihinAgentError | undefined;
701
845
 
702
846
  /**
703
847
  * Security utilities — portado de n8n-nodes-zihin/src/utils/security.ts (zero-dep).
@@ -821,4 +965,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
821
965
  */
822
966
  declare const MAX_TASK_PAYLOAD_BYTES: number;
823
967
 
824
- 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, classifyStreamErrorKind, createClient, errorFromStreamChunk, isCommercialBlock, sanitizeApiError, signRequest, validateApiKeyFormat, validateBaseUrl, validatePromptSize };
968
+ export { type AgentOutcome, type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type ContextValue, 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 IgnoredField, 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 };