agentic-cli 0.4.1__tar.gz → 0.4.2__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 (140) hide show
  1. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/CHANGELOG.md +17 -0
  2. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/CLAUDE.md +10 -3
  3. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/PKG-INFO +1 -1
  4. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/pyproject.toml +1 -1
  5. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/__init__.py +2 -2
  6. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/message_processor.py +219 -124
  7. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/workflow_controller.py +75 -8
  8. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/constants.py +9 -0
  9. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/__init__.py +6 -4
  10. agentic_cli-0.4.1/src/agentic_cli/tools/standard.py → agentic_cli-0.4.2/src/agentic_cli/tools/arxiv_tools.py +5 -205
  11. agentic_cli-0.4.2/src/agentic_cli/tools/execution_tools.py +51 -0
  12. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/file_read.py +2 -4
  13. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/file_write.py +3 -2
  14. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/glob_tool.py +2 -5
  15. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/grep_tool.py +3 -4
  16. agentic_cli-0.4.2/src/agentic_cli/tools/interaction_tools.py +71 -0
  17. agentic_cli-0.4.2/src/agentic_cli/tools/knowledge_tools.py +110 -0
  18. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/memory_tools.py +5 -5
  19. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/task_tools.py +4 -6
  20. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/__init__.py +13 -5
  21. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/adk/__init__.py +3 -7
  22. agentic_cli-0.4.2/src/agentic_cli/workflow/adk/event_processor.py +261 -0
  23. agentic_cli-0.4.1/src/agentic_cli/workflow/adk_manager.py → agentic_cli-0.4.2/src/agentic_cli/workflow/adk/manager.py +22 -207
  24. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/base_manager.py +5 -122
  25. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/context.py +1 -1
  26. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/events.py +8 -2
  27. agentic_cli-0.4.2/src/agentic_cli/workflow/langgraph/graph_builder.py +426 -0
  28. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/manager.py +48 -391
  29. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/state.py +2 -15
  30. agentic_cli-0.4.2/src/agentic_cli/workflow/task_progress.py +154 -0
  31. agentic_cli-0.4.2/src/agentic_cli/workflow/tool_summaries.py +164 -0
  32. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/test_adk_integration.py +2 -2
  33. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/test_langgraph_integration.py +21 -1
  34. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/test_live.py +2 -2
  35. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_langgraph.py +12 -11
  36. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_memory.py +6 -6
  37. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_token_caching.py +7 -13
  38. agentic_cli-0.4.2/tests/test_tool_summaries.py +369 -0
  39. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_tools.py +31 -22
  40. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_workflow.py +11 -11
  41. agentic_cli-0.4.2/tests/test_workflow_controller.py +283 -0
  42. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/.gitignore +0 -0
  43. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/LICENSE +0 -0
  44. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/README.md +0 -0
  45. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/environment.yml +0 -0
  46. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/arxiv_demo.py +0 -0
  47. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/fileops_demo.py +0 -0
  48. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/hello_agent.py +0 -0
  49. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/hello_langgraph.py +0 -0
  50. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/memory_demo.py +0 -0
  51. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/planning_demo.py +0 -0
  52. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/README.md +0 -0
  53. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/__init__.py +0 -0
  54. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/__main__.py +0 -0
  55. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/agents.py +0 -0
  56. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/app.py +0 -0
  57. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/commands.py +0 -0
  58. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/research_demo/settings.py +0 -0
  59. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/shell_demo.py +0 -0
  60. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/webfetch_demo.py +0 -0
  61. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/examples/websearch_demo.py +0 -0
  62. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/requirements-dev.txt +0 -0
  63. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/requirements.txt +0 -0
  64. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/__init__.py +0 -0
  65. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/app.py +0 -0
  66. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/builtin_commands.py +0 -0
  67. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/commands.py +0 -0
  68. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/settings.py +0 -0
  69. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/settings_command.py +0 -0
  70. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/cli/settings_introspection.py +0 -0
  71. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/config.py +0 -0
  72. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/hitl/__init__.py +0 -0
  73. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/hitl/approval.py +0 -0
  74. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/hitl/checkpoints.py +0 -0
  75. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/hitl/config.py +0 -0
  76. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/__init__.py +0 -0
  77. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/embeddings.py +0 -0
  78. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/manager.py +0 -0
  79. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/models.py +0 -0
  80. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/sources.py +0 -0
  81. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/knowledge_base/vector_store.py +0 -0
  82. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/logging.py +0 -0
  83. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/persistence/__init__.py +0 -0
  84. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/persistence/_utils.py +0 -0
  85. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/persistence/artifacts.py +0 -0
  86. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/persistence/session.py +0 -0
  87. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/resolvers.py +0 -0
  88. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/settings_persistence.py +0 -0
  89. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/executor.py +0 -0
  90. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/hitl_tools.py +0 -0
  91. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/planning_tools.py +0 -0
  92. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/registry.py +0 -0
  93. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/search.py +0 -0
  94. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/__init__.py +0 -0
  95. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/audit.py +0 -0
  96. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/classifier.py +0 -0
  97. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/config.py +0 -0
  98. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/executor.py +0 -0
  99. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/models.py +0 -0
  100. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/path_analyzer.py +0 -0
  101. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/preprocessor.py +0 -0
  102. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/risk_assessor.py +0 -0
  103. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/sandbox.py +0 -0
  104. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/shell/tokenizer.py +0 -0
  105. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/__init__.py +0 -0
  106. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/converter.py +0 -0
  107. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/fetcher.py +0 -0
  108. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/robots.py +0 -0
  109. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/summarizer.py +0 -0
  110. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch/validator.py +0 -0
  111. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/tools/webfetch_tool.py +0 -0
  112. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/adk/llm_event_logger.py +0 -0
  113. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/config.py +0 -0
  114. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/__init__.py +0 -0
  115. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/persistence/__init__.py +0 -0
  116. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/persistence/checkpointers.py +0 -0
  117. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/persistence/stores.py +0 -0
  118. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/tools/__init__.py +0 -0
  119. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/tools/file_search.py +0 -0
  120. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/langgraph/tools/shell.py +0 -0
  121. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/settings.py +0 -0
  122. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/src/agentic_cli/workflow/thinking.py +0 -0
  123. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/__init__.py +0 -0
  124. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/conftest.py +0 -0
  125. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/__init__.py +0 -0
  126. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/conftest.py +0 -0
  127. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/integration/helpers.py +0 -0
  128. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_commands.py +0 -0
  129. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_config.py +0 -0
  130. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_embeddings.py +0 -0
  131. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_hitl.py +0 -0
  132. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_knowledge_base.py +0 -0
  133. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_persistence.py +0 -0
  134. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_planning.py +0 -0
  135. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_task_tools.py +0 -0
  136. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_thinking.py +0 -0
  137. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_vector_store.py +0 -0
  138. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/test_webfetch.py +0 -0
  139. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/tools/__init__.py +0 -0
  140. {agentic_cli-0.4.1 → agentic_cli-0.4.2}/tests/tools/test_shell_security.py +0 -0
