@zihin/agent-client 0.5.0 → 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/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
package/dist/index.d.mts CHANGED
@@ -140,8 +140,35 @@ interface InvokeAgentParams {
140
140
  message?: string;
141
141
  /** Anexos multimodais. `message` OU `attachments` é obrigatório. */
142
142
  attachments?: Attachment[];
143
- /** Continuidade de conversa (Supabase agent_sessions). */
143
+ /** Continuidade de conversa (Supabase agent_sessions). Não combina com `sessionFrom`. */
144
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[];
145
172
  options?: InvokeOptions;
146
173
  metadata?: {
147
174
  traceId?: string;
@@ -156,6 +183,15 @@ interface InvokeAgentParams {
156
183
  */
157
184
  includeRaw?: boolean;
158
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
+ }
159
195
  interface Usage {
160
196
  inputTokens?: number;
161
197
  outputTokens?: number;
@@ -196,6 +232,10 @@ interface AgentResult {
196
232
  executionTimeMs?: number;
197
233
  /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
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[];
199
239
  /**
200
240
  * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
241
  * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
@@ -234,11 +274,20 @@ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
234
274
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
235
275
  * warning, response, metrics.
236
276
  */
237
- 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
+ {
238
285
  type: 'metadata';
239
286
  requestId?: string;
240
287
  agentId?: string;
241
288
  sessionId?: string;
289
+ acceptedFields?: string[];
290
+ ignoredFields?: IgnoredField[];
242
291
  data: unknown;
243
292
  }
244
293
  /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
@@ -762,7 +811,7 @@ declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
762
811
  * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
812
  * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
813
  */
765
- declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
814
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server' | 'config';
766
815
  /**
767
816
  * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
817
  * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
@@ -916,4 +965,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
916
965
  */
917
966
  declare const MAX_TASK_PAYLOAD_BYTES: number;
918
967
 
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 };
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 };
package/dist/index.d.ts CHANGED
@@ -140,8 +140,35 @@ interface InvokeAgentParams {
140
140
  message?: string;
141
141
  /** Anexos multimodais. `message` OU `attachments` é obrigatório. */
142
142
  attachments?: Attachment[];
143
- /** Continuidade de conversa (Supabase agent_sessions). */
143
+ /** Continuidade de conversa (Supabase agent_sessions). Não combina com `sessionFrom`. */
144
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[];
145
172
  options?: InvokeOptions;
146
173
  metadata?: {
147
174
  traceId?: string;
@@ -156,6 +183,15 @@ interface InvokeAgentParams {
156
183
  */
157
184
  includeRaw?: boolean;
158
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
+ }
159
195
  interface Usage {
160
196
  inputTokens?: number;
161
197
  outputTokens?: number;
@@ -196,6 +232,10 @@ interface AgentResult {
196
232
  executionTimeMs?: number;
197
233
  /** `execution_id` do turno (evento `metrics`) — handle de correlação no console/telemetria. */
198
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[];
199
239
  /**
200
240
  * `true` = o `content` é a resposta ENLATADA de um gate da plataforma (conversa em atendimento
201
241
  * humano, consumidor bloqueado, sessão suspensa), não do agente: nenhum modelo rodou. Em canal
@@ -234,11 +274,20 @@ type TimeoutClock = 'turn' | 'first_signal' | 'idle' | 'call' | (string & {});
234
274
  * metadata, status, attachments, phase, thinking, tool, action_proposed, token,
235
275
  * warning, response, metrics.
236
276
  */
237
- 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
+ {
238
285
  type: 'metadata';
239
286
  requestId?: string;
240
287
  agentId?: string;
241
288
  sessionId?: string;
289
+ acceptedFields?: string[];
290
+ ignoredFields?: IgnoredField[];
242
291
  data: unknown;
243
292
  }
244
293
  /** `blocked` = motivo do bloqueio do turno (`agent_paused`, `budget_exceeded`…), quando houver. */
@@ -762,7 +811,7 @@ declare function createClient(config: ZihinClientConfig): ZihinAgentClient;
762
811
  * O regex no nome só vale como FALLBACK para backends que ainda não emitem `timed_out`
763
812
  * (self-hosted/antigos). Some numa major futura, depois do rename no BE.
764
813
  */
765
- declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server';
814
+ declare function classifyStreamErrorKind(code: string, timedOut?: boolean): 'timeout' | 'server' | 'config';
766
815
  /**
767
816
  * Converte o chunk `type: 'error'` (evento terminal do BE) num `ZihinAgentError` — a MESMA regra
768
817
  * que o `invokeAgent` aplica, exportada para quem consome `streamAgent` direto (o adapter do
@@ -916,4 +965,4 @@ declare function signRequest(payload: string, secretKey: string, timestamp?: num
916
965
  */
917
966
  declare const MAX_TASK_PAYLOAD_BYTES: number;
918
967
 
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 };
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 };
package/dist/index.js CHANGED
@@ -910,6 +910,19 @@ async function* parseSse(response, opts = {}) {
910
910
  }
911
911
 
912
912
  // src/invoke.ts
913
+ function parseAcceptedFields(v) {
914
+ if (!Array.isArray(v)) return void 0;
915
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
916
+ }
917
+ function parseIgnoredFields(v) {
918
+ if (!Array.isArray(v)) return void 0;
919
+ const out = [];
920
+ for (const x of v) {
921
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
922
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
923
+ }
924
+ return out;
925
+ }
913
926
  function mapEvent(e) {
914
927
  const d = rec(e.data);
915
928
  switch (e.event) {
@@ -919,6 +932,8 @@ function mapEvent(e) {
919
932
  requestId: safeId(d.request_id),
920
933
  agentId: str(d.agent_id),
921
934
  sessionId: safeId(d.session_id),
935
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
936
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
922
937
  data: e.data
923
938
  };
924
939
  case "status":
@@ -996,6 +1011,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
996
1011
  const body = {};
997
1012
  if (params.message !== void 0) body.message = params.message;
998
1013
  if (params.sessionId) body.session_id = params.sessionId;
1014
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
1015
+ if (params.context) body.context = { ...params.context };
1016
+ if (params.bindings) body.bindings = { ...params.bindings };
1017
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
999
1018
  if (params.attachments?.length) {
1000
1019
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
1001
1020
  }
@@ -1034,6 +1053,7 @@ function validateParams(params) {
1034
1053
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
1035
1054
  }
1036
1055
  }
1056
+ validateStructuredInput(params);
1037
1057
  if (params.options?.timeoutMs !== void 0) {
1038
1058
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1039
1059
  }
@@ -1044,12 +1064,136 @@ function validateParams(params) {
1044
1064
  }
1045
1065
  }
1046
1066
  }
1047
- async function* streamAgent(cfg, params) {
1067
+ var MAX_FIELDS = 50;
1068
+ var MAX_VALUE_CHARS = 2e3;
1069
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1070
+ var MAX_SESSION_FROM_FIELDS = 5;
1071
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1072
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1073
+ var MAX_CONSUMER_KEY_CHARS = 200;
1074
+ function snapshotStructuredInput(params) {
1075
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1076
+ return {
1077
+ ...params,
1078
+ context: snap(params.context),
1079
+ bindings: snap(params.bindings),
1080
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1081
+ };
1082
+ }
1083
+ function isPlainObject(v) {
1084
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1085
+ const proto = Object.getPrototypeOf(v);
1086
+ return proto === Object.prototype || proto === null;
1087
+ }
1088
+ function isScalar(v) {
1089
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1090
+ }
1091
+ function validateStructuredInput(params) {
1092
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1093
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1094
+ }
1095
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1096
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1097
+ }
1098
+ for (const name of ["context", "bindings"]) {
1099
+ const v = params[name];
1100
+ if (v === void 0) continue;
1101
+ if (!isPlainObject(v)) {
1102
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1103
+ }
1104
+ const entries = Object.entries(v);
1105
+ if (entries.length > MAX_FIELDS) {
1106
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1107
+ }
1108
+ for (const [k, val] of entries) {
1109
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1110
+ throw new ZihinAgentError(
1111
+ "config",
1112
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1113
+ );
1114
+ }
1115
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1116
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1117
+ }
1118
+ if (!isScalar(val)) {
1119
+ throw new ZihinAgentError(
1120
+ "config",
1121
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1122
+ );
1123
+ }
1124
+ }
1125
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1126
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1127
+ }
1128
+ }
1129
+ if (params.sessionFrom !== void 0) {
1130
+ 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))) {
1131
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1132
+ }
1133
+ if (params.sessionFrom.length && params.sessionId) {
1134
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1135
+ }
1136
+ const available = { ...params.context, ...params.bindings };
1137
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1138
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1139
+ throw new ZihinAgentError(
1140
+ "config",
1141
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1142
+ );
1143
+ }
1144
+ }
1145
+ }
1146
+ function requestedInputFields(params) {
1147
+ const out = [];
1148
+ if (params.context) out.push("context");
1149
+ if (params.bindings) out.push("bindings");
1150
+ if (params.sessionFrom?.length) out.push("session_from");
1151
+ return out;
1152
+ }
1153
+ function checkInputCapability(params, chunk) {
1154
+ const requested = requestedInputFields(params);
1155
+ const ignored = chunk?.ignoredFields ?? [];
1156
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1157
+ const blocking = ignored.filter(
1158
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1159
+ );
1160
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1161
+ if (blocking.length) {
1162
+ const shown = blocking.slice(0, 5);
1163
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1164
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1165
+ 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." : "";
1166
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1167
+ requestId: chunk?.requestId
1168
+ });
1169
+ }
1170
+ if (requested.length && chunk?.acceptedFields === void 0) {
1171
+ throw new ZihinAgentError(
1172
+ "config",
1173
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1174
+ { requestId: chunk?.requestId }
1175
+ );
1176
+ }
1177
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1178
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1179
+ if (notAccepted.length) {
1180
+ throw new ZihinAgentError(
1181
+ "config",
1182
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1183
+ { requestId: chunk?.requestId }
1184
+ );
1185
+ }
1186
+ }
1187
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1188
+ async function* streamAgent(cfg, rawParams) {
1189
+ const params = snapshotStructuredInput(rawParams);
1048
1190
  validateParams(params);
1049
1191
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1050
1192
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1051
1193
  const reportedMs = callerTimeoutMs ?? timeoutMs;
1052
1194
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1195
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1196
+ let gated = false;
1053
1197
  const { response, deadline, endpoint } = await openStream({
1054
1198
  url,
1055
1199
  headers: buildHeaders(cfg.apiKey),
@@ -1077,7 +1221,14 @@ async function* streamAgent(cfg, params) {
1077
1221
  maxEventBytes: cfg.maxStreamEventBytes,
1078
1222
  maxTotalBytes: cfg.maxStreamTotalBytes
1079
1223
  })) {
1080
- yield mapEvent(e);
1224
+ const chunk = mapEvent(e);
1225
+ if (needsCapability) {
1226
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1227
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1228
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1229
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1230
+ }
1231
+ yield chunk;
1081
1232
  }
1082
1233
  } finally {
1083
1234
  deadline.dispose();
@@ -1085,6 +1236,7 @@ async function* streamAgent(cfg, params) {
1085
1236
  }
1086
1237
  function classifyStreamErrorKind(code, timedOut) {
1087
1238
  if (timedOut === true) return "timeout";
1239
+ if (code === "VALIDATION_ERROR") return "config";
1088
1240
  if (timedOut === false) return "server";
1089
1241
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1090
1242
  }
@@ -1177,6 +1329,8 @@ async function invokeAgent(cfg, params) {
1177
1329
  let mintedSessionId;
1178
1330
  let doneChunk;
1179
1331
  let blocked;
1332
+ let acceptedFields;
1333
+ let ignoredFields;
1180
1334
  let tokenBuffer = "";
1181
1335
  for await (const chunk of streamAgent(cfg, params)) {
1182
1336
  if (keepRaw) raw.push(chunk);
@@ -1186,8 +1340,11 @@ async function invokeAgent(cfg, params) {
1186
1340
  if (chunk.type === "response") responseChunk = chunk;
1187
1341
  else if (chunk.type === "metrics") metricsChunk = chunk;
1188
1342
  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;
1343
+ else if (chunk.type === "metadata") {
1344
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1345
+ acceptedFields = chunk.acceptedFields;
1346
+ ignoredFields = chunk.ignoredFields;
1347
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1191
1348
  else if (chunk.type === "done") doneChunk = chunk;
1192
1349
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1193
1350
  }
@@ -1210,6 +1367,8 @@ async function invokeAgent(cfg, params) {
1210
1367
  iterations: metricsChunk?.iterations,
1211
1368
  executionTimeMs: metricsChunk?.executionTimeMs,
1212
1369
  executionId,
1370
+ acceptedFields,
1371
+ ignoredFields,
1213
1372
  raw
1214
1373
  };
1215
1374
  }
@@ -1230,6 +1389,8 @@ async function invokeAgent(cfg, params) {
1230
1389
  outcome: doneChunk?.outcome,
1231
1390
  truncated: doneChunk?.truncated,
1232
1391
  finishReason: doneChunk?.finishReason,
1392
+ acceptedFields,
1393
+ ignoredFields,
1233
1394
  raw
1234
1395
  };
1235
1396
  }
package/dist/index.mjs CHANGED
@@ -876,6 +876,19 @@ async function* parseSse(response, opts = {}) {
876
876
  }
877
877
 
878
878
  // src/invoke.ts
879
+ function parseAcceptedFields(v) {
880
+ if (!Array.isArray(v)) return void 0;
881
+ return v.map((x) => safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name))).filter((x) => !!x);
882
+ }
883
+ function parseIgnoredFields(v) {
884
+ if (!Array.isArray(v)) return void 0;
885
+ const out = [];
886
+ for (const x of v) {
887
+ const field = safeId(typeof x === "string" ? x : str(rec(x).field) ?? str(rec(x).name));
888
+ if (field) out.push(typeof x === "string" ? { field } : { field, reason: safeLabel(rec(x).reason) });
889
+ }
890
+ return out;
891
+ }
879
892
  function mapEvent(e) {
880
893
  const d = rec(e.data);
881
894
  switch (e.event) {
@@ -885,6 +898,8 @@ function mapEvent(e) {
885
898
  requestId: safeId(d.request_id),
886
899
  agentId: str(d.agent_id),
887
900
  sessionId: safeId(d.session_id),
901
+ acceptedFields: parseAcceptedFields(d.accepted_fields),
902
+ ignoredFields: parseIgnoredFields(d.ignored_fields),
888
903
  data: e.data
889
904
  };
890
905
  case "status":
@@ -962,6 +977,10 @@ function buildRequestBody(params, normalizedTimeoutMs) {
962
977
  const body = {};
963
978
  if (params.message !== void 0) body.message = params.message;
964
979
  if (params.sessionId) body.session_id = params.sessionId;
980
+ if (params.consumerKey) body.consumer_key = params.consumerKey;
981
+ if (params.context) body.context = { ...params.context };
982
+ if (params.bindings) body.bindings = { ...params.bindings };
983
+ if (params.sessionFrom?.length) body.session_from = [...params.sessionFrom];
965
984
  if (params.attachments?.length) {
966
985
  body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
967
986
  }
@@ -1000,6 +1019,7 @@ function validateParams(params) {
1000
1019
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
1001
1020
  }
1002
1021
  }
1022
+ validateStructuredInput(params);
1003
1023
  if (params.options?.timeoutMs !== void 0) {
1004
1024
  normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1005
1025
  }
@@ -1010,12 +1030,136 @@ function validateParams(params) {
1010
1030
  }
1011
1031
  }
1012
1032
  }
