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.
Files changed (233) hide show
  1. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/CHANGELOG.md +45 -0
  2. agentic_cli-0.5.3/CLAUDE.md +183 -0
  3. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/PKG-INFO +20 -8
  4. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/README.md +17 -7
  5. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/agents.py +1 -1
  6. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/pyproject.toml +5 -1
  7. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/__init__.py +1 -1
  8. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/app.py +26 -0
  9. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/message_processor.py +83 -17
  10. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/config.py +6 -1
  11. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/file_utils.py +26 -4
  12. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/concepts.py +43 -1
  13. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/vector_store.py +30 -7
  14. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/persistence/session.py +15 -8
  15. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/__init__.py +13 -5
  16. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_source.py +18 -13
  17. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/executor.py +79 -18
  18. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/factories.py +60 -12
  19. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/grep_tool.py +5 -1
  20. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/knowledge_tools.py +271 -110
  21. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/memory_tools.py +52 -41
  22. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/sandbox.py +33 -1
  23. agentic_cli-0.5.3/src/agentic_cli/tools/webfetch/fetcher.py +236 -0
  24. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/manager.py +59 -47
  25. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/base_manager.py +17 -9
  26. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/graph_builder.py +6 -6
  27. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/manager.py +27 -3
  28. agentic_cli-0.5.3/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +145 -0
  29. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/service_registry.py +0 -1
  30. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/settings.py +0 -6
  31. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/tool_summaries.py +3 -1
  32. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/conftest.py +8 -0
  33. agentic_cli-0.5.3/tests/event_replay.py +181 -0
  34. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/conftest.py +40 -0
  35. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_kb_research_flow.py +2 -2
  36. agentic_cli-0.5.3/tests/integration/test_research_scenarios.py +206 -0
  37. agentic_cli-0.5.3/tests/test_arxiv_download_security.py +64 -0
  38. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_arxiv_tools.py +52 -44
  39. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_concept_store.py +37 -0
  40. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_context_window.py +5 -0
  41. agentic_cli-0.5.3/tests/test_executor_security.py +73 -0
  42. agentic_cli-0.5.3/tests/test_file_utils_atomic.py +79 -0
  43. agentic_cli-0.5.3/tests/test_grep_tool_security.py +67 -0
  44. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_sidecar.py +4 -4
  45. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_tool_renames.py +25 -5
  46. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_knowledge_base.py +3 -0
  47. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_knowledge_tools.py +77 -24
  48. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_langgraph.py +46 -5
  49. agentic_cli-0.5.3/tests/test_memory_thread_safety.py +41 -0
  50. agentic_cli-0.5.3/tests/test_message_processor_cancel.py +92 -0
  51. agentic_cli-0.5.3/tests/test_message_processor_render.py +196 -0
  52. agentic_cli-0.5.3/tests/test_session_index_recovery.py +62 -0
  53. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_tool_summaries.py +5 -2
  54. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_tools.py +16 -11
  55. agentic_cli-0.5.3/tests/test_update_setting.py +43 -0
  56. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_vector_store.py +40 -0
  57. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_webfetch.py +102 -14
  58. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_factories.py +6 -4
  59. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_langgraph_state_tools.py +3 -0
  60. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_os_sandbox.py +54 -18
  61. agentic_cli-0.5.3/tests/workflow/test_adk_session_resume.py +60 -0
  62. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_backend_isolation.py +4 -0
  63. agentic_cli-0.5.3/tests/workflow/test_memory_wiring.py +134 -0
  64. agentic_cli-0.5.3/tests/workflow/test_thinking_config.py +82 -0
  65. agentic_cli-0.5.1/src/agentic_cli/tools/reflection_tools.py +0 -145
  66. agentic_cli-0.5.1/src/agentic_cli/tools/webfetch/fetcher.py +0 -203
  67. agentic_cli-0.5.1/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -152
  68. agentic_cli-0.5.1/tests/test_reflection.py +0 -74
  69. agentic_cli-0.5.1/tests/test_update_setting.py +0 -21
  70. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/.gitignore +0 -0
  71. /agentic_cli-0.5.1/CLAUDE.md → /agentic_cli-0.5.3/AGENTS.md +0 -0
  72. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/LICENSE +0 -0
  73. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/environment.yml +0 -0
  74. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/arxiv_demo.py +0 -0
  75. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/fileops_demo.py +0 -0
  76. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/hello_agent.py +0 -0
  77. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/hello_langgraph.py +0 -0
  78. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/memory_demo.py +0 -0
  79. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/__init__.py +0 -0
  80. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/__main__.py +0 -0
  81. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/app.py +0 -0
  82. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/commands.py +0 -0
  83. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/research_demo/settings.py +0 -0
  84. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/shell_demo.py +0 -0
  85. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/webfetch_demo.py +0 -0
  86. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/examples/websearch_demo.py +0 -0
  87. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/requirements-dev.txt +0 -0
  88. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/requirements.txt +0 -0
  89. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/__init__.py +0 -0
  90. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/builtin_commands.py +0 -0
  91. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/commands.py +0 -0
  92. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_command.py +0 -0
  93. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/settings_introspection.py +0 -0
  94. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/usage_tracker.py +0 -0
  95. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/cli/workflow_controller.py +0 -0
  96. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/constants.py +0 -0
  97. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/__init__.py +0 -0
  98. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_bm25_backends.py +0 -0
  99. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mock_bm25.py +0 -0
  100. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/_mocks.py +0 -0
  101. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/bm25_index.py +0 -0
  102. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
  103. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/manager.py +0 -0
  104. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/models.py +0 -0
  105. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sidecar.py +0 -0
  106. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/knowledge_base/sources.py +0 -0
  107. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/logging.py +0 -0
  108. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/persistence/__init__.py +0 -0
  109. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/settings_mixins.py +0 -0
  110. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/settings_persistence.py +0 -0
  111. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/__init__.py +0 -0
  112. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/planning.py +0 -0
  113. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/_core/tasks.py +0 -0
  114. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/__init__.py +0 -0
  115. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/adk/state_tools.py +0 -0
  116. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/arxiv_tools.py +0 -0
  117. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/execution_tools.py +0 -0
  118. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_read.py +0 -0
  119. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/file_write.py +0 -0
  120. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/glob_tool.py +0 -0
  121. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/interaction_tools.py +0 -0
  122. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/__init__.py +0 -0
  123. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/langgraph/state_tools.py +0 -0
  124. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/pdf_utils.py +0 -0
  125. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/registry.py +0 -0
  126. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/__init__.py +0 -0
  127. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/__init__.py +0 -0
  128. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/base.py +0 -0
  129. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/backends/jupyter_local.py +0 -0
  130. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/manager.py +0 -0
  131. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/sandbox/models.py +0 -0
  132. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/search.py +0 -0
  133. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/__init__.py +0 -0
  134. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/audit.py +0 -0
  135. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/classifier.py +0 -0
  136. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/config.py +0 -0
  137. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/executor.py +0 -0
  138. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/models.py +0 -0
  139. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/__init__.py +0 -0
  140. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/base.py +0 -0
  141. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/bubblewrap.py +0 -0
  142. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/detect.py +0 -0
  143. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/policy.py +0 -0
  144. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt.py +0 -0
  145. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/os_sandbox/seatbelt_profile.py +0 -0
  146. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
  147. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
  148. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
  149. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
  150. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
  151. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/converter.py +0 -0
  152. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/robots.py +0 -0
  153. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
  154. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch/validator.py +0 -0
  155. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/tools/webfetch_tool.py +0 -0
  156. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/__init__.py +0 -0
  157. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/__init__.py +0 -0
  158. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/event_processor.py +0 -0
  159. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/permission_plugin.py +0 -0
  160. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/plugins.py +0 -0
  161. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/adk/task_progress_plugin.py +0 -0
  162. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/config.py +0 -0
  163. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/events.py +0 -0
  164. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/factory.py +0 -0
  165. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
  166. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/permission_wrap.py +0 -0
  167. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
  168. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
  169. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/langgraph/state.py +0 -0
  170. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/models.py +0 -0
  171. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/__init__.py +0 -0
  172. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/capabilities.py +0 -0
  173. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/engine.py +0 -0
  174. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/matchers.py +0 -0
  175. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/prompt.py +0 -0
  176. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/rules.py +0 -0
  177. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/permissions/store.py +0 -0
  178. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/src/agentic_cli/workflow/retry.py +0 -0
  179. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/__init__.py +0 -0
  180. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/__init__.py +0 -0
  181. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/helpers.py +0 -0
  182. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_adk_integration.py +0 -0
  183. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_langgraph_integration.py +0 -0
  184. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_live.py +0 -0
  185. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_permission_adk.py +0 -0
  186. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/integration/test_permission_langgraph.py +0 -0
  187. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/__init__.py +0 -0
  188. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_capabilities.py +0 -0
  189. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_engine.py +0 -0
  190. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_matchers.py +0 -0
  191. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_prompt.py +0 -0
  192. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_rules.py +0 -0
  193. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/permissions/test_store.py +0 -0
  194. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_adk_event_processor.py +0 -0
  195. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_adk_plugins.py +0 -0
  196. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_bm25_index.py +0 -0
  197. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_commands.py +0 -0
  198. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_concurrent_stores.py +0 -0
  199. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_config.py +0 -0
  200. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_context_trimming.py +0 -0
  201. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_dual_thinking_boxes.py +0 -0
  202. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_embeddings.py +0 -0
  203. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_grep_tool_cache.py +0 -0
  204. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_input_callback.py +0 -0
  205. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_concepts.py +0 -0
  206. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_helpers.py +0 -0
  207. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_index_md.py +0 -0
  208. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_kb_ingest_log.py +0 -0
  209. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_memory.py +0 -0
  210. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_model_registry.py +0 -0
  211. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_persistence.py +0 -0
  212. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_planning.py +0 -0
  213. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_session_save_resume.py +0 -0
  214. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_settings_persistence.py +0 -0
  215. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_task_tools.py +0 -0
  216. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_token_caching.py +0 -0
  217. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_usage_tracker.py +0 -0
  218. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_workflow.py +0 -0
  219. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/test_workflow_controller.py +0 -0
  220. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/__init__.py +0 -0
  221. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_adk_state_tools.py +0 -0
  222. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_bubblewrap.py +0 -0
  223. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_core_planning.py +0 -0
  224. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_core_tasks.py +0 -0
  225. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_registry.py +0 -0
  226. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_sandbox.py +0 -0
  227. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_seatbelt_profile.py +0 -0
  228. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/tools/test_shell_security.py +0 -0
  229. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/__init__.py +0 -0
  230. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_base_manager_permissions.py +0 -0
  231. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_settings_permissions.py +0 -0
  232. {agentic_cli-0.5.1 → agentic_cli-0.5.3}/tests/workflow/test_task_progress_plugin.py +0 -0
  233. {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.1
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
- Registering via `@register_tool` is optional — you can pass raw callables into `AgentConfig.tools`. Registering gives the tool metadata for the registry, capability-aware permission gating, and tool-summary formatting.
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. Reserved for backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
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, including `kb_ingest` for arXiv URLs.
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, kb_ingest, kb_read, kb_list,
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
- | `kb_ingest` | Ingest content or URL (arXiv URLs auto-fetch metadata + PDF) |
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 / kb_ingest / kb_read / kb_list /
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
- Registering via `@register_tool` is optional — you can pass raw callables into `AgentConfig.tools`. Registering gives the tool metadata for the registry, capability-aware permission gating, and tool-summary formatting.
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. Reserved for backend-internal tools (e.g. ADK `transfer_to_agent`, backend state tools).
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, including `kb_ingest` for arXiv URLs.
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, kb_ingest, kb_read, kb_list,
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
- | `kb_ingest` | Ingest content or URL (arXiv URLs auto-fetch metadata + PDF) |
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 / kb_ingest / kb_read / kb_list /
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 by passing an arXiv PDF URL to `kb_ingest` — this auto-fetches metadata, downloads the PDF, extracts text, embeds chunks, and generates the sidecar in one call. The response includes the LLM-generated `summary` and the `document_id`.
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.1"
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]
@@ -92,4 +92,4 @@ __all__ = [
92
92
  "CLISettingsMixin",
93
93
  ]
94
94
 
95
- __version__ = "0.5.1"
95
+ __version__ = "0.5.3"
@@ -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
- if state.thinking_started and events_ctx is not None:
193
- events_ctx.finish(add_to_history=False)
194
- state.thinking_started = False
195
-
196
- response = await self._prompt_user_input(request, ui)
197
-
198
- events_ctx = ui.start_thinking(
199
- state.get_status, content_format="ansi"
200
- )
201
- state.thinking_started = True
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
- async for event in workflow.process(
214
- message=message,
215
- user_id=settings.default_user,
216
- ):
217
- handler = dispatch.get(event.type)
218
- if handler is not None:
219
- await handler(self, event, state, ui, settings, workflow)
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",