@@ -5,6 +5,23 @@ 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
+ ## [0.4.2] - 2026-02-08
9
+
10
+ ### Added
11
+ - **Claude Model Support**: Auto-switch to LangGraph orchestrator for Anthropic models
12
+ - **Tool Result Summaries**: Meaningful one-liners for 18 tools instead of generic "Returned N fields"
13
+
14
+ ### Changed
15
+ - **LangGraph Native ToolNode**: Replaced custom tool node with LangChain's native ToolNode
16
+ - **Workflow Manager Decomposition**: Extracted cohesive collaborators from workflow managers
17
+ - **Message Processor Dispatch**: Extracted event handlers into dispatch table
18
+ - **Tool Module Split**: Split monolithic standard.py into domain-specific tool modules
19
+ - **Code Cleanup**: Deleted re-export shim, moved adk_manager into adk/, renamed context accessors
20
+ - **Atomic File Writes**: write_file and edit_file now use atomic writes
21
+
22
+ ### Fixed
23
+ - `diff_compare` tool producing ugly dict repr for summary field
24
+
8
25
  ## [0.4.1] - 2026-02-08
9
26
 
10
27
  ### Changed
@@ -32,21 +32,28 @@ agentic-cli/
32
32
  │ │ └── settings*.py # Settings UI (introspection, dialog)
33
33
  │ ├── workflow/
34
34
  │ │ ├── base_manager.py # BaseWorkflowManager (abstract)
35
- │ │ ├── adk_manager.py # GoogleADKWorkflowManager
35
+ │ │ ├── task_progress.py # build_task_progress_event(), parse_plan_progress()
36
36
  │ │ ├── events.py # WorkflowEvent, EventType
37
37
  │ │ ├── thinking.py # ThinkingDetector
38
38
  │ │ ├── config.py # AgentConfig
