agentic-cli 0.5.1__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.1 → agentic_cli-0.5.3}/CHANGELOG.md +45 -0
- agentic_cli-0.5.3/CLAUDE.md +183 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/PKG-INFO +20 -8
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/README.md +17 -7
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/agents.py +1 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/pyproject.toml +5 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/__init__.py +1 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/app.py +26 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/message_processor.py +83 -17
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/config.py +6 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/file_utils.py +26 -4
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/concepts.py +43 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/vector_store.py +30 -7
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/persistence/session.py +15 -8
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/__init__.py +13 -5
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_source.py +18 -13
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/executor.py +79 -18
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/factories.py +60 -12
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/grep_tool.py +5 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/knowledge_tools.py +271 -110
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/memory_tools.py +52 -41
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/sandbox.py +33 -1
- agentic_cli-0.5.3/src/agentic_cli/tools/webfetch/fetcher.py +236 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/manager.py +59 -47
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/base_manager.py +17 -9
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/graph_builder.py +6 -6
- {agentic_cli-0.5.1 → 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.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/service_registry.py +0 -1
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/settings.py +0 -6
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/tool_summaries.py +3 -1
- {agentic_cli-0.5.1 → 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.1 → agentic_cli-0.5.3}/tests/integration/conftest.py +40 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_kb_research_flow.py +2 -2
- 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.1 → agentic_cli-0.5.3}/tests/test_arxiv_tools.py +52 -44
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_concept_store.py +37 -0
- {agentic_cli-0.5.1 → 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.1 → agentic_cli-0.5.3}/tests/test_kb_sidecar.py +4 -4
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_tool_renames.py +25 -5
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_knowledge_base.py +3 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_knowledge_tools.py +77 -24
- {agentic_cli-0.5.1 → 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.1 → agentic_cli-0.5.3}/tests/test_tool_summaries.py +5 -2
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_tools.py +16 -11
- agentic_cli-0.5.3/tests/test_update_setting.py +43 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_vector_store.py +40 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_webfetch.py +102 -14
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_factories.py +6 -4
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_langgraph_state_tools.py +3 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_os_sandbox.py +54 -18
- agentic_cli-0.5.3/tests/workflow/test_adk_session_resume.py +60 -0
- {agentic_cli-0.5.1 → 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.1/src/agentic_cli/tools/reflection_tools.py +0 -145
- agentic_cli-0.5.1/src/agentic_cli/tools/webfetch/fetcher.py +0 -203
- agentic_cli-0.5.1/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -152
- agentic_cli-0.5.1/tests/test_reflection.py +0 -74
- agentic_cli-0.5.1/tests/test_update_setting.py +0 -21
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/.gitignore +0 -0
- /agentic_cli-0.5.1/CLAUDE.md → /agentic_cli-0.5.3/AGENTS.md +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/LICENSE +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/environment.yml +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/arxiv_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/fileops_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/hello_agent.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/hello_langgraph.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/memory_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/__main__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/app.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/commands.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/settings.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/shell_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/webfetch_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/websearch_demo.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/requirements-dev.txt +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/requirements.txt +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/builtin_commands.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/commands.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_command.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_introspection.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/usage_tracker.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/workflow_controller.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/constants.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_bm25_backends.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mock_bm25.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mocks.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/bm25_index.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/manager.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/models.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sidecar.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sources.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/logging.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/persistence/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/settings_mixins.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/settings_persistence.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/planning.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/tasks.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/state_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/execution_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_read.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_write.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/glob_tool.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/interaction_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/state_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/pdf_utils.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/registry.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/base.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/jupyter_local.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/manager.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/models.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/search.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/audit.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/classifier.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/config.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/executor.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/models.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/base.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/bubblewrap.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/detect.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/policy.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt_profile.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/converter.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/robots.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/validator.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch_tool.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/event_processor.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/permission_plugin.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/plugins.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/task_progress_plugin.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/config.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/events.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/factory.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/permission_wrap.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/state.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/models.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/capabilities.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/engine.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/matchers.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/prompt.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/rules.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/store.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/retry.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/helpers.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_adk_integration.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_langgraph_integration.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_live.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_permission_adk.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_permission_langgraph.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_capabilities.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_engine.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_matchers.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_prompt.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_rules.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_store.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_adk_event_processor.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_adk_plugins.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_bm25_index.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_commands.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_concurrent_stores.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_config.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_context_trimming.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_dual_thinking_boxes.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_embeddings.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_grep_tool_cache.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_input_callback.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_concepts.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_helpers.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_index_md.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_ingest_log.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_memory.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_model_registry.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_persistence.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_planning.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_session_save_resume.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_settings_persistence.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_task_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_token_caching.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_usage_tracker.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_workflow.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_workflow_controller.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_adk_state_tools.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_bubblewrap.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_core_planning.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_core_tasks.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_registry.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_sandbox.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_seatbelt_profile.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_shell_security.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/__init__.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_base_manager_permissions.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_settings_permissions.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_task_progress_plugin.py +0 -0
- {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_tool_assembly.py +0 -0
|
@@ -5,6 +5,51 @@ 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
|
+
|
|
41
|
+
## [0.5.2] - 2026-05-01
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
- **`kb_ingest` split into `kb_ingest_text` / `kb_ingest_file` / `kb_ingest_url`**: the previous unified tool declared only `kb.write` (auto-allowed by the builtin `kb.*` rule) while internally reading arbitrary local paths and fetching arbitrary URLs. The split lets the permission engine gate `filesystem.read` and `http.read` at the right entry point, and the URL path now flows through the hardened `ContentFetcher` (URLValidator + manual redirect revalidation + DNS-rebinding mitigation).
|
|
45
|
+
- **Concept-page slug traversal blocked** (`ConceptStore`): explicit slugs are validated against a strict `[a-z0-9-]+` allowlist, and `_concept_path` asserts the resolved path stays under `base_dir`. A slug like `../../etc/passwd` is now rejected before any filesystem write.
|
|
46
|
+
- **Webfetch redirect handling rewritten**: `ContentFetcher.fetch` no longer follows redirects via httpx. Each `Location` is revalidated through `URLValidator` *before* the next GET, cross-host redirects are reported without contacting the new origin, same-host redirects re-check `robots.txt`, and the redirect chain is capped at 5.
|
|
47
|
+
- **`os_sandbox_enabled=True` now fails closed**: when shell or Python execution is requested with sandboxing on, both executors refuse to run if the resolved sandbox is `NoOpSandbox` or wrap fails — instead of silently dropping back to plain subprocess/ulimit.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- **`kb_ingest` tool removed** in favor of the three split entry points; `KB_WRITER_TOOLS` and `make_kb_tools` updated. Internal helper renamed: `_ingest_document_with_kb` → `_ingest_text_with_kb` / `_ingest_file_with_kb` / `_ingest_url_with_kb`.
|
|
51
|
+
- **research_demo agent guide** updated to direct ingestion at `ingest_arxiv_paper` for arXiv papers and at the typed `kb_ingest_*` tools for everything else.
|
|
52
|
+
|
|
8
53
|
## [0.5.1] - 2026-04-18
|
|
9
54
|
|
|
10
55
|
### Added
|
|
@@ -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
|
|
|
@@ -460,7 +466,7 @@ results = search_arxiv("transformer attention", max_results=10, categories=["cs.
|
|
|
460
466
|
paper = fetch_arxiv_paper("1706.03762") # "Attention Is All You Need"
|
|
461
467
|
```
|
|
462
468
|
|
|
463
|
-
The `ArxivSearchSource` (in `knowledge_base/sources.py`) is cached via the service registry and reused across tools,
|
|
469
|
+
The `ArxivSearchSource` (in `knowledge_base/sources.py`) is cached via the service registry and reused across tools. To pull a paper into the KB, use `ingest_arxiv_paper(arxiv_id)` — it composes the arxiv source with `kb_manager`.
|
|
464
470
|
|
|
465
471
|
#### File Operations
|
|
466
472
|
|
|
@@ -513,7 +519,8 @@ Renamed tools and a new concept-pages subsystem:
|
|
|
513
519
|
|
|
514
520
|
```python
|
|
515
521
|
from agentic_cli.tools import (
|
|
516
|
-
kb_search,
|
|
522
|
+
kb_search, kb_ingest_text, kb_ingest_file, kb_ingest_url,
|
|
523
|
+
kb_read, kb_list,
|
|
517
524
|
kb_write_concept, kb_search_concepts,
|
|
518
525
|
KB_READER_TOOLS, KB_WRITER_TOOLS,
|
|
519
526
|
)
|
|
@@ -522,12 +529,16 @@ from agentic_cli.tools import (
|
|
|
522
529
|
| Tool | Purpose |
|
|
523
530
|
|------|---------|
|
|
524
531
|
| `kb_search` | Hybrid BM25 + vector search with RRF fusion, filters by source/date |
|
|
525
|
-
| `
|
|
532
|
+
| `kb_ingest_text` | Ingest in-memory text content (no FS or network access) |
|
|
533
|
+
| `kb_ingest_file` | Ingest a local file; declares `filesystem.read(path)` for the permission engine |
|
|
534
|
+
| `kb_ingest_url` | Ingest content from an http(s) URL; routed through the hardened `ContentFetcher` and declares `http.read(url)` |
|
|
526
535
|
| `kb_read` | Return the per-document markdown sidecar (lazy-generated on first read) |
|
|
527
536
|
| `kb_list` | List documents, optionally filtered |
|
|
528
537
|
| `kb_write_concept` | Agent-authored concept/summary page (slugged, merged on overwrite) |
|
|
529
538
|
| `kb_search_concepts` | Search concept pages (title-weighted, case-insensitive) |
|
|
530
539
|
|
|
540
|
+
The single `kb_ingest` tool was split in 0.5.2 so each entry point declares the right capability — text-only stays under `kb.write`, while file and URL ingestion correctly trip `filesystem.read` / `http.read` checks. For arXiv papers, prefer `ingest_arxiv_paper(arxiv_id)`.
|
|
541
|
+
|
|
531
542
|
Bundle convenience:
|
|
532
543
|
|
|
533
544
|
```python
|
|
@@ -784,7 +795,8 @@ agentic-cli/
|
|
|
784
795
|
│ │ ├── arxiv_source.py # ArxivSearchSource (service-registered)
|
|
785
796
|
│ │ ├── execution_tools.py # execute_python
|
|
786
797
|
│ │ ├── interaction_tools.py # ask_clarification
|
|
787
|
-
│ │ ├── knowledge_tools.py # kb_search /
|
|
798
|
+
│ │ ├── knowledge_tools.py # kb_search / kb_ingest_text / kb_ingest_file /
|
|
799
|
+
│ │ │ # kb_ingest_url / kb_read / kb_list /
|
|
788
800
|
│ │ │ # kb_write_concept / kb_search_concepts
|
|
789
801
|
│ │ ├── file_read.py # read_file, diff_compare
|
|
790
802
|
│ │ ├── file_write.py # write_file, edit_file (atomic)
|
|
@@ -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
|
|
|
@@ -412,7 +416,7 @@ results = search_arxiv("transformer attention", max_results=10, categories=["cs.
|
|
|
412
416
|
paper = fetch_arxiv_paper("1706.03762") # "Attention Is All You Need"
|
|
413
417
|
```
|
|
414
418
|
|
|
415
|
-
The `ArxivSearchSource` (in `knowledge_base/sources.py`) is cached via the service registry and reused across tools,
|
|
419
|
+
The `ArxivSearchSource` (in `knowledge_base/sources.py`) is cached via the service registry and reused across tools. To pull a paper into the KB, use `ingest_arxiv_paper(arxiv_id)` — it composes the arxiv source with `kb_manager`.
|
|
416
420
|
|
|
417
421
|
#### File Operations
|
|
418
422
|
|
|
@@ -465,7 +469,8 @@ Renamed tools and a new concept-pages subsystem:
|
|
|
465
469
|
|
|
466
470
|
```python
|
|
467
471
|
from agentic_cli.tools import (
|
|
468
|
-
kb_search,
|
|
472
|
+
kb_search, kb_ingest_text, kb_ingest_file, kb_ingest_url,
|
|
473
|
+
kb_read, kb_list,
|
|
469
474
|
kb_write_concept, kb_search_concepts,
|
|
470
475
|
KB_READER_TOOLS, KB_WRITER_TOOLS,
|
|
471
476
|
)
|
|
@@ -474,12 +479,16 @@ from agentic_cli.tools import (
|
|
|
474
479
|
| Tool | Purpose |
|
|
475
480
|
|------|---------|
|
|
476
481
|
| `kb_search` | Hybrid BM25 + vector search with RRF fusion, filters by source/date |
|
|
477
|
-
| `
|
|
482
|
+
| `kb_ingest_text` | Ingest in-memory text content (no FS or network access) |
|
|
483
|
+
| `kb_ingest_file` | Ingest a local file; declares `filesystem.read(path)` for the permission engine |
|
|
484
|
+
| `kb_ingest_url` | Ingest content from an http(s) URL; routed through the hardened `ContentFetcher` and declares `http.read(url)` |
|
|
478
485
|
| `kb_read` | Return the per-document markdown sidecar (lazy-generated on first read) |
|
|
479
486
|
| `kb_list` | List documents, optionally filtered |
|
|
480
487
|
| `kb_write_concept` | Agent-authored concept/summary page (slugged, merged on overwrite) |
|
|
481
488
|
| `kb_search_concepts` | Search concept pages (title-weighted, case-insensitive) |
|
|
482
489
|
|
|
490
|
+
The single `kb_ingest` tool was split in 0.5.2 so each entry point declares the right capability — text-only stays under `kb.write`, while file and URL ingestion correctly trip `filesystem.read` / `http.read` checks. For arXiv papers, prefer `ingest_arxiv_paper(arxiv_id)`.
|
|
491
|
+
|
|
483
492
|
Bundle convenience:
|
|
484
493
|
|
|
485
494
|
```python
|
|
@@ -736,7 +745,8 @@ agentic-cli/
|
|
|
736
745
|
│ │ ├── arxiv_source.py # ArxivSearchSource (service-registered)
|
|
737
746
|
│ │ ├── execution_tools.py # execute_python
|
|
738
747
|
│ │ ├── interaction_tools.py # ask_clarification
|
|
739
|
-
│ │ ├── knowledge_tools.py # kb_search /
|
|
748
|
+
│ │ ├── knowledge_tools.py # kb_search / kb_ingest_text / kb_ingest_file /
|
|
749
|
+
│ │ │ # kb_ingest_url / kb_read / kb_list /
|
|
740
750
|
│ │ │ # kb_write_concept / kb_search_concepts
|
|
741
751
|
│ │ ├── file_read.py # read_file, diff_compare
|
|
742
752
|
│ │ ├── file_write.py # write_file, edit_file (atomic)
|
|
@@ -50,7 +50,7 @@ When asked to research papers on a topic:
|
|
|
50
50
|
1. Check if a relevant concept page already exists with `kb_search_concepts(topic)`. If one is relevant, read it first — it already synthesizes prior work and lists the source `doc_ids` you can follow.
|
|
51
51
|
2. Search arXiv for relevant papers. If a search returns `success: false`, report the error instead of retrying with simpler queries.
|
|
52
52
|
3. Fetch metadata for papers of interest.
|
|
53
|
-
4. Ingest papers into the knowledge base
|
|
53
|
+
4. Ingest papers into the knowledge base. For arXiv papers, call `ingest_arxiv_paper(arxiv_id)` — it handles metadata, PDF download, and rate limiting. For other web sources use `kb_ingest_url(url)`; for a local PDF use `kb_ingest_file(path)`; for raw text you already hold use `kb_ingest_text(content)`. Each returns a `document_id` and the LLM-generated `summary`.
|
|
54
54
|
5. Read each paper's sidecar with `kb_read(doc_id)` to plan the per-paper analysis.
|
|
55
55
|
6. Pull full text with `kb_read(doc_id, full=True)` for any sections where the sidecar is insufficient (specific numbers, quotes, methodology details).
|
|
56
56
|
7. Save detailed per-paper analyses via `write_file`.
|
|
@@ -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",
|