agentic-cli 0.5.2__tar.gz → 0.5.3__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.
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/CHANGELOG.md +33 -0
- agentic_cli-0.5.3/CLAUDE.md +183 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/PKG-INFO +10 -4
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/README.md +7 -3
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/pyproject.toml +5 -1
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/__init__.py +1 -1
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/app.py +26 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/message_processor.py +83 -17
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/config.py +6 -1
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/file_utils.py +26 -4
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/vector_store.py +30 -7
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/persistence/session.py +15 -8
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/__init__.py +0 -2
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_source.py +18 -13
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/executor.py +44 -10
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/grep_tool.py +5 -1
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/memory_tools.py +52 -41
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/manager.py +59 -47
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/base_manager.py +14 -8
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/graph_builder.py +6 -6
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/manager.py +27 -3
- agentic_cli-0.5.3/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +145 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/service_registry.py +0 -1
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/settings.py +0 -6
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/conftest.py +8 -0
- agentic_cli-0.5.3/tests/event_replay.py +181 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/conftest.py +40 -0
- agentic_cli-0.5.3/tests/integration/test_research_scenarios.py +206 -0
- agentic_cli-0.5.3/tests/test_arxiv_download_security.py +64 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_arxiv_tools.py +52 -44
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_context_window.py +5 -0
- agentic_cli-0.5.3/tests/test_executor_security.py +73 -0
- agentic_cli-0.5.3/tests/test_file_utils_atomic.py +79 -0
- agentic_cli-0.5.3/tests/test_grep_tool_security.py +67 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_knowledge_base.py +3 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_langgraph.py +46 -5
- agentic_cli-0.5.3/tests/test_memory_thread_safety.py +41 -0
- agentic_cli-0.5.3/tests/test_message_processor_cancel.py +92 -0
- agentic_cli-0.5.3/tests/test_message_processor_render.py +196 -0
- agentic_cli-0.5.3/tests/test_session_index_recovery.py +62 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_tools.py +10 -8
- agentic_cli-0.5.3/tests/test_update_setting.py +43 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_vector_store.py +40 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_langgraph_state_tools.py +3 -0
- agentic_cli-0.5.3/tests/workflow/test_adk_session_resume.py +60 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/test_backend_isolation.py +4 -0
- agentic_cli-0.5.3/tests/workflow/test_memory_wiring.py +134 -0
- agentic_cli-0.5.3/tests/workflow/test_thinking_config.py +82 -0
- agentic_cli-0.5.2/src/agentic_cli/tools/reflection_tools.py +0 -145
- agentic_cli-0.5.2/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -152
- agentic_cli-0.5.2/tests/test_reflection.py +0 -74
- agentic_cli-0.5.2/tests/test_update_setting.py +0 -21
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/.gitignore +0 -0
- /agentic_cli-0.5.2/CLAUDE.md → /agentic_cli-0.5.3/AGENTS.md +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/LICENSE +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/environment.yml +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/arxiv_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/fileops_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/hello_agent.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/hello_langgraph.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/memory_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/__main__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/agents.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/app.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/commands.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/research_demo/settings.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/shell_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/webfetch_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/examples/websearch_demo.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/requirements-dev.txt +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/requirements.txt +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/builtin_commands.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/commands.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_command.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_introspection.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/usage_tracker.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/cli/workflow_controller.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/constants.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_bm25_backends.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mock_bm25.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mocks.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/bm25_index.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/concepts.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/manager.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/models.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sidecar.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sources.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/logging.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/persistence/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/settings_mixins.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/settings_persistence.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/planning.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/tasks.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/state_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/execution_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/factories.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_read.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_write.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/glob_tool.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/interaction_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/knowledge_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/state_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/pdf_utils.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/registry.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/base.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/jupyter_local.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/manager.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/models.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/search.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/audit.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/classifier.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/config.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/executor.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/models.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/base.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/bubblewrap.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/detect.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/policy.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt_profile.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/sandbox.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/converter.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/fetcher.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/robots.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/validator.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch_tool.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/event_processor.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/permission_plugin.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/plugins.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/task_progress_plugin.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/config.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/events.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/factory.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/permission_wrap.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/state.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/models.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/capabilities.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/engine.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/matchers.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/prompt.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/rules.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/store.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/retry.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/src/agentic_cli/workflow/tool_summaries.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/helpers.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_adk_integration.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_kb_research_flow.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_langgraph_integration.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_live.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_permission_adk.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/integration/test_permission_langgraph.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_capabilities.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_engine.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_matchers.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_prompt.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_rules.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/permissions/test_store.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_adk_event_processor.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_adk_plugins.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_bm25_index.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_commands.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_concept_store.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_concurrent_stores.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_config.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_context_trimming.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_dual_thinking_boxes.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_embeddings.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_grep_tool_cache.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_input_callback.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_concepts.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_helpers.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_index_md.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_ingest_log.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_sidecar.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_kb_tool_renames.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_knowledge_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_memory.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_model_registry.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_persistence.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_planning.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_session_save_resume.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_settings_persistence.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_task_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_token_caching.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_tool_summaries.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_usage_tracker.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_webfetch.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_workflow.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/test_workflow_controller.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_adk_state_tools.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_bubblewrap.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_core_planning.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_core_tasks.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_factories.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_os_sandbox.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_registry.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_sandbox.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_seatbelt_profile.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/tools/test_shell_security.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/__init__.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/test_base_manager_permissions.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/test_settings_permissions.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/test_task_progress_plugin.py +0 -0
- {agentic_cli-0.5.2 → agentic_cli-0.5.3}/tests/workflow/test_tool_assembly.py +0 -0
|
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.5.3] - 2026-06-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Session fact extraction now runs on exit** (`auto_extract_session_facts`): `BaseWorkflowManager.on_session_end()` is invoked from the CLI shutdown path (`BaseCLIApp._extract_session_facts_on_exit`), extracting key facts/preferences from the conversation into memory. Previously the hook had no call site. `on_session_end()` now also sources the session messages itself (from the live backend session) when called with no arguments; it remains a safe no-op without a memory store, and never blocks shutdown.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- **Tool Reflection Store removed** (`save_reflection` tool, `ReflectionStore`, `REFLECTION_STORE`, and the `enable_tool_reflections` setting). Introduced in v0.5.0 as a "cross-session learning primitive," it shipped only as a standalone store + tool — the planned injection and session-end wiring were never completed, so it was inert from v0.5.0 through v0.5.2 (the v0.5.0 changelog's "wired via session-end hook" was inaccurate). The design also conflated mechanical failure-capture with LLM heuristic synthesis in a single opportunistic tool the model had to remember to call. Removed rather than finished; tool-failure heuristics, if wanted later, are better served by automatic capture + session-end synthesis (mirroring `auto_extract_session_facts`) or by tagged entries in the existing memory store.
|
|
17
|
+
|
|
18
|
+
### Security
|
|
19
|
+
- **`grep` ripgrep argument injection fixed** (`tools/grep_tool.py`): the LLM-supplied `pattern` was passed to ripgrep as a bare positional, so a value like `--pre=<cmd>` was parsed as a flag and could run an arbitrary program per searched file — remote code execution holding only `filesystem.read`. The pattern is now passed via `-e` and option parsing is terminated with `--` before the path, so neither can be interpreted as a flag.
|
|
20
|
+
- **`execute_python` library escape closed** (`tools/executor.py`): the AST/name allow-list cannot neutralize `numpy`/`pandas`/`scipy`/`sklearn`/`matplotlib`, which re-expose file, network, and pickle I/O through public APIs (e.g. `numpy.DataSource().open()`, `pandas.read_pickle(url)`) — an escape from the in-process restrictions (arbitrary file read, pickle-based code execution) when no OS sandbox is active. These `SANDBOXED_MODULES` are now importable **only when `os_sandbox_enabled=True`**; without it, only the pure-computation `CORE_MODULES` are available. The class docstring now states that the AST validation is defense-in-depth, not a security boundary.
|
|
21
|
+
- **arXiv PDF download SSRF fixed** (`tools/arxiv_source.py`): `download_pdf` issued a raw `httpx.get(follow_redirects=True)` with no URL validation or size cap on a `pdf_url` taken from a remote Atom feed — bypassing the hardened `ContentFetcher`. It now routes through `get_or_create_fetcher().fetch()`, inheriting SSRF validation (private-IP/redirect blocking), DNS-rebinding checks, and the byte cap. The arXiv API endpoints were also switched from plaintext HTTP to HTTPS to remove the on-path feed-rewrite vector.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **`execute_python` no longer exposes `numpy`/`pandas` (etc.) by default.** Enabling them requires `os_sandbox_enabled=True` (with `sandbox-exec` on macOS or `bwrap` on Linux). This is a deliberate, user-visible reduction of the default capability surface.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **ADK session resume was a silent no-op** (`workflow/adk/manager.py`): `_inject_session_messages` appended events to the copy returned by `create_session`, so the stored session stayed empty and "Session resumed" restored nothing on the ADK backend. Now uses `append_event` with real `google.adk.events.Event` objects so events land in the stored session.
|
|
28
|
+
- **Corrupt sessions index no longer hides every session** (`persistence/session.py`): a corrupt or non-dict `_sessions_index.json` read as empty, so the next save reset it to a single entry and all other sessions vanished from `list_sessions()`. The index now rebuilds from the session files on any parse/shape error (and a non-dict payload no longer raises `AttributeError`).
|
|
29
|
+
- **`MemoryStore` is now thread-safe** (`tools/memory_tools.py`): added a lock around `store`/`update`/`delete`/`search` and the full-file `_save` (note `search` is a writer — it bumps access counters). Prevents "dictionary changed size during iteration" and lost writes when LangGraph's `ToolNode` runs tools concurrently in executor threads.
|
|
30
|
+
- **Atomic writes are now durable** (`file_utils.py::_atomic_write`): fsync before the rename (so a crash can't persist the rename ahead of the data and destroy the previously-good file), a unique temp filename (two concurrent writers no longer truncate a shared `.tmp`), and explicit UTF-8 (no locale-dependent `UnicodeEncodeError`). Applies to all persistence consumers (settings, sessions, memory).
|
|
31
|
+
- **LangGraph graph construction was completely broken** (`workflow/langgraph/graph_builder.py`): `build()` called the local `_get_tools(config)` helper in the agent-node loop before it was defined further down the function, so Python treated `_get_tools` as an unbound local and every graph build raised `UnboundLocalError`. The helper (and `_overrides`) are now defined before first use. This went unnoticed because the dev env had no `langgraph` installed, so the build tests were skipped.
|
|
32
|
+
- **LangGraph sqlite/postgres checkpointers were nonfunctional** (`workflow/langgraph/persistence/checkpointers.py`): `create_checkpointer` returned `SqliteSaver/PostgresSaver.from_conn_string(...)` directly, but in current `langgraph-checkpoint-*` that is a context manager (not a saver), and the sync savers can't serve the async `astream_events` path. `create_checkpointer` is now async and uses the async savers (`AsyncSqliteSaver`/`AsyncPostgresSaver`): it enters the connection context, calls `setup()`, and returns `(saver, context_manager)`; `LangGraphWorkflowManager` closes the context manager in `cleanup()`. The `langgraph` extra now also pulls `langgraph-checkpoint-sqlite`/`-postgres`.
|
|
33
|
+
- **LangGraph node `retry` deprecation** (`workflow/langgraph/graph_builder.py`): `graph.add_node(..., retry=...)` updated to `retry_policy=...` (`retry` is deprecated since LangGraph 0.5, removed in 2.0).
|
|
34
|
+
- Test suite is collectable in the default dev env again: `pytest.importorskip` guards added to `test_context_window.py`, `test_langgraph_state_tools.py`, and `test_backend_isolation.py`, which imported `langgraph`/`langchain_core`/`google.genai` unconditionally. The hybrid-search test now guards on `bm25s`, and `conftest.py` sets `KMP_DUPLICATE_LIB_OK=TRUE` so the real-FAISS and torch-backed tests (the `kb` extra) don't abort the interpreter on the duplicate OpenMP runtime.
|
|
35
|
+
- **FAISS index could be left corrupt by a crash** (`knowledge_base/vector_store.py`): `save()` wrote the index in place via `faiss.write_index(self._index, path)`, so a crash mid-write left a truncated `index.faiss` — and `faiss.read_index` raises on a torn file, bricking the whole knowledge base until it was deleted by hand. The index is now written to a temp file in the same directory and `os.replace`d into position (atomic), with the temp cleaned up on failure; the misleading "mappings first is crash-safe" comment was corrected to describe what is actually guaranteed.
|
|
36
|
+
- **Settings UI could persist invalid values** (`config.py::update_setting`): the non-special path used `object.__setattr__`, bypassing Pydantic entirely, so a wrong-typed or out-of-range value from the settings dialog was written straight to the instance and saved to `settings.json`, poisoning later runs. It now goes through `__pydantic_validator__.validate_assignment`, which type- and constraint-checks the value (and rejects unknown keys) before it lands — raising `ValidationError` (a `ValueError`, already handled by `apply_settings`).
|
|
37
|
+
- **Ctrl+C now actually cancels an in-flight workflow** (`cli/message_processor.py`): the status bar advertised "Ctrl+C: cancel", but the binding only finished the thinking boxes — the workflow kept streaming and burning tokens. The event stream is now consumed in a cancellable task that a watcher aborts when the boxes are finished out from under it (the HITL dialog window is exempted), so Ctrl+C stops the run and prints "Cancelled."
|
|
38
|
+
- **Quickstart tools were silently denied** (`README.md`): the README's `greet` example passed an unregistered raw callable and called registration "optional", but the permission engine (on by default) fails closed and denies any tool without a capability declaration on both backends. The quickstart now registers `greet` (`capabilities=EXEMPT`) and the docs state that registration is required for a tool to run.
|
|
39
|
+
- **Gemini 2.5 thinking config caused HTTP 400 on every call** (`workflow/adk/manager.py`): `_get_planner` always built `ThinkingConfig(thinking_level=...)`, but `thinking_level` is a Gemini-3-only field — Gemini 2.5 models (including the default `gemini-2.5-flash`) reject it with `400 "Thinking level is not supported for this model"`. With the default `thinking_effort=medium`, every Gemini call failed (the ADK demo was unusable out of the box). The manager now uses the numeric `thinking_budget` for Gemini 2.5 models and reserves `thinking_level` for Gemini 3. Surfaced by the new live scenario tests.
|
|
40
|
+
|
|
8
41
|
## [0.5.2] - 2026-05-01
|
|
9
42
|
|
|
10
43
|
### Security
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Agentic CLI - Shared Framework for Agentic Applications
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
Agentic CLI is a shared library providing the core infrastructure for building domain-specific CLI applications powered by LLM agents.
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
- **Language**: Python 3.12+
|
|
10
|
+
- **CLI UI**: `thinking-prompt` - enhanced CLI with thinking boxes and markdown
|
|
11
|
+
- **Workflow**: Google ADK + LangGraph - dual orchestration backends (selectable via settings)
|
|
12
|
+
- **Config**: `pydantic-settings` - type-safe configuration
|
|
13
|
+
- **Logging**: `structlog` - structured logging
|
|
14
|
+
|
|
15
|
+
## Project Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
agentic-cli/
|
|
19
|
+
├── src/agentic_cli/
|
|
20
|
+
│ ├── __init__.py # Package exports, lazy imports
|
|
21
|
+
│ ├── config.py # BaseSettings (pydantic-settings)
|
|
22
|
+
│ ├── settings_mixins.py # Composable settings field groups
|
|
23
|
+
│ ├── settings_persistence.py # save_settings() (excludes SECRET_FIELDS)
|
|
24
|
+
│ ├── constants.py # Shared constants, truncate()
|
|
25
|
+
│ ├── file_utils.py # atomic_write_json / atomic_write_text
|
|
26
|
+
│ ├── logging.py
|
|
27
|
+
│ ├── cli/
|
|
28
|
+
│ │ ├── app.py # BaseCLIApp
|
|
29
|
+
│ │ ├── commands.py # Command, CommandRegistry
|
|
30
|
+
│ │ ├── builtin_commands.py
|
|
31
|
+
│ │ ├── workflow_controller.py # WorkflowController (lazy/background init, orchestrator swap)
|
|
32
|
+
│ │ ├── message_processor.py # WorkflowEvent → ThinkingPromptSession rendering
|
|
33
|
+
│ │ ├── settings_command.py # /settings command
|
|
34
|
+
│ │ ├── settings_introspection.py # Pydantic field → UI item introspection
|
|
35
|
+
│ │ └── usage_tracker.py # Token usage / status bar
|
|
36
|
+
│ ├── workflow/
|
|
37
|
+
│ │ ├── base_manager.py # BaseWorkflowManager (abstract; service detection, tool assembly)
|
|
38
|
+
│ │ ├── factory.py # create_workflow_manager_from_settings (ADK vs LangGraph routing)
|
|
39
|
+
│ │ ├── service_registry.py # get_service/require_service + ContextVar registry
|
|
40
|
+
│ │ ├── events.py # WorkflowEvent, EventType
|
|
41
|
+
│ │ ├── config.py # AgentConfig
|
|
42
|
+
│ │ ├── models.py
|
|
43
|
+
│ │ ├── settings.py # Workflow/tool settings schema
|
|
44
|
+
│ │ ├── retry.py # Rate-limit retry helpers
|
|
45
|
+
│ │ ├── tool_summaries.py
|
|
46
|
+
│ │ ├── permissions/ # Framework-independent capability engine
|
|
47
|
+
│ │ │ ├── engine.py # PermissionEngine (deny-wins, default-ASK)
|
|
48
|
+
│ │ │ ├── capabilities.py # Capability, EXEMPT
|
|
49
|
+
│ │ │ ├── matchers.py # PathMatcher, URLMatcher, ShellMatcher, StringGlobMatcher
|
|
50
|
+
│ │ │ └── rules.py, store.py, prompt.py
|
|
51
|
+
│ │ ├── adk/ # ADK orchestrator
|
|
52
|
+
│ │ │ ├── manager.py # GoogleADKWorkflowManager
|
|
53
|
+
│ │ │ ├── event_processor.py # ADKEventProcessor
|
|
54
|
+
│ │ │ ├── permission_plugin.py # PermissionPlugin (gates tool calls)
|
|
55
|
+
│ │ │ ├── task_progress_plugin.py # Emits TASK_PROGRESS events
|
|
56
|
+
│ │ │ └── plugins.py # LLM traffic logging (raw_llm_logging)
|
|
57
|
+
│ │ └── langgraph/ # LangGraph orchestrator
|
|
58
|
+
│ │ ├── manager.py # LangGraphWorkflowManager
|
|
59
|
+
│ │ ├── graph_builder.py # LangGraphBuilder (graph + LLM factory)
|
|
60
|
+
│ │ ├── state.py
|
|
61
|
+
│ │ ├── permission_wrap.py # wrap_tool_for_permission
|
|
62
|
+
│ │ └── persistence/ # Checkpointers, stores
|
|
63
|
+
│ ├── tools/
|
|
64
|
+
│ │ ├── registry.py # ToolRegistry, @register_tool, ToolCategory
|
|
65
|
+
│ │ ├── factories.py # Service-bound tool builders (per-manager flavors)
|
|
66
|
+
│ │ ├── executor.py # SafePythonExecutor (CORE_MODULES; SANDBOXED_MODULES gated on OS sandbox)
|
|
67
|
+
│ │ ├── execution_tools.py # execute_python
|
|
68
|
+
│ │ ├── knowledge_tools.py # kb_search, kb_ingest_{text,file,url}, kb_list, kb_read, kb_write_concept, kb_search_concepts
|
|
69
|
+
│ │ ├── arxiv_tools.py # search_arxiv, fetch_arxiv_paper, ingest_arxiv_paper
|
|
70
|
+
│ │ ├── arxiv_source.py # ArxivSearchSource (feed fetch, download_pdf)
|
|
71
|
+
│ │ ├── pdf_utils.py # extract_pdf_text
|
|
72
|
+
│ │ ├── interaction_tools.py # ask_clarification
|
|
73
|
+
│ │ ├── file_read.py # read_file, diff_compare
|
|
74
|
+
│ │ ├── file_write.py # write_file, edit_file
|
|
75
|
+
│ │ ├── glob_tool.py # glob
|
|
76
|
+
│ │ ├── grep_tool.py # grep
|
|
77
|
+
│ │ ├── search.py # web_search (Tavily/Brave backends)
|
|
78
|
+
│ │ ├── webfetch_tool.py # web_fetch + get_or_create_fetcher (orchestrator)
|
|
79
|
+
│ │ ├── memory_tools.py # save_memory, search_memory, update_memory, delete_memory + MemoryStore
|
|
80
|
+
│ │ ├── _core/ # Backend-neutral tool logic
|
|
81
|
+
│ │ │ ├── planning.py # save_plan/get_plan core (+ checkbox parsing)
|
|
82
|
+
│ │ │ └── tasks.py # save_tasks/get_tasks core (+ progress parsing)
|
|
83
|
+
│ │ ├── adk/state_tools.py # ADK-native plan/task tools (ToolContext.state)
|
|
84
|
+
│ │ ├── langgraph/state_tools.py # LangGraph-native plan/task tools (Command/InjectedState)
|
|
85
|
+
│ │ ├── sandbox/ # Stateful code-execution sandbox (sandbox_execute)
|
|
86
|
+
│ │ ├── shell/ # 8-layer shell security (+ os_sandbox/)
|
|
87
|
+
│ │ └── webfetch/ # Fetcher, converter, validator, robots, summarizer
|
|
88
|
+
│ ├── knowledge_base/
|
|
89
|
+
│ │ ├── models.py # Document, SearchResult
|
|
90
|
+
│ │ ├── embeddings.py # EmbeddingService
|
|
91
|
+
│ │ ├── vector_store.py # VectorStore (FAISS)
|
|
92
|
+
│ │ ├── bm25_index.py # BM25 index (+ _bm25_backends.py: bm25s / rank_bm25)
|
|
93
|
+
│ │ ├── concepts.py # ConceptStore (concept pages)
|
|
94
|
+
│ │ ├── sidecar.py # Markdown sidecar rendering
|
|
95
|
+
│ │ ├── sources.py
|
|
96
|
+
│ │ ├── _mocks.py # MockEmbeddingService, MockVectorStore (+ _mock_bm25.py)
|
|
97
|
+
│ │ └── manager.py # KnowledgeBaseManager
|
|
98
|
+
│ └── persistence/
|
|
99
|
+
│ └── session.py # SessionPersistence
|
|
100
|
+
├── tests/
|
|
101
|
+
│ ├── conftest.py # MockContext, shared fixtures
|
|
102
|
+
│ ├── test_*.py # Unit tests
|
|
103
|
+
│ ├── tools/ # Tool-specific tests
|
|
104
|
+
│ ├── workflow/ # Backend-isolation / workflow tests
|
|
105
|
+
│ └── integration/ # ADK & LangGraph pipeline tests
|
|
106
|
+
└── examples/ # Demo scripts
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Running Commands
|
|
110
|
+
|
|
111
|
+
**IMPORTANT**: Always use `conda run -n agenticcli` prefix for running commands:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Create the environment (first time only)
|
|
115
|
+
conda env create -f environment.yml
|
|
116
|
+
|
|
117
|
+
# Install package
|
|
118
|
+
conda run -n agenticcli pip install -e .
|
|
119
|
+
|
|
120
|
+
# Run tests
|
|
121
|
+
conda run -n agenticcli python -m pytest tests/ -v
|
|
122
|
+
|
|
123
|
+
# Run Python
|
|
124
|
+
conda run -n agenticcli python -c "from agentic_cli import BaseCLIApp; print(BaseCLIApp)"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Branching Strategy
|
|
128
|
+
|
|
129
|
+
- **main**: Stable branch, matches latest release. Only updated via merges from `develop` when releasing.
|
|
130
|
+
- **develop**: Integration branch for ongoing work. Small fixes can be committed directly here.
|
|
131
|
+
- **feature/\***: Feature branches for larger changes. Branch from `develop`, merge back to `develop`.
|
|
132
|
+
- **fix/\***: Fix branches for fixing issues. Branch from `develop`, merge back to `develop`.
|
|
133
|
+
- **refactor/\***: For larger refactoring changes. Branch from `develop`, merge back to `develop`.
|
|
134
|
+
|
|
135
|
+
Workflow:
|
|
136
|
+
1. For small fixes: commit directly to `develop`
|
|
137
|
+
2. For features: create `feature/<name>` (or `fix/<name>` or `refactor/<name>`) from `develop`, work there, merge back to `develop`
|
|
138
|
+
3. When ready to release: merge `develop` → `main` and tag the release
|
|
139
|
+
|
|
140
|
+
### What NOT to commit
|
|
141
|
+
- `docs/` is gitignored on purpose (see `.gitignore`). It is a scratchpad for review notes, plans, and internal analysis. **Never `git add docs/…` or suggest committing anything under `docs/`.** If a document belongs in the repo, it lives elsewhere (README, CHANGELOG, top-level `*.md`).
|
|
142
|
+
|
|
143
|
+
## Development Principles
|
|
144
|
+
|
|
145
|
+
### Code Style
|
|
146
|
+
- Follow PEP 8 style guidelines
|
|
147
|
+
- Use type hints throughout
|
|
148
|
+
- Prefer descriptive variable names
|
|
149
|
+
|
|
150
|
+
### Key Design Decisions
|
|
151
|
+
- **Abstract base classes**: BaseCLIApp and BaseWorkflowManager for domain extension
|
|
152
|
+
- **Dual orchestrator**: ADK and LangGraph backends, selectable via settings
|
|
153
|
+
- **Lazy initialization**: Defer heavy imports until needed
|
|
154
|
+
- **Event-based streaming**: Real-time updates via AsyncGenerator
|
|
155
|
+
- **UI-agnostic workflow**: WorkflowEvent objects can be consumed by any UI
|
|
156
|
+
|
|
157
|
+
### Key Design Patterns
|
|
158
|
+
- **Tool error handling**: All tools return `{"success": bool, ...}` dicts. Never raise `ToolError`.
|
|
159
|
+
- **Tool registration**: Use `@register_tool(category=..., capabilities=..., description=...)` decorator. `capabilities=` is required — pass `EXEMPT` for tools that need no permission check or a list of `Capability(name, target_arg=...)` tuples the engine matches against rules. Tools are auto-discovered via the global `ToolRegistry`.
|
|
160
|
+
- **Permissions**: `workflow/permissions/` holds a framework-independent engine that evaluates declared capabilities against rules from four sources (builtin, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). ADK + LangGraph gate tool calls via `workflow/adk/permission_plugin.py::PermissionPlugin` and `workflow/langgraph/permission_wrap.py::wrap_tool_for_permission`.
|
|
161
|
+
- **Service registry**: Tools access services and shared state via `get_service(key)` from `workflow.service_registry`. A single ContextVar holds a `dict[str, Any]` set by the workflow manager during processing. Complex services (KBManager, SandboxManager, MemoryStore) are lazily created; simple state (plan string, task list) lives directly in the registry dict.
|
|
162
|
+
- **Manager detection**: `BaseWorkflowManager._detect_required_managers()` scans each agent's tool names against the `_TOOL_SERVICE_MAP` (name → service key, in `base_manager.py`); `_ensure_managers_initialized()` then lazily instantiates only the services actually needed (KBManager, SandboxManager, MemoryStore, …). Adding a new service-backed tool means adding its name → service entry to `_TOOL_SERVICE_MAP`. (There is no `@requires` decorator.)
|
|
163
|
+
- **Atomic writes**: Use `atomic_write_json`/`atomic_write_text` from `file_utils.py` for file persistence.
|
|
164
|
+
|
|
165
|
+
### Console Output
|
|
166
|
+
All console output must go through `ThinkingPromptSession` methods. Never use `rich.Console` or `print()` directly.
|
|
167
|
+
|
|
168
|
+
Available session methods:
|
|
169
|
+
- `session.add_response(text, markdown=True)` - Display text/markdown response
|
|
170
|
+
- `session.add_rich(renderable)` - Display Rich renderables (Panel, Table, etc.)
|
|
171
|
+
- `session.add_message(role, content)` - Add message to history
|
|
172
|
+
- `session.add_error(content)` - Display error message
|
|
173
|
+
- `session.add_warning(content)` - Display warning message
|
|
174
|
+
- `session.add_success(content)` - Display success message
|
|
175
|
+
- `session.clear()` - Clear the terminal screen
|
|
176
|
+
|
|
177
|
+
## Testing
|
|
178
|
+
|
|
179
|
+
- **Framework**: pytest with `asyncio_mode = "auto"`
|
|
180
|
+
- **MockContext**: From `tests/conftest.py` — provides isolated settings and temp dirs for all tests
|
|
181
|
+
- **MockVectorStore** and **MockEmbeddingService**: In `knowledge_base/_mocks.py` for testing without ML dependencies
|
|
182
|
+
- **FAISS tests**: Guard with `pytest.importorskip("faiss")` since FAISS is not installed in dev env
|
|
183
|
+
- **Integration tests**: `tests/integration/` covers ADK and LangGraph pipeline tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: A framework for building domain-specific agentic CLI applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/shoom1/agentic-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/shoom1/agentic-cli
|
|
@@ -43,6 +43,8 @@ Requires-Dist: langchain-core>=0.3.0; extra == 'langgraph'
|
|
|
43
43
|
Requires-Dist: langchain-google-genai>=2.0.0; extra == 'langgraph'
|
|
44
44
|
Requires-Dist: langchain-openai>=0.2.0; extra == 'langgraph'
|
|
45
45
|
Requires-Dist: langchain>=0.3.0; extra == 'langgraph'
|
|
46
|
+
Requires-Dist: langgraph-checkpoint-postgres>=2.0; extra == 'langgraph'
|
|
47
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=2.0; extra == 'langgraph'
|
|
46
48
|
Requires-Dist: langgraph>=0.2.0; extra == 'langgraph'
|
|
47
49
|
Description-Content-Type: text/markdown
|
|
48
50
|
|
|
@@ -141,8 +143,12 @@ import asyncio
|
|
|
141
143
|
from agentic_cli import BaseCLIApp, BaseSettings
|
|
142
144
|
from agentic_cli.cli import AppInfo
|
|
143
145
|
from agentic_cli.workflow import AgentConfig
|
|
146
|
+
from agentic_cli.tools import register_tool, ToolCategory
|
|
147
|
+
from agentic_cli.workflow.permissions import EXEMPT
|
|
144
148
|
|
|
145
|
-
# Define your tools
|
|
149
|
+
# Define your tools. Tools are permission-gated, so register each one with
|
|
150
|
+
# the capabilities it needs (EXEMPT = a pure function that touches nothing).
|
|
151
|
+
@register_tool(category=ToolCategory.OTHER, capabilities=EXEMPT)
|
|
146
152
|
def greet(name: str) -> dict:
|
|
147
153
|
"""Greet a person by name."""
|
|
148
154
|
return {"success": True, "greeting": f"Hello, {name}!"}
|
|
@@ -390,14 +396,14 @@ def search_database(query: str, limit: int = 10) -> dict:
|
|
|
390
396
|
return {"success": True, "results": results, "count": len(results)}
|
|
391
397
|
```
|
|
392
398
|
|
|
393
|
-
|
|
399
|
+
**Registration is required for a tool to run.** The permission engine is on by default, and any tool without a capability declaration is denied at call time (fail-closed) on both the ADK and LangGraph backends. You can still pass raw callables into `AgentConfig.tools`, but unless they are registered with `@register_tool` they will be blocked — register every tool with `capabilities=` (or `EXEMPT` for pure, side-effect-free functions). Registration also gives the tool registry metadata and tool-summary formatting.
|
|
394
400
|
|
|
395
401
|
### Capabilities
|
|
396
402
|
|
|
397
403
|
Tool access is gated by the **permission engine** (see the HITL section below). Each registered tool declares what it touches via `capabilities=`:
|
|
398
404
|
|
|
399
405
|
- **`Capability("namespace.action", target_arg="...")`** — e.g. `Capability("filesystem.write", target_arg="path")`, `Capability("http.read", target_arg="url")`, `Capability("shell.exec", target_arg="command")`. The engine resolves `target_arg` against the actual tool-call arguments and matches against rules.
|
|
400
|
-
- **`EXEMPT`** — opts the tool out of the engine entirely.
|
|
406
|
+
- **`EXEMPT`** — opts the tool out of the engine entirely. Use for tools that need no permission check: pure functions with no side effects, and backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
|
|
401
407
|
|
|
402
408
|
### Built-in Tools
|
|
403
409
|
|
|
@@ -93,8 +93,12 @@ import asyncio
|
|
|
93
93
|
from agentic_cli import BaseCLIApp, BaseSettings
|
|
94
94
|
from agentic_cli.cli import AppInfo
|
|
95
95
|
from agentic_cli.workflow import AgentConfig
|
|
96
|
+
from agentic_cli.tools import register_tool, ToolCategory
|
|
97
|
+
from agentic_cli.workflow.permissions import EXEMPT
|
|
96
98
|
|
|
97
|
-
# Define your tools
|
|
99
|
+
# Define your tools. Tools are permission-gated, so register each one with
|
|
100
|
+
# the capabilities it needs (EXEMPT = a pure function that touches nothing).
|
|
101
|
+
@register_tool(category=ToolCategory.OTHER, capabilities=EXEMPT)
|
|
98
102
|
def greet(name: str) -> dict:
|
|
99
103
|
"""Greet a person by name."""
|
|
100
104
|
return {"success": True, "greeting": f"Hello, {name}!"}
|
|
@@ -342,14 +346,14 @@ def search_database(query: str, limit: int = 10) -> dict:
|
|
|
342
346
|
return {"success": True, "results": results, "count": len(results)}
|
|
343
347
|
```
|
|
344
348
|
|
|
345
|
-
|
|
349
|
+
**Registration is required for a tool to run.** The permission engine is on by default, and any tool without a capability declaration is denied at call time (fail-closed) on both the ADK and LangGraph backends. You can still pass raw callables into `AgentConfig.tools`, but unless they are registered with `@register_tool` they will be blocked — register every tool with `capabilities=` (or `EXEMPT` for pure, side-effect-free functions). Registration also gives the tool registry metadata and tool-summary formatting.
|
|
346
350
|
|
|
347
351
|
### Capabilities
|
|
348
352
|
|
|
349
353
|
Tool access is gated by the **permission engine** (see the HITL section below). Each registered tool declares what it touches via `capabilities=`:
|
|
350
354
|
|
|
351
355
|
- **`Capability("namespace.action", target_arg="...")`** — e.g. `Capability("filesystem.write", target_arg="path")`, `Capability("http.read", target_arg="url")`, `Capability("shell.exec", target_arg="command")`. The engine resolves `target_arg` against the actual tool-call arguments and matches against rules.
|
|
352
|
-
- **`EXEMPT`** — opts the tool out of the engine entirely.
|
|
356
|
+
- **`EXEMPT`** — opts the tool out of the engine entirely. Use for tools that need no permission check: pure functions with no side effects, and backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
|
|
353
357
|
|
|
354
358
|
### Built-in Tools
|
|
355
359
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-cli"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3"
|
|
8
8
|
description = "A framework for building domain-specific agentic CLI applications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -56,6 +56,10 @@ langgraph = [
|
|
|
56
56
|
"langchain-openai>=0.2.0",
|
|
57
57
|
"langchain-anthropic>=0.2.0",
|
|
58
58
|
"langchain-google-genai>=2.0.0",
|
|
59
|
+
# Persistent checkpointer backends (sqlite/postgres store_type). The async
|
|
60
|
+
# savers are required because the manager streams via astream_events.
|
|
61
|
+
"langgraph-checkpoint-sqlite>=2.0",
|
|
62
|
+
"langgraph-checkpoint-postgres>=2.0",
|
|
59
63
|
]
|
|
60
64
|
|
|
61
65
|
[project.scripts]
|
|
@@ -456,6 +456,29 @@ class BaseCLIApp:
|
|
|
456
456
|
else:
|
|
457
457
|
logger.error("session_save_on_exit_failed", error=result.get("error"))
|
|
458
458
|
|
|
459
|
+
async def _extract_session_facts_on_exit(self) -> None:
|
|
460
|
+
"""Extract key facts from the session into memory on exit (if enabled).
|
|
461
|
+
|
|
462
|
+
Gated by the ``auto_extract_session_facts`` setting; a no-op when the
|
|
463
|
+
workflow isn't ready or has no memory store. Never raises — fact
|
|
464
|
+
extraction must not block a clean shutdown.
|
|
465
|
+
"""
|
|
466
|
+
if not getattr(self._settings, "auto_extract_session_facts", False):
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
if not self._workflow_controller.is_ready:
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
workflow = self._workflow_controller.workflow
|
|
473
|
+
try:
|
|
474
|
+
facts = await workflow.on_session_end()
|
|
475
|
+
except Exception:
|
|
476
|
+
logger.debug("session_fact_extraction_on_exit_failed", exc_info=True)
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
if facts:
|
|
480
|
+
logger.info("session_facts_extracted", count=len(facts))
|
|
481
|
+
|
|
459
482
|
async def run(self) -> None:
|
|
460
483
|
"""Run the main application loop."""
|
|
461
484
|
logger.info("repl_starting")
|
|
@@ -474,6 +497,9 @@ class BaseCLIApp:
|
|
|
474
497
|
# Run the session - user sees prompt immediately!
|
|
475
498
|
await self.session.run_async()
|
|
476
499
|
|
|
500
|
+
# Extract session facts into memory on exit (if enabled)
|
|
501
|
+
await self._extract_session_facts_on_exit()
|
|
502
|
+
|
|
477
503
|
# Save persistent session on exit
|
|
478
504
|
if self._session_id:
|
|
479
505
|
await self._save_session_on_exit()
|
|
@@ -7,6 +7,7 @@ This module provides:
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
from contextlib import suppress
|
|
10
11
|
from dataclasses import dataclass, field
|
|
11
12
|
from typing import TYPE_CHECKING, ClassVar
|
|
12
13
|
|
|
@@ -74,6 +75,10 @@ class _EventProcessingState:
|
|
|
74
75
|
workflow_controller: "WorkflowController | None" = field(default=None, repr=False)
|
|
75
76
|
status_line: str = "Processing..."
|
|
76
77
|
thinking_started: bool = False
|
|
78
|
+
# True while a HITL dialog is on screen: during that window there are no
|
|
79
|
+
# active thinking boxes, so the Ctrl+C watcher must not mistake the absence
|
|
80
|
+
# of boxes for a cancel request.
|
|
81
|
+
in_hitl: bool = False
|
|
77
82
|
thinking_content: list[str] = field(default_factory=list)
|
|
78
83
|
response_content: list[str] = field(default_factory=list)
|
|
79
84
|
# Prevents double-counting when LangGraph emits both CONTEXT_TRIMMED and LLM_USAGE
|
|
@@ -189,16 +194,21 @@ class MessageProcessor:
|
|
|
189
194
|
# deadlocking the workflow runner.
|
|
190
195
|
async def _handle_input(request: "UserInputRequest") -> str:
|
|
191
196
|
nonlocal events_ctx
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
# Mark the HITL window before tearing down the events box so the
|
|
198
|
+
# cancel watcher doesn't read "no active boxes" as a Ctrl+C.
|
|
199
|
+
state.in_hitl = True
|
|
200
|
+
try:
|
|
201
|
+
if state.thinking_started and events_ctx is not None:
|
|
202
|
+
events_ctx.finish(add_to_history=False)
|
|
203
|
+
state.thinking_started = False
|
|
204
|
+
|
|
205
|
+
response = await self._prompt_user_input(request, ui)
|
|
206
|
+
finally:
|
|
207
|
+
events_ctx = ui.start_thinking(
|
|
208
|
+
state.get_status, content_format="ansi"
|
|
209
|
+
)
|
|
210
|
+
state.thinking_started = True
|
|
211
|
+
state.in_hitl = False
|
|
202
212
|
return response
|
|
203
213
|
|
|
204
214
|
workflow.set_input_callback(_handle_input)
|
|
@@ -210,13 +220,33 @@ class MessageProcessor:
|
|
|
210
220
|
)
|
|
211
221
|
state.thinking_started = True
|
|
212
222
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
# Consume the event stream in a cancellable task so Ctrl+C
|
|
224
|
+
# can abort an in-flight run. thinking_prompt's Ctrl+C
|
|
225
|
+
# binding finishes all thinking boxes (so ui.is_thinking
|
|
226
|
+
# flips False) but never cancels our coroutine, so we watch
|
|
227
|
+
# for that and cancel the task ourselves.
|
|
228
|
+
async def _consume() -> None:
|
|
229
|
+
async for event in workflow.process(
|
|
230
|
+
message=message,
|
|
231
|
+
user_id=settings.default_user,
|
|
232
|
+
):
|
|
233
|
+
handler = dispatch.get(event.type)
|
|
234
|
+
if handler is not None:
|
|
235
|
+
await handler(
|
|
236
|
+
self, event, state, ui, settings, workflow
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
proc_task = asyncio.create_task(_consume())
|
|
240
|
+
if await self._watch_for_cancel(proc_task, ui, state):
|
|
241
|
+
# Ctrl+C already finished every active box; just drop
|
|
242
|
+
# our now-dead references so the next turn starts clean.
|
|
243
|
+
state.thinking_started = False
|
|
244
|
+
self._task_box = None
|
|
245
|
+
self._last_task_content = None
|
|
246
|
+
ui.add_warning("Cancelled.")
|
|
247
|
+
workflow_controller.update_status_bar(ui)
|
|
248
|
+
logger.info("message_cancelled_by_user")
|
|
249
|
+
break
|
|
220
250
|
|
|
221
251
|
# Finish events box only (don't add status to history)
|
|
222
252
|
if state.thinking_started and events_ctx is not None:
|
|
@@ -262,6 +292,42 @@ class MessageProcessor:
|
|
|
262
292
|
self._last_task_content if self._task_box else None
|
|
263
293
|
)
|
|
264
294
|
|
|
295
|
+
async def _watch_for_cancel(
|
|
296
|
+
self,
|
|
297
|
+
proc_task: "asyncio.Task[None]",
|
|
298
|
+
ui: "ThinkingPromptSession",
|
|
299
|
+
state: _EventProcessingState,
|
|
300
|
+
) -> bool:
|
|
301
|
+
"""Watch a running workflow task and cancel it on Ctrl+C.
|
|
302
|
+
|
|
303
|
+
thinking_prompt's Ctrl+C binding finishes all active thinking boxes
|
|
304
|
+
(so ``ui.is_thinking`` becomes False) but does not cancel the coroutine
|
|
305
|
+
driving the workflow. We poll for that transition — ignoring the HITL
|
|
306
|
+
window, when boxes are legitimately absent — and cancel the task when
|
|
307
|
+
it happens.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
proc_task: Task consuming the workflow event stream.
|
|
311
|
+
ui: UI session (provides the public ``is_thinking`` box state).
|
|
312
|
+
state: Shared processing state (``in_hitl`` guards the dialog window).
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if the task was cancelled by the user. Otherwise False, after
|
|
316
|
+
re-raising any exception the task raised.
|
|
317
|
+
"""
|
|
318
|
+
while True:
|
|
319
|
+
done, _ = await asyncio.wait({proc_task}, timeout=0.1)
|
|
320
|
+
if proc_task in done:
|
|
321
|
+
break
|
|
322
|
+
if not state.in_hitl and not ui.is_thinking:
|
|
323
|
+
proc_task.cancel()
|
|
324
|
+
with suppress(asyncio.CancelledError):
|
|
325
|
+
await proc_task
|
|
326
|
+
return True
|
|
327
|
+
# Task completed on its own — surface its result/exception.
|
|
328
|
+
proc_task.result()
|
|
329
|
+
return False
|
|
330
|
+
|
|
265
331
|
async def _prompt_user_input(
|
|
266
332
|
self,
|
|
267
333
|
request: "UserInputRequest",
|
|
@@ -119,7 +119,12 @@ class BaseSettings(WorkflowSettingsMixin, AppSettingsMixin, CLISettingsMixin, Py
|
|
|
119
119
|
elif key == "thinking_effort":
|
|
120
120
|
self.set_thinking_effort(value)
|
|
121
121
|
else:
|
|
122
|
-
object.__setattr__
|
|
122
|
+
# Validate through Pydantic instead of object.__setattr__ so the
|
|
123
|
+
# value is type-checked and constraint-checked before it lands on
|
|
124
|
+
# the instance (and is later persisted to settings.json). An
|
|
125
|
+
# invalid value or unknown key raises ValidationError, which is a
|
|
126
|
+
# subclass of ValueError — exactly what callers already handle.
|
|
127
|
+
self.__pydantic_validator__.validate_assignment(self, key, value)
|
|
123
128
|
|
|
124
129
|
@classmethod
|
|
125
130
|
def settings_customise_sources(
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import fcntl
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
5
7
|
import time
|
|
6
8
|
from contextlib import contextmanager
|
|
7
9
|
from pathlib import Path
|
|
@@ -58,11 +60,31 @@ def sanitize_filename(name: str) -> str:
|
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def _atomic_write(path: Path, content: str) -> None:
|
|
61
|
-
"""Write content to a file atomically
|
|
63
|
+
"""Write content to a file atomically and durably.
|
|
64
|
+
|
|
65
|
+
Writes to a uniquely-named temp file in the same directory, flushes and
|
|
66
|
+
fsyncs it, then atomically renames it over the target. Notes:
|
|
67
|
+
- Unique temp name (tempfile.mkstemp) so two concurrent writers can't
|
|
68
|
+
truncate each other's temp file the way a fixed ``.tmp`` name allows.
|
|
69
|
+
- fsync before the rename so a crash can't persist the rename ahead of the
|
|
70
|
+
data and leave a torn/empty file in place of the previously-good one.
|
|
71
|
+
- Explicit UTF-8 so output doesn't depend on the locale (a C/POSIX locale
|
|
72
|
+
would otherwise raise UnicodeEncodeError on non-ASCII content).
|
|
73
|
+
"""
|
|
62
74
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
76
|
+
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
|
|
77
|
+
)
|
|
78
|
+
tmp_path = Path(tmp_name)
|
|
79
|
+
try:
|
|
80
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
81
|
+
f.write(content)
|
|
82
|
+
f.flush()
|
|
83
|
+
os.fsync(f.fileno())
|
|
84
|
+
os.replace(tmp_path, path)
|
|
85
|
+
except BaseException:
|
|
86
|
+
tmp_path.unlink(missing_ok=True)
|
|
87
|
+
raise
|
|
66
88
|
|
|
67
89
|
|
|
68
90
|
def atomic_write_json(path: Path, data: Any, indent: int = 2) -> None:
|
|
@@ -6,6 +6,8 @@ Provides FAISS-based vector storage and similarity search.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
9
11
|
import threading
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import TYPE_CHECKING
|
|
@@ -207,7 +209,16 @@ class VectorStore:
|
|
|
207
209
|
self._next_id = len(new_id_map)
|
|
208
210
|
|
|
209
211
|
def save(self) -> None:
|
|
210
|
-
"""Persist index and mappings to disk.
|
|
212
|
+
"""Persist index and mappings to disk.
|
|
213
|
+
|
|
214
|
+
Both files are written atomically (temp file + ``os.replace``) so a
|
|
215
|
+
crash mid-write can never leave a truncated ``index.faiss`` — a torn
|
|
216
|
+
index is fatal because ``faiss.read_index`` raises on load, which would
|
|
217
|
+
brick the whole knowledge base. The index is committed before the
|
|
218
|
+
mappings: each file is always complete on its own, and a crash in the
|
|
219
|
+
brief window between the two replaces leaves a complete (if slightly
|
|
220
|
+
stale) pair rather than a corrupt index.
|
|
221
|
+
"""
|
|
211
222
|
with self._lock:
|
|
212
223
|
if self._index is None:
|
|
213
224
|
return
|
|
@@ -217,9 +228,24 @@ class VectorStore:
|
|
|
217
228
|
# Ensure directory exists
|
|
218
229
|
self.index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
219
230
|
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
# the
|
|
231
|
+
# Write the FAISS index atomically: faiss.write_index() takes a
|
|
232
|
+
# path and writes in place (non-atomic), so write to a temp file in
|
|
233
|
+
# the same directory and os.replace() it into position.
|
|
234
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
235
|
+
dir=str(self.index_path.parent),
|
|
236
|
+
prefix=f".{self.index_path.name}.",
|
|
237
|
+
suffix=".tmp",
|
|
238
|
+
)
|
|
239
|
+
os.close(fd) # faiss writes by path, not file descriptor
|
|
240
|
+
tmp_path = Path(tmp_name)
|
|
241
|
+
try:
|
|
242
|
+
faiss.write_index(self._index, str(tmp_path))
|
|
243
|
+
os.replace(tmp_path, self.index_path)
|
|
244
|
+
except BaseException:
|
|
245
|
+
tmp_path.unlink(missing_ok=True)
|
|
246
|
+
raise
|
|
247
|
+
|
|
248
|
+
# Mappings written after the index (also atomically).
|
|
223
249
|
mappings_path = self.index_path.with_suffix(".mappings.json")
|
|
224
250
|
mappings = {
|
|
225
251
|
"id_map": {str(k): v for k, v in self._id_map.items()},
|
|
@@ -228,9 +254,6 @@ class VectorStore:
|
|
|
228
254
|
}
|
|
229
255
|
atomic_write_json(mappings_path, mappings)
|
|
230
256
|
|
|
231
|
-
# Save FAISS index
|
|
232
|
-
faiss.write_index(self._index, str(self.index_path))
|
|
233
|
-
|
|
234
257
|
def load(self) -> None:
|
|
235
258
|
"""Load index and mappings from disk."""
|
|
236
259
|
if not self.index_path.exists():
|