39
39
  │ │ ├── context.py # ContextVars for tool access (get_context_*())
40
- │ │ ├── adk/ # ADK-specific (llm_event_logger)
40
+ │ │ ├── adk/ # ADK orchestrator
41
+ │ │ │ ├── manager.py # GoogleADKWorkflowManager
42
+ │ │ │ ├── event_processor.py # ADKEventProcessor
43
+ │ │ │ └── llm_event_logger.py # LLM traffic logging
41
44
  │ │ └── langgraph/ # LangGraph orchestrator
42
45
  │ │ ├── manager.py # LangGraphWorkflowManager
46
+ │ │ ├── graph_builder.py # LangGraphBuilder (graph + LLM factory)
43
47
  │ │ ├── state.py
44
48
  │ │ ├── persistence/ # Checkpointers, stores
45
49
  │ │ └── tools/ # LangChain-compatible wrappers
46
50
  │ ├── tools/
47
51
  │ │ ├── registry.py # ToolRegistry, @register_tool, ToolCategory, PermissionLevel
48
52
  │ │ ├── executor.py # SafePythonExecutor
49
- │ │ ├── standard.py # search_knowledge_base, execute_python, ask_clarification
53
+ │ │ ├── knowledge_tools.py # search_knowledge_base, ingest_to_knowledge_base
54
+ │ │ ├── arxiv_tools.py # search_arxiv, fetch_arxiv_paper, analyze_arxiv_paper
55
+ │ │ ├── execution_tools.py # execute_python
56
+ │ │ ├── interaction_tools.py # ask_clarification
50
57
  │ │ ├── file_read.py # read_file, diff_compare
51
58
  │ │ ├── file_write.py # write_file, edit_file
52
59
  │ │ ├── glob_tool.py # glob
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentic-cli"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "A framework for building domain-specific agentic CLI applications"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -45,7 +45,7 @@ from agentic_cli.cli.settings import CLISettingsMixin
45
45
 
46
46
  # Heavy imports - lazy loaded on first access
47
47
  _lazy_imports = {
48
- "GoogleADKWorkflowManager": "agentic_cli.workflow.adk_manager",
48
+ "GoogleADKWorkflowManager": "agentic_cli.workflow.adk.manager",
49
49
  "BaseWorkflowManager": "agentic_cli.workflow.base_manager",
50
50
  "LangGraphWorkflowManager": "agentic_cli.workflow.langgraph.manager",
51
51
  }
@@ -95,4 +95,4 @@ __all__ = [
95
95
  "PathResolver",
96
96
  ]
97
97
 
98
- __version__ = "0.4.1"
98
+ __version__ = "0.4.2"
@@ -11,7 +11,7 @@ import asyncio
11
11
  from dataclasses import dataclass, field
12
12
  from datetime import datetime
13
13
  from enum import Enum
14
- from typing import TYPE_CHECKING
14
+ from typing import TYPE_CHECKING, ClassVar
15
15
 
16
16
  from agentic_cli.logging import Loggers, bind_context
17
17
 
@@ -92,6 +92,34 @@ class MessageHistory:
92
92
  return len(self._messages)
93
93
 
94
94
 
95
+ # === Event Processing State ===
96
+
97
+
98
+ @dataclass
99
+ class _EventProcessingState:
100
+ """Mutable state shared across event handlers during process()."""
101
+
102
+ status_line: str = "Processing..."
103
+ task_progress_display: str | None = None
104
+ thinking_started: bool = False
105
+ thinking_content: list[str] = field(default_factory=list)
106
+ response_content: list[str] = field(default_factory=list)
107
+
108
+ def get_status(self) -> str:
109
+ """Build status display with current action and task progress."""
110
+ lines = [self.status_line]
111
+ if self.task_progress_display:
112
+ lines.append(self.task_progress_display)
113
+ return "\n".join(lines)
114
+
115
+ def reset_for_retry(self) -> None:
116
+ """Reset state for retry after rate limit."""
117
+ self.status_line = "Processing..."
118
+ self.task_progress_display = None
119
+ self.thinking_content.clear()
120
+ self.response_content.clear()
121
+
122
+
95
123
  # === Message Processor ===
96
124
 
97
125
 
@@ -156,8 +184,8 @@ class MessageProcessor:
156
184
  )
157
185
  return
158
186
 
