@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 +61 -0
- package/dist/index.d.mts +53 -4
- package/dist/index.d.ts +53 -4
- package/dist/index.js +165 -4
- package/dist/index.mjs +165 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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"
|
|
1190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
1156
|
-
|
|
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.
|
|
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",
|