kader 0.1.6__tar.gz → 1.0.0__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 (94) hide show
  1. {kader-0.1.6 → kader-1.0.0}/PKG-INFO +38 -1
  2. {kader-0.1.6 → kader-1.0.0}/README.md +36 -0
  3. {kader-0.1.6 → kader-1.0.0}/cli/app.py +98 -30
  4. {kader-0.1.6 → kader-1.0.0}/cli/app.tcss +20 -0
  5. {kader-0.1.6 → kader-1.0.0}/cli/utils.py +1 -1
  6. kader-1.0.0/cli/widgets/conversation.py +101 -0
  7. kader-1.0.0/examples/planner_executor_example.py +74 -0
  8. {kader-0.1.6 → kader-1.0.0}/examples/tools_example.py +63 -0
  9. {kader-0.1.6 → kader-1.0.0}/kader/__init__.py +2 -0
  10. {kader-0.1.6 → kader-1.0.0}/kader/agent/agents.py +8 -0
  11. {kader-0.1.6 → kader-1.0.0}/kader/agent/base.py +68 -5
  12. {kader-0.1.6 → kader-1.0.0}/kader/memory/types.py +60 -0
  13. kader-1.0.0/kader/prompts/__init__.py +17 -0
  14. kader-1.0.0/kader/prompts/agent_prompts.py +55 -0
  15. kader-1.0.0/kader/prompts/templates/executor_agent.j2 +70 -0
  16. kader-1.0.0/kader/prompts/templates/kader_planner.j2 +71 -0
  17. {kader-0.1.6 → kader-1.0.0}/kader/providers/ollama.py +2 -2
  18. {kader-0.1.6 → kader-1.0.0}/kader/tools/__init__.py +26 -0
  19. kader-1.0.0/kader/tools/agent.py +452 -0
  20. {kader-0.1.6 → kader-1.0.0}/kader/tools/filesys.py +1 -1
  21. {kader-0.1.6 → kader-1.0.0}/kader/tools/todo.py +43 -2
  22. kader-1.0.0/kader/utils/__init__.py +10 -0
  23. kader-1.0.0/kader/utils/checkpointer.py +371 -0
  24. kader-1.0.0/kader/utils/context_aggregator.py +347 -0
  25. kader-1.0.0/kader/workflows/__init__.py +13 -0
  26. kader-1.0.0/kader/workflows/base.py +71 -0
  27. kader-1.0.0/kader/workflows/planner_executor.py +251 -0
  28. {kader-0.1.6 → kader-1.0.0}/pyproject.toml +2 -1
  29. kader-1.0.0/tests/conftest.py +50 -0
  30. {kader-0.1.6 → kader-1.0.0}/tests/test_agent_logger_integration.py +21 -11
  31. {kader-0.1.6 → kader-1.0.0}/tests/test_todo_tool.py +58 -5
  32. kader-1.0.0/tests/tools/test_agent_tool.py +275 -0
  33. kader-1.0.0/tests/tools/test_agent_tool_persistence.py +213 -0
  34. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_filesystem_tools.py +3 -3
  35. {kader-0.1.6 → kader-1.0.0}/uv.lock +12 -1
  36. kader-0.1.6/cli/widgets/conversation.py +0 -55
  37. kader-0.1.6/kader/prompts/__init__.py +0 -9
  38. kader-0.1.6/kader/prompts/agent_prompts.py +0 -27
  39. kader-0.1.6/tests/conftest.py +0 -14
  40. {kader-0.1.6 → kader-1.0.0}/.github/workflows/ci.yml +0 -0
  41. {kader-0.1.6 → kader-1.0.0}/.github/workflows/release.yml +0 -0
  42. {kader-0.1.6 → kader-1.0.0}/.gitignore +0 -0
  43. {kader-0.1.6 → kader-1.0.0}/.python-version +0 -0
  44. {kader-0.1.6 → kader-1.0.0}/.qwen/QWEN.md +0 -0
  45. {kader-0.1.6 → kader-1.0.0}/.qwen/agents/technical-writer.md +0 -0
  46. {kader-0.1.6 → kader-1.0.0}/.qwen/agents/test-automation-specialist.md +0 -0
  47. {kader-0.1.6 → kader-1.0.0}/cli/README.md +0 -0
  48. {kader-0.1.6 → kader-1.0.0}/cli/__init__.py +0 -0
  49. {kader-0.1.6 → kader-1.0.0}/cli/__main__.py +0 -0
  50. {kader-0.1.6 → kader-1.0.0}/cli/widgets/__init__.py +0 -0
  51. {kader-0.1.6 → kader-1.0.0}/cli/widgets/confirmation.py +0 -0
  52. {kader-0.1.6 → kader-1.0.0}/cli/widgets/loading.py +0 -0
  53. {kader-0.1.6 → kader-1.0.0}/examples/.gitignore +0 -0
  54. {kader-0.1.6 → kader-1.0.0}/examples/README.md +0 -0
  55. {kader-0.1.6 → kader-1.0.0}/examples/memory_example.py +0 -0
  56. {kader-0.1.6 → kader-1.0.0}/examples/ollama_example.py +0 -0
  57. {kader-0.1.6 → kader-1.0.0}/examples/planning_agent_example.py +0 -0
  58. {kader-0.1.6 → kader-1.0.0}/examples/python_developer/main.py +0 -0
  59. {kader-0.1.6 → kader-1.0.0}/examples/python_developer/template.yaml +0 -0
  60. {kader-0.1.6 → kader-1.0.0}/examples/react_agent_example.py +0 -0
  61. {kader-0.1.6 → kader-1.0.0}/examples/simple_agent.py +0 -0
  62. {kader-0.1.6 → kader-1.0.0}/examples/todo_agent/main.py +0 -0
  63. {kader-0.1.6 → kader-1.0.0}/kader/agent/__init__.py +0 -0
  64. {kader-0.1.6 → kader-1.0.0}/kader/agent/logger.py +0 -0
  65. {kader-0.1.6 → kader-1.0.0}/kader/config.py +0 -0
  66. {kader-0.1.6 → kader-1.0.0}/kader/memory/__init__.py +0 -0
  67. {kader-0.1.6 → kader-1.0.0}/kader/memory/conversation.py +0 -0
  68. {kader-0.1.6 → kader-1.0.0}/kader/memory/session.py +0 -0
  69. {kader-0.1.6 → kader-1.0.0}/kader/memory/state.py +0 -0
  70. {kader-0.1.6 → kader-1.0.0}/kader/prompts/base.py +0 -0
  71. {kader-0.1.6 → kader-1.0.0}/kader/prompts/templates/planning_agent.j2 +0 -0
  72. {kader-0.1.6 → kader-1.0.0}/kader/prompts/templates/react_agent.j2 +0 -0
  73. {kader-0.1.6 → kader-1.0.0}/kader/providers/__init__.py +0 -0
  74. {kader-0.1.6 → kader-1.0.0}/kader/providers/base.py +0 -0
  75. {kader-0.1.6 → kader-1.0.0}/kader/providers/mock.py +0 -0
  76. {kader-0.1.6 → kader-1.0.0}/kader/tools/README.md +0 -0
  77. {kader-0.1.6 → kader-1.0.0}/kader/tools/base.py +0 -0
  78. {kader-0.1.6 → kader-1.0.0}/kader/tools/exec_commands.py +0 -0
  79. {kader-0.1.6 → kader-1.0.0}/kader/tools/filesystem.py +0 -0
  80. {kader-0.1.6 → kader-1.0.0}/kader/tools/protocol.py +0 -0
  81. {kader-0.1.6 → kader-1.0.0}/kader/tools/rag.py +0 -0
  82. {kader-0.1.6 → kader-1.0.0}/kader/tools/utils.py +0 -0
  83. {kader-0.1.6 → kader-1.0.0}/kader/tools/web.py +0 -0
  84. {kader-0.1.6 → kader-1.0.0}/tests/providers/test_mock.py +0 -0
  85. {kader-0.1.6 → kader-1.0.0}/tests/providers/test_ollama.py +0 -0
  86. {kader-0.1.6 → kader-1.0.0}/tests/providers/test_providers_base.py +0 -0
  87. {kader-0.1.6 → kader-1.0.0}/tests/test_agent_logger.py +0 -0
  88. {kader-0.1.6 → kader-1.0.0}/tests/test_base_agent.py +0 -0
  89. {kader-0.1.6 → kader-1.0.0}/tests/test_file_memory.py +0 -0
  90. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_exec_commands.py +0 -0
  91. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_filesys_tools.py +0 -0
  92. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_rag.py +0 -0
  93. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_tools_base.py +0 -0
  94. {kader-0.1.6 → kader-1.0.0}/tests/tools/test_web.py +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kader