159
- # Import EventType and WorkflowEvent here (workflow module is now loaded)
160
- from agentic_cli.workflow import EventType, WorkflowEvent
187
+ # Import WorkflowEvent here (workflow module is now loaded)
188
+ from agentic_cli.workflow import WorkflowEvent
161
189
 
162
190
  bind_context(user_id=settings.default_user)
163
191
  logger.info("handling_message", message_length=len(message))
@@ -166,35 +194,17 @@ class MessageProcessor:
166
194
  if settings.log_activity:
167
195
  self._message_history.add(message, MessageType.USER)
168
196
 
169
- # Status line for thinking box (multi-line with task progress)
170
- status_line = "Processing..."
171
- task_progress_display: str | None = None
172
- thinking_started = False
173
-
174
- # Accumulate content for history
175
- thinking_content: list[str] = []
176
- response_content: list[str] = []
177
-
178
- def get_status() -> str:
179
- """Build status display with current action and task progress."""
180
- lines = [status_line]
181
-
182
- # Add task progress if available (from TASK_PROGRESS events)
183
- if task_progress_display:
184
- lines.append(task_progress_display)
185
-
186
- return "\n".join(lines)
187
-
197
+ state = _EventProcessingState()
188
198
  workflow = workflow_controller.workflow
199
+ dispatch = self._get_event_dispatch()
189
200
 
190
201
  # Set up direct callback so HITL tools can prompt the user without
191
202
  # deadlocking (the Future pattern blocks the ADK runner, preventing
192
203
  # the USER_INPUT_REQUIRED event from ever being yielded).
193
204
  async def _handle_input(request: "UserInputRequest") -> str:
194
- nonlocal thinking_started
195
- if thinking_started:
205
+ if state.thinking_started:
196
206
  ui.finish_thinking(add_to_history=False)
197
- thinking_started = False
207
+ state.thinking_started = False
198
208
 
199
209
  wf_event = WorkflowEvent.user_input_required(
200
210
  request_id=request.request_id,
@@ -206,129 +216,52 @@ class MessageProcessor:
206
216
  )
207
217
  response = await self._prompt_user_input(wf_event, workflow, ui)
208
218
 
209
- ui.start_thinking(get_status)
210
- thinking_started = True
219
+ ui.start_thinking(state.get_status)
220
+ state.thinking_started = True
211
221
  return response
212
222
 
213
223
  workflow._user_input_callback = _handle_input
214
224
  try:
215
225
  while True:
216
226
  try:
217
- ui.start_thinking(get_status)
218
- thinking_started = True
227
+ ui.start_thinking(state.get_status)
228
+ state.thinking_started = True
219
229
 
220
230
  async for event in workflow.process(
221
231
  message=message,
222
232
  user_id=settings.default_user,
223
233
  ):
224
- if event.type == EventType.TEXT:
225
- # Stream response directly to console
226
- ui.add_response(event.content, markdown=True)
227
- response_content.append(event.content)
228
-
229
- elif event.type == EventType.THINKING:
230
- # Stream thinking to console only if verbose_thinking is enabled
231
- status_line = "Thinking..."
232
- if settings.verbose_thinking:
233
- ui.add_message("system", event.content)
234
- thinking_content.append(event.content)
235
-
236
- elif event.type == EventType.TOOL_CALL:
237
- # Update status line in thinking box
238
- tool_name = event.metadata.get("tool_name", "unknown")
239
- status_line = f"Calling: {tool_name}"
240
-
241
- elif event.type == EventType.TOOL_RESULT:
242
- # Display tool result summary
243
- tool_name = event.metadata.get("tool_name", "unknown")
244
- success = event.metadata.get("success", True)
245
- duration = event.metadata.get("duration_ms")
246
- icon = "+" if success else "x"
247
- duration_str = f" ({duration}ms)" if duration else ""
248
- status_line = f"{icon} {tool_name}: {event.content}{duration_str}"
249
- # Also show in message area for visibility
250
- style = "green" if success else "red"
251
- ui.add_rich(
252
- f"[{style}]{icon}[/{style}] {tool_name}: {event.content}{duration_str}"
253
- )
254
-
255
- elif event.type == EventType.USER_INPUT_REQUIRED:
256
- # Legacy path: handle USER_INPUT_REQUIRED events
257
- # that may still arrive from the ADK event stream.
258
- if thinking_started:
259
- ui.finish_thinking(add_to_history=False)
260
- thinking_started = False
261
-
262
- response = await self._prompt_user_input(event, workflow, ui)
263
- workflow.provide_user_input(
264
- event.metadata["request_id"],
265
- response,
266
- )
267
-
268
- # Resume thinking box
269
- ui.start_thinking(get_status)
270
- thinking_started = True
271
-
272
- elif event.type == EventType.CODE_EXECUTION:
273
- # Update status with execution result
274
- result_preview = (
275
- event.content[:40] + "..."
276
- if len(event.content) > 40
277
- else event.content
278
- )
279
- status_line = f"Result: {result_preview}"
280
-
281
- elif event.type == EventType.EXECUTABLE_CODE:
282
- # Update status when executing code
283
- lang = event.metadata.get("language", "python")
284
- status_line = f"Running {lang} code..."
285
-
286
- elif event.type == EventType.FILE_DATA:
287
- # Update status with file info
288
- status_line = f"File: {event.content}"
289
-
290
- elif event.type == EventType.TASK_PROGRESS:
291
- # Task progress update - event.content has compact display,
292
- # event.metadata has progress stats
293
- progress = event.metadata.get("progress", {})
294
- if progress.get("total", 0) > 0:
295
- completed = progress.get("completed", 0)
296
- total = progress["total"]
297
- # Build display: separator + task list
298
- task_progress_display = f"--- Tasks: {completed}/{total} ---"
299
- if event.content:
300
- task_progress_display += f"\n{event.content}"
301
-
302
- # Update status line with current task if available
303
- current_task = event.metadata.get("current_task_description")
304
- if current_task:
305
- status_line = f"Working on: {current_task}"
234
+ handler = dispatch.get(event.type)
235
+ if handler is not None:
236
+ await handler(self, event, state, ui, settings, workflow)
306
237
 
