@volund-ia/sdk
v0.4.0
Published
Cliente TypeScript para rodar agentes do Volund OS com streaming em tempo real.
Readme
@volund-ia/sdk
Cliente TypeScript para rodar agentes do Volund OS pelo seu próprio código e receber, em tempo real (streaming), tudo que o agente faz — raciocínio, chamadas de ferramenta e a resposta token a token.
npm install @volund-ia/sdkRequer Node ≥ 18 (usa o
fetchnativo). Funciona também em Deno, Bun, Workers e no browser (parser SSE 100% web-standard).
Quickstart
import { VolundOS } from "@volund-ia/sdk";
const volund = new VolundOS({ apiKey: process.env.VOLUND_API_KEY! });
const run = await volund.agents.run({
agentId: "agt_123",
input: "Pesquise os 3 maiores concorrentes da empresa X e resuma.",
});
// Passo a passo conforme acontece:
for await (const event of run.stream()) {
if (event.type === "assistant_text_delta") process.stdout.write(event.delta);
if (event.type === "tool_call") console.log("→ usou:", event.tool_name);
}
// Ou só o resultado final:
const run2 = await volund.agents.run({ agentId: "agt_123", input: "Oi" });
const { output, usage } = await run2.result();Continuar uma conversa (mesma thread):
const next = await volund.agents.continue({ runId: run.id, input: "E o 4º?" });A DX
Espelha o Cursor SDK (Agent.create() → agent.send() → run.stream()):
new VolundOS() → agents.run() → run.stream() / run.result() /
run.cancel().
Eventos (VolundEvent)
Stream tipado por união discriminada — faça narrowing por event.type:
| type | Campos |
| ----------------------- | ------------------------------------------------- |
| run_started | protocol, run_id, agent_id |
| thinking_delta | delta (raciocínio, streaming) |
| assistant_text_delta | delta (resposta, streaming) |
| tool_call | tool_call_id, tool_name, input |
| tool_result | tool_call_id, output, is_error? |
| awaiting_input | request_id, kind: "vault" \| "approval" (HITL — fecha o stream) |
| run_finished | status, output, usage, error? |
O contrato é snake_case no fio (consistente com a API v1 e o ecossistema
Anthropic/Cursor) e versionado por SCHEMA_VERSION (protocol no run_started).
Erros
Todos herdam de VolundError (tem .code e .status). Roteie por instanceof:
| Classe | Quando |
| --------------------------- | --------------------------------------- |
| VolundAuthError | 401 — chave ausente/inválida |
| VolundForbiddenError | 403 — sem acesso ao agente |
| VolundNotFoundError | 404 — agente/run inexistente |
| VolundRunBusyError | 409 — já há run ativo na thread |
| VolundRunFailedError | run.result() quando o run falha |
| VolundAwaitingInputError | run.result() quando pausa p/ vault ou approval |
Aprovações (HITL)
Se o agente pausar esperando aprovação de uma ferramenta, o stream emite
awaiting_input com kind: "approval". Decida por código e o run retoma:
for await (const ev of run.stream()) {
if (ev.type === "awaiting_input" && ev.kind === "approval") {
await volund.approvals.approve(ev.request_id); // ou .reject(id, { note })
}
}volund.approvals: approve(id), reject(id, { note? }), decide(id, "approve" | "reject", { note? }).
Perguntas do agente
Quando o agente abre um card de pergunta, o stream emite question_asked e a
tool do outro lado fica bloqueada esperando. Responda e o mesmo turno segue:
for await (const ev of run.stream()) {
if (ev.type === "question_asked") {
// desenhe o card com ev.questions e colete a escolha
await volund.questions.answer(ev.request_id, { "Qual sprint?": "Sprint 4" });
}
}volund.questions: answer(id, answers), skip(id).
Repare que aqui não é awaiting_input: aquele é uma pausa que encerra o
stream, enquanto question_asked mantém o for await vivo — é por isso que dá
para responder sem sair do laço. Sem resposta, a tool desiste em ~10 minutos e o
agente encerra o turno dizendo que aguarda; o card continua respondível.
As chaves de answers são os textos das perguntas, como vieram em
ev.questions.
Notas
- 0.4.0: novo evento
question_askede novovolund.questions(answer/skip). Aditivo no wire; se você faz exhaustive switch emev.type, adicione umcase "question_asked". Também novo o código de erroquestion_not_found. - 0.3.0:
AwaitingInputEvent.kindagora inclui"approval"(além de"vault"). Aditivo no wire; se você faz exhaustive switch emev.kind, adicione umcase "approval". stream()é consumível uma única vez (é um stream de rede). Não combinestream()eresult()no mesmoRun.run.cancel()aborta a conexão — o servidor encerra a sandbox.execution: "local"(rodar nocwddo dev, estilo Cursor) chega na V2; o tipo já existe, mas a V1 só roda na nuvem.
Testar contra um preview da Vercel (modo intermediário)
Antes do endpoint de produção, dá pra apontar o SDK pro deployment de preview do PR:
VOLUND_API_KEY=vos_live_... \
VOLUND_AGENT_ID=agt_... \
VOLUND_BASE_URL=https://seu-preview.vercel.app \
npm run exampleSe o preview estiver com Deployment Protection ligada, passe o token de
Protection Bypass for Automation — ele vira um header via defaultHeaders:
VERCEL_BYPASS=<secret> ...demais envs... npm run examplenew VolundOS({
apiKey,
baseUrl: "https://seu-preview.vercel.app",
defaultHeaders: { "x-vercel-protection-bypass": process.env.VERCEL_BYPASS! },
});Timeouts e runs longos
O SDK tem dois timeouts, e nenhum limita a duração total do run:
timeoutMs(default 60s) — só a fase pré-stream: tempo máximo até a resposta (headers) chegar. Assim que o stream começa, é desarmado.idleTimeoutMs(default desligado) — durante o stream: aborta se nenhum dado (evento ou heartbeat) chegar nesse intervalo. Serve para detectar conexões travadas sem matar runs longos saudáveis — o servidor manda heartbeat: ping(~15s) que reseta o ocioso.- Duração total do run NÃO é limitada pelo SDK — depende do servidor/plataforma
(a rota usa
maxDuration; confirme o teto do seu plano de deploy).
new VolundOS({ apiKey, timeoutMs: 30_000, idleTimeoutMs: 120_000 });Desenvolvimento
npm install
npm test # testes do parser SSE (vitest)
npm run typecheck
npm run build # tsdown → ESM + CJS + .d.ts
npm run check:protocol # garante o contrato em sincronia com o volund-osO contrato de eventos é vendorado de volund-os em src/protocol/events.ts
— ver src/protocol/README.md. Atualize só via
npm run sync:protocol.
Consumir um commit que ainda não foi publicado
O release sai por tag (v* dispara o workflow). Entre o merge e a publicação, um
consumidor pode apontar direto para o repositório ou para um tarball local:
npm install volund-ia/os-sdk # ou volund-ia/os-sdk#v0.4.0
npm pack && npm install ./volund-ia-sdk-0.4.0.tgzO prepare builda no install, então o dist/ não precisa estar versionado. O
nome do tarball achata o escopo: @volund-ia/sdk vira volund-ia-sdk-<versão>.
Licença
MIT
