Lampson

Lampson como motor

SDK

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, com uma URL e uma API key. 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. 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§

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§

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:

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
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). 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.

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: {...} })
@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:

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())
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/Pythonferramenta .syn
onde rodano seu appo Lampson roda
com o seu app desligado (tarefas agendadas, equipes)nãosim
o que pode tocaro que o seu app pudersó 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" }):

-- .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, e um exemplo completo com quatro ataques em Receita: um agente de suporte.

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:

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§

JavaScriptPython
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() / withmodo 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:

Lampson.verifySignature(rawBody, req.headers["x-lampson-signature"], process.env.LAMPSON_WEBHOOK_SECRET)
Lampson.verify_signature(raw_body, headers["X-Lampson-Signature"], secret)

Outras linguagens§

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

Ver como Markdown