307
238
  # Finish thinking box (don't add status to history)
308
- if thinking_started:
239
+ if state.thinking_started:
309
240
  ui.finish_thinking(add_to_history=False)
310
241
 
311
242
  # Add accumulated content to message history (if logging enabled)
312
243
  if settings.log_activity:
313
- if thinking_content:
244
+ if state.thinking_content:
314
245
  self._message_history.add(
315
- "".join(thinking_content), MessageType.THINKING
246
+ "".join(state.thinking_content),
247
+ MessageType.THINKING,
316
248
  )
317
- if response_content:
249
+ if state.response_content:
318
250
  self._message_history.add(
319
- "".join(response_content), MessageType.ASSISTANT
251
+ "".join(state.response_content),
252
+ MessageType.ASSISTANT,
320
253
  )
321
254
 
322
255
  logger.debug("message_handled_successfully")
323
256
  break # Success — exit retry loop
324
257
 
325
258
  except Exception as e:
326
- if thinking_started:
259
+ if state.thinking_started:
327
260
  ui.finish_thinking(add_to_history=False)
328
- thinking_started = False
261
+ state.thinking_started = False
329
262
 
330
263
  # Check for 429 rate limit errors — prompt user to wait and retry
331
- from agentic_cli.workflow.adk_manager import (
264
+ from agentic_cli.workflow.adk.event_processor import (
332
265
  _is_rate_limit_error,
333
266
  _parse_retry_delay,
334
267
  )
@@ -342,11 +275,7 @@ class MessageProcessor:
342
275
  if retry:
343
276
  ui.add_warning(f"Waiting {delay:.0f}s before retrying...")
344
277
  await asyncio.sleep(delay)
345
- # Reset state for retry
346
- status_line = "Processing..."
347
- task_progress_display = None
348
- thinking_content.clear()
349
- response_content.clear()
278
+ state.reset_for_retry()
350
279
  continue # Retry the loop
351
280
 
352
281
  # Non-429 or user chose cancel
@@ -406,3 +335,169 @@ class MessageProcessor:
406
335
  default=default or "",
407
336
  )
408
337
  return result or ""
