---
title: SDK (JavaScript y Python)
description: Habla con los agentes de Lampson desde tu código — npm i lampson o pip install lampson — contra un Lampson corriendo (servidor) o con Lampson adentro de tu programa (local).
---
# SDK

```sh
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](/es/docs/api), con una URL y una [API key](/es/docs/api#api-keys). 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`](/es/docs/headless#stdio-un-proceso-abierto-modo-local). 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

```js
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

```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:

```js
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
```

```python
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 atiende `build`, el agente de programación de Lampson, con todas sus herramientas.
- **`lampsonTools: false`** (`lampson_tools` en Python, solo modo local): en ese proceso no existe ninguna herramienta propia de Lampson —ni `bash`, ni `read`, ni `write`, ni `fetch`—, tampoco para los agentes integrados. Tus agentes tienen solo las que les das (plugins, [MCP](/es/docs/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 con `onWarning` (`on_warning`), o `null` (`None`) para silenciarlo; la lista queda en `lampson.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`.

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

```python
@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:

```js
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())
```

```python
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 ser `https`, salvo `localhost`.
- 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 un `call_id` firmado, 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, `uvicorn` con 1 worker y código bloqueante): si ese worker está esperando un `ask`, no puede atender la llamada a la herramienta y los dos se quedan esperando. Node, FastAPI con `async` y 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" }`):

```synsema
-- .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 `task` con 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 `require` son **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 `task` se niega. `env(…)` solo con nombres exactos (`require env("STRIPE_KEY")`); las variables `LAMPSON_*` son de Lampson, salvo `LAMPSON_CTX`. El `.env` de la carpeta no se carga.
- **Corre solo aprobada:** `lampson tools list` muestra cada archivo y lo que puede tocar, y `lampson 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 `.syn` de 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 `test` corren con `synsema test .lampson/tools/estado_pedido.syn`, como cualquier código Synsema.
- Los archivos que empiezan con `_` son ayudantes (`use "./_comun.syn"`), no herramientas. Un `README.md` en 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](/es/docs/security), y un ejemplo completo con cuatro ataques en [Receta: un agente de soporte](/es/docs/support).

### 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:

```js
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:

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

## Otros lenguajes

Cualquier lenguaje que haga un POST usa la [API HTTP](/es/docs/api) directamente, y `/v1/openapi.json` sirve para generar un cliente (Go, Java, C#, PHP…) con `openapi-generator`.
