synaptum 1.0.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. synaptum-1.0.0rc1/.github/humo.py +39 -0
  2. synaptum-1.0.0rc1/.github/workflows/release.yml +120 -0
  3. synaptum-1.0.0rc1/.github/workflows/tests.yml +46 -0
  4. synaptum-1.0.0rc1/.gitignore +14 -0
  5. synaptum-1.0.0rc1/.python-version +1 -0
  6. synaptum-1.0.0rc1/API.md +87 -0
  7. synaptum-1.0.0rc1/CHANGELOG.md +36 -0
  8. synaptum-1.0.0rc1/PKG-INFO +247 -0
  9. synaptum-1.0.0rc1/README.md +214 -0
  10. synaptum-1.0.0rc1/examples/01_agente.py +135 -0
  11. synaptum-1.0.0rc1/examples/02_durabilidad.py +135 -0
  12. synaptum-1.0.0rc1/examples/03_aprobacion.py +140 -0
  13. synaptum-1.0.0rc1/examples/04_streaming.py +111 -0
  14. synaptum-1.0.0rc1/examples/README.md +61 -0
  15. synaptum-1.0.0rc1/examples/_comun.py +134 -0
  16. synaptum-1.0.0rc1/pyproject.toml +61 -0
  17. synaptum-1.0.0rc1/roadmap.md +234 -0
  18. synaptum-1.0.0rc1/src/synaptum/__init__.py +139 -0
  19. synaptum-1.0.0rc1/src/synaptum/agent/__init__.py +5 -0
  20. synaptum-1.0.0rc1/src/synaptum/agent/agent.py +524 -0
  21. synaptum-1.0.0rc1/src/synaptum/core/__init__.py +166 -0
  22. synaptum-1.0.0rc1/src/synaptum/core/codec.py +151 -0
  23. synaptum-1.0.0rc1/src/synaptum/core/errors.py +256 -0
  24. synaptum-1.0.0rc1/src/synaptum/core/events.py +322 -0
  25. synaptum-1.0.0rc1/src/synaptum/core/protocols.py +323 -0
  26. synaptum-1.0.0rc1/src/synaptum/core/types.py +616 -0
  27. synaptum-1.0.0rc1/src/synaptum/prompts/__init__.py +15 -0
  28. synaptum-1.0.0rc1/src/synaptum/prompts/providers.py +170 -0
  29. synaptum-1.0.0rc1/src/synaptum/prompts/template.py +111 -0
  30. synaptum-1.0.0rc1/src/synaptum/providers/__init__.py +18 -0
  31. synaptum-1.0.0rc1/src/synaptum/providers/axonium.py +405 -0
  32. synaptum-1.0.0rc1/src/synaptum/providers/base.py +156 -0
  33. synaptum-1.0.0rc1/src/synaptum/providers/openai_compatible.py +356 -0
  34. synaptum-1.0.0rc1/src/synaptum/py.typed +0 -0
  35. synaptum-1.0.0rc1/src/synaptum/run/__init__.py +17 -0
  36. synaptum-1.0.0rc1/src/synaptum/run/journal.py +167 -0
  37. synaptum-1.0.0rc1/src/synaptum/run/local.py +259 -0
  38. synaptum-1.0.0rc1/src/synaptum/run/sqlite.py +118 -0
  39. synaptum-1.0.0rc1/src/synaptum/run/transport.py +241 -0
  40. synaptum-1.0.0rc1/src/synaptum/schema/__init__.py +15 -0
  41. synaptum-1.0.0rc1/src/synaptum/schema/derive.py +173 -0
  42. synaptum-1.0.0rc1/src/synaptum/schema/protocol.py +188 -0
  43. synaptum-1.0.0rc1/src/synaptum/testing/__init__.py +24 -0
  44. synaptum-1.0.0rc1/src/synaptum/testing/fake.py +242 -0
  45. synaptum-1.0.0rc1/src/synaptum/testing/replay.py +180 -0
  46. synaptum-1.0.0rc1/src/synaptum/tools/__init__.py +5 -0
  47. synaptum-1.0.0rc1/src/synaptum/tools/decorator.py +188 -0
  48. synaptum-1.0.0rc1/tests/contratos.py +56 -0
  49. synaptum-1.0.0rc1/tests/superficie-publica.json +100 -0
  50. synaptum-1.0.0rc1/tests/test_api_surface.py +112 -0
  51. synaptum-1.0.0rc1/tests/test_axonium.py +356 -0
  52. synaptum-1.0.0rc1/tests/test_conformance.py +544 -0
  53. synaptum-1.0.0rc1/tests/test_events.py +195 -0
  54. synaptum-1.0.0rc1/tests/test_independencia.py +130 -0
  55. synaptum-1.0.0rc1/tests/test_loop.py +585 -0
  56. synaptum-1.0.0rc1/tests/test_persistence.py +410 -0
  57. synaptum-1.0.0rc1/tests/test_prompts.py +168 -0
  58. synaptum-1.0.0rc1/tests/test_providers.py +138 -0
  59. synaptum-1.0.0rc1/tests/test_replay.py +155 -0
  60. synaptum-1.0.0rc1/tests/test_schema.py +247 -0
  61. synaptum-1.0.0rc1/tests/test_seam.py +278 -0
  62. synaptum-1.0.0rc1/tests/test_tools.py +351 -0
  63. synaptum-1.0.0rc1/tests/test_transport.py +239 -0
  64. synaptum-1.0.0rc1/tests/test_types.py +209 -0
  65. synaptum-1.0.0rc1/uv.lock +893 -0
