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 atendebuild, o agente de programação do Lampson, com todas as suas ferramentas.lampsonTools: false(lampson_toolsem Python, só no modo local): nesse processo não existe nenhuma ferramenta própria do Lampson — nembash, nemread, nemwrite, nemfetch—, 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 comonWarning(on_warning), ounull(None) para silenciar; a lista fica emlampson.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 serhttps, excetolocalhost. - 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 umcall_idassinado, 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,
uvicorncom 1 worker e código bloqueante): se esse worker está esperando umask, não consegue atender a chamada da ferramenta e os dois ficam esperando. Node, FastAPI comasynce 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" }):
-- .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
taskcom esse nome. - A descrição é o
intent:e os parâmetros são os da assinatura: um com valor padrão é opcional. - Os
requiredele 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áveisLAMPSON_*são do Lampson, excetoLAMPSON_CTX. O.envda pasta não é carregado. - Só roda aprovada:
lampson tools listmostra cada arquivo e o que ele pode tocar, elampson 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
.synda 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
testrodam comsynsema 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. UmREADME.mdna 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§
| 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:
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.