- Resolución dinámica del modelo por sesión (model_resolver): override de
usuario (metadata) → default global (Redis db 0 acai:config:ai:*) → fallback.
Mapea a string litellm; LiteLLMAdapter respeta el modelo por request y
enruta openrouter/* con OPENROUTER_API_KEY del entorno.
- Razonamiento: reasoning_effort por sesión en ModelConfig/AgentProfile,
aplicado al agente y al planner.
- Coste: cost.py calcula por modelo (catálogo OpenRouter/DeepSeek en Redis →
litellm → fijo) y emite tarifas + modelo usado en EXECUTION_COMPLETED.
- Visión nativa: imágenes como bloques image_url en el turno del usuario
(TaskState.image_attachments → Context Engine → adapter), con persistencia
en recent_messages y conteo de tokens de imagen (~1500).
- El turno no se pierde al cancelar: se persiste el mensaje del usuario + marca
de interrupción para que un "vuelve a intentarlo" tenga contexto.
- Fix analyze_image: preservar el subdirectorio de usuario del chat-upload
(basename descartaba "<user>/" → not found).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- mcp-server _docsReader.js: resolveDocsDir → ACAI_DOCS_DIR /
$ACAI_PROJECT_DIR/docs / /app/docs. Arregla DOC_NOT_FOUND en VSCode
(HTTP MCP) y local; el .mcp.json ya inyecta ACAI_PROJECT_DIR
- routes.py: /knowledge/load idempotente — salta embeddings si el hash
de contenido no cambió (clave Redis kbhash), para dispararlo libremente
desde el botón de scaffold sin re-embeber
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Las sesiones largas con DeepSeek quedaban bloqueadas permanentemente con
400 "Messages with role 'tool' must be a response to a preceding message
with 'tool_calls'": el paso de ultimo recurso del compactor colapsaba
assistants con tool_use a un string placeholder dejando huerfanos los
tool_result del user siguiente.
- compactor: paso de ultimo recurso pair-aware + _enforce_tool_pairing
como invariante final (matching por IDs, ambas direcciones, repara
tambien historiales ya corruptos persistidos).
- openai_adapter: _repair_tool_sequence como guard defensivo del contrato
del proveedor (tool huerfano -> user; tool_call sin respuesta -> fuera),
con warning para detectar regresiones.
- recent_messages: trim por presupuesto de tokens al persistir
(AGENTIC_RECENT_MESSAGES_MAX_TOKENS, default 60k) sin cortar pares;
cierra el crecimiento sin limite que empujaba al paso destructivo.
- tests/test_tool_pairing_real.py: 23 tests que importan el codigo REAL
(a diferencia de los tests standalone existentes). Suite completa: 92 ok.
Verificado offline contra los recent_messages reales de la sesion
bloqueada en prod: 0 violaciones con presupuesto normal y agresivo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- session_lock: token uuid + compare-and-delete (Lua), TTL > timeout de
ejecucion; abort solo limpia el lock tras cancelacion confirmada.
Evita doble ejecucion concurrente sobre la misma sesion.
- monitor HTTP (puerto 4545) deshabilitado salvo MCP_MONITOR_ENABLED=true
y atado a 127.0.0.1; no se acumula historial en memoria si esta off.
- DeepSeek/LiteLLM: turnos que llegan solo con reasoning_content (sin
content ni tool_calls) ya no rompen la sesion (400 'Invalid assistant
message') ni se pintan como 'pensando': se promueven a texto en el
historial y en el snapshot persistido.
- litellm pinneado a ==1.80.0 (builds reproducibles).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tercer modo de fallo del conector OpenAI (distinto de followup_mode y de
finish_reason=stop): DeepSeek a veces emite las tool calls en su formato interno
DSML (<||DSML||tool_calls>…, con U+FF5C) como TEXTO en el content, en vez de
como tool_calls nativos. El endpoint OpenAI no lo convierte, asi que el adapter
lo trataba como texto y el agente "se paraba" mostrando DSML inerte (0 tools).
Fix en OpenAIAdapter.stream: reutiliza el parser del claude_adapter
(_parse_xml_tool_calls / _TOOL_CALL_OPEN_RE). Acumula el content; si detecta el
inicio de un tool call en texto deja de emitirlo al usuario (DSML no debe verse);
al cerrar el turno, si no hubo tool_calls nativos, parsea el content y emite los
tool calls encontrados como tool_use para que el engine los ejecute.
Validado: el DSML real de la sesion (2x acai_grep) se parsea correctamente.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sintoma (solo con el conector OpenAI): el agente anuncia la accion en texto
("Voy a crear los modulos…") y se PARA sin ejecutarla — 0 tools.
Causa: el stream del OpenAIAdapter solo emitia los tool_calls acumulados cuando
choice.finish_reason == "tool_calls". Pero DeepSeek (endpoint OpenAI) a veces
cierra el stream con finish_reason="stop" AUNQUE haya emitido tool_calls; en ese
caso caiamos en el branch else (end_turn) y los tool_calls acumulados se
descartaban. base.py solo ejecuta al recibir finish_reason="tool_use", asi que
nunca se ejecutaban. Con el adapter Claude (Anthropic) el finish_reason venia
distinto, por eso solo aparecia tras el cambio de conector.
Fix: disparar los tool_use SIEMPRE que haya tool_calls acumulados al cerrar el
stream, sea cual sea el finish_reason.
Validado: "crea un modulo…" ahora ejecuta acai_write + check_module y completa,
en vez de pararse tras anunciar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sintoma: "el agente se para cuando hace acciones". MCPClient._read_loop lee las
respuestas JSON-RPC con stdout.readline(), cuyo StreamReader tenia buffer de 1MB.
Una respuesta llega en UNA linea; playwright__browser_take_screenshot({fullPage:
true}) devuelve la imagen en base64 en esa linea y supera el limite →
asyncio.LimitOverrunError → el except Exception mataba el read loop y dejaba la
sesion MCP inservible (los turnos siguientes ejecutaban 0 tools).
Fix en dos capas:
- MCP_STREAM_LIMIT=64MB en create_subprocess_exec(limit=...) — cubre cualquier
screenshot real.
- Read loop tolerante: captura (ValueError, LimitOverrunError), descarta solo esa
respuesta re-sincronizando el stream hasta el \n (_drain_until_newline) y sigue
vivo, en vez de matar toda la sesion MCP.
Validado: navegar + screenshot fullPage + glob ejecuta las 4 tools sin "read loop
error" y sin colapsar el contexto.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos bugs encadenados impedían que el agente ejecutara tools (emitía los tool
calls como texto sin ejecutarlos, y degradaba el contexto):
1. Conector: el OpenAIAdapter pasaba los mensajes en formato Anthropic (bloques
tool_use/tool_result) que la API OpenAI de DeepSeek rechaza, y defaulteaba el
modelo a "gpt-4o". Añade `_to_openai_messages()` (assistant.tool_use →
tool_calls; user.tool_result → role:tool con tool_call_id) y `_blocks_text()`,
y usa `settings.default_model_id`. Con esto DeepSeek devuelve tool_calls
nativos vía https://api.deepseek.com (endpoint OpenAI), sin parsear texto y
sin la degradación que sufría el endpoint Anthropic-compat.
2. followup_mode: `_classify_followup_mode` marcaba como "transform" cualquier
PRIMER mensaje que contuviera un marker ("resumen", "estructura", "busca",
"adapta"…), y `_get_allowed_tools` devuelve [] en modo transform → el agente
se quedaba SIN tools. Un follow-up no tiene sentido sin turno anterior, así
que ahora solo se clasifica si hay task_history/recent_messages.
claude_adapter: parser DSML/DeepSeek para tool calls como texto (fallback del
endpoint Anthropic-compat, ya no es la vía principal).
Validado: el prompt de análisis de estilos ("Guarda un resumen…") ahora explora
los módulos y escribe docs/project-styles.md vía save_project_styles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Las sesiones MCP no se limpiaban (registry._sessions crecía sin límite); cada
una arranca 3 subprocesos stdio (acai-code, playwright+chromium, fetch) con sus
read-loops. Con varias vivas a la vez, las sesiones nuevas recibían cada vez
menos contexto/tools al modelo, hasta que el modelo dejaba de recibir tools y
emitía los tool calls como texto sin ejecutarlos (~300 input_tokens). Esto
degradaba el chat a lo largo del día hasta reiniciar el container.
Fix: MAX_ACTIVE_MCP_SESSIONS=2 con evicción LRU (touch last_used en
create/get_for_session, _evict_lru destruye las menos usadas). Seguro porque
send_message reconecta el MCP de una sesión evictada si vuelve a usarse.
Validado: 1 sesión viva era estable, 6 colapsaban; con cap=2, 7 sesiones
secuenciales se mantienen estables (40-115K tokens, tools OK).
Mitigación, no cura de fondo: el motivo por el que N managers vivos degradan
(probable: chromium de playwright) queda pendiente para subir el umbral.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Antes, al parar el agente y mandar un mensaje nuevo, la ejecución previa
seguía viva reteniendo el session_lock: el mensaje nuevo recibía "busy" y el
stream mostraba la ejecución anterior. La tarea detached (create_task) no se
guardaba en ningún sitio y era imposible cancelarla.
- _running_executions: registro de la tarea asyncio por session_id.
- _cancel_running_execution(): cancela y espera a que libere el lock.
- send_message: preempt — un mensaje nuevo cancela la ejecución previa.
- _execute_and_persist: maneja CancelledError dejando la sesión en ACTIVE.
- POST /sessions/{id}/abort: cancela, cierra el stream SSE y limpia el lock.
- RedisStorage.clear_session_lock(): libera locks huérfanos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nueva variable AGENTIC_OPENAI_BASE_URL para proveedores compatibles
con OpenAI API (MiniMax, DeepInfra, Together, etc.).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El modelo repetía tareas anteriores porque el historial se
reconstruía como mensajes user/assistant que parecían peticiones
nuevas. Ahora el historial va como un bloque de contexto marcado
explícitamente con [HISTORIAL — NO ejecutar de nuevo].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- role=tool → role=user con tool_result blocks
- assistant con tool_calls → assistant con tool_use blocks
- Merge mensajes consecutivos del mismo role (Claude requiere alternancia)
- Capturar input_tokens del evento message_start
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El endpoint /context-debug ahora devuelve full_context con el
system_prompt y messages exactos enviados al modelo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extraída lógica de carga a _load_knowledge_from_dir() reutilizable.
Se llama automáticamente en el lifespan después de set_dependencies().
Si falla, solo loguea warning — no bloquea el arranque.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El sistema multi-agente (planner → coder → reviewer) añadía
complejidad y causaba problemas (sobreplanificaci��n, repetición
de tareas, pérdida de contexto entre steps).
Ahora: mensaje → coder → respuesta. Como Claude Code.
- El coder decide si responder directamente o usar tools
- Sin plan intermedio, sin reviewer, sin steps
- Un solo execute() con conversación real completa
- Historial compactado con key_data al finalizar
- System prompt actualizado: asistente de desarrollo, no agente
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GPT-5.4 ignora las convenciones del knowledge base (42K tokens).
Las reglas más críticas se duplican en el system prompt del coder:
- script.js y style.css son ESTÁTICOS (sin Twig)
- Valores dinámicos via data-* attributes
- CmsApi.hook() en vez de fetch
- querySelectorAll con clase raíz en vez de getElementById
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El planner generaba 3+ steps para tareas simples causando que el
coder repitiera acciones en cada step (creaba el módulo varias veces).
Ahora el engine fusiona los steps en 1 coder con descripción combinada.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El plan del planner se emite como tool_use(name="plan") + tool_result
con los steps formateados. El frontend lo renderiza como un bloque
colapsable de herramienta con el plan de ejecución.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Budget de 15K dejaba fuera docs críticos (css-js-conventions,
hooks-and-api). Con 42K tokens totales y 128K de contexto,
incluir todo es la mejor estrategia.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El planner generaba 6+ steps para tareas simples como crear un módulo,
causando que el coder repitiera acciones o creara el módulo dos veces.
- Reglas explícitas: máximo 2-3 steps
- Crear módulo = 1 step coder (archivos + add + config)
- Explorar = 1 step coder
- Reviewer solo para tareas complejas
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El agenticSystem ahora es conversacional — recuerda lo dicho en
mensajes anteriores de la misma sesión.
- engine.py: direct_response guarda en task_history con formato
"User: X → Agent: Y"
- context/engine.py: _build_messages() reconstruye el task_history
como pares user/assistant reales en el array de messages, antes
del mensaje actual. El modelo ve una conversación completa.
- base.py: planner/reviewer no emiten AGENT_DELTA al frontend
(su output es interno, no para el usuario)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El planner genera JSON interno que no debe mostrarse al usuario.
Solo coder y collector emiten AGENT_DELTA al stream.
Para direct_response, el engine emite como agent=coder para que
el ClaudeFormatEmitter lo procese correctamente.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El planner ahora puede devolver direct_response en vez de un plan
cuando el mensaje no requiere herramientas (saludos, preguntas
generales, conversación casual).
- planner.py: prompt actualizado con formato direct_response
- engine.py: si planner devuelve string, emitir como texto y
completar sin ejecutar steps
"hola" → "¡Hola! ¿En qué puedo ayudarte hoy?" (0 steps, 0 tools)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Nuevo ClaudeFormatEmitter traduce eventos nativos al formato exacto
que produce Claude Code CLI: content_block_start/delta/stop, tool_result,
assistant snapshots, result con usage/cost, done.
- streaming/claude_format.py: ClaudeFormatEmitter + DualEmitter
- base.py: enriquecer eventos con tool_call_id, raw_output, tool_arguments
- engine.py: usage/cost en EXECUTION_COMPLETED
- routes.py: ?format=claude en /sessions/{id}/stream
- main.py: DualEmitter wiring (emite a ambos formatos)
El frontend puede consumir el stream sin cambios — mismos event types
que Claude Code CLI. El formato nativo sigue disponible para el dashboard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Config: COST_PER_1M_INPUT y COST_PER_1M_OUTPUT configurables via .env
- OpenAI adapter: stream_options include_usage para capturar tokens reales
- base.py: acumula input/output tokens de cada iteración del agente
- planner.py: devuelve usage junto con el plan
- engine.py: suma tokens de planner + steps + review, calcula coste USD
- Response incluye usage{input_tokens, output_tokens} y total_cost_usd
Formato compatible con el bridge de Claude Code CLI para integración
con el frontend y reporting a Acai webservice.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
El server compila automáticamente al guardar index-base.tpl via
acai_write — no necesita create_module ni compile_module manual.
- mcp-tools-reference.md: flujo actualizado, create_module marcado legacy
- module-creation-guide.md: paso 2 usa acai_write
- ACAI-CLAUDE.md: key workflows actualizados
- coder.py: system prompt alineado
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Task history preserva key_data estructurado (recordNums, sectionIds,
moduleIds, pages) extraído de las tool executions reales — el modelo
retiene contexto entre tasks sin re-consultar.
2. Coder system prompt mejorado: instrucciones explícitas sobre qué tool
usar para cada operación (create_module vs create_or_update_record),
consultar knowledge base antes de actuar, y reutilizar key_data del
historial.
3. Eliminado artifact_memory y working_context del coder context_sections
— ya no son necesarios con conversación real. Reduce acumulación de
artifacts en el context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Tool results completos en conversación (como Claude Code/Cursor)
en vez de resúmenes en system prompt
- Parser multi-tool: trackea tool calls por tool_call_id para
OpenAI streaming interleaved
- Deduplicación por fingerprint + detección de loop cuando todos
los calls de un step son duplicados
- Compactación inteligente por step: el orquestador decide cuándo
comprimir steps anteriores (cambio de agente o >3 steps)
- stdio.js lee URLs del .acai como fallback (local_web_url, local_forge_host)
- Buffer MCP aumentado a 1MB para respuestas grandes
- Dockerfile adaptado para build context desde raíz del proyecto
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add .gitignore to exclude .env, __pycache__, node_modules, and IDE files
- Remove all __pycache__ bytecode files from version control
- Add MCP config files (mcp.json, mcp.json.example)
- Add MCP manager, registry, and config modules
- Update routes, orchestrator engine, and agent base with latest changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>