@@ -0,0 +1,39 @@
1
+ """Lo mínimo que un consumidor hace, contra la copia instalada desde el índice.
2
+
3
+ Vive en un fichero y no incrustado en el workflow porque un heredoc dentro de un
4
+ bloque YAML dentro de un bucle de reintentos es tres capas de citado que nadie
5
+ quiere depurar en un incidente. Además así se puede ejecutar en local, que es
6
+ donde conviene descubrir que está roto.
7
+
8
+ Importa **desde el paquete**, nunca desde un submódulo interno: es la diferencia
9
+ que dejó a Axonium con un rc publicado al que le faltaban seis exportaciones y
10
+ el CI en verde.
11
+ """
12
+
13
+ import asyncio
14
+
15
+ import synaptum
16
+ from synaptum import Agent, Session, tool
17
+ from synaptum.testing import FakeGateway, says
18
+
19
+
20
+ @tool
21
+ async def eco(texto: str) -> str:
22
+ """Devuelve lo que recibe."""
23
+ return texto
24
+
25
+
26
+ async def main() -> None:
27
+ agente = Agent("humo", model="openai-compatible:m", tools=[eco])
28
+ pasos = [
29
+ paso
30
+ async for paso in agente.run(
31
+ "hola", session=Session("r1", FakeGateway(says("hola")))
32
+ )
33
+ ]
34
+ assert pasos[-1].output == "hola", pasos[-1]
35
+ print("instalado desde el índice y funcionando:", synaptum.__version__)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ asyncio.run(main())
@@ -0,0 +1,120 @@
1
+ # SYN-74 · Publicación por Trusted Publishing (OIDC).
2
+ #
3
+ # No hay token en ninguna parte: GitHub firma una identidad efímera y el índice
4
+ # la verifica. El secreto no está mejor guardado — no existe, así que no se
5
+ # puede filtrar, ni caducar sin avisar, ni acabar en el historial de un chat.
6
+ #
7
+ # Lo que hay que configurar en el índice (una vez, en «Publishing»):
8
+ #
9
+ # owner Root1V
10
+ # repository synaptum-framework
11
+ # workflow release.yml
12
+ # environment testpypi (o pypi)
13
+ #
14
+ # El `environment` no es decorativo: es lo que permite exigir una aprobación
15
+ # manual antes de que algo salga, y lo que hace que el publisher no valga para
16
+ # cualquier workflow del repositorio.
17
+ name: release
18
+
19
+ on:
20
+ workflow_dispatch:
21
+ inputs:
22
+ index:
23
+ description: Dónde publicar
24
+ type: choice
25
+ options: [testpypi, pypi]
26
+ default: testpypi
27
+
28
+ permissions:
29
+ contents: read
30
+
31
+ jobs:
32
+ # No se publica lo que no pasa. Reutiliza el workflow de tests en vez de
33
+ # copiarlo: dos suites que se copian divergen, y la que diverge es siempre la
34
+ # que guarda la puerta.
35
+ tests:
36
+ uses: ./.github/workflows/tests.yml
37
+
38
+ build:
39
+ needs: tests
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: astral-sh/setup-uv@v5
44
+
45
+ - run: uv build --out-dir dist
46
+
47
+ # Que el artefacto diga la misma versión que el código. Son dos sitios y
48
+ # se editan a mano; un wheel publicado como una versión y que por dentro
49
+ # dice otra manda a quien depure al sitio equivocado, y no falla nunca.
50
+ - name: la versión del artefacto es la del código
51
+ run: |
52
+ VERSION=$(uv run python -c "import synaptum; print(synaptum.__version__)")
53
+ ls dist/synaptum-${VERSION}-py3-none-any.whl dist/synaptum-${VERSION}.tar.gz
54
+ echo "publicando $VERSION"
55
+
56
+ - uses: actions/upload-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ publish:
62
+ needs: build
63
+ runs-on: ubuntu-latest
64
+ environment: ${{ inputs.index }}
65
+ permissions:
66
+ id-token: write # lo único que hace falta: acuñar el token OIDC
67
+ steps:
68
+ - uses: actions/download-artifact@v4
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+
73
+ - uses: pypa/gh-action-pypi-publish@release/v1
74
+ with:
75
+ repository-url: >-
76
+ ${{ inputs.index == 'pypi'
77
+ && 'https://upload.pypi.org/legacy/'
78
+ || 'https://test.pypi.org/legacy/' }}
79
+
80
+ # Instalar desde el índice y ejecutar ESA copia. Un tag no es una release
81
+ # hasta que alguien puede instalarlo: Axonium publicó un rc cuyo CI estaba en
82
+ # verde y al que le faltaban seis exportaciones, porque sus tests importaban
83
+ # del submódulo interno y ningún consumidor hace eso.
84
+ verify:
85
+ needs: publish
86
+ runs-on: ubuntu-latest
87
+ env:
88
+ # En una sola línea y como variable de entorno: una expresión de GitHub
89
+ # partida en varias líneas dentro de un `run` es legal y difícil de leer,
90
+ # y esto se lee en un incidente.
91
+ INDEX: ${{ inputs.index == 'pypi' && 'https://pypi.org/simple/' || 'https://test.pypi.org/simple/' }}
92
+ steps:
93
+ - uses: actions/checkout@v4
94
+ - uses: astral-sh/setup-uv@v5
95
+
96
+ - name: leer la versión que se acaba de publicar
97
+ run: |
98
+ echo "VERSION=$(python -c "
99
+ import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])
100
+ ")" >> "$GITHUB_ENV"
101
+
102
+ # Un índice tarda en servir lo que acaba de aceptar, y cuánto es variable.
103
+ # La espera y la comprobación son **el mismo paso** a propósito: una sonda
104
+ # aparte puede decir que sí por un motivo que no es el índice —lo hizo, en
105
+ # local, mirando el entorno en vez del índice— y entonces la verificación
106
+ # arranca creyendo que hay algo. Si la instalación funciona, está servido.
107
+ - name: instalar desde el índice y usarlo como un consumidor
108
+ run: |
109
+ for intento in $(seq 1 30); do
110
+ if uv run --isolated --no-project \
111
+ --index-url "$INDEX" --extra-index-url https://pypi.org/simple/ \
112
+ --with "synaptum==$VERSION" python "$GITHUB_WORKSPACE/.github/humo.py"; then
113
+ echo "verificado tras ${intento} intento(s)"
114
+ exit 0
115
+ fi
116
+ echo "intento ${intento}: el índice todavía no lo sirve"
117
+ sleep 10
118
+ done
119
+ echo "synaptum==$VERSION no se pudo instalar desde $INDEX cinco minutos después"
120
+ exit 1
@@ -0,0 +1,46 @@
1
+ # SYN-77 · Los tests corren donde puede verlos cualquiera, no solo en un portátil.
2
+ name: tests
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ workflow_call: # lo reutiliza la publicación: no se publica lo que no pasa
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ pytest:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+
23
+ # Sin SYNAPTUM_CONTRACTS a propósito. Los corpus dorados son artefactos
24
+ # conjuntos de varios proyectos y viven fuera de este repositorio: aquí no
25
+ # están, y la suite tiene que pasar igual. Si su ausencia la rompiera
26
+ # serían una dependencia — y una que no se puede instalar.
27
+ - name: pytest
28
+ run: uv run --frozen pytest
29
+
30
+ # El paquete que se publica es lo que hay que probar, no el árbol de
31
+ # fuentes. Un wheel al que le falte un fichero pasa la suite y falla al
32
+ # instalarse, que es exactamente lo que este paso ve y el anterior no.
33
+ - name: el wheel instala y funciona por sí solo
34
+ run: |
35
+ uv build --out-dir dist
36
+ uvx twine check dist/*
37
+ uv run --isolated --no-project --with dist/*.whl python -c "
38
+ import synaptum, pathlib, sys
39
+ assert (pathlib.Path(synaptum.__file__).parent / 'py.typed').exists(), \
40
+ 'falta py.typed: quien instale esto no ve un solo tipo'
41
+ ajenos = [m for m in sys.modules
42
+ if not m.startswith(('synaptum', '_')) and '.' not in m
43
+ and m not in sys.stdlib_module_names]
44
+ assert not ajenos, f'el núcleo arrastró dependencias: {ajenos}'
45
+ print('ok', synaptum.__version__)
46
+ "
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Environment variables (may contain credentials)
13
+ .env
14
+ *.env
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,87 @@
1
+ # Política de estabilidad de API
2
+
3
+ `SYN-46`. Esta política vale para cualquiera que construya sobre Synaptum, y no depende de quién sea.
4
+
5
+ Nació de un caso concreto: un arnés retiró su DSL de autoría nativo apoyándose en que Synaptum sería
6
+ esa capa. Una decisión así es difícil de revertir, y la contrapartida no podía quedarse en una frase
7
+ de un acuerdo: tiene que decir qué está garantizado, durante cuánto y qué no. Lo que sigue aplica
8
+ igual a quien llegue mañana sin haber estado en aquella conversación.
9
+
10
+ ## Qué es superficie pública
11
+
12
+ **Lo que `synaptum/__init__.py` exporta, y nada más.**
13
+
14
+ Un símbolo alcanzable solo por su módulo —`synaptum.run.journal.Replay`, por ejemplo— es interno
15
+ aunque no lleve guion bajo, y puede cambiar sin aviso. Si algo interno te hace falta, pídelo y lo
16
+ promovemos; usarlo desde fuera es aceptar que se romperá.
17
+
18
+ La superficie está **fijada por un test**: cualquier cambio en `__all__` rompe la suite y obliga a
19
+ actualizar el fichero de referencia a mano. No es burocracia — es que una política que nadie
20
+ comprueba se erosiona, y ya hemos visto esa lección tres veces en este proyecto.
21
+
22
+ ## Qué queda fuera a propósito
23
+
24
+ | Fuera | Por qué |
25
+ |---|---|
26
+ | Representación interna de los eventos | Lo que se garantiza es el **contrato de la costura**, no cómo se guardan los campos en memoria |
27
+ | Mensajes de error y sus textos | Los códigos de razón sí son estables; la prosa no |
28
+ | El módulo `synaptum.testing` | Es andamiaje de desarrollo y evoluciona con lo que haga falta probar |
29
+ | Cualquier cosa marcada `experimental` en su docstring | Declarado a la entrada, sin sorpresa |
30
+
31
+ ## Versionado
32
+
33
+ SemVer **a partir de `1.0.0`**:
34
+
35
+ - **MAJOR** — se quita algo de la superficie pública, o se cambia lo que significa.
36
+ - **MINOR** — se añade. Añadir un campo con valor por defecto, un parámetro opcional o un símbolo
37
+ nuevo es minor.
38
+ - **PATCH** — correcciones que no cambian la superficie.
39
+
40
+ **Antes de `1.0.0` la superficie puede cambiar**, y decirlo claro vale más que fingir lo contrario.
41
+ Estamos en `1.0.0.dev0`. Lo que sí se compromete desde hoy está en la sección siguiente.
42
+
43
+ ## Qué se garantiza ya, antes de 1.0.0
44
+
45
+ Dos bloques, con garantías distintas y conviene no confundirlos:
46
+
47
+ ### Lo que viaja por la costura — garantía del contrato, no de este paquete
48
+
49
+ El vocabulario del modelo, la taxonomía de eventos y los dos protocolos de costura están
50
+ especificados en `contratos/` y versionados aparte, con una ventana de **dos versiones menores
51
+ vivas**. Esa garantía es más fuerte que la de esta política y no depende de ella: alguien puede
52
+ implementar la costura sin usar Synaptum.
53
+
54
+ ### La API de autoría — sobre la que se escriben agentes
55
+
56
+ ```python
57
+ Agent · tool · Tool · Session · Limits · Risk
58
+ ```
59
+
60
+ Sobre estos seis:
61
+
62
+ 1. **No se quitan ni se renombran antes de `1.0.0`** sin un cambio acordado en el canal de
63
+ coordinación, con la razón por escrito.
64
+ 2. **Los parámetros existentes no cambian de significado.** Añadir parámetros opcionales sí es
65
+ posible.
66
+ 3. **Los valores por defecto no se relajan en la dirección permisiva.** `risk=READ` e
67
+ `idempotent=False` son deliberadamente asimétricos —permisivo en riesgo, conservador en
68
+ durabilidad— y cambiarlos alteraría en silencio el comportamiento de código ya escrito. Endurecer
69
+ un default es un cambio MAJOR; relajarlo, directamente no se hace.
70
+
71
+ ## Deprecación
72
+
73
+ Un símbolo que va a desaparecer:
74
+
75
+ 1. Emite `DeprecationWarning` con el sustituto nombrado en el mensaje.
76
+ 2. **Sigue funcionando durante al menos dos versiones menores.** Misma ventana que la costura, por
77
+ coherencia y porque permite a los tres proyectos desplegar sin coordinar una fecha de corte.
78
+ 3. Solo entonces se quita, y en una MAJOR.
79
+
80
+ Un aviso de deprecación que no nombra el sustituto es un aviso incompleto: el que lo recibe tiene
81
+ que poder actuar sin abrir un issue.
82
+
83
+ ## Qué hacer si esto se rompe
84
+
85
+ Si un cambio nuestro rompe a alguien que construye sobre esto **fuera de lo que esta política
86
+ permite**, es un fallo nuestro y se revierte — no se negocia una migración a posteriori. Ese es el
87
+ sentido de haberlo escrito antes.
@@ -0,0 +1,36 @@
1
+ # Registro de cambios
2
+
3
+ Formato [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). Versionado según
4
+ [`API.md`](API.md): SemVer desde `1.0.0`, con ventana de deprecación de dos versiones menores.
5
+
6
+ ## [1.0.0rc1] — 2026-09-13
7
+
8
+ Publicado en TestPyPI. Instalable con `--index-url https://test.pypi.org/simple/`.
9
+
10
+ Primera publicación. **Es un candidato y no una `1.0.0`, a propósito.** La superficie está completa
11
+ contra el contrato de hoy y no va a cambiar por gusto, pero la Fase 2 (economía de contexto) todavía
12
+ puede tocarla, y el compromiso de estabilidad de [`API.md`](API.md) arranca en `1.0.0`. Prometerlo
13
+ antes de tiempo sería peor que esperar.
14
+
15
+
16
+ Primera línea `1.0.x`. La `0.x`, con un diseño distinto, quedó congelada en
17
+ [v0.4.0](https://github.com/Root1V/synaptum-framework/releases/tag/v0.4.0) y **no hay ruta de
18
+ migración**: la superficie no se parece. El estado por elemento está en [`roadmap.md`](roadmap.md).
19
+
20
+ ### Añadido
21
+
22
+ - Bucle del agente como stream de eventos tipados — `Agent.run()` y `Agent.stream()`.
23
+ - Ejecución durable: al reanudar un run, **una inferencia ya pagada no se paga otra vez**.
24
+ `MemoryCheckpointer` y `SqliteCheckpointer` como implementaciones de referencia.
25
+ - Identidad determinista de paso (`000003-model`), publicada como especificación abierta, y que
26
+ además viaja como clave de idempotencia hacia proveedores que la admitan.
27
+ - Herramientas con esquema derivado de la firma: `@tool`. `risk` e `idempotent` se declaran.
28
+ - Vocabulario unificado de modelo con `Usage` de **tres estados** — medido, sin medir, estimado.
29
+ - Adaptador `openai-compatible` y transporte HTTP de desarrollo, ambos sin dependencias.
30
+ - Prompts como configuración versionada.
31
+ - `FakeGateway` y `ReplayGateway` como infraestructura de desarrollo de primera clase.
32
+ - Marcador `py.typed`: los tipos llegan a quien consume el paquete.
33
+
34
+ ### Pendiente antes de `1.0.0`
35
+
36
+ Economía de contexto (Fase 2) y multi-agente (Fase 3). Ver [`roadmap.md`](roadmap.md).
@@ -0,0 +1,247 @@
1
+ Metadata-Version: 2.5
2
+ Name: synaptum
3
+ Version: 1.0.0rc1
4
+ Summary: Provider-agnostic agent framework and durable runtime. Owns execution semantics; the harness owns the substrate.
5
+ Project-URL: Homepage, https://github.com/Root1V/synaptum-framework
6
+ Project-URL: Repository, https://github.com/Root1V/synaptum-framework
7
+ Project-URL: Changelog, https://github.com/Root1V/synaptum-framework/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/Root1V/synaptum-framework/issues
9
+ Author-email: Emeric Espiritu Santiago <emericespiritusantiago@gmail.com>
10
+ License-Expression: MIT
11
+ Keywords: agent-framework,agents,durable-execution,llm,provider-agnostic
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: Implementation :: CPython
17
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.13
20
+ Provides-Extra: anthropic
21
+ Requires-Dist: anthropic>=1; extra == 'anthropic'
22
+ Provides-Extra: axonium
23
+ Requires-Dist: axonium>=1.0.0rc3; extra == 'axonium'
24
+ Provides-Extra: mcp
25
+ Requires-Dist: mcp>=1; extra == 'mcp'
26
+ Provides-Extra: openai
27
+ Requires-Dist: openai>=2; extra == 'openai'
28
+ Provides-Extra: otel
29
+ Requires-Dist: opentelemetry-sdk>=1.30; extra == 'otel'
30
+ Provides-Extra: pydantic
31
+ Requires-Dist: pydantic>=2; extra == 'pydantic'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Synaptum
35
+
36
+ **Framework de agentes y runtime durable, agnóstico al proveedor.**
37
+
38
+ Synaptum es dueño de la *semántica* de ejecución de un agente: qué es un paso, dónde puede cortarse,
39
+ qué puede repetirse, y cómo se vuelve a derivar el contexto. El *sustrato* —dónde se persiste, con
40
+ qué retención, bajo qué política— pertenece al arnés que lo opera.
41
+
42
+ > **Versión:** `1.0.0.dev0` · **Python:** ≥ 3.13 · **Licencia:** MIT
43
+ >
44
+ > **En construcción.** La línea 0.x, con un diseño distinto, está congelada en
45
+ > [v0.4.0](https://github.com/Root1V/synaptum-framework/releases/tag/v0.4.0). El estado real de esta
46
+ > por elemento está en [`roadmap.md`](https://github.com/Root1V/synaptum-framework/blob/main/roadmap.md).
47
+
48
+ ---
49
+
50
+ ## La idea
51
+
52
+ El bucle de un agente no es un `while` escondido: es un generador asíncrono que **cede el control en
53
+ cada frontera significativa**.
54
+
55
+ ```python
56
+ async for step in agent.run(tarea, session=session):
57
+ match step:
58
+ case ToolStep(phase=Phase.ATTEMPTED, risk=Risk.DESTRUCTIVE):
59
+ ...
60
+ case ModelStep(phase=Phase.COMPLETED, usage=consumo):
61
+ ...
62
+ case FinalStep(output=salida):
63
+ ...
64
+ ```
65
+
66
+ De esa forma salen cuatro propiedades, y ninguna otra estructura las da a la vez:
67
+
68
+ 1. **El arnés obtiene sus puntos de enganche** sin que Synaptum sepa que existe. Aprobaciones,
69
+ guardarraíles y métricas son consumidores del stream.
70
+ 2. **Cada `yield` es una frontera de checkpoint natural.**
71
+ 3. **Interrumpir es dejar de iterar**; reanudar es volver a llamar con el mismo `run_id`.
72
+ 4. **Probar es iterar una lista.**
73
+
74
+ ## Lo que justifica todo lo demás
75
+
76
+ Al reanudar un run, **una inferencia ya pagada no se paga otra vez.**
77
+
78
+ ```python
79
+ store = SqliteCheckpointer("runs.db")
80
+
81
+ # Primera vuelta: dos llamadas al modelo, una a una herramienta.
82
+ async for step in agent.run("lee /x", session=Session("run-1", gateway, store)):
83
+ ...
84
+
85
+ # El proceso muere. Otro proceso, otra conexión, mismo run_id.
86
+ async for step in agent.run("lee /x", session=Session("run-1", otro_gateway, store)):
87
+ ... # cero llamadas al modelo, cero a la herramienta
88
+ ```
89
+
90
+ La ventana de contexto no se almacena: se **vuelve a derivar** de los mismos resultados en el mismo
91
+ orden. Guardarla sería guardar dos veces lo mismo y arriesgarse a que discrepen.
92
+
93
+ Esto se apoya en una sola pieza: la **identidad determinista de paso**, publicada como
94
+ especificación abierta. Quien la implemente obtiene la misma propiedad sin importar nada de Synaptum
95
+ ni hablar Python.
96
+
97
+ ## Herramientas
98
+
99
+ El esquema sale de la firma. Un esquema escrito aparte se desincroniza, y cuando lo hace el modelo
100
+ manda argumentos que la función no acepta — lejos del cambio que lo causó y con una inferencia ya
101
+ pagada.
102
+
103
+ ```python
104
+ @tool(risk=Risk.DESTRUCTIVE)
105
+ async def borrar(path: Annotated[str, "Ruta absoluta"], forzar: bool = False) -> str:
106
+ """Borra un fichero del disco."""
107
+ ...
108
+ ```
109
+
110
+ `risk` e `idempotent` se declaran y no se deducen: no son propiedades del tipo sino del **efecto**, y
111
+ ninguna anotación puede saber que una función que devuelve `str` mueve dinero.
112
+
113
+ Los defaults son asimétricos a propósito — **permisivo en riesgo, conservador en durabilidad**: quien
114
+ calla obtiene la clase más inocua y la garantía más cara.
115
+
116
+ Un tipo que no sabemos traducir **falla al decorar**, no al invocar.
117
+
118
+ ## Probarlo
119
+
120
+ ```bash
121
+ uv run python examples/01_agente.py
122
+ ```
123
+
124
+ Corre **sin inferencia**: las respuestas van guionizadas, y el resto es real —las herramientas se
125
+ ejecutan, el journal se escribe, el consumo se mide—. Para apuntar a un modelo de verdad, dos
126
+ variables y **el mismo fichero sin tocar**:
127
+
128
+ ```bash
129
+ export SYNAPTUM_BASE_URL=http://localhost:8080/v1
130
+ export SYNAPTUM_MODEL=qwen3-0.6b
131
+ uv run python examples/01_agente.py
132
+ ```
133
+
134
+ Que el mismo código sirva para las dos cosas no es comodidad: es la propiedad. Ver
135
+ [`examples/`](https://github.com/Root1V/synaptum-framework/tree/main/examples) — el bucle entero, la reanudación **medida** (no afirmada), una aprobación
136
+ humana a mitad de run, y el streaming con cancelación.
137
+
138
+ ## Sin dependencias
139
+
140
+ ```bash
141
+ pip install synaptum # 0 dependencias
142
+ ```
143
+
144
+ El núcleo es stdlib puro. Pydantic, los proveedores, MCP y OpenTelemetry son extras. Los adaptadores
145
+ se descubren por *entry points*, así que el núcleo no conoce a ninguno.
146
+
147
+ ## Desarrollo sin inferencia
148
+
149
+ No hay modelos locales disponibles en modo autónomo, así que el doble de desarrollo es
150
+ infraestructura y no una utilidad de test. Ejercita todo lo que el bucle sabe hacer: llamadas a
151
+ herramientas con ejecución real, streaming con cancelación, las tres disposiciones de denegación, la
152
+ taxonomía de errores y el consumo de tres estados.
153
+
154
+ ```python
155
+ from synaptum.testing import FakeGateway, ReplayGateway, calls, says
156
+
157
+ # Guion escrito a mano: directo, y suficiente para la mayoría.
158
+ gateway = FakeGateway(calls("leer", path="/x"), says("dice hola"), tools=[leer])
159
+
160
+ # Respuestas reales grabadas, normalizadas por el adaptador de verdad.
161
+ gateway = ReplayGateway("fixtures/chat_completion.json", tools=[leer])
162
+ ```
163
+
164
+ Un guion a mano dice lo que uno espera; una grabación dice lo que el proveedor hizo — y la diferencia
165
+ aparece en los caminos que nadie escribe porque no se le ocurren.
166
+
167
+ ## Dónde encaja
168
+
169
+ Synaptum se sostiene solo. Habla con dos piezas por **protocolo**, y trae una implementación de
170
+ referencia completa de cada una:
171
+
172
+ ```
173
+ arnés política, aprobaciones, secretos, retención, escalado
174
+ │ Gateway — decide y ejecuta · por defecto: LocalGateway
175
+ Synaptum semántica de ejecución ← esto
176
+ │ Checkpointer — persiste, no decide · por defecto: SqliteCheckpointer
177
+ almacén
178
+ ─────
179
+ proveedor cualquier endpoint OpenAI-compatible · por defecto: HttpModel
180
+ ```
181
+
182
+ **Las dos costuras son protocolos estructurales (`typing.Protocol`), no clases base.** Quien las
183
+ implemente no hereda ni importa nada nuestro, y puede estar escrito en otro lenguaje al otro lado de
184
+ un socket. No hay ningún arnés, SDK ni plataforma de inferencia concretos en el árbol de
185
+ dependencias: `pip install synaptum` trae **cero** paquetes.
186
+
187
+ En el despliegue donde nació, esas dos ranuras las ocupan un arnés llamado Aeon y un SDK llamado
188
+ Axonium sobre una plataforma de inferencia local. Nada de eso es un requisito, y el paquete no los
189
+ nombra: son **un** relleno posible de un protocolo abierto. El extra `[axonium]` existe para quien
190
+ tenga esa combinación, y es opcional como el de Anthropic o el de OpenAI.
191
+
192
+ `LocalGateway` **avisa de que no aplica política** y marca cada comprobación con `enforced=False` —
193
+ una comprobación dentro del proceso gobernado es advisoria, y que un run pase por ahí sin
194
+ denegaciones no dice nada sobre si pasaría por un gateway real.
195
+
196
+ ## Contratos compartidos
197
+
198
+ Tres especificaciones con casos dorados, ejecutables por cualquier implementación con su propio
199
+ runner. **Viven fuera de este repositorio** porque son artefactos conjuntos de varios proyectos, y
200
+ copiarlos aquí los convertiría en una copia que se desincroniza. Synaptum no depende de ellos: son
201
+ evidencia adicional, y sin ellos la suite pasa igual (209 de 277; el resto se salta).
202
+
203
+ ```bash
204
+ export SYNAPTUM_CONTRACTS=/ruta/a/contratos # opcional
205
+ ```
206
+
207
+
208
+ | Contrato | Estado |
209
+ |---|---|
210
+ | Costura de durabilidad | 8 casos · verdes contra nuestras dos implementaciones |
211
+ | Identidad de paso | 10 casos · verdes |
212
+ | Normalización entre proveedores | 14 casos · verdes contra el adaptador Python, todos menos uno **grabaciones reales** |
213
+
214
+ Los casos describen **resultados observables** y nunca estructuras internas: dos implementaciones sin
215
+ una línea de código en común tienen que poder reproducirlos.
216
+
217
+ Que los cuerpos sean grabaciones reales y no ejemplos escritos a mano ya encontró dos fallos
218
+ nuestros. Un stream que era **solo razonamiento** no emitía ni un evento, porque el cuerpo inventado
219
+ que teníamos antes solo llevaba deltas de texto. Y en un stream de razonamiento que termina llamando
220
+ a una herramienta, el cierre del pensamiento caía **en mitad de la llamada**: cerrábamos el ciclo al
221
+ ver texto, y ahí no había texto.
222
+
223
+ Un caso dorado **no fija cifras**. Una regrabación tiró cinco de los nuestros sin que ninguna
224
+ implementación hubiera cambiado: fijaban tokens y un id de réplica, que pertenecen a una grabación y
225
+ no a la especificación. Lo que se afirma es si un contador está medido o sin medir, y cómo se
226
+ relacionan entre sí.
227
+
228
+ ## Estabilidad
229
+
230
+ Qué se garantiza, durante cuánto y qué no: está **escrito y comprobado por un test**, no recordado.
231
+ Un arnés retiró su DSL de autoría apoyándose en ese compromiso, que es por qué existe por escrito.
232
+ Ver [`API.md`](https://github.com/Root1V/synaptum-framework/blob/main/API.md).
233
+
234
+ ## Estado
235
+
236
+ | Fase | |
237
+ |---|---|
238
+ | **0 · Contratos** | completa |
239
+ | **1 · Núcleo durable** | completa |
240
+ | **2 · Contexto y observabilidad** | sin empezar |
241
+ | **3 · Multi-agente** | sin empezar |
242
+
243
+ `roadmap.md` lleva el detalle por elemento.
244
+
245
+ ---
246
+
247
+ MIT © 2026 · Emeric Espiritu Santiago