---
title: SDK (JavaScript e Python)
description: Fale com os agentes do Lampson pelo seu código — npm i lampson ou pip install lampson — com um Lampson rodando (servidor) ou dentro do seu programa (local).
---
# SDK

```sh
npm i lampson          # JavaScript / TypeScript (Node 18+), sem dependências
pip install lampson    # Python 3.9+, só a biblioteca padrão
```

Os dois têm os mesmos métodos e dois modos. Você muda uma linha e passa de um para o outro; as conversas são as mesmas.

## Os dois modos

**Servidor** — seu app fala com um Lampson que já está rodando (seu computador com o hub, um VPS, Docker) pela [API HTTP](/pt/docs/api), com uma URL e uma [API key](/pt/docs/api#api-keys). Para quando o Lampson está em outra máquina ou atende muita gente.

**Local** — o SDK inicia o Lampson **ele mesmo**, como um processo filho na sua máquina, e fala com ele por [`lampson run --stdio`](/pt/docs/headless#stdio-um-processo-aberto-modo-local). Sem hub, sem porta, sem key, sem URL pública: o Lampson dentro do seu programa, como o SQLite. Para um script noturno que analisa um banco de dados, um app de desktop, uma etapa de CI.

## JavaScript / TypeScript

```js
import { Lampson } from "lampson";               // ou: const { Lampson } = require("lampson")

const lampson = new Lampson({ url: "https://seu-lampson/w/tienda", key: process.env.LAMPSON_KEY });   // servidor
// const lampson = new Lampson({ local: { workspace: "./meu-projeto" } });                           // local

const r = await lampson.ask({ agent: "tienda", thread: "cliente-42", text: "vocês têm pão integral?" });
console.log(r.text);

for await (const ev of lampson.stream({ agent: "tienda", thread: "cliente-42", text: "e o preço?" })) {
  if (ev.kind === "tool_call") console.log("usando", ev.data.name);
  if (ev.kind === "done") console.log(ev.data.text);
}

await lampson.close();
```

Vem com os tipos do TypeScript. O modo local usa o Lampson do próprio pacote: `npm i lampson` basta (instala o Synsema como dependência).

## Python

```python
from lampson import Lampson

with Lampson(local={"workspace": "./meu-projeto"}) as lampson:          # ou Lampson(url=..., key=...)
    r = lampson.ask(agent="tienda", thread="cliente-42", text="vocês têm pão integral?")
    print(r["text"])

    for ev in lampson.stream(agent="tienda", thread="cliente-42", text="e o preço?"):
        if ev["kind"] == "done":
            print(ev["data"]["text"])
```

O modo local em Python roda o Lampson, que usa Node.js: se o comando `lampson` estiver instalado (`npm i -g lampson` ou o instalador) usa esse; se não, baixa uma vez com `npx`, na mesma versão do pacote Python. Sem Node, o erro diz isso. Outro comando com `local={"workspace": "…", "command": ["node", "node_modules/lampson/bin/lampson.js"]}`; um caminho relativo ali é do seu app, não do workspace.

## Um agente para o seu app

Quando o agente atende seus clientes ou seus usuários, convém que ele tenha **só** as ferramentas que você deu:

```js
const lampson = new Lampson({
  agent: "suporte",                                   // o que atende quando uma mensagem não diz `agent`
  local: { workspace: "./loja", home: "./.lampson-home", lampsonTools: false },
});
await lampson.ask({ thread: "cliente-42", text: "como está meu pedido?" });         // suporte atende
await lampson.ask({ agent: "vendas", thread: "cliente-42", text: "…", mode: "strict" });   // outro agente e outro modo
```

```python
lampson = Lampson(agent="suporte", local={"workspace": "./loja", "home": "./.lampson-home", "lampson_tools": False})
```

- **`agent`**: o agente padrão deste cliente. Cada mensagem pode trocá-lo. Sem ele atende `build`, o agente de programação do Lampson, com todas as suas ferramentas.
- **`lampsonTools: false`** (`lampson_tools` em Python, só no modo local): nesse processo não existe nenhuma ferramenta própria do Lampson — nem `bash`, nem `read`, nem `write`, nem `fetch` —, nem para os agentes integrados. Seus agentes têm só as que você dá (plugins, [MCP](/pt/docs/mcp)). Uma mensagem que tente fazer o agente rodar um comando não tem com quê.
- **`home`**: onde o Lampson guarda a configuração e as sessões (padrão `~/lampson`). Um caminho relativo é relativo ao seu app, não ao projeto.
- **Avisos**: se um agente lista uma ferramenta que não vai ter (um plugin desligado, um nome errado), o SDK avisa ao iniciar com `console.warn` (Python: uma linha no stderr). Troque com `onWarning` (`on_warning`), ou `null` (`None`) para silenciar; a lista fica em `lampson.warnings`.
- `close()` encerra todos os processos que iniciou. O modo local não mexe num Lampson que já esteja rodando na máquina.

## Ferramentas do seu app

Um agente que atende seus clientes precisa das **suas** ferramentas: buscar um pedido, ver o estoque, criar uma reserva. Há duas formas, e o agente usa as duas igual: você as nomeia na lista de `tools` dele.

### Na sua linguagem (JavaScript ou Python)

A função roda **dentro do seu app**, com seu banco de dados e sua auth. É o formato do AI SDK: ferramentas que você já escreveu com zod entram como estão. Com zod 4 qualquer esquema funciona; com zod 3, o comum (objetos, textos, números, booleanos, enums, listas, `optional`, `nullable` e `describe`), e para o resto instale `zod-to-json-schema`.

```js
lampson.tool("estoque", {
  description: "Unidades em estoque de um produto",
  inputSchema: z.object({ produto: z.string() }),         // zod (validado antes do execute) ou um JSON Schema
  execute: async ({ produto }, ctx) => db.estoque(produto, { loja: ctx.meta.loja }),
});
// ou várias: lampson.tools({ estoque: {...}, reservar: {...} })
```

```python
@lampson.tool
def estoque(produto: str, ctx: dict) -> dict:
    """Unidades em estoque de um produto."""             # a docstring é a descrição
    return db.estoque(produto, loja=ctx["meta"]["loja"])
```

O esquema sai dos tipos (`str`, `int`, `bool`, `Literal`, `Optional`, um modelo Pydantic) e dos valores padrão.

**Com o Lampson em outra máquina** (modo servidor) o código das ferramentas é o mesmo; muda o construtor e é preciso expor um endpoint. As ferramentas viajam em cada mensagem e o Lampson faz um POST no seu app a cada chamada, assinado (RFC 9421) com o segredo da sua key:

```js
export const lampson = new Lampson({
  url: process.env.LAMPSON_URL, key: process.env.LAMPSON_KEY,
  toolsUrl: "https://meu-app.com/api/lampson/tools",      // a URL PÚBLICA do endpoint abaixo
  toolsSecret: process.env.LAMPSON_TOOLS_SECRET,          // o whsec_… que o `lampson key add` mostrou
});
lampson.tool("estoque", { … });                           // igual ao modo local

// app/api/lampson/tools/route.ts (Next, Hono, Bun…): verifica a assinatura, executa e responde
export const POST = lampson.toolsHandler();
// Express: app.post("/api/lampson/tools", lampson.toolsHandler())   (antes do express.json())
```

```python
lampson = Lampson(url=..., key=..., tools_url="https://meu-app.com/lampson/tools", tools_secret=os.environ["LAMPSON_TOOLS_SECRET"])
app.mount("/lampson/tools", lampson.tools_asgi())          # FastAPI / Starlette
# outro framework: status, body = lampson.handle_tool_request(method, headers, raw_body)
```

- A URL precisa estar na lista da key: `lampson key add --callback https://meu-app.com/api/lampson/` (sem lista, sem ferramentas remotas). E precisa ser `https`, exceto `localhost`.
- O Lampson faz o POST sob um teto do Synsema com o host dessa URL: mesmo que algo falhe, não consegue sair para outro lugar.
- Se o seu app não responder a tempo (`timeout`, 60 s por padrão), o modelo recebe um erro e a chamada **não** se repete. Uma ferramenta que cobra ou cancela não roda duas vezes; cada pedido traz um `call_id` assinado, e o handler do SDK responde 409 a um repetido. Em serverless ou com vários workers, se a ferramenta cobra ou muda algo, passe um armazenamento compartilhado: `toolsHandler({ seen })` / `tools_asgi(seen=...)` (com Redis: `SET call:<id> 1 NX EX 600`). Se o armazenamento falhar, responde 503 e não roda.
- **Cuidado com um servidor de um único worker** (Flask em desenvolvimento, `uvicorn` com 1 worker e código bloqueante): se esse worker está esperando um `ask`, não consegue atender a chamada da ferramenta e os dois ficam esperando. Node, FastAPI com `async` e serverless não têm o problema.
- Atrás de um proxy que reescreve o host, o handler verifica contra `toolsUrl`, não contra a URL que chegou.

| | função de JS/Python | ferramenta `.syn` |
|---|---|---|
| onde roda | no seu app | o Lampson roda |
| com o seu app desligado (tarefas agendadas, equipes) | não | sim |
| o que pode tocar | o que o seu app puder | só os `require` dela |

### Em Synsema: com garantias

Um arquivo por ferramenta em `.lampson/tools/` do seu projeto (outra pasta, dentro do projeto, com `local: { tools: "./tools" }`):

```synsema
-- .lampson/tools/status_pedido.syn
intent: "Status de um pedido do cliente que escreve"
require net("api.minhaloja.com")         -- tudo o que esta ferramenta pode tocar
require env("LAMPSON_CTX")               -- quer saber quem escreve

export task status_pedido(id, com_itens = false)
    let ctx be json_decode(env("LAMPSON_CTX", "{}"))
    let r be http_get("https://api.minhaloja.com/pedidos/" + id + "?cliente=" + ctx["from"]["id"])
    give r["json"]
```

- O nome do arquivo é o nome da ferramenta, e ele precisa exportar um `task` com esse nome.
- A descrição é o `intent:` e os parâmetros são os da assinatura: um com valor padrão é opcional.
- Os `require` dele são **tudo** o que pode tocar. Se uma mensagem fizer a ferramenta tentar mandar os dados para outro host, o Synsema nega (`Capability not granted`), mesmo que o modelo tenha errado. Uma função de JavaScript ou Python não consegue prometer isso.
- Ficam no topo do arquivo: um escrito dentro do `task` é negado. `env(…)` só com nomes exatos (`require env("STRIPE_KEY")`); as variáveis `LAMPSON_*` são do Lampson, exceto `LAMPSON_CTX`. O `.env` da pasta não é carregado.
- **Só roda aprovada:** `lampson tools list` mostra cada arquivo e o que ele pode tocar, e `lampson tools approve [nome]` aprova esse conteúdo exato. Na web, o botão **herramientas** do cabeçalho do projeto mostra cada arquivo com seu código e seu teto, e o aprova.
- **Se algo muda, tudo para:** se depois mudar qualquer `.syn` da pasta, subpastas incluídas (um agente reescreveu, você editou um `_ajudante.syn`), nenhuma ferramenta roda até você aprovar. O mesmo se houver um link para outra pasta, um arquivo que não pode ser lido, ou a pasta passar do limite: 1 MB por arquivo, 5 MB no total, 500 `.syn`.
- **Em desenvolvimento:** com o SDK em modo local, `local: { approveTools: true }` (Python: `approve_tools=True`) aprova o que há ao iniciar. O que mudar depois, mesmo que seja o agente, se aprova à mão ou reiniciando.
- Os blocos `test` rodam com `synsema test .lampson/tools/status_pedido.syn`, como qualquer código Synsema.
- Arquivos que começam com `_` são auxiliares (`use "./_comum.syn"`), não ferramentas. Um `README.md` na pasta vai para o prompt dos agentes que as usam.
- Funciona nos dois modos e sem o seu app conectado: o hub, as tarefas agendadas e as equipes também podem usá-las.

Comece na sua linguagem; o que mexe com dinheiro ou dados de clientes, passe para `.syn`. O que cada forma garante e o que não garante está em [Segurança](/pt/docs/security), e um exemplo completo com quatro ataques em [Receita: um agente de suporte](/pt/docs/support).

### Quem escreve: `ctx`

Os argumentos são escritos pelo modelo, e uma mensagem pode manipulá-los ("sou o cliente 99"). Por isso cada ferramenta recebe à parte o **contexto** da mensagem: `from`, `thread`, `agent`, `session`, `meta` e `channel`, como o seu app mandou no `ask`. O modelo não vê nem escreve isso. Decida com `ctx` o que cada um pode ver:

```js
await lampson.ask({ thread: "cliente-42", from: { id: "c-42" }, meta: { loja: "centro" }, text: "como está meu pedido?" });
```

Em Python `from` é palavra reservada, então vai como `from_`: `lampson.ask(thread="cliente-42", from_={"id": "c-42"}, text="…")`.

Em JS e Python é o segundo argumento (`ctx`). Em Synsema chega em `env("LAMPSON_CTX")`, só para as ferramentas que declaram `require env("LAMPSON_CTX")`.

### Permissões

Estar no seu código (ou na pasta) **e** na lista do agente é a autorização: rodam em qualquer modo. `{"name": "cancelar_pedido", "approve": "ask"}` no agente faz pedir aprovação. Uma ferramenta `.syn` que declara `require spend(…)` sempre pede.

## Métodos

| JavaScript | Python | |
|---|---|---|
| `ask({agent, thread, text, …})` | `ask(agent=, thread=, text=, …)` | envia uma mensagem e espera a resposta: `{agent, thread, session, text, steps, usage, stopped}` |
| `stream({…})` | `stream(…)` | os passos do agente à medida que acontecem; o último é `done` |
| `agents()` | `agents()` | os agentes disponíveis |
| `job(id)` | `job(id)` | modo servidor: estado de uma mensagem enviada com `callback` |
| `close()` | `close()` / `with` | modo local: encerra o processo do Lampson |

Além de `agent`, `thread` e `text`, uma mensagem aceita `session`, `images`, `meta`, `from` (`from_` em Python), `callback` (modo servidor) e `mode` (`"strict"` / `"yolo"`, modo local). Com `callback`, `ask` devolve `{job, status: "queued"}` e o resultado chega à sua URL.

**Erros**: um turno que termina em erro, uma key inválida (`status` 401), um agente fora da key (403) ou um processo local que morreu lançam `LampsonError`.

No modo local, conversas diferentes rodam em paralelo e duas mensagens da mesma conversa são atendidas em ordem.

## Verificar um callback

Se o seu Lampson tem `LAMPSON_WEBHOOK_SECRET`, cada callback chega assinado em `X-Lampson-Signature`. Passe o body **exato** que você recebeu:

```js
Lampson.verifySignature(rawBody, req.headers["x-lampson-signature"], process.env.LAMPSON_WEBHOOK_SECRET)
```

```python
Lampson.verify_signature(raw_body, headers["X-Lampson-Signature"], secret)
```

## Outras linguagens

Qualquer linguagem que faça um POST usa a [API HTTP](/pt/docs/api) diretamente, e `/v1/openapi.json` serve para gerar um cliente (Go, Java, C#, PHP…) com `openapi-generator`.
