@zihin/agent-client 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,6 +50,104 @@ 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
+
114
+ ### Nome do contato e sugestões de resposta (0.6.1)
115
+
116
+ > Backend: zihin-agent-builder `1aa752e6` (+ migration `20261001000002`). Contra um backend
117
+ > anterior os dois recursos só não acontecem: o turno roda normalmente.
118
+
119
+ ```ts
120
+ const r = await zihin.invokeAgent({
121
+ agentId,
122
+ message: 'Quero canetas azuis',
123
+ consumer: { key: 'dev-cotai-1410', name: 'Fernando Produtor' }, // nome aparece nos contatos do console
124
+ capabilities: { blocks: ['quick_replies'] }, // minha tela mostra sugestões
125
+ });
126
+ r.suggestions; // ['Caneta A', 'Caneta B'] — ou undefined quando o agente não sugeriu
127
+ ```
128
+
129
+ - **`consumer: { key, name? }`** é o `consumerKey` com o nome junto. `consumer.key` e `consumerKey`
130
+ são o mesmo campo (mande um; se vierem os dois, têm de ser iguais) e seguem a mesma regra: só
131
+ valem sob API key admin/owner, e o núcleo recusa o turno se o servidor os ignorar. O nome vai como
132
+ `consumer_display_name` (até 255 caracteres) e vale **do presente em diante**: atualiza o
133
+ contato e a conversa em andamento; conversas encerradas guardam o nome da época (o histórico não
134
+ é reescrito). Dado pessoal: mande só o que a operação precisa ver.
135
+ - **Uma `consumer.key` por pessoa.** A chave é a identidade: memória, bloqueio e engajamento são
136
+ dela. Se várias pessoas usam o mesmo canal (ex.: um WhatsApp comercial que troca de vendedor),
137
+ dê a cada uma a sua chave — trocar só o `name` não separa as pessoas.
138
+ - **`capabilities: { blocks: ['quick_replies'] }`** diz que a sua tela renderiza sugestões de
139
+ resposta. Sem declarar, o agente nem recebe a ferramenta — nada é gerado e nada é cobrado. É um
140
+ **pedido**, não uma exigência: qualquer API key pode mandar, e um servidor que não o conheça só o
141
+ reporta em `ignoredFields` (o turno não falha). O dono do agente pode desligar as sugestões
142
+ (`quick_replies_enabled`), e o agente decide quando vale sugerir — nem todo turno traz.
143
+ - **`suggestions`** (em `AgentResult` e no chunk `response`) são os rótulos do bloco `quick_replies`
144
+ de `blocks`, já filtrados: até 4, curtos. Tocar uma sugestão = mandar o texto dela como a próxima
145
+ `message`. Num turno só com sugestões, `content` vem **vazio**.
146
+ - **Descontinuados**: `options.jitMode`, `options.temperature` e `options.maxTokens` não têm efeito no
147
+ `/stream` (temperatura e teto de saída são configuração do agente). Continuam sendo enviados e o
148
+ servidor os lista em `ignoredFields`; o núcleo emite um `ZihinDeprecationWarning` **uma vez por
149
+ processo** (`ZIHIN_DEP001`/`ZIHIN_DEP002`). Saem do SDK na 1.0.
150
+
53
151
  ## Uso — caminho assíncrono (triggers)
54
152
 