338
+
339
+ # === Event handler methods ===
340
+
341
+ async def _handle_text(
342
+ self,
343
+ event: "WorkflowEvent",
344
+ state: _EventProcessingState,
345
+ ui: "ThinkingPromptSession",
346
+ settings: "BaseSettings",
347
+ workflow: object,
348
+ ) -> None:
349
+ """Handle TEXT events — stream response to console."""
350
+ ui.add_response(event.content, markdown=True)
351
+ state.response_content.append(event.content)
352
+
353
+ async def _handle_thinking(
354
+ self,
355
+ event: "WorkflowEvent",
356
+ state: _EventProcessingState,
357
+ ui: "ThinkingPromptSession",
358
+ settings: "BaseSettings",
359
+ workflow: object,
360
+ ) -> None:
361
+ """Handle THINKING events — update status, optionally display."""
362
+ state.status_line = "Thinking..."
363
+ if settings.verbose_thinking:
364
+ ui.add_message("system", event.content)
365
+ state.thinking_content.append(event.content)
366
+
367
+ async def _handle_tool_call(
368
+ self,
369
+ event: "WorkflowEvent",
370
+ state: _EventProcessingState,
371
+ ui: "ThinkingPromptSession",
372
+ settings: "BaseSettings",
373
+ workflow: object,
374
+ ) -> None:
375
+ """Handle TOOL_CALL events — update status line."""
376
+ tool_name = event.metadata.get("tool_name", "unknown")
377
+ state.status_line = f"Calling: {tool_name}"
378
+
379
+ async def _handle_tool_result(
380
+ self,
381
+ event: "WorkflowEvent",
382
+ state: _EventProcessingState,
383
+ ui: "ThinkingPromptSession",
384
+ settings: "BaseSettings",
385
+ workflow: object,
386
+ ) -> None:
387
+ """Handle TOOL_RESULT events — display result summary."""
388
+ tool_name = event.metadata.get("tool_name", "unknown")
389
+ success = event.metadata.get("success", True)
390
+ duration = event.metadata.get("duration_ms")
391
+ icon = "+" if success else "x"
392
+ duration_str = f" ({duration}ms)" if duration else ""
393
+ state.status_line = f"{icon} {tool_name}: {event.content}{duration_str}"
394
+ style = "green" if success else "red"
395
+ ui.add_rich(
396
+ f"[{style}]{icon}[/{style}] {tool_name}: {event.content}{duration_str}"
397
+ )
398
+
399
+ async def _handle_user_input_required(
400
+ self,
401
+ event: "WorkflowEvent",
402
+ state: _EventProcessingState,
403
+ ui: "ThinkingPromptSession",
404
+ settings: "BaseSettings",
405
+ workflow: object,
406
+ ) -> None:
407
+ """Handle USER_INPUT_REQUIRED events — legacy ADK event stream path."""
408
+ if state.thinking_started:
409
+ ui.finish_thinking(add_to_history=False)
410
+ state.thinking_started = False
411
+
412
+ response = await self._prompt_user_input(event, workflow, ui)
413
+ workflow.provide_user_input(
414
+ event.metadata["request_id"],
415
+ response,
416
+ )
417
+
418
+ # Resume thinking box
419
+ ui.start_thinking(state.get_status)
420
+ state.thinking_started = True
421
+
422
+ async def _handle_code_execution(
423
+ self,
424
+ event: "WorkflowEvent",
425
+ state: _EventProcessingState,
426
+ ui: "ThinkingPromptSession",
427
+ settings: "BaseSettings",
428
+ workflow: object,
429
+ ) -> None:
430
+ """Handle CODE_EXECUTION events — update status with result preview."""
431
+ result_preview = (
432
+ event.content[:40] + "..."
433
+ if len(event.content) > 40
434
+ else event.content
435
+ )
436
+ state.status_line = f"Result: {result_preview}"
437
+
438
+ async def _handle_executable_code(
439
+ self,
440
+ event: "WorkflowEvent",
441
+ state: _EventProcessingState,
442
+ ui: "ThinkingPromptSession",
443
+ settings: "BaseSettings",
444
+ workflow: object,
445
+ ) -> None:
446
+ """Handle EXECUTABLE_CODE events — update status with language."""
447
+ lang = event.metadata.get("language", "python")
448
+ state.status_line = f"Running {lang} code..."
449
+
450
+ async def _handle_file_data(
451
+ self,
452
+ event: "WorkflowEvent",
453
+ state: _EventProcessingState,
454
+ ui: "ThinkingPromptSession",
455
+ settings: "BaseSettings",
456
+ workflow: object,
457
+ ) -> None:
458
+ """Handle FILE_DATA events — update status with filename."""
459
+ state.status_line = f"File: {event.content}"
460
+
461
+ async def _handle_task_progress(
462
+ self,
463
+ event: "WorkflowEvent",
464
+ state: _EventProcessingState,
465
+ ui: "ThinkingPromptSession",
466
+ settings: "BaseSettings",
467
+ workflow: object,
468
+ ) -> None:
469
+ """Handle TASK_PROGRESS events — update progress display."""
470
+ progress = event.metadata.get("progress", {})
471
+ if progress.get("total", 0) > 0:
472
+ completed = progress.get("completed", 0)
473
+ total = progress["total"]
474
+ state.task_progress_display = f"--- Tasks: {completed}/{total} ---"
475
+ if event.content:
476
+ state.task_progress_display += f"\n{event.content}"
477
+
478
+ current_task = event.metadata.get("current_task_description")
479
+ if current_task:
480
+ state.status_line = f"Working on: {current_task}"
481
+
482
+ # === Dispatch table ===
483
+
484
+ _EVENT_DISPATCH: ClassVar[dict | None] = None
485
+
486
+ @classmethod
487
+ def _get_event_dispatch(cls) -> dict:
488
+ """Get or build the EventType → handler dispatch table."""
489
+ if cls._EVENT_DISPATCH is None:
490
+ from agentic_cli.workflow import EventType
491
+
492
+ cls._EVENT_DISPATCH = {
493
+ EventType.TEXT: cls._handle_text,
494
+ EventType.THINKING: cls._handle_thinking,
495
+ EventType.TOOL_CALL: cls._handle_tool_call,
496
+ EventType.TOOL_RESULT: cls._handle_tool_result,
497
+ EventType.USER_INPUT_REQUIRED: cls._handle_user_input_required,
498
+ EventType.CODE_EXECUTION: cls._handle_code_execution,
499
+ EventType.EXECUTABLE_CODE: cls._handle_executable_code,
500
+ EventType.FILE_DATA: cls._handle_file_data,
501
+ EventType.TASK_PROGRESS: cls._handle_task_progress,
502
+ }
503
+ return cls._EVENT_DISPATCH
@@ -28,6 +28,20 @@ logger = Loggers.cli()
28
28
  _init_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="workflow-init")