1013
- async function* streamAgent(cfg, params) {
1033
+ var MAX_FIELDS = 50;
1034
+ var MAX_VALUE_CHARS = 2e3;
1035
+ var MAX_TOTAL_CHARS = { context: 2e3, bindings: 16e3 };
1036
+ var MAX_SESSION_FROM_FIELDS = 5;
1037
+ var FIELD_NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
1038
+ var PROTOTYPE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1039
+ var MAX_CONSUMER_KEY_CHARS = 200;
1040
+ function snapshotStructuredInput(params) {
1041
+ const snap = (v) => isPlainObject(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0)) : v;
1042
+ return {
1043
+ ...params,
1044
+ context: snap(params.context),
1045
+ bindings: snap(params.bindings),
1046
+ sessionFrom: Array.isArray(params.sessionFrom) ? [...params.sessionFrom] : params.sessionFrom
1047
+ };
1048
+ }
1049
+ function isPlainObject(v) {
1050
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
1051
+ const proto = Object.getPrototypeOf(v);
1052
+ return proto === Object.prototype || proto === null;
1053
+ }
1054
+ function isScalar(v) {
1055
+ return v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v);
1056
+ }
1057
+ function validateStructuredInput(params) {
1058
+ if (params.consumerKey !== void 0 && (typeof params.consumerKey !== "string" || !params.consumerKey.trim())) {
1059
+ throw new ZihinAgentError("config", "`consumerKey` must be a non-empty string.");
1060
+ }
1061
+ if (typeof params.consumerKey === "string" && params.consumerKey.length > MAX_CONSUMER_KEY_CHARS) {
1062
+ throw new ZihinAgentError("config", `\`consumerKey\` is longer than ${MAX_CONSUMER_KEY_CHARS} characters.`);
1063
+ }
1064
+ for (const name of ["context", "bindings"]) {
1065
+ const v = params[name];
1066
+ if (v === void 0) continue;
1067
+ if (!isPlainObject(v)) {
1068
+ throw new ZihinAgentError("config", `\`${name}\` must be a flat plain object.`);
1069
+ }
1070
+ const entries = Object.entries(v);
1071
+ if (entries.length > MAX_FIELDS) {
1072
+ throw new ZihinAgentError("config", `\`${name}\` accepts at most ${MAX_FIELDS} fields.`);
1073
+ }
1074
+ for (const [k, val] of entries) {
1075
+ if (!FIELD_NAME_RE.test(k) || PROTOTYPE_KEYS.has(k)) {
1076
+ throw new ZihinAgentError(
1077
+ "config",
1078
+ `Invalid field name in \`${name}\`: use letters, digits, \`_\` or \`-\`, starting with a letter or \`_\` (max 64 characters; \`constructor\`, \`prototype\` and \`__proto__\` are not allowed).`
1079
+ );
1080
+ }
1081
+ if (typeof val === "string" && val.length > MAX_VALUE_CHARS) {
1082
+ throw new ZihinAgentError("config", `\`${name}.${k}\` is longer than ${MAX_VALUE_CHARS} characters.`);
1083
+ }
1084
+ if (!isScalar(val)) {
1085
+ throw new ZihinAgentError(
1086
+ "config",
1087
+ `\`${name}.${k}\` must be a string, number, boolean or null \u2014 \`${name}\` is flat (no nested objects or arrays).`
1088
+ );
1089
+ }
1090
+ }
1091
+ if (JSON.stringify(v).length > MAX_TOTAL_CHARS[name]) {
1092
+ throw new ZihinAgentError("config", `\`${name}\` is longer than ${MAX_TOTAL_CHARS[name]} characters in total (as JSON).`);
1093
+ }
1094
+ }
1095
+ if (params.sessionFrom !== void 0) {
1096
+ 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))) {
1097
+ throw new ZihinAgentError("config", `\`sessionFrom\` must be an array of up to ${MAX_SESSION_FROM_FIELDS} field names.`);
1098
+ }
1099
+ if (params.sessionFrom.length && params.sessionId) {
1100
+ throw new ZihinAgentError("config", "Use either `sessionId` or `sessionFrom`, not both.");
1101
+ }
1102
+ const available = { ...params.context, ...params.bindings };
1103
+ const has = (f) => Object.prototype.hasOwnProperty.call(available, f) && Boolean(available[f]);
1104
+ if (params.sessionFrom.length && !params.sessionFrom.some(has)) {
1105
+ throw new ZihinAgentError(
1106
+ "config",
1107
+ `\`sessionFrom\` names ${params.sessionFrom.map((f) => `"${f}"`).join(", ")}, but none of them has a value in \`context\` or \`bindings\`.`
1108
+ );
1109
+ }
1110
+ }
1111
+ }
1112
+ function requestedInputFields(params) {
1113
+ const out = [];
1114
+ if (params.context) out.push("context");
1115
+ if (params.bindings) out.push("bindings");
1116
+ if (params.sessionFrom?.length) out.push("session_from");
1117
+ return out;
1118
+ }
1119
+ function checkInputCapability(params, chunk) {
1120
+ const requested = requestedInputFields(params);
1121
+ const ignored = chunk?.ignoredFields ?? [];
1122
+ const sent = [...requested, ...params.consumerKey ? ["consumer_key"] : []];
1123
+ const blocking = ignored.filter(
1124
+ (f) => f.reason !== "redacted" && sent.some((name) => f.field === name || f.field.startsWith(`${name}.`))
1125
+ );
1126
+ const tail = "the stream was closed to cancel the turn, but the message may already be recorded in the session.";
1127
+ if (blocking.length) {
1128
+ const shown = blocking.slice(0, 5);
1129
+ const more = blocking.length > shown.length ? ` (+${blocking.length - shown.length} more)` : "";
1130
+ const what = shown.map((f) => f.reason ? `\`${f.field}\` (${f.reason})` : `\`${f.field}\``).join(", ") + more;
1131
+ 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." : "";
1132
+ throw new ZihinAgentError("config", `The server ignored ${what}.${hint} The answer is not used: ${tail}`, {
1133
+ requestId: chunk?.requestId
1134
+ });
1135
+ }
1136
+ if (requested.length && chunk?.acceptedFields === void 0) {
1137
+ throw new ZihinAgentError(
1138
+ "config",
1139
+ `This Zihin server does not support ${requested.map((f) => `\`${f}\``).join(", ")} on /stream yet (no \`accepted_fields\` in the stream metadata) \u2014 ${tail}`,
1140
+ { requestId: chunk?.requestId }
1141
+ );
1142
+ }
1143
+ const mustAccept = chunk?.acceptedFields !== void 0 && params.consumerKey ? [...requested, "consumer_key"] : requested;
1144
+ const notAccepted = mustAccept.filter((f) => !chunk?.acceptedFields?.includes(f));
1145
+ if (notAccepted.length) {
1146
+ throw new ZihinAgentError(
1147
+ "config",
1148
+ `The server did not accept ${notAccepted.map((f) => `\`${f}\``).join(", ")} (missing from \`accepted_fields\`) \u2014 ${tail}`,
1149
+ { requestId: chunk?.requestId }
1150
+ );
1151
+ }
1152
+ }
1153
+ var TURN_EVENTS = /* @__PURE__ */ new Set(["phase", "thinking", "tool", "action_proposed", "token", "response", "metrics", "done"]);
1154
+ async function* streamAgent(cfg, rawParams) {
1155
+ const params = snapshotStructuredInput(rawParams);
1014
1156
  validateParams(params);
1015
1157
  const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1016
1158
  const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1017
1159
  const reportedMs = callerTimeoutMs ?? timeoutMs;
1018
1160
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
1161
+ let needsCapability = requestedInputFields(params).length > 0 || !!params.consumerKey;
1162
+ let gated = false;
1019
1163
  const { response, deadline, endpoint } = await openStream({
1020
1164
  url,
1021
1165
  headers: buildHeaders(cfg.apiKey),
@@ -1043,7 +1187,14 @@ async function* streamAgent(cfg, params) {
1043
1187
  maxEventBytes: cfg.maxStreamEventBytes,
1044
1188
  maxTotalBytes: cfg.maxStreamTotalBytes
1045
1189
  })) {
1046
- yield mapEvent(e);
1190
+ const chunk = mapEvent(e);
1191
+ if (needsCapability) {
1192
+ if (chunk.type === "metadata") checkInputCapability(params, chunk);
1193
+ else if (chunk.type === "status" && chunk.phase === "GATED") gated = true;
1194
+ else if (TURN_EVENTS.has(chunk.type) && !gated) checkInputCapability(params, void 0);
1195
+ if (chunk.type === "metadata" || TURN_EVENTS.has(chunk.type) && !gated) needsCapability = false;
1196
+ }
1197
+ yield chunk;
1047
1198
  }
1048
1199
  } finally {
1049
1200
  deadline.dispose();
@@ -1051,6 +1202,7 @@ async function* streamAgent(cfg, params) {
1051
1202
  }
1052
1203
  function classifyStreamErrorKind(code, timedOut) {
1053
1204
  if (timedOut === true) return "timeout";
1205
+ if (code === "VALIDATION_ERROR") return "config";
1054
1206
  if (timedOut === false) return "server";
1055
1207
  return /TIMEOUT/i.test(code) ? "timeout" : "server";
1056
1208
  }
@@ -1143,6 +1295,8 @@ async function invokeAgent(cfg, params) {
1143
1295
  let mintedSessionId;
1144
1296
  let doneChunk;
1145
1297
  let blocked;
1298
+ let acceptedFields;
1299
+ let ignoredFields;
1146
1300
  let tokenBuffer = "";
1147
1301
  for await (const chunk of streamAgent(cfg, params)) {
1148
1302
  if (keepRaw) raw.push(chunk);
@@ -1152,8 +1306,11 @@ async function invokeAgent(cfg, params) {
1152
1306
  if (chunk.type === "response") responseChunk = chunk;
1153
1307
  else if (chunk.type === "metrics") metricsChunk = chunk;
1154
1308
  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;
1309
+ else if (chunk.type === "metadata") {
1310
+ if (chunk.sessionId) metadataSessionId = chunk.sessionId;
1311
+ acceptedFields = chunk.acceptedFields;
1312
+ ignoredFields = chunk.ignoredFields;
1313
+ } else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1157
1314
  else if (chunk.type === "done") doneChunk = chunk;
1158
1315
  else if (chunk.type === "status" && chunk.blocked) blocked = chunk.blocked;
1159
1316
  }
@@ -1176,6 +1333,8 @@ async function invokeAgent(cfg, params) {
1176
1333
  iterations: metricsChunk?.iterations,
1177
1334
  executionTimeMs: metricsChunk?.executionTimeMs,
1178
1335
  executionId,
1336
+ acceptedFields,
1337
+ ignoredFields,
1179
1338
  raw
1180
1339
  };
1181
1340
  }
@@ -1196,6 +1355,8 @@ async function invokeAgent(cfg, params) {
1196
1355
  outcome: doneChunk?.outcome,
1197
1356
  truncated: doneChunk?.truncated,
1198
1357
  finishReason: doneChunk?.finishReason,
1358
+ acceptedFields,
1359
+ ignoredFields,
1199
1360
  raw
1200
1361
  };
1201
1362
  }
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.0",
4
4
  "description": "Núcleo de invocação de agentes Zihin.ai — padrão único para n8n, Paperclip e integrações futuras. Stream direto (SSE bufferizado) + triggers assíncronos com callback.",
5
5
  "keywords": [
6
6
  "zihin",