@volund-ia/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Volund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @volund-ia/sdk
2
+
3
+ Cliente TypeScript para rodar agentes do **Volund OS** pelo seu próprio código e
4
+ receber, em tempo real (streaming), tudo que o agente faz — raciocínio, chamadas
5
+ de ferramenta e a resposta token a token.
6
+
7
+ ```bash
8
+ npm install @volund-ia/sdk
9
+ ```
10
+
11
+ > Requer Node ≥ 18 (usa o `fetch` nativo). Funciona também em Deno, Bun, Workers
12
+ > e no browser (parser SSE 100% web-standard).
13
+
14
+ ## Quickstart
15
+
16
+ ```ts
17
+ import { VolundOS } from "@volund-ia/sdk";
18
+
19
+ const volund = new VolundOS({ apiKey: process.env.VOLUND_API_KEY! });
20
+
21
+ const run = await volund.agents.run({
22
+ agentId: "agt_123",
23
+ input: "Pesquise os 3 maiores concorrentes da empresa X e resuma.",
24
+ });
25
+
26
+ // Passo a passo conforme acontece:
27
+ for await (const event of run.stream()) {
28
+ if (event.type === "assistant_text_delta") process.stdout.write(event.delta);
29
+ if (event.type === "tool_call") console.log("→ usou:", event.tool_name);
30
+ }
31
+
32
+ // Ou só o resultado final:
33
+ const run2 = await volund.agents.run({ agentId: "agt_123", input: "Oi" });
34
+ const { output, usage } = await run2.result();
35
+ ```
36
+
37
+ Continuar uma conversa (mesma thread):
38
+
39
+ ```ts
40
+ const next = await volund.agents.continue({ runId: run.id, input: "E o 4º?" });
41
+ ```
42
+
43
+ ## A DX
44
+
45
+ Espelha o Cursor SDK (`Agent.create()` → `agent.send()` → `run.stream()`):
46
+ `new VolundOS()` → `agents.run()` → `run.stream()` / `run.result()` /
47
+ `run.cancel()`.
48
+
49
+ ## Eventos (`VolundEvent`)
50
+
51
+ Stream tipado por união discriminada — faça narrowing por `event.type`:
52
+
53
+ | `type` | Campos |
54
+ | ----------------------- | ------------------------------------------------- |
55
+ | `run_started` | `protocol`, `run_id`, `agent_id` |
56
+ | `thinking_delta` | `delta` (raciocínio, streaming) |
57
+ | `assistant_text_delta` | `delta` (resposta, streaming) |
58
+ | `tool_call` | `tool_call_id`, `tool_name`, `input` |
59
+ | `tool_result` | `tool_call_id`, `output`, `is_error?` |
60
+ | `awaiting_input` | `request_id`, `kind: "vault"` (HITL — fecha o stream) |
61
+ | `run_finished` | `status`, `output`, `usage`, `error?` |
62
+
63
+ O contrato é **snake_case no fio** (consistente com a API v1 e o ecossistema
64
+ Anthropic/Cursor) e versionado por `SCHEMA_VERSION` (`protocol` no `run_started`).
65
+
66
+ ## Erros
67
+
68
+ Todos herdam de `VolundError` (tem `.code` e `.status`). Roteie por `instanceof`:
69
+
70
+ | Classe | Quando |
71
+ | --------------------------- | --------------------------------------- |
72
+ | `VolundAuthError` | 401 — chave ausente/inválida |
73
+ | `VolundForbiddenError` | 403 — sem acesso ao agente |
74
+ | `VolundNotFoundError` | 404 — agente/run inexistente |
75
+ | `VolundRunBusyError` | 409 — já há run ativo na thread |
76
+ | `VolundRunFailedError` | `run.result()` quando o run falha |
77
+ | `VolundAwaitingInputError` | `run.result()` quando pausa p/ vault |
78
+
79
+ ## Notas
80
+
81
+ - **`stream()` é consumível uma única vez** (é um stream de rede). Não combine
82
+ `stream()` e `result()` no mesmo `Run`.
83
+ - `run.cancel()` aborta a conexão — o servidor encerra a sandbox.
84
+ - `execution: "local"` (rodar no `cwd` do dev, estilo Cursor) chega na **V2**; o
85
+ tipo já existe, mas a V1 só roda na nuvem.
86
+
87
+ ## Testar contra um preview da Vercel (modo intermediário)
88
+
89
+ Antes do endpoint de produção, dá pra apontar o SDK pro deployment de preview do
90
+ PR:
91
+
92
+ ```bash
93
+ VOLUND_API_KEY=vos_live_... \
94
+ VOLUND_AGENT_ID=agt_... \
95
+ VOLUND_BASE_URL=https://seu-preview.vercel.app \
96
+ npm run example
97
+ ```
98
+
99
+ Se o preview estiver com **Deployment Protection** ligada, passe o token de
100
+ *Protection Bypass for Automation* — ele vira um header via `defaultHeaders`:
101
+
102
+ ```bash
103
+ VERCEL_BYPASS=<secret> ...demais envs... npm run example
104
+ ```
105
+
106
+ ```ts
107
+ new VolundOS({
108
+ apiKey,
109
+ baseUrl: "https://seu-preview.vercel.app",
110
+ defaultHeaders: { "x-vercel-protection-bypass": process.env.VERCEL_BYPASS! },
111
+ });
112
+ ```
113
+
114
+ ## Instalar antes da publicação no NPM (beta)
115
+
116
+ Enquanto `@volund-ia/sdk` não está publicado:
117
+
118
+ ```bash
119
+ npm install anaraque-l/volund-sdk # do GitHub (builda no install via `prepare`)
120
+ # ou um tarball:
121
+ npm pack && npm install ./volund-sdk-0.2.0.tgz
122
+ ```
123
+
124
+ ## Desenvolvimento
125
+
126
+ ```bash
127
+ npm install
128
+ npm test # testes do parser SSE (vitest)
129
+ npm run typecheck
130
+ npm run build # tsdown → ESM + CJS + .d.ts
131
+ npm run check:protocol # garante o contrato em sincronia com o volund-os
132
+ ```
133
+
134
+ O contrato de eventos é **vendorado** de `volund-os` em `src/protocol/events.ts`
135
+ — ver [`src/protocol/README.md`](src/protocol/README.md). Atualize só via
136
+ `npm run sync:protocol`.
137
+
138
+ ## Licença
139
+
140
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,537 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let eventsource_parser = require("eventsource-parser");
3
+ //#region src/errors.ts
4
+ /** Erro base de todo o SDK. */
5
+ var VolundError = class extends Error {
6
+ code;
7
+ status;
8
+ constructor(message, opts) {
9
+ super(message, { cause: opts.cause });
10
+ this.name = "VolundError";
11
+ this.code = opts.code;
12
+ this.status = opts.status;
13
+ Object.setPrototypeOf(this, new.target.prototype);
14
+ }
15
+ };
16
+ /** 401 — chave ausente ou inválida. */
17
+ var VolundAuthError = class extends VolundError {
18
+ constructor(message, code = "invalid_api_key", cause) {
19
+ super(message, {
20
+ code,
21
+ status: 401,
22
+ cause
23
+ });
24
+ this.name = "VolundAuthError";
25
+ }
26
+ };
27
+ /** 403 — a chave não tem acesso a este agente/run. */
28
+ var VolundForbiddenError = class extends VolundError {
29
+ constructor(message, cause) {
30
+ super(message, {
31
+ code: "forbidden",
32
+ status: 403,
33
+ cause
34
+ });
35
+ this.name = "VolundForbiddenError";
36
+ }
37
+ };
38
+ /** 404 — agente ou run inexistente. */
39
+ var VolundNotFoundError = class extends VolundError {
40
+ constructor(message, code = "agent_not_found", cause) {
41
+ super(message, {
42
+ code,
43
+ status: 404,
44
+ cause
45
+ });
46
+ this.name = "VolundNotFoundError";
47
+ }
48
+ };
49
+ /** 409 — já existe um run ativo na thread (só na continuação). */
50
+ var VolundRunBusyError = class extends VolundError {
51
+ constructor(message, cause) {
52
+ super(message, {
53
+ code: "run_busy",
54
+ status: 409,
55
+ cause
56
+ });
57
+ this.name = "VolundRunBusyError";
58
+ }
59
+ };
60
+ /** O run terminou com `status: "failed"`. Lançado por `run.result()`. */
61
+ var VolundRunFailedError = class extends VolundError {
62
+ constructor(message, cause) {
63
+ super(message, {
64
+ code: "run_failed",
65
+ cause
66
+ });
67
+ this.name = "VolundRunFailedError";
68
+ }
69
+ };
70
+ /**
71
+ * O run pausou esperando ação humana (HITL — na V1, preenchimento de cofre).
72
+ * `run.result()` lança isto porque o stream termina sem `run_finished`. Quem
73
+ * usa `run.stream()` recebe o evento `awaiting_input` normalmente, sem exceção.
74
+ */
75
+ var VolundAwaitingInputError = class extends VolundError {
76
+ requestId;
77
+ kind;
78
+ constructor(requestId, kind) {
79
+ super(`Run pausou aguardando entrada do tipo "${kind}" (request ${requestId}).`, { code: "awaiting_input" });
80
+ this.name = "VolundAwaitingInputError";
81
+ this.requestId = requestId;
82
+ this.kind = kind;
83
+ }
84
+ };
85
+ /** Constrói a subclasse certa a partir de uma resposta de erro da API. */
86
+ function errorFromApiResponse(status, body) {
87
+ const code = body.error ?? "internal_error";
88
+ const message = body.message ?? `Requisição falhou (HTTP ${status}).`;
89
+ switch (code) {
90
+ case "missing_api_key":
91
+ case "invalid_api_key": return new VolundAuthError(message, code);
92
+ case "forbidden": return new VolundForbiddenError(message);
93
+ case "agent_not_found":
94
+ case "run_not_found": return new VolundNotFoundError(message, code);
95
+ case "run_busy": return new VolundRunBusyError(message);
96
+ default: return new VolundError(message, {
97
+ code,
98
+ status
99
+ });
100
+ }
101
+ }
102
+ /** Base do backoff exponencial (ms): 300, 600, 1200... */
103
+ const RETRY_BASE_MS = 300;
104
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
105
+ /**
106
+ * Combina o sinal externo (cancel do usuário) com um timeout. O fetch recebe o
107
+ * sinal combinado. `clearTimer()` desarma o timeout (chame ao receber a resposta,
108
+ * p/ o timer não abortar o stream em andamento); a ligação com o sinal externo
109
+ * permanece, então `run.cancel()` segue funcionando durante todo o stream.
110
+ */
111
+ function linkAbort(external, timeoutMs) {
112
+ const controller = new AbortController();
113
+ let timedOut = false;
114
+ const onExternalAbort = () => controller.abort();
115
+ if (external) if (external.aborted) controller.abort();
116
+ else external.addEventListener("abort", onExternalAbort, { once: true });
117
+ const timer = timeoutMs > 0 ? setTimeout(() => {
118
+ timedOut = true;
119
+ controller.abort();
120
+ }, timeoutMs) : void 0;
121
+ return {
122
+ signal: controller.signal,
123
+ /** Foi o timeout (e não o cancel do usuário) que abortou? */
124
+ timedOut: () => timedOut,
125
+ /** Desarma o timeout MANTENDO a ligação com o sinal externo. Use no sucesso,
126
+ * p/ `run.cancel()` seguir abortando o stream em andamento. */
127
+ clearTimer: () => {
128
+ if (timer) clearTimeout(timer);
129
+ },
130
+ /** Desarma o timer E solta o listener do sinal externo. Use em tentativas
131
+ * abandonadas (erro/retry) p/ não vazar listeners no signal do usuário. */
132
+ dispose: () => {
133
+ if (timer) clearTimeout(timer);
134
+ if (external) external.removeEventListener("abort", onExternalAbort);
135
+ }
136
+ };
137
+ }
138
+ /**
139
+ * Faz POST num endpoint `/stream` e devolve a `Response` SSE crua. Lança a
140
+ * subclasse de `VolundError` apropriada se a resposta for um erro. Aplica timeout
141
+ * pré-stream e retry (rede/5xx) conforme a config.
142
+ */
143
+ async function postStream(cfg, path, body, signal) {
144
+ const url = `${cfg.baseUrl.replace(/\/+$/, "")}${path}`;
145
+ const timeoutMs = cfg.timeoutMs ?? 6e4;
146
+ const maxRetries = cfg.maxRetries ?? 0;
147
+ const sleep = cfg.sleep ?? defaultSleep;
148
+ const payload = JSON.stringify(body);
149
+ let lastError;
150
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
151
+ if (signal?.aborted) throw new VolundError(`Requisição a ${path} cancelada.`, {
152
+ code: "network_error",
153
+ cause: signal.reason
154
+ });
155
+ const link = linkAbort(signal, timeoutMs);
156
+ let res;
157
+ try {
158
+ res = await cfg.fetch(url, {
159
+ method: "POST",
160
+ headers: {
161
+ ...cfg.defaultHeaders,
162
+ Authorization: `Bearer ${cfg.apiKey}`,
163
+ "Content-Type": "application/json",
164
+ Accept: "text/event-stream"
165
+ },
166
+ body: payload,
167
+ signal: link.signal
168
+ });
169
+ } catch (cause) {
170
+ link.dispose();
171
+ if (link.timedOut()) lastError = new VolundError(`Timeout (${timeoutMs}ms) esperando resposta de ${path}.`, {
172
+ code: "timeout",
173
+ cause
174
+ });
175
+ else if (signal?.aborted) throw new VolundError(`Requisição a ${path} cancelada.`, {
176
+ code: "network_error",
177
+ cause
178
+ });
179
+ else lastError = new VolundError(`Falha de rede ao chamar ${path}.`, {
180
+ code: "network_error",
181
+ cause
182
+ });
183
+ if (attempt < maxRetries) {
184
+ await sleep(RETRY_BASE_MS * 2 ** attempt);
185
+ continue;
186
+ }
187
+ throw lastError;
188
+ }
189
+ link.clearTimer();
190
+ const contentType = res.headers.get("content-type") ?? "";
191
+ if (res.ok && contentType.includes("text/event-stream")) return res;
192
+ link.dispose();
193
+ let errBody = {};
194
+ try {
195
+ errBody = await res.json();
196
+ } catch {
197
+ errBody = { message: `Resposta inesperada (HTTP ${res.status}).` };
198
+ }
199
+ const mapped = errorFromApiResponse(res.status, errBody);
200
+ if (res.status >= 500 && attempt < maxRetries) {
201
+ lastError = mapped;
202
+ await sleep(RETRY_BASE_MS * 2 ** attempt);
203
+ continue;
204
+ }
205
+ throw mapped;
206
+ }
207
+ throw lastError ?? new VolundError(`Falha ao chamar ${path}.`, { code: "network_error" });
208
+ }
209
+ //#endregion
210
+ //#region src/sse.ts
211
+ /**
212
+ * Parser SSE → `VolundEvent`. Camada fina sobre `eventsource-parser`, o mesmo
213
+ * parser usado pelo Vercel AI SDK e pelo SDK da OpenAI. Ele resolve, de graça,
214
+ * as três armadilhas do wire do Volund OS (ver `sse-adapter.ts` no servidor):
215
+ *
216
+ * 1. Heartbeat `: ping\n\n` — linhas de comentário são ignoradas pelo parser.
217
+ * 2. Campo `id: <n>` por frame — exposto como `event.id` (reservado p/
218
+ * reconexão na V2); a V1 não o usa.
219
+ * 3. `data:` multi-linha / partido entre chunks — o parser bufferiza e
220
+ * concatena conforme a spec SSE.
221
+ *
222
+ * Cada `event.data` é um JSON de um único `VolundEvent`. JSON inválido ou tipo
223
+ * desconhecido é IGNORADO (regra de ouro [D4]: clientes ignoram o que não
224
+ * conhecem — permite minor bumps sem quebrar).
225
+ *
226
+ * Usamos a API de callback (`createParser`) + um reader manual em vez do
227
+ * `EventSourceParserStream` para evitar o atrito de variância de tipos entre
228
+ * `TextDecoderStream` e `ReadableStream<Uint8Array>` no `lib.dom`.
229
+ */
230
+ const KNOWN_TYPES = /* @__PURE__ */ new Set([
231
+ "run_started",
232
+ "thinking_delta",
233
+ "assistant_text_delta",
234
+ "tool_call",
235
+ "tool_result",
236
+ "awaiting_input",
237
+ "run_finished"
238
+ ]);
239
+ function isVolundEvent(value) {
240
+ return typeof value === "object" && value !== null && typeof value.type === "string" && KNOWN_TYPES.has(value.type);
241
+ }
242
+ /**
243
+ * Transforma o corpo de uma resposta `text/event-stream` numa sequência de
244
+ * `VolundEvent`. Web-standard: roda em Node ≥18, Deno, Bun, Workers e browser.
245
+ */
246
+ async function* parseVolundSSE(body) {
247
+ const queue = [];
248
+ const parser = (0, eventsource_parser.createParser)({ onEvent(event) {
249
+ if (!event.data) return;
250
+ let parsed;
251
+ try {
252
+ parsed = JSON.parse(event.data);
253
+ } catch {
254
+ return;
255
+ }
256
+ if (isVolundEvent(parsed)) queue.push(parsed);
257
+ } });
258
+ const reader = body.getReader();
259
+ const decoder = new TextDecoder();
260
+ try {
261
+ while (true) {
262
+ const { done, value } = await reader.read();
263
+ if (done) break;
264
+ parser.feed(decoder.decode(value, { stream: true }));
265
+ while (queue.length > 0) yield queue.shift();
266
+ }
267
+ const tail = decoder.decode();
268
+ if (tail) {
269
+ parser.feed(tail);
270
+ while (queue.length > 0) yield queue.shift();
271
+ }
272
+ } finally {
273
+ reader.releaseLock();
274
+ }
275
+ }
276
+ //#endregion
277
+ //#region src/run.ts
278
+ /**
279
+ * `Run` — uma execução de agente em andamento. Espelha o `run` do Cursor SDK:
280
+ * `.stream()` (eventos ao vivo), `.result()` (atalho p/ o texto final) e
281
+ * `.cancel()` (fecha a conexão; o servidor mata a sandbox).
282
+ */
283
+ var Run = class {
284
+ #id;
285
+ #response;
286
+ #abort;
287
+ #consumed = false;
288
+ constructor(response, id, abort) {
289
+ this.#response = response;
290
+ this.#id = id;
291
+ this.#abort = abort;
292
+ }
293
+ /**
294
+ * `run_id` (== `thread_id` no Volund OS). Use em `agents.continue`.
295
+ * Para um run NOVO, só fica disponível depois que o primeiro evento
296
+ * (`run_started`) é consumido via `stream()`/`result()` — antes disso é "".
297
+ * Em `agents.continue`, já vem preenchido (você passou o `runId`).
298
+ */
299
+ get id() {
300
+ return this.#id;
301
+ }
302
+ /**
303
+ * Itera os `VolundEvent` conforme chegam. ⚠️ Consumível UMA vez (é um stream
304
+ * de rede) — não chame `stream()` e `result()` no mesmo `Run`.
305
+ *
306
+ * Se você ABANDONAR o stream no meio (um `break`/`throw` antes de um evento
307
+ * terminal), a conexão é fechada automaticamente — o servidor mata o sandbox e
308
+ * não vaza recurso (§3.6/§4.2 da proposta). Já em `run_finished`/`awaiting_input`
309
+ * o servidor encerra sozinho, então NÃO abortamos (abortar no `awaiting_input`
310
+ * mataria um run parqueado p/ vault e quebraria o resume — §3.5).
311
+ */
312
+ async *stream() {
313
+ if (this.#consumed) throw new VolundError("Este run já foi consumido (stream/result só uma vez).", { code: "stream_error" });
314
+ this.#consumed = true;
315
+ const body = this.#response.body;
316
+ if (!body) throw new VolundError("Resposta de streaming sem corpo legível.", { code: "stream_error" });
317
+ let serverClosing = false;
318
+ try {
319
+ for await (const event of parseVolundSSE(body)) {
320
+ if (event.type === "run_started" && event.run_id) this.#id = event.run_id;
321
+ if (event.type === "run_finished" || event.type === "awaiting_input") serverClosing = true;
322
+ yield event;
323
+ }
324
+ } catch (err) {
325
+ if (this.#abort.signal.aborted) return;
326
+ throw err;
327
+ } finally {
328
+ if (!serverClosing) this.#abort.abort();
329
+ }
330
+ }
331
+ /**
332
+ * Espera o run terminar e devolve o texto final + uso de tokens.
333
+ * Lança `VolundRunFailedError` se o run falhar e `VolundAwaitingInputError`
334
+ * se ele pausar para HITL (vault). Para esses casos, prefira `stream()`.
335
+ */
336
+ async result() {
337
+ let text = "";
338
+ for await (const event of this.stream()) switch (event.type) {
339
+ case "assistant_text_delta":
340
+ text += event.delta;
341
+ break;
342
+ case "awaiting_input": throw new VolundAwaitingInputError(event.request_id, event.kind);
343
+ case "run_finished":
344
+ if (event.status === "failed") throw new VolundRunFailedError(event.error ?? "Run falhou sem motivo informado.");
345
+ return {
346
+ output: event.output ?? text,
347
+ usage: event.usage
348
+ };
349
+ }
350
+ return {
351
+ output: text,
352
+ usage: null
353
+ };
354
+ }
355
+ /** Cancela o run: aborta a conexão → o servidor mata a sandbox. */
356
+ cancel() {
357
+ this.#abort.abort();
358
+ }
359
+ };
360
+ //#endregion
361
+ //#region src/agents.ts
362
+ /**
363
+ * `volund.agents` — dispara (`run`) e continua (`continue`) execuções de agente.
364
+ * DX espelha o Cursor SDK: `agents.run(...)` → `Run` com `.stream()/.result()`.
365
+ */
366
+ /** Combina o sinal do usuário com o sinal interno de `run.cancel()`. */
367
+ function linkSignals(controller, external) {
368
+ if (!external) return;
369
+ if (external.aborted) {
370
+ controller.abort();
371
+ return;
372
+ }
373
+ external.addEventListener("abort", () => controller.abort(), { once: true });
374
+ }
375
+ /** V1 só roda na nuvem; o gancho `execution` já existe p/ a V2 (local.cwd). */
376
+ function assertCloud(execution) {
377
+ if (execution && execution !== "cloud") throw new VolundError("execution: \"local\" ainda não é suportado (chega na V2). Use \"cloud\" ou omita.", { code: "unsupported" });
378
+ }
379
+ var Agents = class {
380
+ #http;
381
+ constructor(http) {
382
+ this.#http = http;
383
+ }
384
+ /** Dispara um run novo (cria uma thread) e devolve um `Run` em streaming. */
385
+ async run(options) {
386
+ assertCloud(options.execution);
387
+ const controller = new AbortController();
388
+ linkSignals(controller, options.signal);
389
+ const body = { input: options.input };
390
+ if (options.files?.length) body.files = options.files;
391
+ const res = await postStream(this.#http, `/api/v1/agents/${encodeURIComponent(options.agentId)}/stream`, body, controller.signal);
392
+ return new Run(res, runIdFromResponse(res), controller);
393
+ }
394
+ /** Continua uma conversa existente (mesma thread). */
395
+ async continue(options) {
396
+ assertCloud(options.execution);
397
+ const controller = new AbortController();
398
+ linkSignals(controller, options.signal);
399
+ const body = { input: options.input };
400
+ if (options.files?.length) body.files = options.files;
401
+ return new Run(await postStream(this.#http, `/api/v1/runs/${encodeURIComponent(options.runId)}/stream`, body, controller.signal), options.runId, controller);
402
+ }
403
+ };
404
+ /**
405
+ * Para um run NOVO o `run_id` só existe no primeiro evento (`run_started`) — o
406
+ * servidor não o devolve em header. Então iniciamos o `Run` com "" e ele faz o
407
+ * backfill do id ao consumir o `run_started` (ver `Run.stream`). Mantemos um
408
+ * fast-path opcional por header caso o servidor passe a ecoá-lo no futuro.
409
+ */
410
+ function runIdFromResponse(res) {
411
+ return res.headers.get("x-volund-run-id") ?? "";
412
+ }
413
+ //#endregion
414
+ //#region src/client.ts
415
+ /**
416
+ * `VolundOS` — ponto de entrada do SDK. Configuração mínima: uma API key.
417
+ *
418
+ * const volund = new VolundOS({ apiKey: process.env.VOLUND_API_KEY! });
419
+ * const run = await volund.agents.run({ agentId, input });
420
+ */
421
+ /** Endpoint de produção do Volund OS (decisão de proposta §6). */
422
+ const DEFAULT_BASE_URL = "https://os.volund.com.br";
423
+ var VolundOS = class {
424
+ /** Disparo e continuação de runs de agente. */
425
+ agents;
426
+ constructor(config) {
427
+ if (!config?.apiKey || typeof config.apiKey !== "string") throw new VolundError("apiKey é obrigatória.", { code: "missing_api_key" });
428
+ const fetchImpl = config.fetch ?? globalThis.fetch;
429
+ if (typeof fetchImpl !== "function") throw new VolundError("fetch global indisponível. Use Node ≥18 ou injete `fetch` no config.", { code: "unsupported" });
430
+ const http = {
431
+ apiKey: config.apiKey,
432
+ baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
433
+ fetch: (...args) => fetchImpl(...args),
434
+ ...config.defaultHeaders ? { defaultHeaders: config.defaultHeaders } : {},
435
+ ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
436
+ ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {}
437
+ };
438
+ this.agents = new Agents(http);
439
+ }
440
+ };
441
+ //#endregion
442
+ //#region src/protocol/events.ts
443
+ /**
444
+ * ⚠️ ARQUIVO VENDORADO — NÃO EDITE À MÃO ABAIXO DO SENTINEL.
445
+ *
446
+ * Cópia fiel de `lib/agent/connectors/api/events.ts` do repo `volund-os`
447
+ * (a FONTE ÚNICA do contrato VolundEvent v1). O servidor emite estes eventos;
448
+ * este pacote os entrega tipados. Manter os dois em sincronia é invariante do
449
+ * projeto — o CI roda `scripts/check-protocol-drift.mjs`, que falha se o
450
+ * conteúdo abaixo do sentinel divergir do upstream.
451
+ *
452
+ * Para atualizar: rode `npm run sync:protocol` (copia do volund-os) — nunca
453
+ * edite o corpo manualmente. Promover para um pacote `@volund/protocol`
454
+ * publicado no futuro é trivial: este diretório não importa nada do SDK.
455
+ */
456
+ /**
457
+ * Contrato público de eventos do Volund OS SDK — VERSÃO 1.
458
+ *
459
+ * Fonte única (graduada de `docs/prototypes/sse-adapter/events.ts`). É o que o
460
+ * servidor (Parte A) emite via SSE e o que o SDK (Parte B) entregará como
461
+ * AsyncIterable<VolundEvent>. Clientes externos dependem disso por anos —
462
+ * NÃO altere os tipos públicos sem bump de SCHEMA_VERSION.
463
+ *
464
+ * DECISÕES DESTA VERSÃO (aprovadas pelo time em 22/06):
465
+ * [D1] Naming = snake_case no wire. Consistente com a API pública que JÁ
466
+ * existe (GET /api/v1/runs/{id} e webhook) E com o padrão do ecossistema:
467
+ * Anthropic é 100% snake_case no fio; Cursor espelha em snake_case os
468
+ * campos estilo Claude Code. camelCase fica reservado p/ ergonomia futura
469
+ * na borda da linguagem (mapeado no SDK), não no contrato.
470
+ * [D2] Status reusa o vocabulário do GET /runs: "completed" | "failed".
471
+ * Pausa (HITL) = UM evento genérico `awaiting_input { kind }`. Na V1 o
472
+ * único `kind` é "vault": runs via API rodam com
473
+ * `--permission-mode bypassPermissions` (lib/agent/v2/run.ts), então NÃO
474
+ * pausam por aprovação de ferramenta. "approval" foi descopado da V1
475
+ * (decisão do time, 22/06) — reintroduzir quando/se a API suportar.
476
+ * [D3] tool_result.is_error CONFIRMADO real no nível stream-json: vem como
477
+ * `is_error: true` dentro do tool_result (no evento cru `user`). O
478
+ * types.ts da Volund não o tipa → o adapter lê do cru via cast. Mantido
479
+ * opcional (só presente/true em erro de ferramenta).
480
+ * [D4] Versionamento (modelo combinado): campo `protocol` no run_started
481
+ * (portátil p/ HTTP e CLI) p/ MAJOR; minor/patch via política
482
+ * "ignore unknown fields" (clientes ignoram campos desconhecidos).
483
+ * [D5] SSE `id:` por evento é emitido pelo ADAPTER (não é campo de payload),
484
+ * RESERVADO p/ reconexão futura (V2). A V1 NÃO promete retomada.
485
+ *
486
+ * ROTEAMENTO DE ERROS (por `type`, nunca por posição):
487
+ * - erro de ferramenta → tool_result.is_error (este arquivo, ToolResultEvent)
488
+ * - falha do run inteiro → run_finished status:"failed" + error
489
+ * - erro de transporte → cru `system/api_retry`: NÃO exposto na V1 (interno;
490
+ * retries são tratados dentro da nuvem).
491
+ *
492
+ * REGRAS DE OURO:
493
+ * 1. NÃO vazar interno: nada de session_id, sandbox, scratchDir, nome do
494
+ * executor (claude-code/cursor), api_retry, nem formatos do AI SDK.
495
+ * 2. Versionar (SCHEMA_VERSION). Mudança incompatível => bump MAJOR.
496
+ * 3. input/output são `unknown` mas DEVEM ser JSON-serializáveis.
497
+ * 4. Fonte única: servidor importa daqui; o pacote @volund/sdk publica daqui.
498
+ *
499
+ * INVARIANTES DO ADAPTER (blindagem contra inconsistências):
500
+ * I1. tool_call.input é emitido COMPLETO. O input chega vazio no 1º snapshot e
501
+ * completo depois. O adapter acumula input_json_delta e só emite no
502
+ * content_block_stop — nunca um input meio-vazio.
503
+ * I2. tool_result.output é NORMALIZADO: bloco MCP [{type:"text",text}] vira
504
+ * string; imagem `data:image/...` vira placeholder (não trafega binário);
505
+ * tamanho limitado. Saída sempre JSON-serializável e enxuta.
506
+ * I3. Sentinel de vault (__vault_request_pending__:<id>) NUNCA vaza como
507
+ * tool_result — o adapter detecta e emite awaiting_input{kind:"vault"},
508
+ * suprimindo o sentinel e o run_finished subsequente.
509
+ * I4. Texto duplicado: o adapter faz diff entre partials e snapshot cumulativo
510
+ * (gate hasSeenPartials), nunca reemite texto já enviado.
511
+ * I5. Stream drenado por inteiro (dispara persister + hooks via o tap em
512
+ * runAgentV2); abort do cliente → kill(). RESSALVA HITL: no vault o run
513
+ * termina sozinho (emite `result`); o adapter NÃO dá kill — só suprime a
514
+ * saída ao cliente e continua drenando até o `result` natural, pra o
515
+ * persister flipar a thread pra `awaiting_vault` e `handle.finished`
516
+ * resolver. Ver sse-adapter.ts.
517
+ * I6. V1 achata turnos: um único stream ordenado de deltas, sem id de bloco/
518
+ * turno no payload.
519
+ */
520
+ /** Versão do schema. Bump em qualquer mudança incompatível. */
521
+ const SCHEMA_VERSION = "v1";
522
+ //#endregion
523
+ exports.Agents = Agents;
524
+ exports.Run = Run;
525
+ exports.SCHEMA_VERSION = SCHEMA_VERSION;
526
+ exports.VolundAuthError = VolundAuthError;
527
+ exports.VolundAwaitingInputError = VolundAwaitingInputError;
528
+ exports.VolundError = VolundError;
529
+ exports.VolundForbiddenError = VolundForbiddenError;
530
+ exports.VolundNotFoundError = VolundNotFoundError;
531
+ exports.VolundOS = VolundOS;
532
+ exports.VolundRunBusyError = VolundRunBusyError;
533
+ exports.VolundRunFailedError = VolundRunFailedError;
534
+ exports.errorFromApiResponse = errorFromApiResponse;
535
+ exports.parseVolundSSE = parseVolundSSE;
536
+
537
+ //# sourceMappingURL=index.cjs.map