29
29
 
30
30
 
31
+ def _is_claude_model(model: str | None) -> bool:
32
+ """Check if a model string refers to a Claude (Anthropic) model."""
33
+ return model is not None and model.startswith("claude-")
34
+
35
+
36
+ def _resolve_effective_model(
37
+ model: str | None, settings: "BaseSettings"
38
+ ) -> str | None:
39
+ """Resolve the effective model from explicit override or settings."""
40
+ if model:
41
+ return model
42
+ return getattr(settings, "default_model", None)
43
+
44
+
31
45
  def create_workflow_manager_from_settings(
32
46
  agent_configs: list["AgentConfig"],
33
47
  settings: "BaseSettings",
@@ -38,7 +52,9 @@ def create_workflow_manager_from_settings(
38
52
  """Factory function to create the appropriate workflow manager based on settings.
39
53
 
40
54
  Creates either a GoogleADKWorkflowManager or LangGraphWorkflowManager
41
- based on the settings.orchestrator configuration.
55
+ based on the settings.orchestrator configuration. Claude models are
56
+ automatically routed to LangGraph because ADK's LiteLLM adapter has
57
+ critical issues with tool calling, thinking, and streaming.
42
58
 
43
59
  Args:
44
60
  agent_configs: List of agent configurations.
@@ -61,8 +77,19 @@ def create_workflow_manager_from_settings(
61
77
  from agentic_cli.workflow.settings import OrchestratorType
62
78
 
63
79
  orchestrator = getattr(settings, "orchestrator", OrchestratorType.ADK)
80
+ effective_model = _resolve_effective_model(model, settings)
81
+ use_langgraph = orchestrator == OrchestratorType.LANGGRAPH or _is_claude_model(
82
+ effective_model
83
+ )
84
+
85
+ if use_langgraph and _is_claude_model(effective_model) and orchestrator != OrchestratorType.LANGGRAPH:
86
+ logger.info(
87
+ "auto_switching_to_langgraph",
88
+ model=effective_model,
89
+ reason="Claude models require LangGraph orchestrator (ADK LiteLLM adapter has critical issues)",
90
+ )
64
91
 
65
- if orchestrator == OrchestratorType.LANGGRAPH:
92
+ if use_langgraph:
66
93
  try:
67
94
  from agentic_cli.workflow.langgraph import LangGraphWorkflowManager
68
95
 
@@ -77,13 +104,13 @@ def create_workflow_manager_from_settings(
77
104
  )
78
105
  except ImportError as e:
79
106
  raise ImportError(
80
- f"LangGraph orchestrator selected but dependencies not installed. "
107
+ f"LangGraph orchestrator required but dependencies not installed. "
81
108
  f"Install with: pip install agentic-cli[langgraph]\n"
82
109
  f"Original error: {e}"
83
110
  ) from e
84
111
 
85
112
  else: # Default to ADK
86
- from agentic_cli.workflow.adk_manager import GoogleADKWorkflowManager
113
+ from agentic_cli.workflow.adk.manager import GoogleADKWorkflowManager
87
114
 
88
115
  return GoogleADKWorkflowManager(
89
116
  agent_configs=agent_configs,
@@ -129,15 +156,15 @@ class WorkflowController:
129
156
  settings: Application settings instance
130
157
  """
131
158
  self._settings = settings
159
+ self._agent_configs = agent_configs
160
+ self._app_name = settings.app_name
132
161
 
133
162
  # Closure captures agent_configs for lazy creation (used by _background_init)
134
- app_name = settings.app_name
135
-
136
163
  def _create_workflow() -> "BaseWorkflowManager":
137
164
  return create_workflow_manager_from_settings(
138
165
  agent_configs=agent_configs,
139
166
  settings=settings,
140
- app_name=app_name,
167
+ app_name=self._app_name,
141
168
  )
142
169
 
143
170
  self._create_fn = _create_workflow
@@ -250,9 +277,35 @@ class WorkflowController:
250
277
 
251
278
  return self._workflow is not None
252
279
 
280
+ def _needs_orchestrator_swap(self, new_model: str | None) -> bool:
281
+ """Check if switching to new_model requires a different orchestrator.
282
+
283
+ Returns True when the model family changes (e.g. Gemini → Claude or
284
+ Claude → Gemini) and the current manager type doesn't match what the
285
+ factory would create for the new model.
286
+ """
287
+ if new_model is None or self._workflow is None:
288
+ return False
289
+
290
+ from agentic_cli.workflow.settings import OrchestratorType
291
+
292
+ orchestrator = getattr(self._settings, "orchestrator", OrchestratorType.ADK)
293
+ new_needs_langgraph = orchestrator == OrchestratorType.LANGGRAPH or _is_claude_model(new_model)
294
+
295
+ # Check current manager type by class name to avoid isinstance issues
296
+ # when the class may not be importable (LangGraph not installed)
297
+ current_cls_name = type(self._workflow).__name__
298
+ current_is_langgraph = current_cls_name == "LangGraphWorkflowManager"
299
+
300
+ return new_needs_langgraph != current_is_langgraph
301
+
253
302
  async def reinitialize(self, model: str | None = None) -> None:
254
303
  """Reinitialize the workflow with optional new model.
255
304
 
305
+ If the new model requires a different orchestrator (e.g. switching from
306
+ Gemini on ADK to Claude on LangGraph), the entire workflow manager is
307
+ replaced. Otherwise, the existing manager is reinitalized in place.
308
+
256
309
  Args:
257
310
  model: Optional new model to use
258
311
 
@@ -263,7 +316,21 @@ class WorkflowController:
263
316
  if self._workflow is None:
264
317
  raise RuntimeError("Cannot reinitialize - workflow not initialized")
265
318
 
266
- await self._workflow.reinitialize(model=model, preserve_sessions=True)
319
+ if self._needs_orchestrator_swap(model):
320
+ logger.info(
321
+ "orchestrator_swap",
322
+ old_model=self._workflow.model,
323
+ new_model=model,
324
+ )
325
+ self._workflow = create_workflow_manager_from_settings(
326
+ agent_configs=self._agent_configs,
327
+ settings=self._settings,
328
+ app_name=self._app_name,
329
+ model=model,
330
+ )
331
+ await self._workflow.initialize_services()
332
+ else:
333
+ await self._workflow.reinitialize(model=model, preserve_sessions=True)
267
334
 
268
335
  async def cancel_init(self) -> None:
269
336
  """Cancel pending initialization task if still running."""
@@ -10,3 +10,12 @@ def truncate(text: str, max_length: int = CONTENT_PREVIEW_LENGTH) -> str:
10
10
  if len(text) > max_length:
11
11
  return text[:max_length] + "..."
12
12
  return text
13
+
14
+
15
+ def format_size(size_bytes: int) -> str:
16
+ """Format byte count as human-readable string (B, KB, MB)."""
17
+ if size_bytes < 1024:
18
+ return f"{size_bytes}B"
19
+ elif size_bytes < 1024 * 1024:
20
+ return f"{size_bytes / 1024:.1f}KB"
21
+ return f"{size_bytes / (1024 * 1024):.1f}MB"