validatePlanAgainstCatalog), grafo sin ciclos por Kahn, tope de 10 pasos. Un humano no dibujó este grafo; el sistema lo derivó y lo probó.Un substrato simbólico
para agentes que compongan
¿Lo querés como texto? Descargar el documento completo en Markdown (.md · ~73 KB · sin los diagramas SVG)
¿Qué es un substrato?
Un substrato es la capa por debajo de los agentes y los workflows donde todo lo que producen queda tipado y consultable. No es una base de datos. No es un knowledge base con embeddings. Es un grafo donde cada decisión, cada output, cada juicio humano y cada inferencia que un agente afirma se deposita como un nodo con relaciones explícitas a los demás.
La diferencia con un asistente conversacional es directa: el asistente es amnésico, el substrato es coherente con su propio pasado. Cada respuesta nueva puede consultar todo lo que se decidió antes y razonar contra eso. La aprobación que diste ayer es contexto disponible para el agente que arranca mañana.
(subject, predicate, object), no nubes de números. Cada afirmación es defendible porque tiene estructura y provenance.↓ (nada queda registrado)
Workflow B arranca sin contexto
→ repite preguntas, contradice decisiones previas
- Cada conversación empieza de cero, incluso entre agentes del mismo equipo
- El usuario hace de memoria del sistema (le pegás contexto en cada prompt)
- Las decisiones se evaporan: nadie sabe quién aprobó qué cosa, ni por qué
- Imposible auditar afirmaciones: el modelo dijo X, nadie sabe de dónde lo sacó
↓ (grafo crece)
Workflow B lee Claims de A antes de empezar
→ razona contra decisiones previas, no contra cero
- Cada decisión humana se materializa como Claim consultable por workflows futuros
- El equipo de agentes acumula memoria estructurada — no solo logs, conocimiento tipado
- Trazabilidad nativa: cualquier afirmación tiene provenance (qué Trace la produjo, qué humano la validó)
- Network effects: cada workflow extra agrega contexto a todos los siguientes
Empezó con una charla
en Stanford
8090
Las herramientas se desgastan; los substratos compuestan. Lo que Chamath nombraba como control plane no era un producto sino una capa por debajo de los agentes donde cada decisión, claim, intent y artifact queda trazado y disponible para futuras inferencias. Eso es lo que hace que un equipo de agentes mejore con el uso.
El punto era simple y duro a la vez: si construyo el consumer Agent Squad sin substrato, repito el patrón de cada SaaS de IA — los usuarios producen pixels pero el sistema no aprende. Si construyo el substrato primero, cada workflow que después monto encima compone valor en el grafo. Agent Squad consumer pasa a ser la primera ontology overlay, no el producto final.
Y entonces vino la pieza estratégica. Chamath estaba conectando control plane con network effect: el grafo de Claims tipados es lo que crea el efecto compuesto cross-empresa, no la app que se monta arriba.
Decidimos no construir un Photoshop. El producto es el substrato. La app que Miles está terminando es la primera demostración pública de lo que ese substrato permite. La idea de fondo: { Intent, Plan, Trace, Claim, Artifact } son los primitives que cualquier workflow agentic puede compilar a una DAG ejecutable, y cualquier decisión humana se vuelve contexto tipado para la próxima.
Siete decisiones estratégicas
tomadas con velocidad
Diez primitives y dos reglas duras
knowledge_access.requires_vectorDoce decisiones de tech
tomadas en una hora
| Capa | Tech | Por qué |
|---|---|---|
| Spec | JSON Schema + Zod + TypeScript | Spec abierta, no DSL propio. Cualquiera con JSON Schema válida outputs sin tocar el motor. |
| Motor | v2 event-sourced + Inngest OSS | El compilador produce graph_nodes; reducer puro + aplicar-evento proyectan runs/run_events; un worker consume work_items. Inngest conserva eventos y crons durables. execute-plan es el executor v1 en deprecación. |
| Substrate DB | Postgres 16 + pgvector + pgvectorscale | DiskANN para 3072-dim embeddings (HNSW topa a 2000). Dedicada en Hetzner, NO en InsForge. |
| Workspace DB | InsForge | Sigue siendo para la app consumer (auth, user data, outputs feed). Separado del substrato por diseño. |
| LLM | Max CLI (claude-cli, plan Max; sin API de Anthropic) | Sonnet 4.5 y Opus para composers y scorers; Grok para synthesis. El path Vercel AI SDK + @ai-sdk/anthropic quedó como gateway dormido (Fase 1). |
| Embedding | OpenAI text-embedding-3-large (3072d) | Mejor precision/recall para retrieval semántico. Backup a R2 desde día uno por si migramos vector DB. |
| Reranker | Cohere Rerank v3.5 | Capa de relevancia sobre top-K. Activable cuando el grafo crezca. Hoy aún no lo necesita. |
| Evaluators | Propios en packages/substrate-spec | No LangSmith. Evaluators son código testeado que vive con el spec, no servicio externo. |
| Observability | Langfuse OSS v3 self-hosted | Web + worker + ClickHouse + MinIO + Redis + Postgres. Tracing por trace_id del substrato. |
| API runtime | Hono sobre Bun 1.3 | Cold-start nulo, footprint mínimo. Una sola Hono app expone /api/intents, /api/approvals, /api/health. |
| API contract | tRPC interno · REST + OpenAPI 3.1 externo | Internamente tipos compartidos. Externamente OpenAPI para que cualquier cliente implemente. |
| Compute | Apps en Vercel · Motor + DB + Obs en Hetzner | Hetzner 4CPU/8GB ya pagado. El motor stateful vive donde tenemos disco; las apps stateless siguen en Vercel. |
Una caja Hetzner,
todos los servicios encendidos
El backup corre nightly a las 3 AM vía cron sobre ~/substrate-infra/scripts/backup-substrate-db.sh. Genera dump a ~/backups/substrate/ y, si existe el remote rclone r2-substrate:, también empuja a Cloudflare R2. Las migraciones SQL del substrato viven en agent-squad-app/db/substrate/migrations/0001_init.sql: 15 tablas, índices DiskANN sobre vectores de Claims y Artifacts, partitioning por fecha en traces y step_executions.
Pensalo así: imaginá que en una pared tenés colgado el brief que aprobaste ayer. A su lado van apareciendo post-its que el sistema escribe automáticamente. Cada uno de esos post-its es un Claim.
| Pieza | Qué responde | Ejemplos |
|---|---|---|
| sujeto | A qué cosa se pega el post-it | un brief · tu voz · un proyecto · una decisión · un prospecto |
| predicado | Qué dice sobre eso | approvedBy · hasKind · voiceTone · fitScore · producedByOp |
| objeto | Qué valor lleva | "human:roberto" · "doc" · "directo sin rodeos" · 0.75 |
| provenance | De dónde vino (auditoría nativa) | qué Trace lo emitió · qué Step · qué humano lo confirmó |
(brief 3aea, approvedBy, human:roberto) + provenanceclaim.recall_decisions sobre el grafo y le pasa al modelo: "hace 1 día, sobre un brief similar, Roberto aprobó la variante Hook 2 con el comentario 'gana sobre los otros 3'". El LLM no adivina ni recuerda — hereda esa decisión como input. Después de 100 workflows no tenés 100 logs ni 100 vectores opacos: tenés un grafo de 500–1000 afirmaciones tipadas sobre tu voz, tus criterios, tus decisiones pasadas. Y cualquier workflow futuro puede consultarlo antes de actuar. Eso es el efecto compuesto.
Cuatro alternativas conocidas
y dónde rompen para este caso
Una librería de capabilities
de la que cualquier Plan tira
- research.synthesize@1.0.0
- research.keyword_expand@1.0.0
- research.reddit_search@1.0.0
- research.youtube_comments@1.0.0
- research.rank_opportunities@1.0.0
- research.audience_intelligence@1.0.0
- document.ingest@1.0.0
- document.chunk@1.0.0
- document.extract@1.0.0
- document.query@1.0.0
- media.ingest@1.0.0
- media.transcribe@1.0.0
- url.fetch_transcript@1.0.0
- video.script_draft@1.0.0
- broll.generate@1.0.0
- heygen.generate@1.0.0
- voice.tts@1.0.0
- junta.panel@1.0.0
- junta.compose_verdict@1.0.0
- junta.load_report@1.0.0
- text.compose_narrative@2.0.0
- text.compose_brief@1.0.0
- trace.query@1.0.0
- artifact.publish@1.0.0
- claim.recall_decisions@1.0.0
- claim.recall_voice@1.0.0
- evaluator.run@1.0.0
- prospect.search@1.0.0
- pricing.observe@1.0.0
- metrics.digest@1.0.0
- audience.load@1.0.0
Muestra representativa. El catálogo completo son 73 Operations registradas en el motor (inngest/operations), con research como el dominio más denso.
Cuatro DAGs productivos
cada uno con su propio agente
Git (historial de commits), Excel (referencias entre celdas), Airflow y Temporal (data pipelines). El motor le hace topo-sort al DAG para arrancar cada Step solo cuando todos sus inputs terminaron — lo que permite ejecutar varias ramas en paralelo.El gate durable
donde la persona es autoridad final
step.waitForEvent: ACTIVAR_GATE crea un flow durable en artifact_approval_flows. Revisión, aprobación/elección, acuse del executor y cierre son estados persistidos; deadline_at + el barrido aplican el fallback con CAS, y required_role define quién puede decidir.Bug crítico aprendido: el match en step.waitForEvent debe usar async.data.X, no event.data.X. event referencia el trigger original (plan.compiled) — usarlo ahí evalúa al tiempo de la suspension, no del evento futuro, y matchea con null. async.data referencia el evento que se está esperando. Sin esto, todos los gates se quedan colgados indefinidamente.
Lo que esto desbloquea: el humano puede aprobar/rechazar/agregar comment, y la decisión queda como Claim tipado consultable por futuros workflows. La próxima vez que armemos un brief sobre el mismo topic, claim.recall_decisions ya tiene "este angle ganó" como contexto. Esto es el efecto compuesto del substrato — no es retrieval, es construcción de memoria estructurada.
Cuatro templates activados
con Sonnet 4.5 verdadero
Lo no trivial: Sonnet razonó sobre el propio backlog del workspace. El narrative de standup-digest mencionó textualmente cuántos digests anteriores quedaron queued y awaiting_human, porque trace.query + artifact.list_recent + claim.recall le feedearon esos datos. La cadena retrieval → composer está validada end-to-end con datos reales del substrato.
Aprobamos el brief
y nacieron cinco Claims
"Substrato vs RAG"
Este es el momento donde el substrato deja de ser teoría y empieza a comportarse como capital. La próxima vez que armemos un brief sobre el mismo topic, claim.recall_decisions va a recuperar "Hook 2 ganó sobre los otros 3" como contexto del composer. Sonnet no necesita recordarlo — el grafo se lo entrega. Eso es lo que distingue un substrato de un RAG: no busca documentos parecidos, sirve afirmaciones tipadas con provenance.
Después de una sola sesión, el workspace de Roberto tiene cuatro PlanTemplates productivos, diecisiete Operations atómicas, sesenta y pico de Claims acumulados como contexto compuesto, y un costo real de operación de aproximadamente dos centavos por workflow ejecutado. La pieza importante no es eso. La pieza importante es que cada uno de esos Claims va a estar acá mañana, y dentro de un año, alimentando los próximos workflows que armemos.
El substrato absorbe
un estándar de la comunidad
Archivo Markdown plano (AUDIENCE.md) versionable en git con doce secciones canónicas: nombre, summary, primary audiences, jobs-to-be-done, pains, motivations, decision criteria, language and tone, anti-goals, evidence vs assumptions, open questions. Frontmatter YAML opcional con status, owners, last_reviewed. Pensado para que cualquier agente LLM lo lea como contexto base antes de operar sobre la audiencia.
Cero primitivas nuevas. Cuatro alineaciones limpias.
Conceptualmente equivalente a cualquier otro Artifact publicado, solo que su lifecycle es "siempre presente" en lugar de "producido por una Op". Vive en filesystem, no en R2.
Define qué predicados son válidos para hablar de la audiencia (hasJob, hasPain, hasDecisionCriterion). Otros workspaces pueden tener overlays distintos.
Esta es la coincidencia perfecta. Evidence con source verificable = Claim confidence alta + source_refs poblado. Assumption = confidence baja + source_refs vacío. El estándar y el substrato describen exactamente la misma distinción.
Cumple Rule A: declarativo, versionable, durable. Idéntica filosofía al manifest del workspace.
Un step nuevo. Cero refactors al resto del DAG.
El primer AUDIENCE.md derivado del brand blueprint de Roberto.
--- audience_id: ai4managers status: stable owners: [roberto@aguirre] last_reviewed: 2026-05-20 source_doc: basic-memory:/sistema/reference_ai4m_brand_blueprint.md --- # Audience: AI4Managers ## Summary Spanish-speaking middle-to-senior managers (40–55) who built consolidated careers before the AI shift and now sense the rules of their profession changed... ## Jobs To Be Done - Understand what AI changes in MY specific role this quarter - Operate AI tools and agents without becoming an engineer - Preserve professional standing against AI-fluent juniors ## Language And Tone **Core terms**: operar, desplegar, agentes en producción, manager, battle-tested **Prohibited terms**: "estamos en la era de…", "imaginá si…", "lab", emojis, jerga ingenieril sin traducción (LLM, RAG, embeddings, fine-tuning) ## Anti-Goals - Beginner-tutorial content - Engineering deep-dives that require coding fluency - English-only or US-only case studies ## Evidence - Roberto's professional transition (Manager Senior → Head of AI Ops Engineering) - Corporate training engagements (only currently validated monetization) - LinkedIn outreach with ~207 unique authors, 33 batches ## Assumptions - LATAM and Spain share enough vocabulary to be a single audience - Status preservation > upside ambition (untested at scale) ... 4 secciones más: Motivations, Decision Criteria, Open Questions, Content Pillars
El prompt ahora tiene una regla de precedencia y un guard de pillar.
- If an AUDIENCE DEFINITION block is provided in the user message, it is the
authoritative source of audience truth. The short "Target audience" label is
only a hint.
- audience_profile MUST reflect the canonical document: pains, motivations,
decision criteria, jobs-to-be-done.
- angle, hook_patterns and key_message MUST respect Language guidance:
use core terms, avoid prohibited terms verbatim.
- If the topic does not fit any allowed content pillar declared in the document,
set ready_for_production=false and put a one-line reason in key_message
starting with "PILLAR_MISMATCH:"
El guard de pillar es la pieza interesante. Si alguien dispatcha un Intent para escribir, por ejemplo, un tutorial básico de prompt engineering — que no encaja en ninguno de los cuatro pillars del brand blueprint — Sofia no produce el brief: lo marca PILLAR_MISMATCH con razón. El human-gate lo recibe y Roberto decide si ampliar los pillars o descartar la pieza.
Workspaces sin AUDIENCE.md siguen funcionando idéntico al día anterior. El handler retorna { present: false, doc: null } y Sofia hace fallback al string target_audience como siempre lo hizo. Los smoke tests previos (standup, lead, brief sin audience, video) pasan sin tocar nada. La extensión es opt-in al nivel de workspace, no un cambio de comportamiento global.
120 líneas insertadas. 6 archivos modificados. 5 archivos nuevos. 6 tests más.
La lectura arquitectural. Esta es la primera vez que un estándar comunitario externo mapea limpio a las primitivas del substrato sin forzar. Eso confirma una hipótesis que llevábamos asumiendo: el modelo simbólico tiene poder expresivo suficiente para absorber convenciones de la comunidad sin reescribirlas. La próxima extensión candidata es llms.txt (estándar de Jeremy Howard para que crawlers AI consuman docs) — si vuelve a mapear a Artifact + WorkspaceManifest, el patrón "absorb don't reinvent" se vuelve doctrina.
La lectura operativa. El próximo Intent produce_artifact / content-brief dispatched contra el workspace AI4Managers va a generar briefs alineados al brand blueprint sin que nadie tenga que pegarle el documento al prompt cada vez. La tesis, los pillars, las palabras prohibidas, los anti-goals — todo está en el grafo, y se aplica automáticamente.
La arquitectura hoy,
pieza por pieza
El sistema en una frase: una app stateless en Vercel le declara intenciones a un motor en una caja Hetzner; el motor las compila a planes tipados, los ejecuta de forma durable con un LLM cuyo costo se contabiliza paso a paso, fuerza una aprobación humana al final de cada plan, y convierte cada decisión humana en memoria estructurada consultable con SQL.
Visto de afuera, un plan en ejecución parece un grafo de pasos con un nodo que consulta una base vectorial. De ahí sale la crítica fácil: "esto es una plantilla de n8n con un nodo de vector, y ya." Confunde el artefacto (un DAG en runtime) con el proceso que lo generó. n8n es un flujo que un humano dibuja a mano. Aquí el flujo no se dibuja: se compone y se valida. Tres diferencias de categoría:
fallback:'fail': el modelo no tiene camino para saltárselo. No es "el LLM hace todo" ni "todo hardcoded".La sofisticación no está donde la critican (que exista un DAG, que exista un vector) sino donde no miran: la composición-validada, la inversión catálogo↔personas y el gate estructural. Cuatro arquitectos reales — Cherny, Embiricos, Chase, Majors — sometieron este as-built a revisión: acta de la sesión Master-Arq →
app_state · user_access (gate) · magic_links (one-time)proxy_buffering off para SSE · Inngest NO es públicoidleTimeout: 120step.waitForEvent ≤24h · sobrevive reiniciostext_delta sale al usuario~/backups/substrate/ (+R2 opcional) · standup digest 07:30 con preflight del CLITres dominios de cómputo, cada uno donde le conviene: lo stateless en Vercel (escala solo, no guarda nada), lo stateful en una caja con disco (Postgres, Inngest, Langfuse: 3 stacks de Docker Compose + un systemd), y la identidad en un BaaS aparte — desacoplada del substrato por diseño. Memoria total observada del lado motor: ~1.7 GB de 15 disponibles.
Qué es. Una app full-stack con runes (reactividad compilada) servida serverless. 13 páginas de producto (welcome, onboarding, office, briefing, library, activity, outputs, squads, hire, squad-proposal, discover, deep-dive, share — aparte quedan la demo pública, el callback de OAuth y las legales) cada una con su +page.server.ts que carga datos reales del motor — y degrada a contenido de ejemplo etiquetado si el motor no responde.
Para qué sirve. Es la cara del producto: todo lo que el founder ve y toca. Stateless a propósito: no guarda nada, así Vercel puede crear y destruir instancias sin que se pierda estado.
Qué es. Una escena voxel renderizada en el browser: 16 agentes en 3 zonas de squad, 12 escritorios, layout calculado por una función pura (testeada con vitest, sin tocar WebGL).
Para qué sirve. Convierte "tenés agentes trabajando" en algo que se ve. El recap de la oficina ("N shipped · M await review") viene de un endpoint del motor que agrega trazas reales de las últimas 24h.
Qué es. El chat con cada agente. Un parser SSE propio (~60 líneas, puro, testeado) pinta cada fragmento de la respuesta en una burbuja transitoria; al abrir el drawer con sesión vacía, hidrata el hilo persistido en el motor.
Para qué sirve. Conversar con un agente sin proceso esperando: la "continuidad" del agente vive en su memoria (briefing + trabajos + hilo), que el motor arma en el instante del mensaje. Si el stream se corta antes del primer fragmento, degrada al camino clásico con un único retry.
Qué es. La UI de "pedile a Nova": un textarea en lenguaje natural y un modal que muestra el desenlace — match con un SuperSkill existente, plan nuevo compuesto (con costo estimado y pasos en humano), o un "no puedo" honesto. Desde ahí el plan se guarda con nombre propio.
Para qué sirve. El usuario nunca navega un catálogo técnico: pide. Y lo que Nova compone puede promoverse a SuperSkill relanzable desde la library.
Qué es. Un backend-as-a-service que aporta auth.users con perfil JSONB passthrough, más dos tablas propias: user_access (gate booleano de beta privada, fail-closed) y magic_links (tokens CSPRNG de un solo uso, 15 min, consumo atómico con UPDATE … WHERE consumed_at IS NULL).
Para qué sirve. Toda la identidad, separada del substrato por diseño. El estado de la app del usuario (squad, onboarding, SuperSkills instalados) vive en profile.app_state — el perfil ES la base de datos del consumer.
Qué es. El hook de SvelteKit que corre en CADA request del server: lee la cookie httpOnly, renueva el access token con el refresh si expiró (~15 min), y consulta user_access con la service key.
Para qué sirve. Fail-closed de verdad: error leyendo el gate = no autorizado. El bypass para tests E2E existe pero está doblemente gateado (dev && CI) — en build de producción esa rama es código muerto.
Qué es. Login, OAuth de Google y magic links se intercambian en el server (client_type=server), nunca en el browser. Razón: el SDK de browser hace el intercambio PKCE en su constructor y jamás expone el refresh token — sin él, las sesiones morían a los 15 minutos.
Para qué sirve. Sesiones de 30 días con renovación silenciosa, y tokens que viven solo en cookie httpOnly/secure.
Qué es. El único endpoint que escribe app_state: hace read-merge-write del estado completo, re-sanitiza campos con pick explícito (el junk no persiste) y rechaza bodies >64KB. Del lado cliente, una write-queue serializa los POSTs (dos escrituras en vuelo se pisarían) con update optimista y revert compare-and-swap.
Para qué sirve. Multi-dispositivo sin servidor de estado: creás un squad en un browser, lo ves en otro. localStorage queda como cache write-through, con precedencia perfil > cache > derivación.
Qué es. TLS con certificado origin de Cloudflare y un solo site de nginx que proxya 443 → 127.0.0.1:4000 exponiendo únicamente /health, /api/workspaces/*, /api/approvals y /api/intents. El endpoint de Inngest no existe para internet. Para el SSE del chat: proxy_buffering off y read timeout de 120s.
Para qué sirve. Que el motor tenga exactamente la superficie que la app necesita y ni un path más. El puerto 4000 está cerrado al público por firewall; solo loopback y el bridge de Docker llegan.
Qué es. Un token bearer server-to-server comparado en tiempo constante (anti timing-attack), y un rate limiter de ventana deslizante en memoria: chat 10/min, compose 4/min, intents 10/min — montado después del bearer (un 401 no consume cupo), respondiendo 429 con Retry-After.
Para qué sirve. El LLM es el recurso caro; los límites protegen la sesión de un loop accidental o un script ansioso. El browser jamás ve este token: solo la app server-side lo usa.
Qué es. Una sola app HTTP con 10 archivos de ruta: POST /api/intents, POST /api/approvals, y bajo /api/workspaces/:id/ — outputs, activity, brief (GET/PUT), manifest, chat (+history), compose (+launch/discard), superskills (+launch). Corre como unidad systemd con restart on-failure y logs a archivo.
Para qué sirve. Es el único punto de entrada al substrato. Detalle con cicatriz: idleTimeout: 120 en el export del server — el default de Bun (10s) mataba el SSE cuando el modelo razonaba más de 10s en silencio. Lo cazó el manual vivo, no un test unitario.
Qué es. Setenta y tres refs tipadas y versionadas repartidas en 33 dominios (trace.query, claim.recall_decisions, text.compose_brief, video.compose…); son 70 ids, porque tres conviven en dos versiones vivas. El dominio más grande es research, con 17. Todas tienen handler de runtime salvo dos — human_gate.approve y human_task — que no lo tienen a propósito: las ejecuta el propio executor suspendiendo el workflow.
Para qué sirve. La operation es la unidad con contrato (inputs tipados → outputs + costo) y la costura donde se pondría en cuarentena cualquier dependencia volátil — un framework de agentes cabe adentro de una operation; la inversa no.
Qué es. Un Intent declarado se compila a un Plan: DAG topo-sorteado de steps donde cada step referencia una operation del catálogo. Los planes que compone Nova pasan una validación server-side de 4 capas (cada operación existe en el catálogo, el grafo no tiene ciclos — Kahn —, tope de 10 steps, y la aprobación final la agrega el sistema); todo lanzamiento — de fábrica, compuesto o guardado — re-valida contra el catálogo y exige estructuralmente el gate humano terminal. Los evaluators emiten verdicts por paso.
Para qué sirve. Que "el modelo decidió" nunca sea la explicación de una ejecución: lo que corre es un artefacto tipado que el servidor validó, con costo estimado calculado server-side (el modelo no puede mentirlo).
Qué es. Una sola consulta al modelo decide: MATCH (el pedido corresponde a un SuperSkill existente — validado server-side contra la lista real, el modelo no puede inventar ids), COMPOSE (arma un plan nuevo usando SOLO operations del catálogo), o CANNOT (lo dice honesto y el pedido queda registrado como demanda).
Para qué sirve. Es el único lugar del sistema donde el LLM "diseña" — y por eso es el más vigilado: lo que propone entra por su validación de 4 capas y por el mismo gate estructural que todo lo demás. Cada pedido queda en plan_drafts como telemetría: los "no pude" priorizan qué construir.
Qué es. Un plan compuesto que gustó se promueve con nombre propio al catálogo del workspace: se re-valida contra el catálogo ANTES del insert, y un unique parcial en DB hace imposible el doble-promote (gana exactamente uno, el otro recibe 409). El lanzamiento usa un helper compartido con los planes de fábrica: assertHumanGatePresent verifica estructuralmente ≥1 gate humano terminal con fallback 'fail'.
Para qué sirve. Que lo que el usuario crea pidiendo tenga las mismas garantías que lo que construyó el equipo. El formulario de relanzado se deriva mecánicamente de las variables que el propio plan declara.
Qué es. Un adapter propio con dos backends: Claude Code headless por CLI (sesión de suscripción Max — costo marginal $0) y la API oficial como fallback. Para streaming parsea NDJSON línea a línea y emite SOLO text_delta — el razonamiento interno del modelo jamás llega al usuario; la línea result final gana sobre lo acumulado.
Para qué sirve. Separar arquitectura de etapa: la beta corre a costo cero, y como cada paso registra su costo notional en dólares, la economía unitaria del switch a API se conoce HOY ($0.013–0.022 por digest). Prender la API es configuración, no re-arquitectura.
Qué es. El runtime durable self-hosted para eventos, crons y reintentos operativos. handle-intent-declared selecciona v2 y arranca una corrida sobre el grafo compilado; worker-orquestacion da cuerda al worker que consume work_items; los barridos recuperan leases y gates vencidos; materialize-manifest conserva su cron. execute-plan permanece únicamente como executor v1 en deprecación y canario anti-zombie.
Para qué sirve. Inngest hace durable la entrega y el housekeeping, pero ya no es la autoridad sobre el avance del DAG. Esa verdad vive en Postgres: runs + run_events, plegados por el reducer, y la cola transaccional que procesa el worker v2. Un deploy puede interrumpir un tick; no puede borrar lo que la corrida ya decidió.
Qué es. Una instancia dedicada (Docker, :5433) con 77 tablas lógicas y 32 particiones mensuales. Las dos de mayor volumen — traces y step_executions — particionadas por mes. Los embeddings (3072d, text-embedding-3-large) son una columna en claims y artifacts con índice StreamingDiskANN — HNSW topa en 2000 dimensiones, por eso pgvectorscale.
Para qué sirve. Memoria semántica y hechos tipados en el MISMO store: un JOIN entre similitud vectorial y claims estructurados, sin dual-write contra un vector DB externo. La consistencia es gratis porque hay una sola base.
Qué es. La cadena intents → plans (steps, edges) → traces → step_executions → artifacts / claims, más lineage_edges con una vista recursiva (profundidad máx 12, guard de ciclos). Regla dura: todo claim y artifact DEBE tener provenance {trace_id, step_id}.
Para qué sirve. Auditoría nativa: de cualquier afirmación del sistema se puede responder "¿de dónde salió esto?" caminando el grafo — qué trace la emitió, qué step, qué humano la confirmó.
Qué es. Un snapshot navegable por workspace (squad, intents activos, artifacts recientes, decisiones, índice de navegación) materializado cada noche y on-demand por evento. Desde la migración 0006, la columna manifest JSONB es la fuente de verdad.
Para qué sirve. Que un agente arranque sabiendo dónde está parado sin listar media base: lee UN registro. Es cache derivado — se puede reconstruir entero desde el grafo.
Qué es. Stack self-hosted de 6 servicios (web, worker, ClickHouse, MinIO, Redis, Postgres propio) que traza cada llamada LLM con latencia, tokens y costo, correlacionada por trace_id del substrato. Con cicatriz propia: ClickHouse capado a 2 CPUs después de un runaway que clavó 7.7 cores durante 19 horas.
Para qué sirve. Responder "¿qué hizo exactamente el modelo en este paso y cuánto costó?" con un click. El único Redis del sistema vive adentro de este stack — no está en el camino de la app.
Qué es. Dos crons: backup nightly (03:00 UTC, pg_dump gzip a ~/backups/substrate/, retención 30 días, push opcional a R2) y standup digest diario (07:30 UTC, declara un Intent real vía la API pública con bearer — con preflight que verifica la salud del CLI antes de disparar).
Para qué sirve. El backup es la respuesta al dominio de falla catastrófico (RPO ≤24h). El digest es el latido del producto: el sistema se usa a sí mismo todos los días. Los dumps crecen con el sistema: 680K en mayo, 1.8M hoy.
Qué es. Tres anillos: 13.333 tests unit y de integración (4.102 de la app, 9.058 del motor contra Postgres real, 173 del spec), 443 E2E/visuales con Playwright repartidos en 81 archivos (journeys completos + regresión de screenshots), y el anillo exterior — un manual de 34 pasos que un navegador real re-ejecuta contra producción antes de cada demo, con usuario efímero y limpieza quirúrgica del workspace.
Para qué sirve. El anillo exterior caza lo que los internos no ven: el bug del idleTimeout (SSE muerto a los 10s de silencio) pasó todos los tests unitarios y E2E — lo encontró el manual. Si un paso se rompe en producción, su sello deja de decir VERIFICADO.
La arquitectura estática es la mitad de la historia. Estos son los tres caminos que más enseñan, paso por paso:
| Tabla | Qué guarda | El porqué |
|---|---|---|
| intents | Qué quiere lograr quién, en qué workspace | El punto de partida auditado de TODA ejecución; soporta árbol (parent_intent_id) |
| plans + steps + plan_edges | El DAG ejecutable compilado del Intent | Versionado por intent; cada step lleva operation_ref, retry policy, timeout y human_gate opcional |
| operations | El catálogo: metadatos de cada operación, versionados | Deprecar sin borrar; el plan referencia id@version |
| traces | Cada ejecución concreta de un plan (estado, costo real, verdict) | Particionada por mes; entrada al lineage |
| step_executions | Cada step ejecutado: inputs/outputs snapshot, error, verdict, costo | La auditoría fina; particionada igual que traces |
| claims | Hechos tipados (sujeto · predicado · objeto) + provenance + confidence + embedding 3072d | La memoria del equipo; recallable por SQL y por similitud (DiskANN) |
| artifacts | Lo producido (docs, videos, digests…) content-addressable (sha256), con status machine | pending_review → approved/rejected/expired; dedup por contenido |
| lineage_edges | Aristas de provenance entre claims y artifacts | Vista recursiva (profundidad 12, cycle guard): "¿de dónde salió esto?" |
| evaluators | Catálogo de cómo evaluar (deterministic / llm_judge / human) con threshold | Los verdicts por paso salen de acá |
| ontology_overlays | Relabels por vertical sobre el mismo grafo | Workspace A dice "video reel", workspace B "blueprint" — mismos primitives |
| workspace_manifests | El snapshot navegable por workspace (Regla A) | Cache derivado, reconstruible; columna manifest JSONB como SSOT |
| chat_messages | El hilo founder ↔ agente por conversación | El motor lee los últimos 20 para armar el prompt; la app hidrata al recargar |
| plan_drafts | Cada pedido a Nova con su desenlace (proposed/matched/rejected/launched) | Telemetría de demanda: los "no pude" priorizan el roadmap |
| superskills | Planes promovidos con nombre, por workspace | Unique parcial anti doble-promote; archive en vez de delete |
+ en InsForge (identidad, separada del substrato): user_access (gate fail-closed de beta privada) · magic_links (tokens one-time con consumo atómico) · profile.app_state (el estado del consumer) | ||
Detalle que un estudiante debería notar: workspace_id está en todas las tablas de datos del workspace desde la migración 0001 (intents, traces, claims, artifacts, lineage_edges, manifests, chat, drafts, superskills); las de definición (plans, steps) lo heredan vía su intent, y las de catálogo (operations, evaluators, overlays) son globales por diseño. El modelo de datos es multi-tenant desde el día uno — el aislamiento pendiente está en la capa de routing, no en los datos.
workspace_id en cada tabla de datos desde la 0001 (las de catálogo son globales por diseño). Cuando llegue multi-tenancy real, es trabajo de capa de routing — no una migración de datos con el producto andando.Medidas contra el repo el 2026-09-03, no estimadas: el catálogo se cuenta importando OPERATION_CATALOG, y los tests corriendo las suites. Cualquiera de estos números que quede viejo es un bug del documento.
Routing multi-tenant (el modelo de datos está listo; la beta sirve un workspace con gate de founder) · alta disponibilidad y RTO formalizado (hay durabilidad de runs y backup nightly, no hay réplica ni drill de restore documentado) · alerting proactivo con SLOs (hay observabilidad, nadie despierta a las 3am) · regresión offline de evals (el human gate ya acumula el dataset etiquetado; falta el harness en CI). Las cuatro están asumidas, clasificadas y comprometidas — el detalle vive en el red-team de arquitectura.
Para cerrar el caso de estudio. La base de este sistema son marcas que cualquier estudiante reconoce: Postgres, nginx, Vercel, Cloudflare, Anthropic, Inngest. Lo custom es una capa fina, tipada y testeada. La pregunta que ordenó cada decisión no fue "¿qué tecnología está de moda?" sino "¿qué cosa madura y barata cubre esto sin pintarnos en una esquina?" — y la única apuesta original de verdad, la memoria estructurada de decisiones humanas, es exactamente la pieza que ningún framework vendía. El recorrido completo del producto, con cada paso verificado contra producción, vive en el manual del operador.
La Junta responde
IRIS es andamiaje para un cuello de botella que este sistema no tiene. Espejar todo a RAM resuelve latencia de alto tráfico sincrónico — cientos de miles de usuarios, 10-15 tool calls por pregunta. La carga de Agent Squad es la opuesta: pocos workflows profundos y asíncronos, un digest tarda minutos por diseño. Cuando agregás una capa, preguntate si sobrevive al próximo modelo o si la estás poniendo porque el de hoy no llega.
El costo que IRIS te cobra es el que más duele en producción: una segunda copia de tus datos por CDC es un segundo sistema que mantener consistente — y la consistencia entre dos stores es exactamente lo que falla un domingo a las 3am, sin que lo hayas anticipado. No operes lo que no necesitás operar.
IRIS es una solución de contexto para alto tráfico. Acá el contexto se controla por el catálogo y los claims tipados — sabés exactamente qué entra al LLM en cada paso, que es el problema difícil de verdad. Matiz honesto: si algún día aparece un agente sincrónico de alto tráfico, el patrón de IRIS cabe adentro de una operation. Hoy no.
La intuición es correcta y ya se actuó sobre ella: hay una conversación inicial donde el sistema descubre qué trabajo necesitás, propone las capacidades y deriva el equipo de ahí. El "3" era un default de presentación, nunca una restricción. Lo que sigue es tu segunda mitad — la última milla: que el agente produzca con más autonomía, no que el humano conduzca paso a paso.
El squad nunca fue el centro del diseño. Las personas son etiquetas de identidad; la capacidad vive en el catálogo de operations. El planner compone desde el catálogo y nunca lee el squad — así que el número de personas emerge del trabajo, no se fija de antemano. Hardcodear el tamaño habría sido el error que el estudiante temía; el sistema lo evita por construcción.
Lo que ves no es hardcode ni autonomía total: es un router/state-machine que genera el plan desde lenguaje natural y lo valida contra un catálogo, con guardrails. El equipo es un subproducto de qué operations entraron al plan. Esa es la diferencia entre dibujar un flujo a mano y derivarlo.
Que entienda el modelo y cómo darle herramientas y un objetivo — no que se case con un framework de moda que el próximo modelo deja obsoleto. La habilidad durable es saber cuánto andamiaje poner y cuánto borrar.
Que aprenda a dirigir y delegar a agentes. El cuello de botella del futuro no es la capacidad del modelo, es la velocidad del humano para encargar y revisar. Quien sepa orquestar varios agentes a la vez rinde por diez.
Sigue siendo ingeniería de sistemas. El trabajo difícil de los agentes no es el modelo: es controlar qué información entra a cada paso. Fundamentos de sistemas + "context engineering" no caducan con el próximo release.
Que aprenda a operar sistemas en producción — observabilidad, "¿cómo sé que esto funciona y cómo lo arreglo cuando falle?". Los sistemas con IA son no-deterministas; entender cómo se comportan cuando se rompen es una habilidad que la IA no automatiza pronto.
En el anillo de verificación: el robot que recorre el producto entero contra producción cada noche. Para verificar un solo paso tenés que entender TODAS las capas que lo atraviesan — pantalla, servidor, motor, base. Es el único asiento desde donde se ve el mapa completo, y con riesgo cero: usa usuarios de prueba que se autodestruyen. Test in prod, desde el día uno.
El practicante es el caso de uso del sistema: un trabajador con energía y sin criterio formado todavía — igual que el modelo. Las mismas barandas que contienen a la IA lo contienen a él, así que puede hacer trabajo real desde la semana uno, que es la única forma de aprender.
Después del anillo, dale una Operation propia del catálogo: una pieza determinista de punta a punta. Ahí aprende a controlar exactamente qué entra y qué sale de un paso — el músculo central.
Framework open-source de Nous Research (feb 2026): un agente personal, self-hosted, que vive en tus mensajeros (CLI, Telegram, Discord, Slack), recuerda entre sesiones y se auto-mejora — tras una tarea compleja escribe una "skill" reutilizable, sin pedir permiso — y se conecta a 200+ modelos. Sus barandas son de infraestructura (container hardening, root de solo lectura), no de aprobación de las acciones. La pregunta no es ociosa: es de las herramientas que más crecen en 2026. hermes-agent.org
Hermes lleva la autonomía hasta el final: se auto-mejora, corre largo, saca al humano del loop. Para un asistente personal es exactamente lo que querés — el humano es el cuello de botella, quitalo. Agent Squad pone una compuerta humana a propósito, porque sus efectos caen sobre un negocio ajeno: un brief que se manda a un cliente, no un script que corrés en tu máquina. Si tu caso es personal y tolera autonomía, Hermes encaja. Si alguien tiene que firmar cada entregable, esa compuerta no es burocracia.
La diferencia más honesta: Hermes pone la baranda en el contenedor (que el agente no te rompa la máquina); Agent Squad la pone en la acción (que no haga nada con efecto sin firma) y suma provenance y SLOs. Un agente que aprende solo y actúa solo en tus mensajeros: ¿cómo sabés qué aprendió, cómo lo auditás cuando hace algo raro a las 3am? Para uso personal, esa opacidad es tu riesgo — aceptable. Para trabajo que cae sobre clientes, no.
Hermes auto-crea skills (orquestación emergente): flexible para tareas personales variadas. Agent Squad deriva un grafo tipado validado contra un catálogo (orquestación explícita): controlable para flujos de negocio repetibles y auditables. Y un punto de humildad: el "closed learning loop" de Hermes y nuestros SuperSkills promovidos son la misma idea — capturar un procedimiento para reusarlo. La diferencia es quién aprieta el gatillo: Hermes lo hace solo; nosotros, con un humano que promueve.
Hermes es model-agnostic (200+ modelos) y trae harness: memoria, learning loop, 40+ tools. Eso es opcionalidad y features — y también andamiaje que el próximo modelo puede volver innecesario. Agent Squad apuesta a un solo modelo frontera y pone andamiaje mínimo + barandas duras. Ninguna es "mejor": son la apuesta opuesta. La pregunta que importa no es cuál tiene más, sino cuál de las dos apuestas le sirve a tu problema.
De dos motores
a uno
El motor original (v1) vivía en inngest/functions/execute-plan.ts. Recibía plan.compiled, caminaba el DAG de plans/steps y mantenía un workflow suspendido con step.waitForEvent cuando encontraba un human gate. Funcionó, dejó trazas útiles y probó la tesis. También concentró demasiada autoridad dentro de una función difícil de reconstruir, inspeccionar y reemplazar.
v2 cambió la unidad de verdad. El compilador convierte el template en un grafo versionado de graph_nodes. Una corrida vive en runs; lo que le ocurrió vive, en orden, en run_events. Un reducer puro decide transiciones y efectos; aplicar-evento los persiste transaccionalmente; el worker reclama leases de work_items, ejecuta operaciones y devuelve nuevos eventos. Estado derivado, log reproducible, cola explícita. Esa arquitectura ganó porque se puede explicar después de un crash, no solamente mientras todo está sano.
Qué hacía. execute-plan recorría el Plan, escribía steps y traces, reintentaba operaciones y suspendía el workflow en los gates con step.waitForEvent.
Por qué sale. Su estado efectivo estaba repartido entre el historial de Inngest y las proyecciones del substrato. Reanudar, reconstruir o cambiar el motor exigía entender una ejecución viva, no plegar un log estable.
Qué hace. Compila templates a grafos inmutables, registra cada transición como evento y mueve trabajo mediante una cola con leases. El reducer no hace I/O: declara. aplicar-evento persiste. El worker ejecuta.
Qué conserva Inngest. Entrada durable de eventos, crons, retries operativos y el tick que da cuerda al worker. Inngest sigue en el sistema; dejó de ser el lugar donde se esconde la máquina de estados de una corrida.
Ya no existe un waitForEvent sosteniendo el Plan. El efecto ACTIVAR_GATE crea transaccionalmente un registro en artifact_approval_flows: el entregable aparece para revisión, la persona aprueba y, cuando el protocolo lo requiere, elige una opción; el executor acusa recibo y recién entonces el flow completa.
consumirHandoffV2 aplica la decisión a la corrida y encadena los sucesores. El orden de locks es una invariante, no una recomendación: runs → artifact_approval_flows → work_items. Evita que aprobación, timeout y worker cierren el mismo gate en órdenes incompatibles.
Cada flow congela deadline_at, política de fallback y required_role. El cron barrido-gates-vencidos reclama solamente el estado que observó mediante CAS: state = fase AND deadline_at <= now(). Una aprobación que ganó la carrera no puede ser pisada por el timeout.
El gate no es la única barrera. Un evaluator configurado como bloqueante emite un veredicto hard-stop: si falla, el downstream no se despacha. La corrida no convierte una mala salida en trabajo posterior solamente porque el worker siga vivo.
“Volver a v1” sonaba reversible, pero no lo era. v1 no podía reanudar una corrida v2, no se ejercitaba con tráfico real y ejecutaba el template, no el grafo compilado que v2 había autorizado. Cambiar la bandera no recuperaba una corrida: creaba otra interpretación del trabajo.
Con dos motores, apagar el worker v2 podía parecer una precaución. Con v2 como único motor es un footgun: acepta corridas, deja work_items en ready y nada avanza. Por eso el default ahora es on; apagarlo exige una decisión explícita y observable.
Los tres resets operativos obedecen la misma conclusión: normalizan el ruteo a orchestration_state='active' y orchestration_engine='v2' sin tocar enabled. Resetear el progreso no revoca permiso ni resucita un motor retirado.
Del ruido a la audiencia
La investigación de mercado del substrato ya era profunda. Medía la demanda compuesta de cada oportunidad por tres dimensiones separadas, nunca una disfrazada de otra: la búsqueda del ángulo, el mercado del tema, y el respaldo social de Reddit y de los comentarios de YouTube, atribuido contra una tabla cerrada para que el modelo nunca escriba un número que no vio. Un pase dedicado ataba la evidencia, y un ranking por frentes de Pareto ordenaba las oportunidades sin sumar peras con manzanas.
Y entonces un fundador dudó de un cero. El informe decía que no había comentarios de valor en YouTube, y él no se lo creyó: "yo manualmente encuentro casos que no son elogios". La duda, insistida cuatro veces, destapó capas reales. Primero, cuatro defectos en cómo el motor descubría los videos: buscaba consejos genéricos ("cómo crecer"), no los videos de problema ("por qué mi canal no crece") donde la audiencia pregunta. Y un hallazgo de fondo: el clasificador léxico no podía distinguir un problema de una curiosidad, porque la diferencia era semántica; la señal limpia estaba en otra capa, en la procedencia de la búsqueda, no en el título.
Pero lo mayor llegó con un video que el fundador trajo a mano: una masterclass de instructora, cuyos comentarios eran historias personales. Ahí el valor no era demanda. Era inteligencia de audiencia: el perfil de quién mira, sus motivaciones, sus barreras, y los huecos que revela contando su historia. El motor lo perseguía sin verlo. Así nació un pase nuevo, r14, que lee los comentarios que el research ya muestreó y extrae cinco cosas honestas contra una tabla cerrada: perfil, motivaciones, barreras, huecos, y frases de dolor como cita textual literal, lista para el guion.
En una corrida real de producción, en cinco idiomas, el informe escribió lo que antes se perdía: un creador de 70 años que usa IA como asistente, uno que se identifica como neurodiverso y pierde el hilo si preproduce varios videos, uno que escribe en francés "j'espère que je vais y arriver avant que la maladie m'emporte". Palabras reales de personas reales, escritas en el brief, listas para volverse contenido. El cero que el fundador no se creyó nunca fue el final: era la punta del hilo.
El guardián entre el motor y producción
El substrato dejó de depender de terceros para su propia integración. El código y el CI se volvieron soberanos: viven en una caja propia, sin pedirle permiso a nadie. Y con esa mudanza llegó la pregunta que importa de verdad: cuando el motor cambia, ¿qué es lo que decide si ese cambio sale a producción o no?
La suite de pruebas de extremo a extremo llevaba días marcada como "en rojo" y fuera de esa decisión. Al mirarla de cerca apareció algo distinto: no estaba rota, llevaba cinco noches seguidas en verde. El semáforo mentía. Se corrigió lo que el comentario del gate declaraba, y quedó a la vista la deuda real, la única que faltaba pagar: que esa suite volviera a frenar un deploy malo, no solo a mirarlo pasar.
El hallazgo que ordenó todo fue este: no hay un despliegue, hay dos. El de la cara visible y el del motor, cada uno por su camino. Y no se validan igual. Las pruebas de extremo a extremo corren contra un simulador del substrato, así que dicen la verdad sobre la interfaz pero no sobre el motor, que tiene su propia suite. Tratarlos como uno solo habría hecho que un tropiezo de la cara frenara al motor sin motivo. Cada guardián cuida su propia puerta.
Con la puerta clara, la suite de la cara pasó a bloquear su deploy: las pruebas livianas de interfaz en cada cambio, y las pesadas de la escena viva cuando el cambio toca esa escena. El motor sigue su camino, validado por lo suyo. El substrato ya no solo se construye rápido: se publica sin que un cambio roto se cuele, y esa garantía está probada a mano, mordida por mordida.
Y el guardián se afinó dos veces más. Dejó de exponer su llave: el token con el que publica ya no viaja en la línea de comando, donde cualquier proceso del servidor podía leerlo, sino en el entorno, fuera de la vista. Y aprendió a no congelarse: una vez la herramienta de publicación se colgó en una llamada de red y se quedó clavada hasta el tope del turno, cuarenta y cinco minutos, hasta que hubo que matarla a mano. Ahora cada paso de publicación tiene un límite de tiempo duro y se reintenta solo, así un cuelgue se corta a los pocos minutos en vez de trabar todo. Un guardián que además no deja su llave a la vista ni se duerme en la puerta.