3
- Version: 0.1.6
3
+ Version: 1.0.0
4
4
  Summary: kader coding agent
5
5
  Requires-Python: >=3.11
6
+ Requires-Dist: aiofiles>=25.1.0
6
7
  Requires-Dist: faiss-cpu>=1.9.0
7
8
  Requires-Dist: jinja2>=3.1.6
8
9
  Requires-Dist: loguru>=0.7.3
@@ -32,6 +33,7 @@ Kader is an intelligent coding agent designed to assist with software developmen
32
33
  - 🔄 **ReAct Agent Framework** - Reasoning and Acting agent architecture
33
34
  - 🗂️ **File System Tools** - Read, write, search, and edit files
34
35
  - 🔍 **Planning Agent** - Task planning and execution capabilities
36
+ - 🤝 **Agent-As-Tool** - Spawn sub-agents for specific tasks with isolated memory
35
37
 
36
38
  ## Installation
37
39
 
@@ -177,6 +179,40 @@ Kader provides several agent types:
177
179
  - **PlanningAgent**: Agent that plans multi-step tasks
178
180
  - **BaseAgent**: Base agent class for creating custom agents
179
181
 
182
+ ### Agent-As-Tool (AgentTool)
183
+
184
+ The `AgentTool` allows you to wrap a `ReActAgent` as a callable tool, enabling agents to spawn sub-agents for specific tasks with isolated memory contexts.
185
+
186
+ ```python
187
+ from kader.tools import AgentTool
188
+
189
+ # Autonomous execution (runs without pausing for confirmation)
190
+ autonomous_agent = AgentTool(
191
+ name="research_agent",
192
+ description="Research topics autonomously",
193
+ interrupt_before_tool=False,
194
+ )
195
+ result = autonomous_agent.execute(task="Find info about topic X")
196
+
197
+ # Interactive execution (pauses for user confirmation before each tool)
198
+ def my_callback(tool_call_dict, llm_content=None):
199
+ user_input = input("Execute? [y/n]: ")
200
+ return (user_input.lower() == 'y', None)
201
+
202
+ interactive_agent = AgentTool(
203
+ name="interactive_agent",
204
+ interrupt_before_tool=True,
205
+ tool_confirmation_callback=my_callback,
206
+ )
207
+ result = interactive_agent.execute(task="Analyze data and generate report")
208
+ ```
209
+
210
+ **Key Features:**
211
+ - Each sub-agent has isolated memory (separate `SlidingWindowConversationManager`)
212
+ - Default tools included: filesystem, web search, command executor
213
+ - Optional `interrupt_before_tool` for user confirmation before tool execution
214
+ - Task completes when the agent returns its final response
215
+
180
216
  ### Memory Management
