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.
- {kader-0.1.6 → kader-1.0.0}/PKG-INFO +38 -1
- {kader-0.1.6 → kader-1.0.0}/README.md +36 -0
- {kader-0.1.6 → kader-1.0.0}/cli/app.py +98 -30
- {kader-0.1.6 → kader-1.0.0}/cli/app.tcss +20 -0
- {kader-0.1.6 → kader-1.0.0}/cli/utils.py +1 -1
- kader-1.0.0/cli/widgets/conversation.py +101 -0
- kader-1.0.0/examples/planner_executor_example.py +74 -0
- {kader-0.1.6 → kader-1.0.0}/examples/tools_example.py +63 -0
- {kader-0.1.6 → kader-1.0.0}/kader/__init__.py +2 -0
- {kader-0.1.6 → kader-1.0.0}/kader/agent/agents.py +8 -0
- {kader-0.1.6 → kader-1.0.0}/kader/agent/base.py +68 -5
- {kader-0.1.6 → kader-1.0.0}/kader/memory/types.py +60 -0
- kader-1.0.0/kader/prompts/__init__.py +17 -0
- kader-1.0.0/kader/prompts/agent_prompts.py +55 -0
- kader-1.0.0/kader/prompts/templates/executor_agent.j2 +70 -0
- kader-1.0.0/kader/prompts/templates/kader_planner.j2 +71 -0
- {kader-0.1.6 → kader-1.0.0}/kader/providers/ollama.py +2 -2
- {kader-0.1.6 → kader-1.0.0}/kader/tools/__init__.py +26 -0
- kader-1.0.0/kader/tools/agent.py +452 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/filesys.py +1 -1
- {kader-0.1.6 → kader-1.0.0}/kader/tools/todo.py +43 -2
- kader-1.0.0/kader/utils/__init__.py +10 -0
- kader-1.0.0/kader/utils/checkpointer.py +371 -0
- kader-1.0.0/kader/utils/context_aggregator.py +347 -0
- kader-1.0.0/kader/workflows/__init__.py +13 -0
- kader-1.0.0/kader/workflows/base.py +71 -0
- kader-1.0.0/kader/workflows/planner_executor.py +251 -0
- {kader-0.1.6 → kader-1.0.0}/pyproject.toml +2 -1
- kader-1.0.0/tests/conftest.py +50 -0
- {kader-0.1.6 → kader-1.0.0}/tests/test_agent_logger_integration.py +21 -11
- {kader-0.1.6 → kader-1.0.0}/tests/test_todo_tool.py +58 -5
- kader-1.0.0/tests/tools/test_agent_tool.py +275 -0
- kader-1.0.0/tests/tools/test_agent_tool_persistence.py +213 -0
- {kader-0.1.6 → kader-1.0.0}/tests/tools/test_filesystem_tools.py +3 -3
- {kader-0.1.6 → kader-1.0.0}/uv.lock +12 -1
- kader-0.1.6/cli/widgets/conversation.py +0 -55
- kader-0.1.6/kader/prompts/__init__.py +0 -9
- kader-0.1.6/kader/prompts/agent_prompts.py +0 -27
- kader-0.1.6/tests/conftest.py +0 -14
- {kader-0.1.6 → kader-1.0.0}/.github/workflows/ci.yml +0 -0
- {kader-0.1.6 → kader-1.0.0}/.github/workflows/release.yml +0 -0
- {kader-0.1.6 → kader-1.0.0}/.gitignore +0 -0
- {kader-0.1.6 → kader-1.0.0}/.python-version +0 -0
- {kader-0.1.6 → kader-1.0.0}/.qwen/QWEN.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/.qwen/agents/technical-writer.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/.qwen/agents/test-automation-specialist.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/README.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/__init__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/__main__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/widgets/__init__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/widgets/confirmation.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/cli/widgets/loading.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/.gitignore +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/README.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/memory_example.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/ollama_example.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/planning_agent_example.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/python_developer/main.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/python_developer/template.yaml +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/react_agent_example.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/simple_agent.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/examples/todo_agent/main.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/agent/__init__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/agent/logger.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/config.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/memory/__init__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/memory/conversation.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/memory/session.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/memory/state.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/prompts/base.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/prompts/templates/planning_agent.j2 +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/prompts/templates/react_agent.j2 +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/providers/__init__.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/providers/base.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/providers/mock.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/README.md +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/base.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/exec_commands.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/filesystem.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/protocol.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/rag.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/utils.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/kader/tools/web.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/providers/test_mock.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/providers/test_ollama.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/providers/test_providers_base.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/test_agent_logger.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/test_base_agent.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/test_file_memory.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/tools/test_exec_commands.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/tools/test_filesys_tools.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/tools/test_rag.py +0 -0
- {kader-0.1.6 → kader-1.0.0}/tests/tools/test_tools_base.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
109
|
-
"""Create a new
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
435
|
-
self.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
502
|
-
conversation.add_message(
|
|
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.
|
|
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
|
|
552
|
-
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.
|
|
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.
|
|
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.
|
|
637
|
-
usage = self.
|
|
638
|
-
model = self.
|
|
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 {
|
|
@@ -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
|
]
|