ayder-cli 2.4.0__tar.gz → 2.4.1__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.
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/PKG-INFO +7 -4
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/README.md +6 -3
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/GATE.md +1 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/PROJECT_STRUCTURE.md +4 -2
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/config.toml.example +1 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/pyproject.toml +1 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/execution_policy.py +24 -3
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/validation.py +6 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/config.py +1 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/context.py +33 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/default_context_manager.py +33 -27
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/ollama_context_manager.py +122 -30
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/loops/chat_loop.py +438 -236
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/claude.py +26 -7
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/gemini.py +26 -22
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/context.py +36 -5
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/note_tool.py +22 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/notes.py +33 -10
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/task_tool.py +29 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/tasks.py +59 -13
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/normalization.py +48 -29
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/app.py +2 -2
- ayder_cli-2.4.1/tests/application/test_argument_canonicalization.py +136 -0
- ayder_cli-2.4.1/tests/core/test_default_context_newest_unit.py +133 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_event_emission.py +3 -2
- ayder_cli-2.4.1/tests/core/test_max_output_tokens_default.py +121 -0
- ayder_cli-2.4.1/tests/core/test_ollama_compaction_persistence.py +235 -0
- ayder_cli-2.4.1/tests/loops/test_tool_argument_integrity.py +247 -0
- ayder_cli-2.4.1/tests/loops/test_tool_batch_cancellation.py +506 -0
- ayder_cli-2.4.1/tests/providers/test_claude_stream_tool_calls.py +242 -0
- ayder_cli-2.4.1/tests/providers/test_gemini_function_calls.py +179 -0
- ayder_cli-2.4.1/tests/tools/test_context_storage_confinement.py +150 -0
- ayder_cli-2.4.1/tests/tools/test_notes_confinement.py +129 -0
- ayder_cli-2.4.1/tests/tools/test_task_path_confinement.py +145 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/uv.lock +1 -1
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/.github/workflows/ci.yml +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/.github/workflows/python-package.yml +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/.gitignore +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/.python-version +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/AGENTS.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/LICENSE +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/README_AGENT_WORKFLOW.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/README_OLLAMA.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/cc.png +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/superpowers/plans/2026-06-26-file-editor-improvements.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/superpowers/plans/2026-06-26-session-save-resume.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/superpowers/specs/2026-06-26-file-editor-improvements-design.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/docs/superpowers/specs/2026-06-26-session-save-resume-design.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/check_exception_handlers.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/check_log_arguments.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/check_logging_imports.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/exception_baseline.txt +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/find_percent_logging.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/log_argument_baseline.txt +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/scripts/logging_gates.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/__main__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/callbacks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/run.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/runner.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/agents/worktree.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/README.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/message_contract.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/application/runtime_factory.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/cli.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/cli_callbacks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/cli_runner.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/console.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/cache_monitor.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/config_migration.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/context_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/context_manager_factory.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/reasoning.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/result.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/core/session.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/diagnostics.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/log.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/logging_config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/loops/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/masking.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/parser.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/process_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/prompts.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/base.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/evren.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/deepseek.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/glm.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/_errors.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/base.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/deepseek.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/generic_native.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/generic_xml.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/matrix.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/minimax.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/qwen3.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_drivers/registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/ollama_inspector.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/openai.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/impl/qwen.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/orchestrator.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/providers/retry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/services/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/services/interactions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/services/messaging_inbox.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/themes/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/themes/ayder.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/themes/claude.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/background_process_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/context_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/filesystem.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/filesystem_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/notes_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/process_manager_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/search.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/search_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/shell.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/shell_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/skill.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/skill_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/task_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/utils_tools.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/utils_tools_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/web.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/builtins/web_definitions.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/definition.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/execution.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/hooks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/mcp.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/mcp_client.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/plugin_api.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/plugin_github.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/plugin_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/plugin_status.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/schemas.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tools/utils.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/adapter.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/commands.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/helpers.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/keybindings.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/parser.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/rendering.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/screens.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/theme_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/types.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/tui/widgets.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/ui.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/src/ayder_cli/version.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/COVERAGE.md +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_agent_consolidated.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_agent_severity.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_agent_tools.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_callbacks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_integration.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_queue.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_run.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_runner.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_runner_provider_unavailable.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_worktree_helpers.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/agents/test_worktree_isolation.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/application/test_execution_result_truthfulness.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/application/test_message_contract.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/application/test_runtime_factory.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/client/test_main.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/conftest.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/convergence/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/convergence/test_execution_policy_parity.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/convergence/test_runtime_wiring.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/convergence/test_validation_path.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_asyncio_bridge.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_cache_monitor.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config_coverage.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config_migration.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config_provider_logging.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config_retry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_config_v2.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_context_manager_factory.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_default_config_evren.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_default_context_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_default_context_manager_should_compact.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_event_stream.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_exception_guarantee.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_exception_hooks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_log_facade.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_logging_config_keys.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_logging_sinks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_loguru_caplog.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_ollama_context_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_ollama_context_manager_cache_monitor.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_ollama_context_manager_max_history.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_parameter_aliasing.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_parser.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_reasoning_config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_session.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_session_id_threading.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/core/test_sink_masking.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/test_base.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/test_chat_loop_hook.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/test_chat_loop_tool_truncation.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/test_loop_severity.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/loops/test_required_args_aliases.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_base.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_deepseek.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_errors.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_fallback.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_generic_native.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_generic_xml.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_matrix.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_minimax.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_provider_integration.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_qwen3.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/ollama_drivers/test_registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_evren.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_glm_async.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_inspector.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_native.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_parallel_tool_call_ids.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_xml.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_xml_autoroute.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_ollama_xml_usage.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_openai_list_models.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_orchestrator_capabilities.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_provider_channel_binding.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_provider_severity.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_qwen_async.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_reasoning_effort.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_retry_classifier.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_retry_integration.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/providers/test_retrying_provider.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/scripts/test_exception_gate.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/scripts/test_log_argument_gate.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/scripts/test_logging_policy_gates.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/scripts/test_loguru_private_shape.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/services/test_boundary.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/services/test_interaction_sink.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/services/test_messaging_inbox.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_cli.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_cli_callbacks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_context.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_env_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_logging_config.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_notes.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_process_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/test_tasks.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_agent_notes.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_background_process_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_bash_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_definition_discovery.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_di_injection.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_file_editor_hardening.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_file_editor_size_limit.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_impl.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_impl_coverage.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_mcp.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_mcp_client.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_note_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_notes_api.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_path_security.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_plugin_api.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_plugin_github.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_plugin_loading.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_plugin_manager.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_plugin_status.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_registry.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_registry_coverage.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_result.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_schemas.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_search_codebase.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_search_hardening.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_skill_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_task_tool.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_tool_severity.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_unexpected_args.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_utils.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tools/test_web.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/__init__.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_agent_list_screen.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_do_clear_coordination.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_evren_terms.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_follow_mode.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_help_screen.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_input_box.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_keybindings.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_multi_select_screen.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_multiline_input.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_paste.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_peer_message_turn.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_pending_compact_consumer.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_pull_delivery.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_resume.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_status_bar_badges.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_themes.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_turn_consumer.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/tui/test_widgets.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_confirm_screen.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_diff_preview.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_plugin_autoenable.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_provider_switch.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_run_tui_startup.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_assistant_sanitization.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_chat_loop.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_agent.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_effort.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_logging.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_plugin.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_provider.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_commands_skill.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_helpers.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_temporal_command.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_tui_widgets.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_ui.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_ui_coverage.py +0 -0
- {ayder_cli-2.4.0 → ayder_cli-2.4.1}/tests/ui/test_ui_verbose.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ayder-cli
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: AI agent for any LLMs
|
|
5
5
|
Project-URL: Homepage, https://github.com/ayder/ayder-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/ayder/ayder-cli.git
|
|
@@ -204,7 +204,7 @@ editor = "vim" # Editor for /task-edit command
|
|
|
204
204
|
verbose = false # Show file contents after write + LLM debug
|
|
205
205
|
max_background_processes = 5 # Max concurrent background processes (1-20)
|
|
206
206
|
max_iterations = 10 # Max agentic iterations per message (1-100)
|
|
207
|
-
max_output_tokens =
|
|
207
|
+
max_output_tokens = 32768 # Max tokens in LLM response
|
|
208
208
|
max_history_messages = 30 # Messages kept in history
|
|
209
209
|
prompt = "STANDARD" # System prompt tier: MINIMAL, STANDARD, EXTENDED
|
|
210
210
|
tool_tags = ["core", "metadata"] # Enabled tool tags (see /plugin)
|
|
@@ -414,12 +414,13 @@ ayder-cli includes a context manager that automatically keeps your conversation
|
|
|
414
414
|
- **Real token counts**: Uses Ollama's `prompt_eval_count` and `eval_count` from native API responses instead of estimates. Compaction decisions are based on actual usage, not heuristics.
|
|
415
415
|
- **Cache monitoring**: Tracks `prompt_eval_duration / prompt_eval_count` ratio to detect KV-cache hits (hot/warm/miss). When the cache is hot, compaction is delayed (threshold raised from 70% to 90%) to avoid invalidating it.
|
|
416
416
|
- **Auto-detected context length**: On startup, queries `/api/show` to get the model's real context length instead of relying on `num_ctx`. Falls back to `num_ctx` if detection fails.
|
|
417
|
-
- **Heuristic compaction**: When the context fills up, oldest message units
|
|
417
|
+
- **Heuristic compaction**: When the context fills up, the oldest half of the message units is replaced with a text summary — no LLM call needed. The newest unit is never folded. The summary persists: every later request reuses the same summary message, so each request extends the previous one until the next compaction. A new compaction folds the previous summary into one capped summary (8,000 characters).
|
|
418
|
+
- **Prefix contract**: Message-level prefix stability holds while the model, tools and `max_history = 0` stay unchanged and the history is append-only. Rewriting history (`/clear`, a compact, loading a session) starts a new generation: the summary is dropped and compaction waits for a fresh token count. This guarantees a stable message prefix; whether the server reuses its cache is up to the server.
|
|
418
419
|
- **Native SDK**: Uses `ollama.AsyncClient` directly (not the OpenAI compatibility layer) for full timing data, native thinking support, and `keep_alive=-1` to prevent model unloading.
|
|
419
420
|
|
|
420
421
|
**General-purpose manager** (Anthropic, OpenAI, Gemini, and other drivers):
|
|
421
422
|
|
|
422
|
-
Every message is assigned an importance tier (system > recent user > recent assistant > tool results > old history). When the conversation approaches the token budget, old tool results are compressed (JSON structurally summarized, large text head/tail truncated) and the lowest-priority messages are pruned. Tool call + result pairs are kept as atomic units.
|
|
423
|
+
Every message is assigned an importance tier (system > recent user > recent assistant > tool results > old history). When the conversation approaches the token budget, old tool results are compressed (JSON structurally summarized, large text head/tail truncated) and the lowest-priority messages are pruned. Tool call + result pairs are kept as atomic units. The newest unit is always kept, even when it alone exceeds the budget, so the model always sees the request it is answering.
|
|
423
424
|
|
|
424
425
|
The context manager is configured under `[context_manager]` in `config.toml`:
|
|
425
426
|
|
|
@@ -545,6 +546,8 @@ ayder-cli includes a built-in task system:
|
|
|
545
546
|
|
|
546
547
|
Tasks are stored as markdown files in `.ayder/tasks/` using slug-based filenames (e.g., `TASK-001-add-auth-middleware.md`).
|
|
547
548
|
|
|
549
|
+
The `task` tool resolves identifiers (a path, filename, ID or slug) only to files inside `.ayder/tasks/`. An identifier that points elsewhere, including through a symlink, is not resolved. If `.ayder/tasks/`, `.ayder/context/` or `.ayder/notes/` itself resolves outside the project, those tools return a security error.
|
|
550
|
+
|
|
548
551
|
```
|
|
549
552
|
> /tasks # Interactive task browser
|
|
550
553
|
> /task-edit 1 # Open TASK-001 in the in-app editor
|
|
@@ -161,7 +161,7 @@ editor = "vim" # Editor for /task-edit command
|
|
|
161
161
|
verbose = false # Show file contents after write + LLM debug
|
|
162
162
|
max_background_processes = 5 # Max concurrent background processes (1-20)
|
|
163
163
|
max_iterations = 10 # Max agentic iterations per message (1-100)
|
|
164
|
-
max_output_tokens =
|
|
164
|
+
max_output_tokens = 32768 # Max tokens in LLM response
|
|
165
165
|
max_history_messages = 30 # Messages kept in history
|
|
166
166
|
prompt = "STANDARD" # System prompt tier: MINIMAL, STANDARD, EXTENDED
|
|
167
167
|
tool_tags = ["core", "metadata"] # Enabled tool tags (see /plugin)
|
|
@@ -371,12 +371,13 @@ ayder-cli includes a context manager that automatically keeps your conversation
|
|
|
371
371
|
- **Real token counts**: Uses Ollama's `prompt_eval_count` and `eval_count` from native API responses instead of estimates. Compaction decisions are based on actual usage, not heuristics.
|
|
372
372
|
- **Cache monitoring**: Tracks `prompt_eval_duration / prompt_eval_count` ratio to detect KV-cache hits (hot/warm/miss). When the cache is hot, compaction is delayed (threshold raised from 70% to 90%) to avoid invalidating it.
|
|
373
373
|
- **Auto-detected context length**: On startup, queries `/api/show` to get the model's real context length instead of relying on `num_ctx`. Falls back to `num_ctx` if detection fails.
|
|
374
|
-
- **Heuristic compaction**: When the context fills up, oldest message units
|
|
374
|
+
- **Heuristic compaction**: When the context fills up, the oldest half of the message units is replaced with a text summary — no LLM call needed. The newest unit is never folded. The summary persists: every later request reuses the same summary message, so each request extends the previous one until the next compaction. A new compaction folds the previous summary into one capped summary (8,000 characters).
|
|
375
|
+
- **Prefix contract**: Message-level prefix stability holds while the model, tools and `max_history = 0` stay unchanged and the history is append-only. Rewriting history (`/clear`, a compact, loading a session) starts a new generation: the summary is dropped and compaction waits for a fresh token count. This guarantees a stable message prefix; whether the server reuses its cache is up to the server.
|
|
375
376
|
- **Native SDK**: Uses `ollama.AsyncClient` directly (not the OpenAI compatibility layer) for full timing data, native thinking support, and `keep_alive=-1` to prevent model unloading.
|
|
376
377
|
|
|
377
378
|
**General-purpose manager** (Anthropic, OpenAI, Gemini, and other drivers):
|
|
378
379
|
|
|
379
|
-
Every message is assigned an importance tier (system > recent user > recent assistant > tool results > old history). When the conversation approaches the token budget, old tool results are compressed (JSON structurally summarized, large text head/tail truncated) and the lowest-priority messages are pruned. Tool call + result pairs are kept as atomic units.
|
|
380
|
+
Every message is assigned an importance tier (system > recent user > recent assistant > tool results > old history). When the conversation approaches the token budget, old tool results are compressed (JSON structurally summarized, large text head/tail truncated) and the lowest-priority messages are pruned. Tool call + result pairs are kept as atomic units. The newest unit is always kept, even when it alone exceeds the budget, so the model always sees the request it is answering.
|
|
380
381
|
|
|
381
382
|
The context manager is configured under `[context_manager]` in `config.toml`:
|
|
382
383
|
|
|
@@ -502,6 +503,8 @@ ayder-cli includes a built-in task system:
|
|
|
502
503
|
|
|
503
504
|
Tasks are stored as markdown files in `.ayder/tasks/` using slug-based filenames (e.g., `TASK-001-add-auth-middleware.md`).
|
|
504
505
|
|
|
506
|
+
The `task` tool resolves identifiers (a path, filename, ID or slug) only to files inside `.ayder/tasks/`. An identifier that points elsewhere, including through a symlink, is not resolved. If `.ayder/tasks/`, `.ayder/context/` or `.ayder/notes/` itself resolves outside the project, those tools return a security error.
|
|
507
|
+
|
|
505
508
|
```
|
|
506
509
|
> /tasks # Interactive task browser
|
|
507
510
|
> /task-edit 1 # Open TASK-001 in the in-app editor
|
|
@@ -12,7 +12,7 @@ Input: the candidate SHA.
|
|
|
12
12
|
gates and the test suite, and stops at the first failing task.
|
|
13
13
|
4. Copy the final pytest summary line (`=== N passed, M skipped in …s ===`) verbatim.
|
|
14
14
|
|
|
15
|
-
Last measured: `
|
|
15
|
+
Last measured: `2606 passed, 2 skipped` on `fix/max-output-tokens-default` (2026-10-16).
|
|
16
16
|
|
|
17
17
|
## Report format
|
|
18
18
|
|
|
@@ -605,9 +605,11 @@ previously-split `AgentLoopBase` and `LoopConfig` were merged back in):
|
|
|
605
605
|
- Routes *every* LLM response through the injected `ContextManagerProtocol` (`prepare_messages()` / `update_from_response()`); tool outputs are truncated at insertion time via `truncate_tool_result()` so the KV-cache prefix remains stable
|
|
606
606
|
|
|
607
607
|
Key features:
|
|
608
|
-
- Auto-approved tools run in parallel (`asyncio.
|
|
608
|
+
- Auto-approved tools run in parallel (`asyncio.as_completed`); confirmation-required tools run sequentially
|
|
609
609
|
- Confirmation gate applies to **both** OpenAI-format and XML/JSON custom tool calls
|
|
610
|
-
- `ExecutionPolicy.execute_with_registry()` is the single execution entry (validate → permission → execute)
|
|
610
|
+
- `ExecutionPolicy.execute_with_registry()` is the single execution entry (object check → `canonicalize_arguments()` → validate → permission → execute). Canonicalization applies aliases and `"123"`/`"true"`/`"false"` coercion; the loop's required-argument gate uses the same function
|
|
611
|
+
- Argument integrity: `_parse_tool_arguments()` reports `ok`, `repaired` or `invalid`. Non-object arguments never run. Arguments repaired after being cut off (output token limit) run only for tools registered explicitly as `r`; other tools get an error asking the model to resend in smaller calls
|
|
612
|
+
- Cancellation: each batch owns a `_ToolBatch`. Workers refuse to start once the batch is cancelled and record their own result and `tool_call` event. When a batch ends early (Ctrl+C or a raising callback), a `finally` appends exactly one tool message per unanswered call: its result, an "interrupted, may have partially applied" notice, or a "cancelled before it ran" notice. Running workers are not awaited, so Ctrl+C returns at once
|
|
611
613
|
- Token usage tracking and iteration limiting
|
|
612
614
|
- Pre-iteration hook (`pre_iteration_hook`) used by the agent system to inject summaries
|
|
613
615
|
- Graceful cancellation via `is_cancelled()`
|
|
@@ -201,7 +201,7 @@ think = true # default; set false if the selected model r
|
|
|
201
201
|
# api_key = "sk-..." # your key (or "ollama"/"none" for local servers)
|
|
202
202
|
# model = "gpt-5.5"
|
|
203
203
|
# num_ctx = 65536 # sizes the context-manager budget (default 65536); NOT sent to the API
|
|
204
|
-
# max_output_tokens = 8192 # -> max_tokens / max_completion_tokens (default
|
|
204
|
+
# max_output_tokens = 8192 # -> max_tokens / max_completion_tokens (default 32768); keep WELL below num_ctx
|
|
205
205
|
# stop_sequences = [] # -> stop (default [])
|
|
206
206
|
# reasoning_effort = "medium" # choose a value supported by the selected model
|
|
207
207
|
# # temperature / think are NOT applied by the openai driver.
|
|
@@ -158,19 +158,40 @@ class ExecutionPolicy:
|
|
|
158
158
|
*,
|
|
159
159
|
pre_approved: bool = False,
|
|
160
160
|
) -> ExecutionResult:
|
|
161
|
-
"""Single shared execution path: validate →
|
|
161
|
+
"""Single shared execution path: canonicalize → validate → permission → execute.
|
|
162
162
|
|
|
163
163
|
Both CLI and TUI route through this method so registry.execute is never
|
|
164
164
|
called directly from interface-specific code.
|
|
165
165
|
|
|
166
|
+
Order:
|
|
167
|
+
1. Arguments that are not a JSON object are a validation error.
|
|
168
|
+
2. ``canonicalize_arguments``: aliases, integer and boolean coercion.
|
|
169
|
+
3. ``ValidationAuthority`` on the canonical arguments.
|
|
170
|
+
4. Permission check (unless pre-approved), then ``registry.execute``
|
|
171
|
+
with the canonical arguments.
|
|
172
|
+
|
|
166
173
|
Args:
|
|
167
174
|
pre_approved: If True, skip the permission check (user already
|
|
168
175
|
confirmed via the confirmation dialog).
|
|
169
176
|
"""
|
|
170
177
|
from ayder_cli.application.validation import ValidationAuthority
|
|
178
|
+
from ayder_cli.tools.normalization import canonicalize_arguments
|
|
179
|
+
|
|
180
|
+
if not isinstance(request.arguments, dict):
|
|
181
|
+
return ExecutionResult(
|
|
182
|
+
success=False,
|
|
183
|
+
error=ValidationError(
|
|
184
|
+
tool_name=request.name,
|
|
185
|
+
message=(
|
|
186
|
+
"arguments must be a JSON object, got "
|
|
187
|
+
f"{type(request.arguments).__name__}"
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
canonical = canonicalize_arguments(request.name, request.arguments)
|
|
171
192
|
|
|
172
193
|
authority = ValidationAuthority()
|
|
173
|
-
v_request = ToolRequest(name=request.name, arguments=
|
|
194
|
+
v_request = ToolRequest(name=request.name, arguments=canonical)
|
|
174
195
|
valid, err = authority.validate(v_request)
|
|
175
196
|
if not valid:
|
|
176
197
|
return ExecutionResult(success=False, error=err)
|
|
@@ -180,7 +201,7 @@ class ExecutionPolicy:
|
|
|
180
201
|
if perm_err is not None:
|
|
181
202
|
return ExecutionResult(success=False, error=perm_err)
|
|
182
203
|
|
|
183
|
-
raw = registry.execute(request.name,
|
|
204
|
+
raw = registry.execute(request.name, v_request.arguments)
|
|
184
205
|
if isinstance(raw, ToolError):
|
|
185
206
|
# A tool that ran and failed is NOT a successful execution.
|
|
186
207
|
return ExecutionResult(success=False, was_confirmed=pre_approved, error=raw)
|
|
@@ -101,6 +101,12 @@ class SchemaValidator:
|
|
|
101
101
|
field=param_name,
|
|
102
102
|
message=f"'{param_name}' must be integer, got {type(value).__name__}",
|
|
103
103
|
)
|
|
104
|
+
if expected_type == "boolean" and not isinstance(value, bool):
|
|
105
|
+
return False, ValidationError(
|
|
106
|
+
tool_name=request.name,
|
|
107
|
+
field=param_name,
|
|
108
|
+
message=f"'{param_name}' must be boolean, got {type(value).__name__}",
|
|
109
|
+
)
|
|
104
110
|
if expected_type == "string" and not isinstance(value, str):
|
|
105
111
|
return False, ValidationError(
|
|
106
112
|
tool_name=request.name,
|
|
@@ -352,7 +352,7 @@ class Config(BaseModel):
|
|
|
352
352
|
max_background_processes: int = Field(default=5)
|
|
353
353
|
# Local Unix-socket inbox that accepts prompts from peer processes.
|
|
354
354
|
messaging_enabled: bool = Field(default=True)
|
|
355
|
-
max_output_tokens: int = Field(default=
|
|
355
|
+
max_output_tokens: int = Field(default=32768)
|
|
356
356
|
# 0 = unlimited: let the context manager's token budget + compaction be the
|
|
357
357
|
# bound. The old fixed 30 predates large-context models and thrashes a tiny
|
|
358
358
|
# history window on a big context.
|
|
@@ -44,6 +44,39 @@ class ProjectContext:
|
|
|
44
44
|
|
|
45
45
|
return target_path
|
|
46
46
|
|
|
47
|
+
def harness_dir(self, *parts: str) -> Path:
|
|
48
|
+
"""Return ``<root>/.ayder/<parts>``, refusing a location outside the root.
|
|
49
|
+
|
|
50
|
+
``validate_path`` resolves symlinks, so a ``.ayder/<parts>`` symlink that
|
|
51
|
+
points outside the project raises instead of redirecting writes.
|
|
52
|
+
The directory is not created.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If the resolved directory is outside the project root.
|
|
56
|
+
"""
|
|
57
|
+
return self.validate_path(str(Path(".ayder", *parts)))
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def within(directory: Path, path: Path | str) -> Path:
|
|
61
|
+
"""Return ``path`` unchanged if it resolves inside ``directory``.
|
|
62
|
+
|
|
63
|
+
The resolved form is only checked, never returned, so names and
|
|
64
|
+
relative paths stay stable when an in-directory symlink is followed.
|
|
65
|
+
``os.path.realpath`` is used rather than ``Path.resolve``, which also
|
|
66
|
+
stats the final target. Resolving still lstats and reads links along
|
|
67
|
+
the path, including outside components; what it never does is read or
|
|
68
|
+
follow-stat the target's content, so a refused entry stays unread.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If ``path`` resolves outside ``directory``.
|
|
72
|
+
"""
|
|
73
|
+
resolved = Path(os.path.realpath(path))
|
|
74
|
+
if not resolved.is_relative_to(Path(os.path.realpath(directory))):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Security Alert: Path '{resolved}' is outside '{directory}'"
|
|
77
|
+
)
|
|
78
|
+
return Path(path)
|
|
79
|
+
|
|
47
80
|
def to_relative(self, absolute_path: Path) -> str:
|
|
48
81
|
"""Convert an absolute path to relative path for LLM output.
|
|
49
82
|
|
|
@@ -371,39 +371,45 @@ class DefaultContextManager:
|
|
|
371
371
|
overhead = effective_system_tokens + effective_schema_tokens
|
|
372
372
|
available = self._available_budget(overhead)
|
|
373
373
|
|
|
374
|
-
if available <= 0:
|
|
375
|
-
# Overhead ate the whole budget: every history message is dropped.
|
|
376
|
-
kept = 1 if system_msg else 0
|
|
377
|
-
self._emit_trim(len(self._messages), kept, "budget_exhausted")
|
|
378
|
-
return [system_msg] if system_msg else []
|
|
379
|
-
|
|
380
374
|
start_idx = 1 if system_msg else 0
|
|
381
375
|
history_msgs = self._messages[start_idx:]
|
|
382
376
|
|
|
383
377
|
units = self._group_into_units(history_msgs)
|
|
384
378
|
|
|
385
379
|
result: List[Dict[str, Any]] = []
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
380
|
+
if available <= 0:
|
|
381
|
+
# Overhead ate the whole budget: every history message is dropped
|
|
382
|
+
# except the newest unit, the request the model must see (F8).
|
|
383
|
+
if units:
|
|
384
|
+
result = list(units[-1])
|
|
385
|
+
kept = (1 if system_msg else 0) + len(result)
|
|
386
|
+
self._emit_trim(len(self._messages), kept, "budget_exhausted")
|
|
387
|
+
else:
|
|
388
|
+
used = 0
|
|
389
|
+
messages_count = 0
|
|
390
|
+
reason: str | None = None
|
|
391
|
+
for unit in reversed(units):
|
|
392
|
+
# The newest unit is always kept, even when it alone exceeds
|
|
393
|
+
# the budget or max_history: dropping it would send a request
|
|
394
|
+
# the model never saw (F8). If it truly exceeds the model's
|
|
395
|
+
# window, the provider's error reaches the user.
|
|
396
|
+
if result and max_history > 0 and messages_count + len(unit) > max_history:
|
|
397
|
+
reason = "max_history"
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
cost = self._counter.count_messages(unit)
|
|
401
|
+
if result and used + cost > available:
|
|
402
|
+
reason = "token_budget"
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
result = unit + result
|
|
406
|
+
used += cost
|
|
407
|
+
messages_count += len(unit)
|
|
408
|
+
|
|
409
|
+
# Emitted BEFORE the truncation marker below: that marker ADDS a
|
|
410
|
+
# message, and counting after it would under-report `dropped` by one.
|
|
411
|
+
if reason is not None:
|
|
412
|
+
self._emit_trim(len(history_msgs), len(result), reason)
|
|
407
413
|
|
|
408
414
|
if result and result[0].get("role") != "user":
|
|
409
415
|
result.insert(
|
|
@@ -23,6 +23,10 @@ from ayder_cli.providers.impl.ollama_inspector import OllamaInspector
|
|
|
23
23
|
|
|
24
24
|
logger = get_logger("context")
|
|
25
25
|
|
|
26
|
+
SUMMARY_HEADER = "[Previous conversation summary]"
|
|
27
|
+
# Cap on the accumulated summary body. Oldest lines are evicted first.
|
|
28
|
+
_SUMMARY_MAX_CHARS = 8000
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class OllamaContextStats(ContextStats):
|
|
@@ -46,6 +50,22 @@ class OllamaContextManager:
|
|
|
46
50
|
3. Tool results are truncated at insertion time (immutable after that).
|
|
47
51
|
4. Compaction is the only mutation — oldest units replaced with a heuristic
|
|
48
52
|
text summary, never an LLM call.
|
|
53
|
+
|
|
54
|
+
Compaction persists. The caller's history is never mutated; the manager
|
|
55
|
+
remembers the folded message objects and one summary dict, and every later
|
|
56
|
+
call replaces exactly those objects with that same summary. Between two
|
|
57
|
+
compactions or resets, each request's message list therefore extends the
|
|
58
|
+
previous one, provided the model, tools and rendering are unchanged,
|
|
59
|
+
``max_history`` is 0, and the history is append-only with no sent message
|
|
60
|
+
dict edited in place. That last point is an assumption of this manager's
|
|
61
|
+
history contract (object identity cannot see an in-place edit). The
|
|
62
|
+
system message is exempt: the frozen copy is always sent.
|
|
63
|
+
|
|
64
|
+
A history that is not an identity extension of the previous call's input
|
|
65
|
+
(``/clear``, a pending compact, a session load, a removed skill message)
|
|
66
|
+
is a new generation: the summary is dropped and compaction waits for a
|
|
67
|
+
fresh ``prompt_tokens`` measurement, since the stale one described the old
|
|
68
|
+
history. Every compaction or reset is an intentional prefix change.
|
|
49
69
|
"""
|
|
50
70
|
|
|
51
71
|
def __init__(
|
|
@@ -85,6 +105,12 @@ class OllamaContextManager:
|
|
|
85
105
|
self._compaction_count: int = 0
|
|
86
106
|
self._messages_compacted: int = 0
|
|
87
107
|
|
|
108
|
+
# Persistent compaction state for the current history generation.
|
|
109
|
+
self._summary_msg: dict | None = None # sent verbatim on every call
|
|
110
|
+
self._folded: list[dict] = [] # caller objects the summary replaces
|
|
111
|
+
self._last_seen: list[dict] = [] # previous call's untrimmed input
|
|
112
|
+
self._awaiting_measurement: bool = False # block until fresh prompt_tokens
|
|
113
|
+
|
|
88
114
|
self._cache_monitor: CacheMonitor = CacheMonitor()
|
|
89
115
|
|
|
90
116
|
# Event-correlation ids. The factory that builds this manager knows
|
|
@@ -211,21 +237,43 @@ class OllamaContextManager:
|
|
|
211
237
|
history = list(messages)
|
|
212
238
|
|
|
213
239
|
if not history:
|
|
240
|
+
self._reset_generation()
|
|
241
|
+
self._last_seen = []
|
|
214
242
|
return [system_msg] if system_msg else []
|
|
215
243
|
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
if self.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
244
|
+
# A history that does not extend the previous call's input is a new
|
|
245
|
+
# generation: the old summary and the old measurement do not apply.
|
|
246
|
+
if not self._extends_last_seen(history):
|
|
247
|
+
self._reset_generation()
|
|
248
|
+
self._last_seen = list(history)
|
|
249
|
+
|
|
250
|
+
# Re-apply the current summary in place of the messages it folded.
|
|
251
|
+
surviving = history[len(self._folded):] if self._summary_msg else history
|
|
252
|
+
if not surviving:
|
|
253
|
+
self._reset_generation()
|
|
254
|
+
surviving = history
|
|
255
|
+
|
|
256
|
+
# Compact at most once per fresh measurement of this generation.
|
|
257
|
+
if not self._awaiting_measurement and self.should_compact():
|
|
258
|
+
surviving, newly_folded = self._compact(surviving)
|
|
259
|
+
if newly_folded:
|
|
260
|
+
previous = self._summary_msg["content"] if self._summary_msg else None
|
|
261
|
+
self._summary_msg = {
|
|
262
|
+
"role": "user",
|
|
263
|
+
"content": self._merge_summary(
|
|
264
|
+
previous, self._summary_lines(newly_folded)
|
|
265
|
+
),
|
|
266
|
+
}
|
|
267
|
+
self._folded = self._folded + newly_folded
|
|
268
|
+
self._awaiting_measurement = True
|
|
269
|
+
self._emit_trim(len(history), len(surviving), "compaction")
|
|
222
270
|
|
|
223
271
|
# Apply max_history cap — trim from the head on unit boundaries so
|
|
224
272
|
# assistant+tool_calls stays atomic with its tool_result responses.
|
|
225
273
|
# See opus47.md finding #3.
|
|
226
|
-
if max_history > 0 and len(
|
|
227
|
-
before_trim = len(
|
|
228
|
-
units = self._group_into_units(
|
|
274
|
+
if max_history > 0 and len(surviving) > max_history:
|
|
275
|
+
before_trim = len(surviving)
|
|
276
|
+
units = self._group_into_units(surviving)
|
|
229
277
|
kept: list[list[dict]] = []
|
|
230
278
|
msg_count = 0
|
|
231
279
|
for unit in reversed(units):
|
|
@@ -233,16 +281,16 @@ class OllamaContextManager:
|
|
|
233
281
|
break
|
|
234
282
|
kept.insert(0, unit)
|
|
235
283
|
msg_count += len(unit)
|
|
236
|
-
|
|
237
|
-
self._emit_trim(before_trim, len(
|
|
284
|
+
surviving = [m for unit in kept for m in unit]
|
|
285
|
+
self._emit_trim(before_trim, len(surviving), "max_history")
|
|
238
286
|
|
|
239
287
|
# Build the output — stable prefix order
|
|
240
288
|
result: list[dict] = []
|
|
241
289
|
if system_msg:
|
|
242
290
|
result.append(system_msg)
|
|
243
|
-
if
|
|
244
|
-
result.append(
|
|
245
|
-
result.extend(
|
|
291
|
+
if self._summary_msg is not None:
|
|
292
|
+
result.append(self._summary_msg)
|
|
293
|
+
result.extend(surviving)
|
|
246
294
|
self._estimated_prompt_tokens = self._counter.count_messages(result)
|
|
247
295
|
return result
|
|
248
296
|
|
|
@@ -256,6 +304,9 @@ class OllamaContextManager:
|
|
|
256
304
|
self._real_completion_tokens = usage.get(
|
|
257
305
|
"completion_tokens", self._real_completion_tokens
|
|
258
306
|
)
|
|
307
|
+
if "prompt_tokens" in usage:
|
|
308
|
+
# A fresh measurement of the history that was just sent.
|
|
309
|
+
self._awaiting_measurement = False
|
|
259
310
|
self._last_prompt_eval_ns = usage.get("prompt_eval_ns", self._last_prompt_eval_ns)
|
|
260
311
|
self._last_eval_ns = usage.get("eval_ns", self._last_eval_ns)
|
|
261
312
|
|
|
@@ -398,8 +449,21 @@ class OllamaContextManager:
|
|
|
398
449
|
|
|
399
450
|
return units
|
|
400
451
|
|
|
401
|
-
def
|
|
402
|
-
"""
|
|
452
|
+
def _extends_last_seen(self, history: list[dict]) -> bool:
|
|
453
|
+
"""True if the previous call's input is an identity prefix of ``history``."""
|
|
454
|
+
last = self._last_seen
|
|
455
|
+
if len(history) < len(last):
|
|
456
|
+
return False
|
|
457
|
+
return all(history[i] is last[i] for i in range(len(last)))
|
|
458
|
+
|
|
459
|
+
def _reset_generation(self) -> None:
|
|
460
|
+
"""Forget the summary; wait for a measurement of the new history."""
|
|
461
|
+
self._summary_msg = None
|
|
462
|
+
self._folded = []
|
|
463
|
+
self._awaiting_measurement = True
|
|
464
|
+
|
|
465
|
+
def _summary_lines(self, messages: list[dict]) -> list[str]:
|
|
466
|
+
"""One heuristic summary line per message, without calling an LLM."""
|
|
403
467
|
parts: list[str] = []
|
|
404
468
|
for msg in messages:
|
|
405
469
|
role = msg["role"]
|
|
@@ -418,29 +482,57 @@ class OllamaContextManager:
|
|
|
418
482
|
elif role == "tool":
|
|
419
483
|
name = msg.get("name", "unknown")
|
|
420
484
|
parts.append(f"Tool {name} returned result")
|
|
421
|
-
return
|
|
485
|
+
return parts
|
|
486
|
+
|
|
487
|
+
def _heuristic_summarize(self, messages: list[dict]) -> str:
|
|
488
|
+
"""Produce a compact text summary of messages without calling an LLM."""
|
|
489
|
+
return SUMMARY_HEADER + "\n" + "\n".join(self._summary_lines(messages))
|
|
490
|
+
|
|
491
|
+
def _merge_summary(self, previous: str | None, new_lines: list[str]) -> str:
|
|
492
|
+
"""Fold ``new_lines`` into the previous summary under one header.
|
|
493
|
+
|
|
494
|
+
The body keeps the newest lines whose total length, newlines
|
|
495
|
+
included, is at most ``_SUMMARY_MAX_CHARS``. If the newest line alone
|
|
496
|
+
is longer, it is kept as its first ``_SUMMARY_MAX_CHARS - 1``
|
|
497
|
+
characters plus an ellipsis.
|
|
498
|
+
"""
|
|
499
|
+
lines: list[str] = []
|
|
500
|
+
if previous:
|
|
501
|
+
body = previous.split("\n", 1)[1] if "\n" in previous else ""
|
|
502
|
+
lines = body.split("\n") if body else []
|
|
503
|
+
lines += new_lines
|
|
504
|
+
|
|
505
|
+
kept: list[str] = []
|
|
506
|
+
total = 0
|
|
507
|
+
for line in reversed(lines):
|
|
508
|
+
cost = len(line) + (1 if kept else 0)
|
|
509
|
+
if total + cost > _SUMMARY_MAX_CHARS:
|
|
510
|
+
break
|
|
511
|
+
kept.append(line)
|
|
512
|
+
total += cost
|
|
513
|
+
if not kept and lines:
|
|
514
|
+
kept = [lines[-1][: _SUMMARY_MAX_CHARS - 1] + "…"]
|
|
515
|
+
return SUMMARY_HEADER + "\n" + "\n".join(reversed(kept))
|
|
422
516
|
|
|
423
517
|
def _compact(
|
|
424
518
|
self, history: list[dict]
|
|
425
|
-
) -> tuple[list[dict], dict]:
|
|
426
|
-
"""
|
|
519
|
+
) -> tuple[list[dict], list[dict]]:
|
|
520
|
+
"""Fold the oldest half of message units, never the newest unit.
|
|
427
521
|
|
|
428
|
-
Returns
|
|
522
|
+
Returns ``(surviving, folded)``. With fewer than two units nothing is
|
|
523
|
+
folded: the newest unit is the request the model must see.
|
|
429
524
|
"""
|
|
430
525
|
units = self._group_into_units(history)
|
|
526
|
+
if len(units) < 2:
|
|
527
|
+
return history, []
|
|
431
528
|
|
|
432
|
-
# Compact the oldest half of units (at least 1)
|
|
433
|
-
compact_count = max(1, len(units) // 2)
|
|
529
|
+
# Compact the oldest half of units (at least 1, never the newest)
|
|
530
|
+
compact_count = min(max(1, len(units) // 2), len(units) - 1)
|
|
434
531
|
units_to_compact = units[:compact_count]
|
|
435
532
|
units_to_keep = units[compact_count:]
|
|
436
533
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
compacted_msg_count = len(msgs_to_compact)
|
|
440
|
-
|
|
441
|
-
summary_text = self._heuristic_summarize(msgs_to_compact)
|
|
442
|
-
summary_msg = {"role": "user", "content": summary_text}
|
|
443
|
-
|
|
534
|
+
folded = [msg for unit in units_to_compact for msg in unit]
|
|
535
|
+
compacted_msg_count = len(folded)
|
|
444
536
|
surviving = [msg for unit in units_to_keep for msg in unit]
|
|
445
537
|
|
|
446
538
|
self._compaction_count += 1
|
|
@@ -453,4 +545,4 @@ class OllamaContextManager:
|
|
|
453
545
|
self._compaction_count,
|
|
454
546
|
)
|
|
455
547
|
|
|
456
|
-
return surviving,
|
|
548
|
+
return surviving, folded
|