181
217
 
182
218
  Kader's memory system includes:
@@ -194,6 +230,7 @@ Kader includes a rich set of tools:
194
230
  - **Command Executor**: Execute shell commands safely
195
231
  - **Web Tools**: Search and fetch web content
196
232
  - **RAG Tools**: Retrieval Augmented Generation capabilities
233
+ - **AgentTool**: Spawn sub-agents for specific tasks
197
234
 
198
235
  ## Examples
199
236
 
@@ -15,6 +15,7 @@ Kader is an intelligent coding agent designed to assist with software developmen
15
15
  - 🔄 **ReAct Agent Framework** - Reasoning and Acting agent architecture
16
16
  - 🗂️ **File System Tools** - Read, write, search, and edit files
17
17
  - 🔍 **Planning Agent** - Task planning and execution capabilities
18
+ - 🤝 **Agent-As-Tool** - Spawn sub-agents for specific tasks with isolated memory
18
19
 
19
20
  ## Installation
20
21
 
@@ -160,6 +161,40 @@ Kader provides several agent types:
160
161
  - **PlanningAgent**: Agent that plans multi-step tasks
161
162
  - **BaseAgent**: Base agent class for creating custom agents
162
163
 
164
+ ### Agent-As-Tool (AgentTool)
165
+
166
+ The `AgentTool` allows you to wrap a `ReActAgent` as a callable tool, enabling agents to spawn sub-agents for specific tasks with isolated memory contexts.
167
+
168
+ ```python
169
+ from kader.tools import AgentTool
170
+
171
+ # Autonomous execution (runs without pausing for confirmation)
172
+ autonomous_agent = AgentTool(
173
+ name="research_agent",
174
+ description="Research topics autonomously",
175
+ interrupt_before_tool=False,
176
+ )
177
+ result = autonomous_agent.execute(task="Find info about topic X")
178
+
179
+ # Interactive execution (pauses for user confirmation before each tool)
180
+ def my_callback(tool_call_dict, llm_content=None):
181
+ user_input = input("Execute? [y/n]: ")
182
+ return (user_input.lower() == 'y', None)
183
+
184
+ interactive_agent = AgentTool(
185
+ name="interactive_agent",
186
+ interrupt_before_tool=True,
187
+ tool_confirmation_callback=my_callback,
188
+ )
189
+ result = interactive_agent.execute(task="Analyze data and generate report")
190
+ ```
191
+
192
+ **Key Features:**
193
+ - Each sub-agent has isolated memory (separate `SlidingWindowConversationManager`)
194
+ - Default tools included: filesystem, web search, command executor
195
+ - Optional `interrupt_before_tool` for user confirmation before tool execution
196
+ - Task completes when the agent returns its final response
197
+
163
198
  ### Memory Management