55
153
  ```ts
package/dist/index.d.mts CHANGED
@@ -118,7 +118,17 @@ interface Attachment {
118
118
  type: AttachmentType;
119
119
  }
120
120
  interface InvokeOptions {
121
+ /**
122
+ * @deprecated Sem efeito: o `/stream` usa a temperatura da configuração do agente
123
+ * (`llm_config.temperature`, no console ou via `update_agent`). O servidor ignora o campo e o
124
+ * reporta em `ignoredFields`. Será retirado do SDK na 1.0.
125
+ */
121
126
  temperature?: number;
127
+ /**
128
+ * @deprecated Sem efeito: o teto de saída é a configuração do agente (`llm_config.max_tokens`,
129
+ * no console ou via `update_agent`). O servidor ignora o campo e o reporta em `ignoredFields`.
130
+ * Será retirado do SDK na 1.0.
131
+ */
122
132
  maxTokens?: number;
123
133
  /**
124
134
  * Teto do TURNO desta chamada (ms); sobrepõe o do cliente. Vai ao BE como `options.timeout_ms`
@@ -130,9 +140,41 @@ interface InvokeOptions {
130
140
  * local soma `DEADLINE_GRACE_MS` a este valor para ainda receber o desfecho do servidor.
131
141
  */
132
142
  timeoutMs?: number;
133
- /** Just-in-time tool loading (Sprint 33). */
143
+ /**
144
+ * @deprecated Recurso descontinuado no servidor (ago/2026): sem efeito, reportado em
145
+ * `ignoredFields`. Remova-o. Será retirado do SDK na 1.0.
146
+ */
134
147
  jitMode?: boolean;
135
148
  }
149
+ /** Blocos estruturados que a sua interface sabe renderizar (`capabilities.blocks`). */
150
+ type ClientBlockType = 'quick_replies';
151
+ /**
152
+ * O que a SUA tela sabe mostrar. O agente só oferece o que você declarar — sem declarar, nada é
153
+ * gerado e nada é cobrado. Não exige privilégio de API key.
154
+ */
155
+ interface ClientCapabilities {
156
+ /**
157
+ * `quick_replies`: até 4 sugestões curtas de resposta ao fim do turno (o usuário toca e o texto
158
+ * vira a próxima mensagem). Chegam em `AgentResult.suggestions` (e no bloco `quick_replies` de
159
+ * `blocks`). O dono do agente pode desligá-las (`quick_replies_enabled`); o agente decide quando
160
+ * vale sugerir — nem todo turno traz sugestões.
161
+ */
162
+ blocks?: ClientBlockType[];
163
+ }
164
+ /** A pessoa do outro lado da conversa. Só vale sob API key **admin/owner**. */
165
+ interface ConsumerIdentity {
166
+ /**
167
+ * Identificador estável da pessoa no SEU sistema (memória por pessoa, denylist, gate de
168
+ * engajamento). Para continuar conversas que um webhook já gravava, use o MESMO valor que o
169
+ * webhook usava. Enviado como `consumer_key`.
170
+ */
171
+ key: string;
172
+ /**
173
+ * Nome para exibição no console da Zihin (lista de contatos). Enviado como
174
+ * `consumer_display_name`. Dado pessoal: mande só o que a operação precisa ver.
175
+ */
176
+ name?: string;
177
+ }
136
178
  interface InvokeAgentParams {
137
179
  /** UUID do agente (path :agent_id). */
138
180
  agentId: string;
@@ -140,8 +182,42 @@ interface InvokeAgentParams {
140
182
  message?: string;
141
183
  /** Anexos multimodais. `message` OU `attachments` é obrigatório. */
142
184
  attachments?: Attachment[];
143
- /** Continuidade de conversa (Supabase agent_sessions). */
185
+ /** Continuidade de conversa (Supabase agent_sessions). Não combina com `sessionFrom`. */
144
186
  sessionId?: string;
187
+ /**
188
+ * Identidade da pessoa do outro lado (memória por pessoa, denylist, gate de engajamento). Só vale
189
+ * sob API key **admin/owner**. Para continuar conversas que um webhook já gravava, use o MESMO
190
+ * valor que o webhook usava. Enviado como `consumer_key`.
191
+ */
192
+ consumerKey?: string;
193
+ /**
194
+ * A pessoa do outro lado: `{ key, name? }`. `consumer.key` é o mesmo que `consumerKey` (use um
195
+ * dos dois; se vierem os dois, têm de ser iguais). Só vale sob API key admin/owner.
196
+ */
197
+ consumer?: ConsumerIdentity;
198
+ /** O que a sua interface sabe renderizar — ex.: `{ blocks: ['quick_replies'] }`. */
199
+ capabilities?: ClientCapabilities;
200
+ /**
201
+ * Contexto estruturado que o **modelo lê** — bloco de plataforma junto da mensagem do usuário
202
+ * (não no system prompt). **Fica gravado** com a mensagem (`declared_context`) e se repete em
203
+ * todo turno; o BE redige segredos por nome de chave E por formato de valor (ex.: `sk-…`), o
204
+ * que pode alterar valores legítimos — ids que vão a tools ou ao `sessionFrom` ficam melhor em
205
+ * `bindings`. Plano, só escalares. Tools também o recebem. Só vale sob key admin/owner.
206
+ * ⚠️ Credencial não vai aqui nem em `bindings`: use um secret do tenant.
207
+ */
208
+ context?: Record<string, ContextValue>;
209
+ /**
210
+ * Valores que **só as tools** recebem — nunca vão ao prompt (ex.: id interno de fornecedor).
211
+ * Plano, só escalares; **nunca gravado** nem redigido (vive só no contexto das tools durante o
212
+ * turno). Só vale sob key admin/owner.
213
+ */
214
+ bindings?: Record<string, ContextValue>;
215
+ /**
216
+ * Deriva o `session_id` dos valores destes campos de `context ∪ bindings`, com a MESMA função
217
+ * do webhook (`derive: [campo]`) — o mesmo valor cai na mesma sessão pelos dois caminhos. Não
218
+ * combina com `sessionId`. O id derivado volta em `AgentResult.sessionId`.
219
+ */
220
+ sessionFrom?: string[];
145
221
  options?: InvokeOptions;
146
222
  metadata?: {
147
223
  traceId?: string;
@@ -156,6 +232,15 @@ interface InvokeAgentParams {
156
232
  */
157
233
  includeRaw?: boolean;
158
234
  }
235
+ /** Valor aceito em `context`/`bindings`: plano e escalar (regra 8 do contrato de entrada). */
236
+ type ContextValue = string | number | boolean | null;
237
+ /** Campo que o BE recebeu e NÃO aplicou (evento `metadata.ignored_fields`). */
238
+ interface IgnoredField {
239
+ /** Nome no fio (`context`, `bindings`, `session_from`, `consumer_key`, `options.temperature`…). */
240
+ field: string;
241
+ /** Motivo reportado pelo BE (ex.: papel da key insuficiente), quando houver. */
242
+ reason?: string;
243
+ }
159
244
  interface Usage {
160
245
  inputTokens?: number;
161
246
  outputTokens?: number;
@@ -192,10 +277,20 @@ interface AgentResult {
192
277
  type: string;
193
278
  [k: string]: unknown;
194
279
  }>;
280
+ /**
281
+ * Sugestões de resposta do turno (bloco `quick_replies`), já extraídas de `blocks`. Só vêm se
282
+ * você declarou `capabilities: { blocks: ['quick_replies'] }`. O texto de cada uma é o que o
283
+ * usuário "diz" ao tocá-la. Num turno só com sugestões, `content` vem vazio.
284
+ */
285
+ suggestions?: string[];
195
286
  iterations?: number;
196
287
  executionTimeMs?: number;
197
288
  /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
289
  executionId?: string;
290
+ /** Campos do pedido que o BE aplicou (`metadata.accepted_fields`). Ausente = backend antigo. */
291
+ acceptedFields?: string[];
292
+ /** Campos do pedido que o BE recebeu e ignorou (`metadata.ignored_fields`). */
293
+ ignoredFields?: IgnoredField[];
199
294
  /**
200
295
  * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
296
  * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
@@ -234,11 +329,20 @@ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
234
329
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
235
330
  * warning, response, metrics.
236
331
  */
237
- type AgentStreamChunk = {
332
+ type AgentStreamChunk =
333
+ /**
334
+ * Abertura do stream. `acceptedFields`/`ignoredFields` dizem o que o BE fez com cada campo do
335
+ * pedido (ausentes = backend anterior ao contrato de entrada). Se o chamador mandou
336
+ * `context`/`bindings`/`sessionFrom` e o BE não os aceitou, o `streamAgent` lança
337
+ * `ZihinAgentError('config')` ANTES de entregar este chunk — nunca segue em silêncio.
338
+ */
339
+ {
238
340
  type: 'metadata';
239
341
  requestId?: string;
240
342
  agentId?: string;
241
343
  sessionId?: string;
344
+ acceptedFields?: string[];
345
+ ignoredFields?: IgnoredField[];
242
346
  data: unknown;
243
347
  }
244
348
  /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
@@ -328,6 +432,8 @@ type AgentStreamChunk = {
328
432
  type: string;
329
433
  [k: string]: unknown;
330
434
  }>;
435
+ /** Sugestões de resposta (bloco `quick_replies`), extraídas de `blocks`. */
436
+ suggestions?: string[];
331
437
  /** Resposta ENLATADA de um gate (handoff humano, denylist, sessão suspensa) — não do agente. */
332
438
  gated?: boolean;
333
439
  data: unknown;
@@ -762,7 +868,7 @@ declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
762
868
  * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
869
  * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
870
  */
765
- declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
871
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server' | 'config';
766
872
  /**
767
873
  * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
874
  * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
@@ -916,4 +1022,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
916
1022
  */
917
1023
  declare const MAX_TASK_PAYLOAD_BYTES: number;
918
1024
 
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 };
1025
+ export { type AgentOutcome, type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type ClientBlockType, type ClientCapabilities, type ConsumerIdentity, 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 };
package/dist/index.d.ts CHANGED
@@ -118,7 +118,17 @@ interface Attachment {
118
118
  type: AttachmentType;
119
119
  }
120
120
  interface InvokeOptions {
121
+ /**
122
+ * @deprecated Sem efeito: o `/stream` usa a temperatura da configuração do agente
123
+ * (`llm_config.temperature`, no console ou via `update_agent`). O servidor ignora o campo e o
124
+ * reporta em `ignoredFields`. Será retirado do SDK na 1.0.
125
+ */
121
126
  temperature?: number;
127
+ /**
128
+ * @deprecated Sem efeito: o teto de saída é a configuração do agente (`llm_config.max_tokens`,
129
+ * no console ou via `update_agent`). O servidor ignora o campo e o reporta em `ignoredFields`.
130
+ * Será retirado do SDK na 1.0.
131
+ */
122
132
  maxTokens?: number;
123
133
  /**
124
134
  * Teto do TURNO desta chamada (ms); sobrepõe o do cliente. Vai ao BE como `options.timeout_ms`
@@ -130,9 +140,41 @@ interface InvokeOptions {
130
140
  * local soma `DEADLINE_GRACE_MS` a este valor para ainda receber o desfecho do servidor.
131
141
  */
132
142
  timeoutMs?: number;
133
- /** Just-in-time tool loading (Sprint 33). */
143
+ /**
144
+ * @deprecated Recurso descontinuado no servidor (ago/2026): sem efeito, reportado em
145
+ * `ignoredFields`. Remova-o. Será retirado do SDK na 1.0.
146
+ */
134
147
  jitMode?: boolean;
135
148
  }
149
+ /** Blocos estruturados que a sua interface sabe renderizar (`capabilities.blocks`). */
150
+ type ClientBlockType = 'quick_replies';
151
+ /**
152
+ * O que a SUA tela sabe mostrar. O agente só oferece o que você declarar — sem declarar, nada é
153
+ * gerado e nada é cobrado. Não exige privilégio de API key.
154
+ */
155
+ interface ClientCapabilities {
156
+ /**
157
+ * `quick_replies`: até 4 sugestões curtas de resposta ao fim do turno (o usuário toca e o texto
158
+ * vira a próxima mensagem). Chegam em `AgentResult.suggestions` (e no bloco `quick_replies` de
159
+ * `blocks`). O dono do agente pode desligá-las (`quick_replies_enabled`); o agente decide quando
160
+ * vale sugerir — nem todo turno traz sugestões.
161
+ */
162
+ blocks?: ClientBlockType[];
163
+ }
164
+ /** A pessoa do outro lado da conversa. Só vale sob API key **admin/owner**. */
165
+ interface ConsumerIdentity {
166
+ /**
167
+ * Identificador estável da pessoa no SEU sistema (memória por pessoa, denylist, gate de
168
+ * engajamento). Para continuar conversas que um webhook já gravava, use o MESMO valor que o
169
+ * webhook usava. Enviado como `consumer_key`.
170
+ */
171
+ key: string;
172
+ /**
173
+ * Nome para exibição no console da Zihin (lista de contatos). Enviado como
174
+ * `consumer_display_name`. Dado pessoal: mande só o que a operação precisa ver.
175
+ */
176
+ name?: string;
177
+ }
136
178
  interface InvokeAgentParams {
137
179
  /** UUID do agente (path :agent_id). */
138
180
  agentId: string;
@@ -140,8 +182,42 @@ interface InvokeAgentParams {
140
182
  message?: string;
141
183
  /** Anexos multimodais. `message` OU `attachments` é obrigatório. */
142
184
  attachments?: Attachment[];
143
- /** Continuidade de conversa (Supabase agent_sessions). */
185
+ /** Continuidade de conversa (Supabase agent_sessions). Não combina com `sessionFrom`. */
144
186
  sessionId?: string;
187
+ /**
188
+ * Identidade da pessoa do outro lado (memória por pessoa, denylist, gate de engajamento). Só vale
189
+ * sob API key **admin/owner**. Para continuar conversas que um webhook já gravava, use o MESMO
190
+ * valor que o webhook usava. Enviado como `consumer_key`.
191
+ */
192
+ consumerKey?: string;
193
+ /**
194
+ * A pessoa do outro lado: `{ key, name? }`. `consumer.key` é o mesmo que `consumerKey` (use um
195
+ * dos dois; se vierem os dois, têm de ser iguais). Só vale sob API key admin/owner.
196
+ */
197
+ consumer?: ConsumerIdentity;
198
+ /** O que a sua interface sabe renderizar — ex.: `{ blocks: ['quick_replies'] }`. */
199
+ capabilities?: ClientCapabilities;
200
+ /**
201
+ * Contexto estruturado que o **modelo lê** — bloco de plataforma junto da mensagem do usuário
202
+ * (não no system prompt). **Fica gravado** com a mensagem (`declared_context`) e se repete em
203
+ * todo turno; o BE redige segredos por nome de chave E por formato de valor (ex.: `sk-…`), o
204
+ * que pode alterar valores legítimos — ids que vão a tools ou ao `sessionFrom` ficam melhor em
205
+ * `bindings`. Plano, só escalares. Tools também o recebem. Só vale sob key admin/owner.
206
+ * ⚠️ Credencial não vai aqui nem em `bindings`: use um secret do tenant.
207
+ */
208
+ context?: Record<string, ContextValue>;
209
+ /**
210
+ * Valores que **só as tools** recebem — nunca vão ao prompt (ex.: id interno de fornecedor).
211
+ * Plano, só escalares; **nunca gravado** nem redigido (vive só no contexto das tools durante o
212
+ * turno). Só vale sob key admin/owner.
213
+ */
214
+ bindings?: Record<string, ContextValue>;
215
+ /**
216
+ * Deriva o `session_id` dos valores destes campos de `context ∪ bindings`, com a MESMA função
217
+ * do webhook (`derive: [campo]`) — o mesmo valor cai na mesma sessão pelos dois caminhos. Não
218
+ * combina com `sessionId`. O id derivado volta em `AgentResult.sessionId`.
219
+ */
220
+ sessionFrom?: string[];
145
221
  options?: InvokeOptions;
146
222
  metadata?: {
147
223
  traceId?: string;
@@ -156,6 +232,15 @@ interface InvokeAgentParams {
156
232
  */
157
233
  includeRaw?: boolean;
158
234
  }
235
+ /** Valor aceito em `context`/`bindings`: plano e escalar (regra 8 do contrato de entrada). */
236
+ type ContextValue = string | number | boolean | null;
237
+ /** Campo que o BE recebeu e NÃO aplicou (evento `metadata.ignored_fields`). */
238
+ interface IgnoredField {
239
+ /** Nome no fio (`context`, `bindings`, `session_from`, `consumer_key`, `options.temperature`…). */
240
+ field: string;
241
+ /** Motivo reportado pelo BE (ex.: papel da key insuficiente), quando houver. */
242
+ reason?: string;
243
+ }
159
244
  interface Usage {
160
245
  inputTokens?: number;
161
246
  outputTokens?: number;
@@ -192,10 +277,20 @@ interface AgentResult {
192
277
  type: string;
193
278
  [k: string]: unknown;
194
279
  }>;
280
+ /**
281
+ * Sugestões de resposta do turno (bloco `quick_replies`), já extraídas de `blocks`. Só vêm se
282
+ * você declarou `capabilities: { blocks: ['quick_replies'] }`. O texto de cada uma é o que o
283
+ * usuário "diz" ao tocá-la. Num turno só com sugestões, `content` vem vazio.
284
+ */
285
+ suggestions?: string[];
195
286
  iterations?: number;
196
287
  executionTimeMs?: number;
197
288
  /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
289
  executionId?: string;
290
+ /** Campos do pedido que o BE aplicou (`metadata.accepted_fields`). Ausente = backend antigo. */
291
+ acceptedFields?: string[];
292
+ /** Campos do pedido que o BE recebeu e ignorou (`metadata.ignored_fields`). */
293
+ ignoredFields?: IgnoredField[];
199
294
  /**
200
295
  * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
296
  * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
@@ -234,11 +329,20 @@ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
234
329
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
235
330
  * warning, response, metrics.
236
331
  */
237
- type AgentStreamChunk = {
332
+ type AgentStreamChunk =
333
+ /**
334
+ * Abertura do stream. `acceptedFields`/`ignoredFields` dizem o que o BE fez com cada campo do
335
+ * pedido (ausentes = backend anterior ao contrato de entrada). Se o chamador mandou
336
+ * `context`/`bindings`/`sessionFrom` e o BE não os aceitou, o `streamAgent` lança
337
+ * `ZihinAgentError('config')` ANTES de entregar este chunk — nunca segue em silêncio.
338
+ */
339
+ {
238
340
  type: 'metadata';
239
341
  requestId?: string;
240
342
  agentId?: string;
241
343
  sessionId?: string;
344
+ acceptedFields?: string[];
345
+ ignoredFields?: IgnoredField[];
242
346
  data: unknown;
243
347
  }
244
348
  /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
@@ -328,6 +432,8 @@ type AgentStreamChunk = {
328
432
  type: string;
329
433
  [k: string]: unknown;
330
434
  }>;
435
+ /** Sugestões de resposta (bloco `quick_replies`), extraídas de `blocks`. */
436
+ suggestions?: string[];
331
437
  /** Resposta ENLATADA de um gate (handoff humano, denylist, sessão suspensa) — não do agente. */
332
438
  gated?: boolean;
333
439
  data: unknown;
@@ -762,7 +868,7 @@ declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
762
868
  * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
869
  * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
870
  */
765
- declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
871
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server' | 'config';
766
872
  /**
767
873
  * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
874
  * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
@@ -916,4 +1022,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
916
1022
  */
917
1023
  declare const MAX_TASK_PAYLOAD_BYTES: number;
918
1024
 
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 };
1025
+ export { type AgentOutcome, type AgentResult, type AgentStreamChunk, type AgentSummary, type Attachment, type AttachmentType, type CancelTaskParams, type CancelTaskResult, type ClientBlockType, type ClientCapabilities, type ConsumerIdentity, 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 };
package/dist/index.js CHANGED
@@ -89,6 +89,20 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
89
89
  function isTerminalStatus(status) {
90
90
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
91
91
  }
92
+ var _warnedDeprecations = /* @__PURE__ */ new Set();
93
+ function warnDeprecatedOnce(code, message) {
94
+ if (_warnedDeprecations.has(code)) return;
95
+ _warnedDeprecations.add(code);
96
+ try {
97
+ const proc = globalThis.process;
98
+ if (typeof proc?.emitWarning === "function") {
99
+ proc.emitWarning(message, { type: "ZihinDeprecationWarning", code });
100
+ return;
101
+ }
102
+ console.warn(`[${code}] ${message}`);
103
+ } catch {
104
+ }
105
+ }
92
106
 
93
107
  // src/security.ts
94
108
  var PRIVATE_IP_PATTERNS = [
@@ -910,6 +924,29 @@ async function* parseSse(response, opts = {}) {
910
924
  }
911
925
 
912
926
  // src/invoke.ts
927
+ function parseAcceptedFields(v) {
928
+ if (!Array.isArray(v)) return void 0;
929
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
930
+ }
931
+ function parseIgnoredFields(v) {
932
+ if (!Array.isArray(v)) return void 0;
933
+ const out = [];
934
+ for (const x of v) {
935
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
936
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
937
+ }
938
+ return out;
939
+ }
940
+ var MAX_SUGGESTIONS = 10;
941
+ var MAX_SUGGESTION_CHARS = 200;
942
+ function suggestionsFromBlocks(blocks) {
943
+ if (!Array.isArray(blocks)) return void 0;
944
+ const qr = [...blocks].reverse().find((b) => rec(b).type === "quick_replies");
945
+ const labels = rec(rec(qr).data).labels;
946
+ if (!Array.isArray(labels)) return void 0;
947
+ const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
948
+ return out.length ? out : void 0;
949
+ }
913
950
  function mapEvent(e) {
914
951
  const d = rec(e.data);
915
952
  switch (e.event) {
@@ -919,6 +956,8 @@ function mapEvent(e) {
919
956
  requestId: safeId(d.request_id),
920
957
  agentId: str(d.agent_id),
921
958
  sessionId: safeId(d.session_id),
959
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
960
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
922
961
  data: e.data
923
962
  };
924
963
  case "status":
@@ -974,6 +1013,7 @@ function mapEvent(e) {
974
1013
  gated: d.gated === true ? true : void 0,
975
1014
  sources: Array.isArray(d.sources) ? d.sources : void 0,
976
1015
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
1016
+ suggestions: suggestionsFromBlocks(d.blocks),
977
1017
  data: e.data
978
1018
  };
979
1019
  case "metrics": {
@@ -996,6 +1036,12 @@ function buildRequestBody(params, normalizedTimeoutMs) {
996
1036
  const body = {};
997
1037
  if (params.message !== void 0) body.message = params.message;
998
1038
  if (params.sessionId) body.session_id = params.sessionId;
1039
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
1040
+ if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
1041
+ if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
1042
+ if (params.context) body.context = { ...params.context };
1043
+ if (params.bindings) body.bindings = { ...params.bindings };
1044
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
999
1045
  if (params.attachments?.length) {
1000
1046
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
1001
1047
  }
@@ -1034,6 +1080,8 @@ function validateParams(params) {
1034
1080
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
1035
1081
  }
1036
1082
  }
1083
+ validateStructuredInput(params);
1084
+ warnDeprecatedOptions(params);
1037
1085
  if (params.options?.timeoutMs !== void 0) {
1038
1086
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1039
1087
  }
@@ -1044,12 +1092,193 @@ function validateParams(params) {
1044
1092
  }
1045
1093
  }
1046
1094
  }
1047
- async function* streamAgent(cfg, params) {
1095
+ function warnDeprecatedOptions(params) {
1096
+ const o = params.options;
1097
+ if (!o) return;
1098
+ if (o.jitMode !== void 0) {
1099
+ warnDeprecatedOnce(
1100
+ "ZIHIN_DEP001",
1101
+ "`options.jitMode` is deprecated and has no effect (the server discontinued it). Remove it; it will be dropped from the SDK in 1.0."
1102
+ );
1103
+ }
1104
+ if (o.temperature !== void 0 || o.maxTokens !== void 0) {
1105
+ warnDeprecatedOnce(
1106
+ "ZIHIN_DEP002",
1107
+ "`options.temperature`/`options.maxTokens` have no effect: the agent's own configuration (`llm_config`) decides them \u2014 change it in the Zihin console or with `update_agent`. They will be dropped from the SDK in 1.0."
1108
+ );
1109
+ }
1110
+ }
1111
+ var MAX_FIELDS = 50;
1112
+ var MAX_VALUE_CHARS = 2e3;
1113
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1114
+ var MAX_SESSION_FROM_FIELDS = 5;
1115
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1116
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1117
+ var MAX_CONSUMER_KEY_CHARS = 200;
1118
+ var MAX_CONSUMER_NAME_CHARS = 255;
1119
+ var MAX_CLIENT_BLOCKS = 10;
1120
+ function snapshotStructuredInput(params) {
1121
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1122
+ return {
1123
+ ...params,
1124
+ ...normalizeConsumer(params),
1125
+ context: snap(params.context),
1126
+ bindings: snap(params.bindings),
1127
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom,
1128
+ capabilities: isPlainObject(params.capabilities) ? {
1129
+ ...params.capabilities,
1130
+ blocks: Array.isArray(params.capabilities.blocks) ? [...params.capabilities.blocks] : params.capabilities.blocks
1131
+ } : params.capabilities
1132
+ };
1133
+ }
1134
+ function normalizeConsumer(params) {
1135
+ const c = params.consumer;
1136
+ if (c === void 0) return {};
1137
+ if (!isPlainObject(c)) {
1138
+ throw new ZihinAgentError("config", "`consumer` must be an object: `{ key, name? }`.");
1139
+ }
1140
+ const key = c.key;
1141
+ if (params.consumerKey !== void 0 && key !== void 0 && params.consumerKey !== key) {
1142
+ throw new ZihinAgentError("config", "`consumerKey` and `consumer.key` differ \u2014 send only one of them.");
1143
+ }
1144
+ const resolved = key ?? params.consumerKey;
1145
+ return {
1146
+ consumerKey: resolved,
1147
+ consumer: { key: resolved, ...c.name !== void 0 ? { name: c.name } : {} }
1148
+ };
1149
+ }
1150
+ function isPlainObject(v) {
1151
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1152
+ const proto = Object.getPrototypeOf(v);
1153
+ return proto === Object.prototype || proto === null;
1154
+ }
1155
+ function isScalar(v) {
1156
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1157
+ }
1158
+ function validateStructuredInput(params) {
1159
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1160
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1161
+ }
1162
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1163
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1164
+ }
1165
+ if (params.consumer !== void 0) {
1166
+ if (params.consumerKey === void 0) {
1167
+ throw new ZihinAgentError("config", "`consumer.key` is required (the name has nothing to attach to without it).");
1168
+ }
1169
+ const n = params.consumer.name;
1170
+ if (n !== void 0 && (typeof n !== "string" || !n.trim() || n.length > MAX_CONSUMER_NAME_CHARS)) {
1171
+ throw new ZihinAgentError("config", `\`consumer.name\` must be a non-empty string of up to ${MAX_CONSUMER_NAME_CHARS} characters.`);
1172
+ }
1173
+ }
1174
+ if (params.capabilities !== void 0) {
1175
+ const blocks = isPlainObject(params.capabilities) ? params.capabilities.blocks : null;
1176
+ if (blocks === null || blocks !== void 0 && (!Array.isArray(blocks) || blocks.length > MAX_CLIENT_BLOCKS || blocks.some((b) => typeof b !== "string"))) {
1177
+ throw new ZihinAgentError(
1178
+ "config",
1179
+ `\`capabilities\` must be \`{ blocks: [...] }\` with up to ${MAX_CLIENT_BLOCKS} block names (e.g. \`['quick_replies']\`).`
1180
+ );
1181
+ }
1182
+ }
1183
+ for (const name of ["context", "bindings"]) {
1184
+ const v = params[name];
1185
+ if (v === void 0) continue;
1186
+ if (!isPlainObject(v)) {
1187
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1188
+ }
1189
+ const entries = Object.entries(v);
1190
+ if (entries.length > MAX_FIELDS) {
1191
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1192
+ }
1193
+ for (const [k, val] of entries) {
1194
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1195
+ throw new ZihinAgentError(
1196
+ "config",
1197
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1198
+ );
1199
+ }
1200
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1201
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1202
+ }
1203
+ if (!isScalar(val)) {
1204
+ throw new ZihinAgentError(
1205
+ "config",
1206
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1207
+ );
1208
+ }
1209
+ }
1210
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1211
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1212
+ }
1213
+ }
1214
+ if (params.sessionFrom !== void 0) {
1215
+ if (!Array.isArray(params.sessionFrom) || params.sessionFrom.length > MAX_SESSION_FROM_FIELDS || params.sessionFrom.some((f) => typeof f !== "string" || !FIELD_NAME_RE.test(f) || PROTOTYPE_KEYS.has(f))) {
1216
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1217
+ }
1218
+ if (params.sessionFrom.length && params.sessionId) {
1219
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1220
+ }
1221
+ const available = { ...params.context, ...params.bindings };
1222
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1223
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1224
+ throw new ZihinAgentError(
1225
+ "config",
1226
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1227
+ );
1228
+ }
1229
+ }
1230
+ }
1231
+ function requestedInputFields(params) {
1232
+ const out = [];
1233
+ if (params.context) out.push("context");
1234
+ if (params.bindings) out.push("bindings");
1235
+ if (params.sessionFrom?.length) out.push("session_from");
1236
+ return out;
1237
+ }
1238
+ function checkInputCapability(params, chunk) {
1239
+ const requested = requestedInputFields(params);
1240
+ const ignored = chunk?.ignoredFields ?? [];
1241
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1242
+ const blocking = ignored.filter(
1243
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1244
+ );
1245
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1246
+ if (blocking.length) {
1247
+ const shown = blocking.slice(0, 5);
1248
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1249
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1250
+ const hint = blocking.some((f) => f.reason === "reserved_key") ? " Reserved keys are names the agent engine uses internally; rename them." : blocking.some((f) => f.reason === "requires_admin_api_key") ? " These fields only apply with an admin/owner API key." : "";
1251
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1252
+ requestId: chunk?.requestId
1253
+ });
1254
+ }
1255
+ if (requested.length && chunk?.acceptedFields === void 0) {
1256
+ throw new ZihinAgentError(
1257
+ "config",
1258
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1259
+ { requestId: chunk?.requestId }
1260
+ );
1261
+ }
1262
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1263
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1264
+ if (notAccepted.length) {
1265
+ throw new ZihinAgentError(
1266
+ "config",
1267
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1268
+ { requestId: chunk?.requestId }
1269
+ );
1270
+ }
1271
+ }
1272
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1273
+ async function* streamAgent(cfg, rawParams) {
1274
+ const params = snapshotStructuredInput(rawParams);
1048
1275
  validateParams(params);
1049
1276
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1050
1277
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1051
1278
  const reportedMs = callerTimeoutMs ?? timeoutMs;
1052
1279
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1280
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1281
+ let gated = false;
1053
1282
  const { response, deadline, endpoint } = await openStream({
1054
1283
  url,
1055
1284
  headers: buildHeaders(cfg.apiKey),
@@ -1077,7 +1306,14 @@ async function* streamAgent(cfg, params) {
1077
1306
  maxEventBytes: cfg.maxStreamEventBytes,
1078
1307
  maxTotalBytes: cfg.maxStreamTotalBytes
1079
1308
  })) {
1080
- yield mapEvent(e);
1309
+ const chunk = mapEvent(e);
1310
+ if (needsCapability) {
1311
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1312
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1313
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1314
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1315
+ }
1316
+ yield chunk;
1081
1317
  }
1082
1318
  } finally {
1083
1319
  deadline.dispose();
@@ -1085,6 +1321,7 @@ async function* streamAgent(cfg, params) {
1085
1321
  }
1086
1322
  function classifyStreamErrorKind(code, timedOut) {
1087
1323
  if (timedOut === true) return "timeout";
1324
+ if (code === "VALIDATION_ERROR") return "config";
1088
1325
  if (timedOut === false) return "server";
1089
1326
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1090
1327
  }
@@ -1177,6 +1414,8 @@ async function invokeAgent(cfg, params) {
1177
1414
  let mintedSessionId;
1178
1415
  let doneChunk;
1179
1416
  let blocked;
1417
+ let acceptedFields;
1418
+ let ignoredFields;
1180
1419
  let tokenBuffer = "";
1181
1420
  for await (const chunk of streamAgent(cfg, params)) {
1182
1421
  if (keepRaw) raw.push(chunk);
@@ -1186,8 +1425,11 @@ async function invokeAgent(cfg, params) {
1186
1425
  if (chunk.type === "response") responseChunk = chunk;
1187
1426
  else if (chunk.type === "metrics") metricsChunk = chunk;
1188
1427
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1189
- else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1190
- else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1428
+ else if (chunk.type === "metadata") {
1429
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1430
+ acceptedFields = chunk.acceptedFields;
1431
+ ignoredFields = chunk.ignoredFields;
1432
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1191
1433
  else if (chunk.type === "done") doneChunk = chunk;
1192
1434
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1193
1435
  }
@@ -1210,6 +1452,8 @@ async function invokeAgent(cfg, params) {
1210
1452
  iterations: metricsChunk?.iterations,
1211
1453
  executionTimeMs: metricsChunk?.executionTimeMs,
1212
1454
  executionId,
1455
+ acceptedFields,
1456
+ ignoredFields,
1213
1457
  raw
1214
1458
  };
1215
1459
  }
@@ -1223,6 +1467,7 @@ async function invokeAgent(cfg, params) {
1223
1467
  usage: metricsChunk?.usage,
1224
1468
  sources: responseChunk.sources,
1225
1469
  blocks: responseChunk.blocks,
1470
+ suggestions: responseChunk.suggestions,
1226
1471
  gated: responseChunk.gated,
1227
1472
  iterations: metricsChunk?.iterations,
1228
1473
  executionTimeMs: metricsChunk?.executionTimeMs,
@@ -1230,6 +1475,8 @@ async function invokeAgent(cfg, params) {
1230
1475
  outcome: doneChunk?.outcome,
1231
1476
  truncated: doneChunk?.truncated,
1232
1477
  finishReason: doneChunk?.finishReason,
1478
+ acceptedFields,
1479
+ ignoredFields,
1233
1480
  raw
1234
1481
  };
1235
1482
  }
package/dist/index.mjs CHANGED
@@ -55,6 +55,20 @@ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
55
55
  function isTerminalStatus(status) {
56
56
  return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
57
57
  }
58
+ var _warnedDeprecations = /* @__PURE__ */ new Set();
59
+ function warnDeprecatedOnce(code, message) {
60
+ if (_warnedDeprecations.has(code)) return;
61
+ _warnedDeprecations.add(code);
62
+ try {
63
+ const proc = globalThis.process;
64
+ if (typeof proc?.emitWarning === "function") {
65
+ proc.emitWarning(message, { type: "ZihinDeprecationWarning", code });
66
+ return;
67
+ }
68
+ console.warn(`[${code}] ${message}`);
69
+ } catch {
70
+ }
71
+ }
58
72
 
59
73
  // src/security.ts
60
74
  var PRIVATE_IP_PATTERNS = [
@@ -876,6 +890,29 @@ async function* parseSse(response, opts = {}) {
876
890
  }
877
891
 
878
892
  // src/invoke.ts
893
+ function parseAcceptedFields(v) {
894
+ if (!Array.isArray(v)) return void 0;
895
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
896
+ }
897
+ function parseIgnoredFields(v) {
898
+ if (!Array.isArray(v)) return void 0;
899
+ const out = [];
900
+ for (const x of v) {
901
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
902
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
903
+ }
904
+ return out;
905
+ }
906
+ var MAX_SUGGESTIONS = 10;
907
+ var MAX_SUGGESTION_CHARS = 200;
908
+ function suggestionsFromBlocks(blocks) {
909
+ if (!Array.isArray(blocks)) return void 0;
910
+ const qr = [...blocks].reverse().find((b) => rec(b).type === "quick_replies");
911
+ const labels = rec(rec(qr).data).labels;
912
+ if (!Array.isArray(labels)) return void 0;
913
+ const out = labels.filter((l) => typeof l === "string" && l.trim().length > 0 && l.length <= MAX_SUGGESTION_CHARS).slice(0, MAX_SUGGESTIONS);
914
+ return out.length ? out : void 0;
915
+ }
879
916
  function mapEvent(e) {
880
917
  const d = rec(e.data);
881
918
  switch (e.event) {
@@ -885,6 +922,8 @@ function mapEvent(e) {
885
922
  requestId: safeId(d.request_id),
886
923
  agentId: str(d.agent_id),
887
924
  sessionId: safeId(d.session_id),
925
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
926
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
888
927
  data: e.data
889
928
  };
890
929
  case "status":
@@ -940,6 +979,7 @@ function mapEvent(e) {
940
979
  gated: d.gated === true ? true : void 0,
941
980
  sources: Array.isArray(d.sources) ? d.sources : void 0,
942
981
  blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
982
+ suggestions: suggestionsFromBlocks(d.blocks),
943
983
  data: e.data
944
984
  };
945
985
  case "metrics": {
@@ -962,6 +1002,12 @@ function buildRequestBody(params, normalizedTimeoutMs) {
962
1002
  const body = {};
963
1003
  if (params.message !== void 0) body.message = params.message;
964
1004
  if (params.sessionId) body.session_id = params.sessionId;
1005
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
1006
+ if (params.consumer?.name !== void 0) body.consumer_display_name = params.consumer.name;
1007
+ if (params.capabilities?.blocks?.length) body.capabilities = { blocks: [...params.capabilities.blocks] };
1008
+ if (params.context) body.context = { ...params.context };
1009
+ if (params.bindings) body.bindings = { ...params.bindings };
1010
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
965
1011
  if (params.attachments?.length) {
966
1012
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
967
1013
  }
@@ -1000,6 +1046,8 @@ function validateParams(params) {
1000
1046
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
1001
1047
  }
1002
1048
  }
1049
+ validateStructuredInput(params);
1050
+ warnDeprecatedOptions(params);
1003
1051
  if (params.options?.timeoutMs !== void 0) {
1004
1052
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1005
1053
  }
@@ -1010,12 +1058,193 @@ function validateParams(params) {
1010
1058
  }
1011
1059
  }
1012
1060
  }
1013
- async function* streamAgent(cfg, params) {
1061
+ function warnDeprecatedOptions(params) {
1062
+ const o = params.options;
1063
+ if (!o) return;
1064
+ if (o.jitMode !== void 0) {
1065
+ warnDeprecatedOnce(
1066
+ "ZIHIN_DEP001",
1067
+ "`options.jitMode` is deprecated and has no effect (the server discontinued it). Remove it; it will be dropped from the SDK in 1.0."
1068
+ );
1069
+ }
1070
+ if (o.temperature !== void 0 || o.maxTokens !== void 0) {
1071
+ warnDeprecatedOnce(
1072
+ "ZIHIN_DEP002",
1073
+ "`options.temperature`/`options.maxTokens` have no effect: the agent's own configuration (`llm_config`) decides them \u2014 change it in the Zihin console or with `update_agent`. They will be dropped from the SDK in 1.0."
1074
+ );
1075
+ }
1076
+ }
1077
+ var MAX_FIELDS = 50;
1078
+ var MAX_VALUE_CHARS = 2e3;
1079
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1080
+ var MAX_SESSION_FROM_FIELDS = 5;
1081
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1082
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1083
+ var MAX_CONSUMER_KEY_CHARS = 200;
1084
+ var MAX_CONSUMER_NAME_CHARS = 255;
1085
+ var MAX_CLIENT_BLOCKS = 10;
1086
+ function snapshotStructuredInput(params) {
1087
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1088
+ return {
1089
+ ...params,
1090
+ ...normalizeConsumer(params),
1091
+ context: snap(params.context),
1092
+ bindings: snap(params.bindings),
1093
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom,
1094
+ capabilities: isPlainObject(params.capabilities) ? {
1095
+ ...params.capabilities,
1096
+ blocks: Array.isArray(params.capabilities.blocks) ? [...params.capabilities.blocks] : params.capabilities.blocks
1097
+ } : params.capabilities
1098
+ };
1099
+ }
1100
+ function normalizeConsumer(params) {
1101
+ const c = params.consumer;
1102
+ if (c === void 0) return {};
1103
+ if (!isPlainObject(c)) {
1104
+ throw new ZihinAgentError("config", "`consumer` must be an object: `{ key, name? }`.");
1105
+ }
1106
+ const key = c.key;
1107
+ if (params.consumerKey !== void 0 && key !== void 0 && params.consumerKey !== key) {
1108
+ throw new ZihinAgentError("config", "`consumerKey` and `consumer.key` differ \u2014 send only one of them.");
1109
+ }
1110
+ const resolved = key ?? params.consumerKey;
1111
+ return {
1112
+ consumerKey: resolved,
1113
+ consumer: { key: resolved, ...c.name !== void 0 ? { name: c.name } : {} }
1114
+ };
1115
+ }
1116
+ function isPlainObject(v) {
1117
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1118
+ const proto = Object.getPrototypeOf(v);
1119
+ return proto === Object.prototype || proto === null;
1120
+ }
1121
+ function isScalar(v) {
1122
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1123
+ }
1124
+ function validateStructuredInput(params) {
1125
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1126
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1127
+ }
1128
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1129
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1130
+ }
1131
+ if (params.consumer !== void 0) {
1132
+ if (params.consumerKey === void 0) {
1133
+ throw new ZihinAgentError("config", "`consumer.key` is required (the name has nothing to attach to without it).");
1134
+ }
1135
+ const n = params.consumer.name;
1136
+ if (n !== void 0 && (typeof n !== "string" || !n.trim() || n.length > MAX_CONSUMER_NAME_CHARS)) {
1137
+ throw new ZihinAgentError("config", `\`consumer.name\` must be a non-empty string of up to ${MAX_CONSUMER_NAME_CHARS} characters.`);
1138
+ }
1139
+ }
1140
+ if (params.capabilities !== void 0) {
1141
+ const blocks = isPlainObject(params.capabilities) ? params.capabilities.blocks : null;
1142
+ if (blocks === null || blocks !== void 0 && (!Array.isArray(blocks) || blocks.length > MAX_CLIENT_BLOCKS || blocks.some((b) => typeof b !== "string"))) {
1143
+ throw new ZihinAgentError(
1144
+ "config",
1145
+ `\`capabilities\` must be \`{ blocks: [...] }\` with up to ${MAX_CLIENT_BLOCKS} block names (e.g. \`['quick_replies']\`).`
1146
+ );
1147
+ }
1148
+ }
1149
+ for (const name of ["context", "bindings"]) {
1150
+ const v = params[name];
1151
+ if (v === void 0) continue;
1152
+ if (!isPlainObject(v)) {
1153
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1154
+ }
1155
+ const entries = Object.entries(v);
1156
+ if (entries.length > MAX_FIELDS) {
1157
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1158
+ }
1159
+ for (const [k, val] of entries) {
1160
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1161
+ throw new ZihinAgentError(
1162
+ "config",
1163
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1164
+ );
1165
+ }
1166
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1167
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1168
+ }
1169
+ if (!isScalar(val)) {
1170
+ throw new ZihinAgentError(
1171
+ "config",
1172
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1173
+ );
1174
+ }
1175
+ }
1176
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1177
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1178
+ }
1179
+ }
1180
+ if (params.sessionFrom !== void 0) {
1181
+ if (!Array.isArray(params.sessionFrom) || params.sessionFrom.length > MAX_SESSION_FROM_FIELDS || params.sessionFrom.some((f) => typeof f !== "string" || !FIELD_NAME_RE.test(f) || PROTOTYPE_KEYS.has(f))) {
1182
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1183
+ }
1184
+ if (params.sessionFrom.length && params.sessionId) {
1185
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1186
+ }
1187
+ const available = { ...params.context, ...params.bindings };
1188
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1189
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1190
+ throw new ZihinAgentError(
1191
+ "config",
1192
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1193
+ );
1194
+ }
1195
+ }
1196
+ }
1197
+ function requestedInputFields(params) {
1198
+ const out = [];
1199
+ if (params.context) out.push("context");
1200
+ if (params.bindings) out.push("bindings");
1201
+ if (params.sessionFrom?.length) out.push("session_from");
1202
+ return out;
1203
+ }
1204
+ function checkInputCapability(params, chunk) {
1205
+ const requested = requestedInputFields(params);
1206
+ const ignored = chunk?.ignoredFields ?? [];
1207
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1208
+ const blocking = ignored.filter(
1209
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1210
+ );
1211
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1212
+ if (blocking.length) {
1213
+ const shown = blocking.slice(0, 5);
1214
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1215
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1216
+ const hint = blocking.some((f) => f.reason === "reserved_key") ? " Reserved keys are names the agent engine uses internally; rename them." : blocking.some((f) => f.reason === "requires_admin_api_key") ? " These fields only apply with an admin/owner API key." : "";
1217
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1218
+ requestId: chunk?.requestId
1219
+ });
1220
+ }
1221
+ if (requested.length && chunk?.acceptedFields === void 0) {
1222
+ throw new ZihinAgentError(
1223
+ "config",
1224
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1225
+ { requestId: chunk?.requestId }
1226
+ );
1227
+ }
1228
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1229
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1230
+ if (notAccepted.length) {
1231
+ throw new ZihinAgentError(
1232
+ "config",
1233
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1234
+ { requestId: chunk?.requestId }
1235
+ );
1236
+ }
1237
+ }
1238
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1239
+ async function* streamAgent(cfg, rawParams) {
1240
+ const params = snapshotStructuredInput(rawParams);
1014
1241
  validateParams(params);
1015
1242
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1016
1243
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1017
1244
  const reportedMs = callerTimeoutMs ?? timeoutMs;
1018
1245
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1246
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1247
+ let gated = false;
1019
1248
  const { response, deadline, endpoint } = await openStream({
1020
1249
  url,
1021
1250
  headers: buildHeaders(cfg.apiKey),
@@ -1043,7 +1272,14 @@ async function* streamAgent(cfg, params) {
1043
1272
  maxEventBytes: cfg.maxStreamEventBytes,
1044
1273
  maxTotalBytes: cfg.maxStreamTotalBytes
1045
1274
  })) {
1046
- yield mapEvent(e);
1275
+ const chunk = mapEvent(e);
1276
+ if (needsCapability) {
1277
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1278
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1279
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1280
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1281
+ }
1282
+ yield chunk;
1047
1283
  }
1048
1284
  } finally {
1049
1285
  deadline.dispose();
@@ -1051,6 +1287,7 @@ async function* streamAgent(cfg, params) {
1051
1287
  }
1052
1288
  function classifyStreamErrorKind(code, timedOut) {
1053
1289
  if (timedOut === true) return "timeout";
1290
+ if (code === "VALIDATION_ERROR") return "config";
1054
1291
  if (timedOut === false) return "server";
1055
1292
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1056
1293
  }
@@ -1143,6 +1380,8 @@ async function invokeAgent(cfg, params) {
1143
1380
  let mintedSessionId;
1144
1381
  let doneChunk;
1145
1382
  let blocked;
1383
+ let acceptedFields;
1384
+ let ignoredFields;
1146
1385
  let tokenBuffer = "";
1147
1386
  for await (const chunk of streamAgent(cfg, params)) {
1148
1387
  if (keepRaw) raw.push(chunk);
@@ -1152,8 +1391,11 @@ async function invokeAgent(cfg, params) {
1152
1391
  if (chunk.type === "response") responseChunk = chunk;
1153
1392
  else if (chunk.type === "metrics") metricsChunk = chunk;
1154
1393
  else if (chunk.type === "token") tokenBuffer += chunk.content;
1155
- else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1156
- else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1394
+ else if (chunk.type === "metadata") {
1395
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1396
+ acceptedFields = chunk.acceptedFields;
1397
+ ignoredFields = chunk.ignoredFields;
1398
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1157
1399
  else if (chunk.type === "done") doneChunk = chunk;
1158
1400
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1159
1401
  }
@@ -1176,6 +1418,8 @@ async function invokeAgent(cfg, params) {
1176
1418
  iterations: metricsChunk?.iterations,
1177
1419
  executionTimeMs: metricsChunk?.executionTimeMs,
1178
1420
  executionId,
1421
+ acceptedFields,
1422
+ ignoredFields,
1179
1423
  raw
1180
1424
  };
1181
1425
  }
@@ -1189,6 +1433,7 @@ async function invokeAgent(cfg, params) {
1189
1433
  usage: metricsChunk?.usage,
1190
1434
  sources: responseChunk.sources,
1191
1435
  blocks: responseChunk.blocks,
1436
+ suggestions: responseChunk.suggestions,
1192
1437
  gated: responseChunk.gated,
1193
1438
  iterations: metricsChunk?.iterations,
1194
1439
  executionTimeMs: metricsChunk?.executionTimeMs,
@@ -1196,6 +1441,8 @@ async function invokeAgent(cfg, params) {
1196
1441
  outcome: doneChunk?.outcome,
1197
1442
  truncated: doneChunk?.truncated,
1198
1443
  finishReason: doneChunk?.finishReason,
1444
+ acceptedFields,
1445
+ ignoredFields,
1199
1446
  raw
1200
1447
  };
1201
1448
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zihin/agent-client",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",