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.
- synaptum-1.0.0rc1/.github/humo.py +39 -0
- synaptum-1.0.0rc1/.github/workflows/release.yml +120 -0
- synaptum-1.0.0rc1/.github/workflows/tests.yml +46 -0
- synaptum-1.0.0rc1/.gitignore +14 -0
- synaptum-1.0.0rc1/.python-version +1 -0
- synaptum-1.0.0rc1/API.md +87 -0
- synaptum-1.0.0rc1/CHANGELOG.md +36 -0
- synaptum-1.0.0rc1/PKG-INFO +247 -0
- synaptum-1.0.0rc1/README.md +214 -0
- synaptum-1.0.0rc1/examples/01_agente.py +135 -0
- synaptum-1.0.0rc1/examples/02_durabilidad.py +135 -0
- synaptum-1.0.0rc1/examples/03_aprobacion.py +140 -0
- synaptum-1.0.0rc1/examples/04_streaming.py +111 -0
- synaptum-1.0.0rc1/examples/README.md +61 -0
- synaptum-1.0.0rc1/examples/_comun.py +134 -0
- synaptum-1.0.0rc1/pyproject.toml +61 -0
- synaptum-1.0.0rc1/roadmap.md +234 -0
- synaptum-1.0.0rc1/src/synaptum/__init__.py +139 -0
- synaptum-1.0.0rc1/src/synaptum/agent/__init__.py +5 -0
- synaptum-1.0.0rc1/src/synaptum/agent/agent.py +524 -0
- synaptum-1.0.0rc1/src/synaptum/core/__init__.py +166 -0
- synaptum-1.0.0rc1/src/synaptum/core/codec.py +151 -0
- synaptum-1.0.0rc1/src/synaptum/core/errors.py +256 -0
- synaptum-1.0.0rc1/src/synaptum/core/events.py +322 -0
- synaptum-1.0.0rc1/src/synaptum/core/protocols.py +323 -0
- synaptum-1.0.0rc1/src/synaptum/core/types.py +616 -0
- synaptum-1.0.0rc1/src/synaptum/prompts/__init__.py +15 -0
- synaptum-1.0.0rc1/src/synaptum/prompts/providers.py +170 -0
- synaptum-1.0.0rc1/src/synaptum/prompts/template.py +111 -0
- synaptum-1.0.0rc1/src/synaptum/providers/__init__.py +18 -0
- synaptum-1.0.0rc1/src/synaptum/providers/axonium.py +405 -0
- synaptum-1.0.0rc1/src/synaptum/providers/base.py +156 -0
- synaptum-1.0.0rc1/src/synaptum/providers/openai_compatible.py +356 -0
- synaptum-1.0.0rc1/src/synaptum/py.typed +0 -0
- synaptum-1.0.0rc1/src/synaptum/run/__init__.py +17 -0
- synaptum-1.0.0rc1/src/synaptum/run/journal.py +167 -0
- synaptum-1.0.0rc1/src/synaptum/run/local.py +259 -0
- synaptum-1.0.0rc1/src/synaptum/run/sqlite.py +118 -0
- synaptum-1.0.0rc1/src/synaptum/run/transport.py +241 -0
- synaptum-1.0.0rc1/src/synaptum/schema/__init__.py +15 -0
- synaptum-1.0.0rc1/src/synaptum/schema/derive.py +173 -0
- synaptum-1.0.0rc1/src/synaptum/schema/protocol.py +188 -0
- synaptum-1.0.0rc1/src/synaptum/testing/__init__.py +24 -0
- synaptum-1.0.0rc1/src/synaptum/testing/fake.py +242 -0
- synaptum-1.0.0rc1/src/synaptum/testing/replay.py +180 -0
- synaptum-1.0.0rc1/src/synaptum/tools/__init__.py +5 -0
- synaptum-1.0.0rc1/src/synaptum/tools/decorator.py +188 -0
- synaptum-1.0.0rc1/tests/contratos.py +56 -0
- synaptum-1.0.0rc1/tests/superficie-publica.json +100 -0
- synaptum-1.0.0rc1/tests/test_api_surface.py +112 -0
- synaptum-1.0.0rc1/tests/test_axonium.py +356 -0
- synaptum-1.0.0rc1/tests/test_conformance.py +544 -0
- synaptum-1.0.0rc1/tests/test_events.py +195 -0
- synaptum-1.0.0rc1/tests/test_independencia.py +130 -0
- synaptum-1.0.0rc1/tests/test_loop.py +585 -0
- synaptum-1.0.0rc1/tests/test_persistence.py +410 -0
- synaptum-1.0.0rc1/tests/test_prompts.py +168 -0
- synaptum-1.0.0rc1/tests/test_providers.py +138 -0
- synaptum-1.0.0rc1/tests/test_replay.py +155 -0
- synaptum-1.0.0rc1/tests/test_schema.py +247 -0
- synaptum-1.0.0rc1/tests/test_seam.py +278 -0
- synaptum-1.0.0rc1/tests/test_tools.py +351 -0
- synaptum-1.0.0rc1/tests/test_transport.py +239 -0
- synaptum-1.0.0rc1/tests/test_types.py +209 -0
- 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 @@
|
|
|
1
|
+
3.13
|
synaptum-1.0.0rc1/API.md
ADDED
|
@@ -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
|