164
199
 
165
200
  Kader's memory system includes:
@@ -177,6 +212,7 @@ Kader includes a rich set of tools:
177
212
  - **Command Executor**: Execute shell commands safely
178
213
  - **Web Tools**: Search and fetch web content
179
214
  - **RAG Tools**: Retrieval Augmented Generation capabilities
215
+ - **AgentTool**: Spawn sub-agents for specific tasks
180
216
 
181
217
  ## Examples
182
218
 
@@ -1,7 +1,9 @@
1
1
  """Kader CLI - Modern Vibe Coding CLI with Textual."""
2
2
 
3
3
  import asyncio
4
+ import atexit
4
5
  import threading
6
+ from concurrent.futures import ThreadPoolExecutor
5
7
  from importlib.metadata import version as get_version
6
8
  from pathlib import Path
7
9
  from typing import Optional
@@ -18,13 +20,11 @@ from textual.widgets import (
18
20
  Tree,
19
21
  )
20
22
 
21
- from kader.agent.agents import ReActAgent
22
23
  from kader.memory import (
23
24
  FileSessionManager,
24
25
  MemoryConfig,
25
- SlidingWindowConversationManager,
26
26
  )
27
- from kader.tools import get_default_registry
27
+ from kader.workflows import PlannerExecutorWorkflow
28
28
 
