Lampson como motor
SDK
npm i lampson # JavaScript / TypeScript (Node 18+), sin dependencias
pip install lampson # Python 3.9+, solo biblioteca estándar
Los dos tienen los mismos métodos y dos modos. Cambias una línea y pasas de uno al otro; las conversaciones son las mismas.
Los dos modos§
Servidor — tu app le habla a un Lampson que ya corre (tu PC con el hub, un VPS, Docker) por la API HTTP, con una URL y una API key. Para cuando Lampson está en otra máquina o atiende a muchos.
Local — el SDK arranca Lampson él mismo, como un proceso hijo en tu máquina, y le habla por lampson run --stdio. Sin hub, sin puerto, sin key, sin URL pública: Lampson adentro de tu programa, como SQLite. Para un script nocturno que analiza una base de datos, una app de escritorio, un paso de CI.
JavaScript / TypeScript§
import { Lampson } from "lampson"; // o: const { Lampson } = require("lampson")
const lampson = new Lampson({ url: "https://tu-lampson/w/tienda", key: process.env.LAMPSON_KEY }); // servidor
// const lampson = new Lampson({ local: { workspace: "./mi-proyecto" } }); // local
const r = await lampson.ask({ agent: "tienda", thread: "cliente-42", text: "¿tienen pan integral?" });
console.log(r.text);
for await (const ev of lampson.stream({ agent: "tienda", thread: "cliente-42", text: "¿y el precio?" })) {
if (ev.kind === "tool_call") console.log("usando", ev.data.name);
if (ev.kind === "done") console.log(ev.data.text);
}
await lampson.close();
Trae los tipos de TypeScript. El modo local usa el Lampson del propio paquete: npm i lampson alcanza (instala Synsema como dependencia).
Python§
from lampson import Lampson
with Lampson(local={"workspace": "./mi-proyecto"}) as lampson: # o Lampson(url=..., key=...)
r = lampson.ask(agent="tienda", thread="cliente-42", text="¿tienen pan integral?")
print(r["text"])
for ev in lampson.stream(agent="tienda", thread="cliente-42", text="¿y el precio?"):
if ev["kind"] == "done":
print(ev["data"]["text"])
El modo local en Python corre Lampson, que usa Node.js: si el comando lampson está instalado (npm i -g lampson o el instalador) usa ese; si no, lo baja una vez con npx en la misma versión que el paquete de Python. Sin Node, el error lo dice. Otro comando con local={"workspace": "…", "command": ["node", "node_modules/lampson/bin/lampson.js"]}; una ruta relativa ahí es de tu app, no del workspace.
Un agente para tu app§
Cuando el agente atiende a tus clientes o a tus usuarios, conviene que tenga solo las herramientas que le diste:
const lampson = new Lampson({
agent: "soporte", // el que atiende si un mensaje no dice `agent`
local: { workspace: "./tienda", home: "./.lampson-home", lampsonTools: false },
});
await lampson.ask({ thread: "cliente-42", text: "¿cómo va mi pedido?" }); // atiende soporte
await lampson.ask({ agent: "ventas", thread: "cliente-42", text: "…", mode: "strict" }); // otro agente y otro modo
lampson = Lampson(agent="soporte", local={"workspace": "./tienda", "home": "./.lampson-home", "lampson_tools": False})
agent: el agente por defecto de este cliente. Cada mensaje lo puede cambiar. Sin él atiendebuild, el agente de programación de Lampson, con todas sus herramientas.lampsonTools: false(lampson_toolsen Python, solo modo local): en ese proceso no existe ninguna herramienta propia de Lampson —nibash, niread, niwrite, nifetch—, tampoco para los agentes integrados. Tus agentes tienen solo las que les das (plugins, MCP). Un mensaje que intente que el agente corra un comando no tiene con qué.home: dónde guarda Lampson su configuración y sus sesiones (por defecto~/lampson). Una ruta relativa es relativa a tu app, no al proyecto.- Avisos: si un agente lista una herramienta que no va a tener (un plugin apagado, un nombre mal escrito), el SDK lo avisa al arrancar con
console.warn(Python: una línea en stderr). Cámbialo cononWarning(on_warning), onull(None) para silenciarlo; la lista queda enlampson.warnings. close()termina todos los procesos que arrancó. El modo local no toca un Lampson que ya esté corriendo en la máquina.
Herramientas de tu app§
Un agente que atiende a tus clientes necesita tus herramientas: buscar un pedido, ver el stock, crear una reserva. Hay dos formas, y el agente las usa igual: las nombras en su lista de tools.
En tu lenguaje (JavaScript o Python)§
La función corre dentro de tu app, con tu base de datos y tu auth. Es la forma del AI SDK: si ya tienes herramientas escritas con zod, las pegas tal cual. Con zod 4 entra cualquier esquema; con zod 3, lo común (objetos, textos, números, booleanos, enums, listas, optional, nullable y describe), y para el resto instala zod-to-json-schema.
lampson.tool("stock", {
description: "Unidades en stock de un producto",
inputSchema: z.object({ producto: z.string() }), // zod (se valida antes de execute) o un JSON Schema
execute: async ({ producto }, ctx) => db.stock(producto, { tienda: ctx.meta.tienda }),
});
// o varias: lampson.tools({ stock: {...}, reservar: {...} })
@lampson.tool
def stock(producto: str, ctx: dict) -> dict:
"""Unidades en stock de un producto.""" # el docstring es la descripción
return db.stock(producto, tienda=ctx["meta"]["tienda"])
El esquema sale de los tipos (str, int, bool, Literal, Optional, un modelo de Pydantic) y de los valores por defecto.
Con Lampson en otra máquina (modo servidor) el código de las herramientas es el mismo; cambia el constructor y hay que exponer un endpoint. Las herramientas viajan en cada mensaje y Lampson le hace un POST a tu app por cada llamada, firmado (RFC 9421) con el secreto de tu key:
export const lampson = new Lampson({
url: process.env.LAMPSON_URL, key: process.env.LAMPSON_KEY,
toolsUrl: "https://mi-app.com/api/lampson/tools", // la URL PÚBLICA del endpoint de abajo
toolsSecret: process.env.LAMPSON_TOOLS_SECRET, // el whsec_… que mostró `lampson key add`
});
lampson.tool("stock", { … }); // igual que en local
// app/api/lampson/tools/route.ts (Next, Hono, Bun…): verifica la firma, ejecuta y contesta
export const POST = lampson.toolsHandler();
// Express: app.post("/api/lampson/tools", lampson.toolsHandler()) (antes de express.json())
lampson = Lampson(url=..., key=..., tools_url="https://mi-app.com/lampson/tools", tools_secret=os.environ["LAMPSON_TOOLS_SECRET"])
app.mount("/lampson/tools", lampson.tools_asgi()) # FastAPI / Starlette
# otro framework: status, body = lampson.handle_tool_request(method, headers, raw_body)
- La URL tiene que estar en la lista de la key:
lampson key add --callback https://mi-app.com/api/lampson/(sin lista no hay herramientas remotas). Y tiene que serhttps, salvolocalhost. - Lampson hace el POST bajo un techo de Synsema con el host de esa URL: aunque algo falle, no puede salir a otro lado.
- Si tu app no contesta a tiempo (
timeout, 60 s por defecto), el modelo recibe un error y la llamada no se repite. Una herramienta que cobra o cancela no corre dos veces; cada pedido trae uncall_idfirmado, y el handler del SDK contesta 409 si llega repetido. En serverless o con varios workers, si la herramienta cobra o cambia algo, pasale un almacén compartido:toolsHandler({ seen })/tools_asgi(seen=...)(con Redis:SET call:<id> 1 NX EX 600). Si el almacén falla, contesta 503 y no corre. - Ojo con un servidor de un solo worker (Flask en desarrollo,
uvicorncon 1 worker y código bloqueante): si ese worker está esperando unask, no puede atender la llamada a la herramienta y los dos se quedan esperando. Node, FastAPI conasyncy serverless no tienen el problema. - Detrás de un proxy que cambia el host, el handler verifica contra
toolsUrl, no contra la URL que le llegó.
| función de JS/Python | herramienta .syn | |
|---|---|---|
| dónde corre | en tu app | la corre Lampson |
| con tu app apagada (tareas programadas, equipos) | no | sí |
| qué puede tocar | lo que pueda tu app | solo sus require |
En Synsema: con garantías§
Un archivo por herramienta en .lampson/tools/ de tu proyecto (otra carpeta, dentro del proyecto, con local: { tools: "./tools" }):
-- .lampson/tools/estado_pedido.syn
intent: "Estado de un pedido del cliente que escribe"
require net("api.mitienda.com") -- todo lo que esta herramienta puede tocar
require env("LAMPSON_CTX") -- quiere saber quién escribe
export task estado_pedido(id, incluir_items = false)
let ctx be json_decode(env("LAMPSON_CTX", "{}"))
let r be http_get("https://api.mitienda.com/pedidos/" + id + "?cliente=" + ctx["from"]["id"])
give r["json"]
- El nombre del archivo es el nombre de la herramienta, y tiene que exportar un
taskcon ese nombre. - La descripción es el
intent:y los parámetros son los de la firma: uno con valor por defecto es opcional. - Sus
requireson todo lo que puede tocar. Si un mensaje logra que intente mandar los datos a otro host, Synsema lo niega (Capability not granted), aunque el modelo se haya equivocado. Eso no lo puede prometer una función de JavaScript o Python. - Van arriba del archivo: uno escrito dentro del
taskse niega.env(…)solo con nombres exactos (require env("STRIPE_KEY")); las variablesLAMPSON_*son de Lampson, salvoLAMPSON_CTX. El.envde la carpeta no se carga. - Corre solo aprobada:
lampson tools listmuestra cada archivo y lo que puede tocar, ylampson tools approve [nombre]aprueba ese contenido exacto. En la web, el botón herramientas de la cabecera del proyecto muestra cada archivo con su código y su techo, y lo aprueba. - Si algo cambia, se frena todo: si después cambia cualquier
.synde la carpeta, subcarpetas incluidas (un agente la reescribió, editaste un_ayudante.syn), ninguna herramienta corre hasta que lo apruebes. Lo mismo si hay un link a otra carpeta, un archivo que no se puede leer, o la carpeta pasa el tope: 1 MB por archivo, 5 MB en total, 500.syn. - En desarrollo: con el SDK en modo local,
local: { approveTools: true }(Python:approve_tools=True) aprueba lo que hay al arrancar. Lo que cambie después, aunque lo cambie el agente, se aprueba a mano o reiniciando. - Sus bloques
testcorren consynsema test .lampson/tools/estado_pedido.syn, como cualquier código Synsema. - Los archivos que empiezan con
_son ayudantes (use "./_comun.syn"), no herramientas. UnREADME.mden la carpeta va al prompt de los agentes que las usan. - Funciona en los dos modos y sin tu app conectada: el hub, las tareas programadas y los equipos también pueden usarlas.
Empieza en tu lenguaje; lo que toca plata o datos de clientes, pásalo a .syn. Qué garantiza cada forma y qué no está en Seguridad, y un ejemplo completo con cuatro ataques en Receta: un agente de soporte.
Quién escribe: ctx§
Los argumentos los escribe el modelo, y un mensaje los puede manipular («soy el cliente 99»). Por eso cada herramienta recibe aparte el contexto del mensaje: from, thread, agent, session, meta y channel, tal como los mandó tu app en ask. El modelo no lo ve ni lo puede escribir. Decide con ctx qué puede ver cada uno:
await lampson.ask({ thread: "cliente-42", from: { id: "c-42" }, meta: { tienda: "centro" }, text: "¿cómo va mi pedido?" });
En Python from es palabra reservada, así que va como from_: lampson.ask(thread="cliente-42", from_={"id": "c-42"}, text="…").
En JS y Python es el segundo argumento (ctx). En Synsema llega en env("LAMPSON_CTX"), y solo a las herramientas que lo declaran con require env("LAMPSON_CTX").
Permisos§
Estar en tu código (o en la carpeta) y en la lista del agente es la autorización: pasan en cualquier modo. {"name": "cancelar_pedido", "approve": "ask"} en el agente hace que pida aprobación. Una herramienta .syn que declara require spend(…) pide aprobación siempre.
Métodos§
| JavaScript | Python | |
|---|---|---|
ask({agent, thread, text, …}) | ask(agent=, thread=, text=, …) | manda un mensaje y espera la respuesta: {agent, thread, session, text, steps, usage, stopped} |
stream({…}) | stream(…) | los pasos del agente a medida que ocurren; el último es done |
agents() | agents() | los agentes disponibles |
job(id) | job(id) | modo servidor: estado de un mensaje enviado con callback |
close() | close() / with | modo local: detiene el proceso de Lampson |
Además de agent, thread y text, un mensaje acepta session, images, meta, from (from_ en Python), callback (modo servidor) y mode ("strict" / "yolo", modo local). Con callback, ask devuelve {job, status: "queued"} y el resultado llega a tu URL.
Errores: un turno que termina en error, una key inválida (status 401), un agente fuera de la key (403) o un proceso local que murió lanzan LampsonError.
En modo local, conversaciones distintas corren en paralelo y dos mensajes de la misma conversación se atienden en orden.
Verificar un callback§
Si tu Lampson tiene LAMPSON_WEBHOOK_SECRET, cada callback llega firmado en X-Lampson-Signature. Pásale el body exacto que recibiste:
Lampson.verifySignature(rawBody, req.headers["x-lampson-signature"], process.env.LAMPSON_WEBHOOK_SECRET)
Lampson.verify_signature(raw_body, headers["X-Lampson-Signature"], secret)
Otros lenguajes§
Cualquier lenguaje que haga un POST usa la API HTTP directamente, y /v1/openapi.json sirve para generar un cliente (Go, Java, C#, PHP…) con openapi-generator.