29
29
  from .utils import (
30
30
  DEFAULT_MODEL,
@@ -103,22 +103,79 @@ class KaderApp(App):
103
103
  self._model_selector: Optional[ModelSelector] = None
104
104
  self._update_info: Optional[str] = None # Latest version if update available
105
105
 
106
- self._agent = self._create_agent(self._current_model)
106
+ # Dedicated thread pool for agent invocation (isolated from default pool)
107
+ self._agent_executor = ThreadPoolExecutor(
108
+ max_workers=2, thread_name_prefix="kader_agent"
109
+ )
110
+ # Ensure executor is properly shut down on exit
111
+ atexit.register(self._agent_executor.shutdown, wait=False)
112
+
113
+ self._workflow = self._create_workflow(self._current_model)
107
114
 
108
- def _create_agent(self, model_name: str) -> ReActAgent:
109
- """Create a new ReActAgent with the specified model."""
110
- registry = get_default_registry()
111
- memory = SlidingWindowConversationManager(window_size=10)
112
- return ReActAgent(
115
+ def _create_workflow(self, model_name: str) -> PlannerExecutorWorkflow:
116
+ """Create a new PlannerExecutorWorkflow with the specified model."""
117
+ return PlannerExecutorWorkflow(
113
118
  name="kader_cli",
114
- tools=registry,
115
- memory=memory,
116
119
  model_name=model_name,
117
- use_persistence=True,
118
120
  interrupt_before_tool=True,
119
121
  tool_confirmation_callback=self._tool_confirmation_callback,
122
+ direct_execution_callback=self._direct_execution_callback,
123
+ tool_execution_result_callback=self._tool_execution_result_callback,
124
+ use_persistence=True,
125
+ executor_names=["executor"],
126
+ )
127
+
128
+ def _direct_execution_callback(self, message: str, tool_name: str) -> None:
129
+ """
130
+ Callback for direct execution tools - called from agent thread.
131
+
132
+ Shows a message in the conversation view without blocking for confirmation.
133
+ """
134
+ # Schedule message display on main thread
135
+ self.call_from_thread(self._show_direct_execution_message, message, tool_name)
136
+
137
+ def _show_direct_execution_message(self, message: str, tool_name: str) -> None:
138
+ """Show a direct execution message in the conversation view."""
139
+ try:
140
+ conversation = self.query_one("#conversation-view", ConversationView)
141
+ # User-friendly message showing the tool is executing
142
+ friendly_message = f"[>] Executing {tool_name}..."
143
+ conversation.add_message(friendly_message, "assistant")
144
+ conversation.scroll_end()
145
+ except Exception:
146
+ pass
147
+
148
+ def _tool_execution_result_callback(
149
+ self, tool_name: str, success: bool, result: str
150
+ ) -> None:
151
+ """
152
+ Callback for tool execution results - called from agent thread.
153
+
154
+ Updates the conversation view with the execution result.
155
+ """
156
+ # Schedule result display on main thread
157
+ self.call_from_thread(
158
+ self._show_tool_execution_result, tool_name, success, result
120
159
  )
121
160
 
161
+ def _show_tool_execution_result(
162
+ self, tool_name: str, success: bool, result: str
163
+ ) -> None:
164
+ """Show the tool execution result in the conversation view."""
165
+ try:
166
+ conversation = self.query_one("#conversation-view", ConversationView)
167
+ if success:
168
+ # User-friendly success message
169
+ friendly_message = f"(+) {tool_name} completed successfully"
170
+ else:
171
+ # User-friendly error message with truncated result
172
+ error_preview = result[:100] + "..." if len(result) > 100 else result
173
+ friendly_message = f"(-) {tool_name} failed: {error_preview}"
174
+ conversation.add_message(friendly_message, "assistant")
175
+ conversation.scroll_end()
176
+ except Exception:
177
+ pass
178
+
122
179
  def _tool_confirmation_callback(self, message: str) -> tuple[bool, Optional[str]]:
123
180
  """
124
181
  Callback for tool confirmation - called from agent thread.
@@ -135,7 +192,10 @@ class KaderApp(App):
135
192
 
136
193
  # Wait for user response (blocking in agent thread)
137
194
  # This is safe because we're in a background thread
138
- self._confirmation_event.wait()
195
+ # Timeout after 5 minutes to prevent indefinite blocking
196
+ if not self._confirmation_event.wait(timeout=300):
197
+ # Timeout occurred - decline tool execution gracefully
198
+ return (False, "Tool confirmation timed out after 5 minutes")
139
199
 
140
200
  # Return the result
141
201
  return self._confirmation_result
@@ -183,7 +243,8 @@ class KaderApp(App):
183
243
  if event.confirmed:
184
244
  if tool_message:
185
245
  conversation.add_message(tool_message, "assistant")
186
- conversation.add_message("(+) Executing tool...", "assistant")
246
+ # Show executing message - will be updated by result callback
247
+ conversation.add_message("[>] Executing tool...", "assistant")
187
248
  # Restart spinner
188
249
  try:
189
250
  spinner = self.query_one(LoadingSpinner)
@@ -249,7 +310,7 @@ class KaderApp(App):
249
310
  # Update model and recreate agent
250
311
  old_model = self._current_model
251
312
  self._current_model = event.model
252
- self._agent = self._create_agent(self._current_model)
313
+ self._workflow = self._create_workflow(self._current_model)
253
314
 
254
315
  conversation.add_message(
255
316
  f"(+) Model changed from `{old_model}` to `{self._current_model}`",
@@ -431,8 +492,8 @@ Please resize your terminal."""
431
492
  await self._show_model_selector(conversation)
432
493
  elif cmd == "/clear":
433
494
  conversation.clear_messages()
434
- self._agent.memory.clear()
435
- self._agent.provider.reset_tracking() # Reset usage/cost tracking
495
+ self._workflow.planner.memory.clear()
496
+ self._workflow.planner.provider.reset_tracking() # Reset usage/cost tracking
436
497
  self._current_session_id = None
437
498
  self.notify("Conversation cleared!", severity="information")
438
499
  elif cmd == "/save":
@@ -462,7 +523,7 @@ Please resize your terminal."""
462
523
  )
463
524
 
464
525
  async def _handle_chat(self, message: str) -> None:
465
- """Handle regular chat messages with ReActAgent."""
526
+ """Handle regular chat messages with PlannerExecutorWorkflow."""
466
527
  if self._is_processing:
467
528
  self.notify("Please wait for the current response...", severity="warning")
468
529
  return
@@ -490,16 +551,21 @@ Please resize your terminal."""
490
551
  spinner = self.query_one(LoadingSpinner)
491
552
 
492
553
  try:
493
- # Run the agent invoke in a thread
554
+ # Run the workflow in a dedicated thread pool
494
555
  loop = asyncio.get_event_loop()
495
556
  response = await loop.run_in_executor(
496
- None, lambda: self._agent.invoke(message)
557
+ self._agent_executor, lambda: self._workflow.run(message)
497
558
  )
498
559
 
499
560
  # Hide spinner and show response (this runs on main thread via await)
500
561
  spinner.stop()
501
- if response and response.content:
502
- conversation.add_message(response.content, "assistant")
562
+ if response:
563
+ conversation.add_message(
564
+ response,
565
+ "assistant",
566
+ model_name=self._workflow.planner.provider.model,
567
+ usage_cost=self._workflow.planner.provider.total_cost.total_cost,
568
+ )
503
569
 
504
570
  except Exception as e:
505
571
  spinner.stop()
@@ -516,7 +582,7 @@ Please resize your terminal."""
516
582
  """Clear the conversation (Ctrl+L)."""
517
583
  conversation = self.query_one("#conversation-view", ConversationView)
518
584
  conversation.clear_messages()
519
- self._agent.memory.clear()
585
+ self._workflow.planner.memory.clear()
520
586
  self.notify("Conversation cleared!", severity="information")
521
587
 
522
588
  def action_save_session(self) -> None:
@@ -548,8 +614,10 @@ Please resize your terminal."""
548
614
  session = self._session_manager.create_session("kader_cli")
549
615
  self._current_session_id = session.session_id
550
616
 
551
- # Get messages from agent memory and save
552
- messages = [msg.message for msg in self._agent.memory.get_messages()]
617
+ # Get messages from planner memory and save
618
+ messages = [
619
+ msg.message for msg in self._workflow.planner.memory.get_messages()
620
+ ]
553
621
  self._session_manager.save_conversation(self._current_session_id, messages)
554
622
 
555
623
  conversation.add_message(
@@ -580,11 +648,11 @@ Please resize your terminal."""
580
648
 
581
649
  # Clear current state
582
650
  conversation.clear_messages()
583
- self._agent.memory.clear()
651
+ self._workflow.planner.memory.clear()
584
652
 
585
653
  # Add loaded messages to memory and UI
586
654
  for msg in messages:
587
- self._agent.memory.add_message(msg)
655
+ self._workflow.planner.memory.add_message(msg)
588
656
  role = msg.get("role", "user")
589
657
  content = msg.get("content", "")
590
658
  if role in ["user", "assistant"] and content:
@@ -633,9 +701,9 @@ Please resize your terminal."""
633
701
  """Display LLM usage costs."""
634
702
  try:
635
703
  # Get cost and usage from the provider
636
- cost = self._agent.provider.total_cost
637
- usage = self._agent.provider.total_usage
638
- model = self._agent.provider.model
704
+ cost = self._workflow.planner.provider.total_cost
705
+ usage = self._workflow.planner.provider.total_usage
706
+ model = self._workflow.planner.provider.model
639
707
 
640
708
  lines = [
641
709
  "## Usage Costs ($)\n",
@@ -132,6 +132,26 @@ ConversationView {
132
132
  scrollbar-size: 1 1;
133
133
  }
134
134
 
135
+ .message-footer {
136
+ height: auto;
137
+ margin-top: 0;
138
+ padding: 0 1;
139
+ border-top: none;
140
+ }
141
+
142
+ .footer-left {
143
+ color: $secondary;
144
+ text-style: italic;
145
+ width: 1fr;
146
+ }
147
+
148
+ .footer-right {
149
+ color: $success;
150
+ text-style: bold;
151
+ text-align: right;
152
+ width: auto;
153
+ }
154
+
135
155
  /* ===== Welcome Message ===== */
136
156
 
137
157
  #welcome {
@@ -3,7 +3,7 @@
3
3
  from kader.providers import OllamaProvider
4
4
 
5
5
  # Default model
6
- DEFAULT_MODEL = "qwen3-coder:480b-cloud"
6
+ DEFAULT_MODEL = "kimi-k2.5:cloud"
7
7
 
8
8
  HELP_TEXT = """## Kader CLI Commands
9
9
 
@@ -0,0 +1,101 @@
1
+ """Conversation display widget for Kader CLI."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Horizontal, VerticalScroll
5
+ from textual.widgets import Markdown, Static
6
+
7
+
8
+ class Message(Static):
9
+ """A single message in the conversation."""
10
+
11
+ def __init__(
12
+ self,
13
+ content: str,
14
+ role: str = "user",
15
+ model_name: str | None = None,
16
+ usage_cost: float | None = None,
17
+ ) -> None:
18
+ super().__init__()
19
+ self.content = content
20
+ self.role = role
21
+ self.model_name = model_name
22
+ self.usage_cost = usage_cost
23
+ self.add_class(f"message-{role}")
24
+
25
+ def compose(self) -> ComposeResult:
26
+ prefix = "(**) **You:**" if self.role == "user" else "(^^) **Kader:**"
27
+ yield Markdown(f"{prefix}\n\n{self.content}")
28
+
29
+ if self.role == "assistant" and (
30
+ self.model_name or self.usage_cost is not None
31
+ ):
32
+ with Horizontal(classes="message-footer"):
33
+ model_label = f"[*] {self.model_name}" if self.model_name else ""
34
+ yield Static(model_label, classes="footer-left")
35
+
36
+ usage_label = (
37
+ f"($) {self.usage_cost:.6f}" if self.usage_cost is not None else ""
38
+ )
39
+ yield Static(usage_label, classes="footer-right")
40
+
41
+
42
+ class ConversationView(VerticalScroll):
43
+ """Scrollable conversation history with markdown rendering."""
44
+
45
+ DEFAULT_CSS = """
46
+ ConversationView {
47
+ padding: 1 2;
48
+ }
49
+
50
+ ConversationView Message {
51
+ margin-bottom: 1;
52
+ padding: 1;
53
+ }
54
+
55
+ ConversationView .message-user {
56
+ background: $surface;
57
+ border-left: thick $primary;
58
+ }
59
+
60
+ ConversationView .message-assistant {
61
+ background: $surface-darken-1;
62
+ border-left: thick $success;
63
+ }
64
+
65
+ .message-footer {
66
+ height: auto;
67
+ margin-top: 0;
68
+ padding: 0 1;
69
+ border-top: none;
70
+ }
71
+
72
+ .footer-left {
73
+ color: $secondary;
74
+ text-style: italic;
75
+ width: 1fr;
76
+ }
77
+
78
+ .footer-right {
79
+ color: $success;
80
+ text-style: bold;
81
+ text-align: right;
82
+ width: auto;
83
+ }
84
+ """
85
+
86
+ def add_message(
87
+ self,
88
+ content: str,
89
+ role: str = "user",
90
+ model_name: str | None = None,
91
+ usage_cost: float | None = None,
92
+ ) -> None:
93
+ """Add a message to the conversation."""
94
+ message = Message(content, role, model_name, usage_cost)
95
+ self.mount(message)
96
+ self.scroll_end(animate=True)
97
+
98
+ def clear_messages(self) -> None:
99
+ """Clear all messages from the conversation."""
100
+ for child in self.query(Message):
101
+ child.remove()
@@ -0,0 +1,74 @@
1
+ """
2
+ Planner-Executor Workflow Example.
3
+
4
+ Demonstrates the PlannerExecutorWorkflow that uses a PlanningAgent with
5
+ TodoTool for task management and AgentTool for sub-task delegation.
6
+ """
7
+
8
+ import asyncio
9
+ import os
10
+ import sys
11
+
12
+ # Add project root to path for direct execution
13
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
14
+
15
+ from kader.workflows import PlannerExecutorWorkflow
16
+
17
+
18
+ async def main():
19
+ print("=== Planner-Executor Workflow Demo ===")
20
+ print("This workflow uses a PlanningAgent to:")
21
+ print(" 1. Create a plan using TodoTool")
22
+ print(" 2. Delegate sub-tasks to executor agents")
23
+ print(" 3. Track progress and update todo status")
24
+ print("\nType '/exit' or '/close' to quit.\n")
25
+
26
+ # Initialize the workflow
27
+ # Note: The planner (TodoTool, AgentTool) executes without interruption
28
+ # Sub-agents spawned by AgentTool will respect interrupt_before_tool setting
29
+ workflow = PlannerExecutorWorkflow(
30
+ name="demo_workflow",
31
+ model_name="qwen3-coder:480b-cloud",
32
+ interrupt_before_tool=True, # Applies to sub-agents inside AgentTool
33
+ use_persistence=True,
34
+ executor_names=["code_executor", "research_executor"],
35
+ )
36
+
37
+ print(f"Workflow '{workflow.name}' initialized")
38
+ print(f"Planner session ID: {workflow.planner.session_id}")
39
+ print(f"Available tools: {list(workflow.planner.tools_map.keys())}\n")
40
+
41
+ # Interactive Loop
42
+ while True:
43
+ try:
44
+ user_input = input("\nYou: ").strip()
45
+
46
+ if not user_input:
47
+ continue
48
+
49
+ if user_input.lower() in ["/exit", "/close", "exit", "quit"]:
50
+ print("Goodbye!")
51
+ break
52
+
53
+ if user_input.lower() == "/reset":
54
+ workflow.reset()
55
+ print("Workflow reset. Fresh planner created.")
56
+ continue
57
+
58
+ # Run the workflow
59
+ print("\nPlanner is analyzing and creating a plan...")
60
+ try:
61
+ result = workflow.run(user_input)
62
+ print(f"\n[Workflow Result]:\n{result}\n")
63
+ except Exception as e:
64
+ print(f"\nError during workflow execution: {e}")
65
+
66
+ except KeyboardInterrupt:
67
+ print("\nGoodbye!")
68
+ break
69
+ except Exception as e:
70
+ print(f"\nAn error occurred: {e}")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ asyncio.run(main())
@@ -18,6 +18,8 @@ from pathlib import Path
18
18
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
19
19
 
20
20
  from kader.tools import (
21
+ # Agent Tool
22
+ AgentTool,
21
23
  # Command execution
22
24
  CommandExecutorTool,
23
25
  GrepTool,
@@ -371,6 +373,66 @@ def demo_tool_parameters():
371
373
  print(f"\nTool schema (first 200 chars): {str(schema)[:200]}...")
372
374
 
373
375
 
376
+ def demo_agent_tool():
377
+ """Demonstrate the AgentTool that wraps a ReActAgent."""
378
+ print("\n=== Agent Tool Demo ===")
379
+
380
+ # Example 1: Autonomous execution (no tool interrupts)
381
+ print("\n--- Autonomous AgentTool ---")
382
+ autonomous_agent = AgentTool(
383
+ name="research_agent",
384
+ description="An agent that can research topics and perform tasks autonomously",
385
+ interrupt_before_tool=False, # Default: runs without pausing
386
+ )
387
+
388
+ print(f"Tool created: {autonomous_agent.name}")
389
+ print(f"Description: {autonomous_agent.description}")
390
+ print(f"Category: {autonomous_agent.schema.category.value}")
391
+ print(f"Interrupt before tool: {autonomous_agent._interrupt_before_tool}")
392
+
393
+ # Show parameters
394
+ print("\nParameters:")
395
+ for param in autonomous_agent.schema.parameters:
396
+ print(
397
+ f" - {param.name}: {param.description} (type: {param.type}, required: {param.required})"
398
+ )
399
+
400
+ # Test interruption message
401
+ task = "Find the current stock price of AAPL and summarize the market trends"
402
+ msg = autonomous_agent.get_interruption_message(task=task)
403
+ print(f"\nInterruption message: {msg}")
404
+
405
+ # Example 2: Interactive execution with tool confirmation
406
+ print("\n--- Interactive AgentTool ---")
407
+
408
+ # Define a custom confirmation callback
409
+ def example_callback(tool_call_dict, llm_content=None):
410
+ """Example callback that would normally prompt the user."""
411
+ # In a real scenario, this would prompt the user
412
+ # Return (should_execute, optional_user_feedback)
413
+ return (True, None)
414
+
415
+ interactive_agent = AgentTool(
416
+ name="interactive_agent",
417
+ description="An agent that confirms each tool execution with the user",
418
+ interrupt_before_tool=True, # Pause before each tool
419
+ tool_confirmation_callback=example_callback,
420
+ )
421
+
422
+ print(f"Tool created: {interactive_agent.name}")
423
+ print(f"Interrupt before tool: {interactive_agent._interrupt_before_tool}")
424
+ print(
425
+ f"Callback provided: {interactive_agent._tool_confirmation_callback is not None}"
426
+ )
427
+
428
+ # Note: Actual execution would require LLM access
429
+ print("\n[Note] To actually run the agent, LLM access is required.")
430
+ print(
431
+ "When interrupt_before_tool=True, the agent pauses before each tool execution "
432
+ "for confirmation. The task completes only when the agent returns its final response."
433
+ )
434
+
435
+
374
436
  def main():
375
437
  """Run all tool demos."""
376
438
  print("Kader Tools Examples")
@@ -390,6 +452,7 @@ def main():
390
452
  demo_custom_tool()
391
453
  demo_async_operations()
392
454
  demo_tool_parameters()
455
+ demo_agent_tool()
393
456
 
394
457
  print("\n[OK] All tool demos completed!")
395
458
 
@@ -8,6 +8,7 @@ creating the .kader directory in the user's home directory.
8
8
  from .config import ENV_FILE_PATH, KADER_DIR, initialize_kader_config
9
9
  from .providers import * # noqa: F401, F403
10
10
  from .tools import * # noqa: F401, F403
11
+ from .utils import Checkpointer
11
12
 
12
13
  # Initialize the configuration when the module is imported
13
14
  initialize_kader_config()
@@ -18,5 +19,6 @@ __all__ = [
18
19
  "KADER_DIR",
19
20
  "ENV_FILE_PATH",
20
21
  "initialize_kader_config",
22
+ "Checkpointer",
21
23
  # Export everything from providers and tools
22
24
  ]