kader 0.1.6__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- cli/app.py +98 -30
- cli/app.tcss +20 -0
- cli/utils.py +1 -1
- cli/widgets/conversation.py +50 -4
- kader/__init__.py +2 -0
- kader/agent/agents.py +8 -0
- kader/agent/base.py +68 -5
- kader/memory/types.py +60 -0
- kader/prompts/__init__.py +9 -1
- kader/prompts/agent_prompts.py +28 -0
- kader/prompts/templates/executor_agent.j2 +70 -0
- kader/prompts/templates/kader_planner.j2 +71 -0
- kader/providers/ollama.py +2 -2
- kader/tools/__init__.py +26 -0
- kader/tools/agent.py +452 -0
- kader/tools/filesys.py +1 -1
- kader/tools/todo.py +43 -2
- kader/utils/__init__.py +10 -0
- kader/utils/checkpointer.py +371 -0
- kader/utils/context_aggregator.py +347 -0
- kader/workflows/__init__.py +13 -0
- kader/workflows/base.py +71 -0
- kader/workflows/planner_executor.py +251 -0
- {kader-0.1.6.dist-info → kader-1.0.0.dist-info}/METADATA +38 -1
- {kader-0.1.6.dist-info → kader-1.0.0.dist-info}/RECORD +27 -18
- {kader-0.1.6.dist-info → kader-1.0.0.dist-info}/WHEEL +0 -0
- {kader-0.1.6.dist-info → kader-1.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
You are Kader, an Executor Agent specialized in deep and complex tasks.
|
|
2
|
+
|
|
3
|
+
You excel at:
|
|
4
|
+
- Writing high-quality code and implementing features
|
|
5
|
+
- Deep research and comprehensive analysis
|
|
6
|
+
- Complex problem-solving and debugging
|
|
7
|
+
- Thorough documentation and testing
|
|
8
|
+
|
|
9
|
+
You have access to the following tools:
|
|
10
|
+
|
|
11
|
+
{% for tool in tools %}
|
|
12
|
+
{{ tool.name }}: {{ tool.description }}
|
|
13
|
+
{% endfor %}
|
|
14
|
+
|
|
15
|
+
## Your Objective
|
|
16
|
+
|
|
17
|
+
Complete the assigned task safely and thoroughly. Think carefully before each action.
|
|
18
|
+
|
|
19
|
+
## Execution Process
|
|
20
|
+
|
|
21
|
+
1. **Analyze the Task**: Before taking any action, carefully think about:
|
|
22
|
+
- What exactly needs to be done
|
|
23
|
+
- What could go wrong and how to prevent it
|
|
24
|
+
- The safest approach to complete the task
|
|
25
|
+
|
|
26
|
+
2. **Plan Your Steps**: Create a mental action plan before executing
|
|
27
|
+
|
|
28
|
+
3. **Execute Safely**: For each action:
|
|
29
|
+
- Think about potential side effects
|
|
30
|
+
- Verify inputs before executing
|
|
31
|
+
- Handle errors gracefully
|
|
32
|
+
|
|
33
|
+
4. **Track Your Work**: Keep track of:
|
|
34
|
+
- Files created or modified
|
|
35
|
+
- Commands executed
|
|
36
|
+
- Any issues encountered
|
|
37
|
+
|
|
38
|
+
## Response Format
|
|
39
|
+
|
|
40
|
+
Use the following format for reasoning:
|
|
41
|
+
|
|
42
|
+
Thought: Analyze what needs to be done and plan the approach
|
|
43
|
+
Action: the action to take, should be one of [{{ tool_names }}]
|
|
44
|
+
Action Input: the input to the action
|
|
45
|
+
Observation: the result of the action
|
|
46
|
+
... (this Thought/Action/Action Input/Observation can repeat N times)
|
|
47
|
+
Thought: I have completed the task
|
|
48
|
+
Final Answer: [Your detailed execution report]
|
|
49
|
+
|
|
50
|
+
## CRITICAL: Final Answer Requirements
|
|
51
|
+
|
|
52
|
+
Your Final Answer MUST include a structured execution report with:
|
|
53
|
+
|
|
54
|
+
### What Has Been Done
|
|
55
|
+
- List each action completed with details
|
|
56
|
+
|
|
57
|
+
### Files Created/Modified
|
|
58
|
+
- Full path and purpose of each file
|
|
59
|
+
- If no files: state "No files were created or modified"
|
|
60
|
+
|
|
61
|
+
### Execution Summary
|
|
62
|
+
- Brief summary of the overall execution
|
|
63
|
+
- Whether the task was completed successfully
|
|
64
|
+
|
|
65
|
+
### Issues/Notes
|
|
66
|
+
- Any errors encountered and how they were resolved
|
|
67
|
+
- Any warnings or important notes for the caller
|
|
68
|
+
- If no issues: state "No issues encountered"
|
|
69
|
+
|
|
70
|
+
Begin!
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
You are Kader, an intelligent planning agent. You have access to the following tools:
|
|
2
|
+
|
|
3
|
+
{% for tool in tools %}
|
|
4
|
+
{{ tool.name }}: {{ tool.description }}
|
|
5
|
+
{% endfor %}
|
|
6
|
+
|
|
7
|
+
{% if context %}
|
|
8
|
+
## Previous Context (What Has Been Done)
|
|
9
|
+
|
|
10
|
+
The following summarizes what has been accomplished in previous rounds. Consider this context when planning your next steps to avoid repeating completed work:
|
|
11
|
+
|
|
12
|
+
{{ context }}
|
|
13
|
+
|
|
14
|
+
{% endif %}
|
|
15
|
+
Your goal is to complete the user's request by creating a plan and executing it step-by-step.
|
|
16
|
+
|
|
17
|
+
## Core Instructions
|
|
18
|
+
|
|
19
|
+
1. Break down the user request into logical steps.
|
|
20
|
+
2. For each step, determine if a tool is needed.
|
|
21
|
+
3. Execute necessary tools.
|
|
22
|
+
4. CRITICAL: Always create a TODO using the TODO Tool for the plan.
|
|
23
|
+
5. CRITICAL: Follow the instructions of the TODO list strictly. The current item in the TODO list is your PRIMARY instruction.
|
|
24
|
+
6. CRITICAL: After completing a step (e.g., creating a file, running a test), you MUST immediately use the TODO Tool to update the status of that item to 'completed'. Do not proceed to the next item without updating the TODO list.
|
|
25
|
+
7. If you have enough information, provide the final answer.
|
|
26
|
+
|
|
27
|
+
## Using Agent as a Tool
|
|
28
|
+
|
|
29
|
+
When delegating tasks to sub-agents using the Agent Tool, follow these guidelines:
|
|
30
|
+
|
|
31
|
+
### Task Parameter
|
|
32
|
+
- **Primary Instruction**: This MUST come directly from the current item in your TODO list (e.g., "Implement unit tests", "Create database schema").
|
|
33
|
+
- **Secondary Instructions (Objectives)**: These are the supporting details or objectives for the sub-agent (e.g., "View the code to understand the context", "Ensure 100% coverage").
|
|
34
|
+
- **Structure**:
|
|
35
|
+
- Start with the Primary Instruction.
|
|
36
|
+
- Follow with "Objectives:" listing the Secondary Instructions.
|
|
37
|
+
- **Example**:
|
|
38
|
+
- If the TODO item is "Implement feature X", and you need the agent to also read the docs:
|
|
39
|
+
- Task: "Implement feature X. Objectives: 1. Read documentation at doc/feature.md. 2. Verify implementation with tests."
|
|
40
|
+
- **REQUIRED**: Include instruction that the agent MUST return:
|
|
41
|
+
- What has been done (actions completed)
|
|
42
|
+
- What files have been written or modified (if any)
|
|
43
|
+
- A summary of the execution
|
|
44
|
+
- Any issues or errors that occurred during execution
|
|
45
|
+
|
|
46
|
+
### Context Parameter (REQUIRED)
|
|
47
|
+
The context parameter MUST include:
|
|
48
|
+
1. **Brief about latest actions**: Summarize what has been done so far and the current state
|
|
49
|
+
2. **Completed actions to avoid repetition**: List actions that were successfully completed so the sub-agent does NOT repeat them
|
|
50
|
+
|
|
51
|
+
Example Agent Tool usage:
|
|
52
|
+
```
|
|
53
|
+
task: "Create unit tests for the user authentication module"
|
|
54
|
+
context: "We are building a user management system. So far, the User model and AuthService have been implemented in src/auth/. Completed actions: 1) Created User model with email/password fields, 2) Implemented AuthService with login/register methods, 3) Set up pytest configuration. Do NOT recreate these files."
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Example Planning Flow
|
|
58
|
+
|
|
59
|
+
Task: Implement a ML Inference Endpoint
|
|
60
|
+
|
|
61
|
+
Plan:
|
|
62
|
+
1. Analyze the requirements and design the system architecture
|
|
63
|
+
2. Implement the endpoint using a web framework
|
|
64
|
+
3. Test the endpoint to ensure it works as expected
|
|
65
|
+
4. Deploy the endpoint to a production environment
|
|
66
|
+
|
|
67
|
+
When executing this plan:
|
|
68
|
+
- Create the TODO list first
|
|
69
|
+
- For each step, consider if delegation to an Agent Tool is appropriate
|
|
70
|
+
- Update TODO status after completing each step
|
|
71
|
+
- Provide context to sub-agents about completed work
|
kader/providers/ollama.py
CHANGED
|
@@ -433,11 +433,11 @@ class OllamaProvider(BaseLLMProvider):
|
|
|
433
433
|
models_config = {}
|
|
434
434
|
for model in models:
|
|
435
435
|
models_config[model] = client.show(model)
|
|
436
|
+
accepted_capabilities = ["completion", "tools"]
|
|
436
437
|
return [
|
|
437
438
|
model
|
|
438
439
|
for model, config in models_config.items()
|
|
439
|
-
if config.capabilities
|
|
440
|
-
in [["completion", "tools", "thinking"], ["completion", "tools"]]
|
|
440
|
+
if set(accepted_capabilities).issubset(set(config.capabilities))
|
|
441
441
|
]
|
|
442
442
|
except Exception:
|
|
443
443
|
return []
|
kader/tools/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from kader.tools.exec_commands import (
|
|
|
9
9
|
CommandExecutorTool,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
+
from .agent import AgentTool
|
|
12
13
|
from .base import (
|
|
13
14
|
# Core classes
|
|
14
15
|
BaseTool,
|
|
@@ -84,6 +85,28 @@ def get_default_registry() -> ToolRegistry:
|
|
|
84
85
|
return registry
|
|
85
86
|
|
|
86
87
|
|
|
88
|
+
# Module-level cached registry singleton
|
|
89
|
+
_cached_default_registry: ToolRegistry | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_cached_default_registry() -> ToolRegistry:
|
|
93
|
+
"""
|
|
94
|
+
Get a cached registry populated with all standard tools.
|
|
95
|
+
|
|
96
|
+
This is more efficient than get_default_registry() when called multiple times,
|
|
97
|
+
as it avoids repeated tool instantiation and registration.
|
|
98
|
+
|
|
99
|
+
The cached registry is created once and reused for all subsequent calls.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Cached ToolRegistry with all standard tools registered.
|
|
103
|
+
"""
|
|
104
|
+
global _cached_default_registry
|
|
105
|
+
if _cached_default_registry is None:
|
|
106
|
+
_cached_default_registry = get_default_registry()
|
|
107
|
+
return _cached_default_registry
|
|
108
|
+
|
|
109
|
+
|
|
87
110
|
__all__ = [
|
|
88
111
|
# Core classes
|
|
89
112
|
"BaseTool",
|
|
@@ -125,6 +148,9 @@ __all__ = [
|
|
|
125
148
|
"CommandExecutorTool",
|
|
126
149
|
# Todo Tool
|
|
127
150
|
"TodoTool",
|
|
151
|
+
# Agent Tool
|
|
152
|
+
"AgentTool",
|
|
128
153
|
# Helpers
|
|
129
154
|
"get_default_registry",
|
|
155
|
+
"get_cached_default_registry",
|
|
130
156
|
]
|
kader/tools/agent.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Tool - Use a ReActAgent as a callable tool.
|
|
3
|
+
|
|
4
|
+
Allows spawning sub-agents to execute specific tasks with isolated memory contexts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from kader.memory import SlidingWindowConversationManager
|
|
12
|
+
from kader.memory.types import aread_text, save_json
|
|
13
|
+
from kader.prompts import ExecutorAgentPrompt
|
|
14
|
+
from kader.providers.base import BaseLLMProvider, Message
|
|
15
|
+
from kader.utils import Checkpointer, ContextAggregator
|
|
16
|
+
|
|
17
|
+
from .base import BaseTool, ParameterSchema, ToolCategory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PersistentSlidingWindowConversationManager(SlidingWindowConversationManager):
|
|
21
|
+
"""
|
|
22
|
+
SlidingWindowConversationManager with JSON persistence.
|
|
23
|
+
|
|
24
|
+
Saves the entire message history (dict format) to a JSON file
|
|
25
|
+
after every add_message(s) call.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, file_path: Path, window_size: int = 20) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize with a file path for persistence.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(window_size=window_size)
|
|
33
|
+
self.file_path = file_path
|
|
34
|
+
|
|
35
|
+
def _save(self) -> None:
|
|
36
|
+
"""Save entire history to JSON."""
|
|
37
|
+
try:
|
|
38
|
+
# We want to save plain dicts
|
|
39
|
+
messages_dicts = [msg.message for msg in self._messages]
|
|
40
|
+
data = {"messages": messages_dicts}
|
|
41
|
+
# Ensure parent temp-directory exists is done by caller usually,
|
|
42
|
+
# but best effort here:
|
|
43
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
save_json(self.file_path, data)
|
|
45
|
+
except Exception:
|
|
46
|
+
# Best effort save
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def add_message(self, message: Any) -> Any:
|
|
50
|
+
# Call super
|
|
51
|
+
result = super().add_message(message)
|
|
52
|
+
# Save
|
|
53
|
+
self._save()
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def add_messages(self, messages: list[Any]) -> list[Any]:
|
|
57
|
+
# Call super
|
|
58
|
+
result = super().add_messages(messages)
|
|
59
|
+
# Save
|
|
60
|
+
self._save()
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AgentTool(BaseTool[str]):
|
|
65
|
+
"""
|
|
66
|
+
Tool that spawns a ReActAgent to execute a specific task.
|
|
67
|
+
|
|
68
|
+
Creates an agent with its own memory context and default tools
|
|
69
|
+
(filesystem, web, command executor) to complete the given task.
|
|
70
|
+
|
|
71
|
+
When `interrupt_before_tool=True`, the agent will pause before each tool
|
|
72
|
+
execution and use the `tool_confirmation_callback` to get user confirmation.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
# Autonomous execution (no interrupts)
|
|
76
|
+
tool = AgentTool(name="research_agent", interrupt_before_tool=False)
|
|
77
|
+
result = tool.execute(task="Find the current stock price of AAPL")
|
|
78
|
+
|
|
79
|
+
# Interactive execution with tool confirmation
|
|
80
|
+
def my_callback(tool_info: str) -> Tuple[bool, Optional[str]]:
|
|
81
|
+
user_input = input(f"Execute {tool_info}? [y/n]: ")
|
|
82
|
+
return (user_input.lower() == 'y', None)
|
|
83
|
+
|
|
84
|
+
tool = AgentTool(
|
|
85
|
+
name="research_agent",
|
|
86
|
+
interrupt_before_tool=True,
|
|
87
|
+
tool_confirmation_callback=my_callback
|
|
88
|
+
)
|
|
89
|
+
result = tool.execute(task="Find info about topic X")
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
name: str,
|
|
95
|
+
description: str = "Execute a specific task using an AI agent",
|
|
96
|
+
provider: Optional[BaseLLMProvider] = None,
|
|
97
|
+
model_name: str = "qwen3-coder:480b-cloud",
|
|
98
|
+
interrupt_before_tool: bool = True,
|
|
99
|
+
tool_confirmation_callback: Optional[
|
|
100
|
+
Callable[..., Tuple[bool, Optional[str]]]
|
|
101
|
+
] = None,
|
|
102
|
+
direct_execution_callback: Optional[Callable[..., None]] = None,
|
|
103
|
+
tool_execution_result_callback: Optional[Callable[..., None]] = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Initialize the AgentTool.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
name: Name of the agent tool (used as identifier).
|
|
110
|
+
description: Description of what this agent does.
|
|
111
|
+
provider: Optional LLM provider (uses OllamaProvider by default).
|
|
112
|
+
model_name: Model to use for the agent.
|
|
113
|
+
interrupt_before_tool: If True, pause before tool execution for user
|
|
114
|
+
confirmation. The task will only complete when the agent returns
|
|
115
|
+
its final response.
|
|
116
|
+
tool_confirmation_callback: Callback function for tool confirmation.
|
|
117
|
+
Should return (should_execute: bool, additional_context: Optional[str]).
|
|
118
|
+
If not provided and interrupt_before_tool=True, uses stdin prompts.
|
|
119
|
+
"""
|
|
120
|
+
super().__init__(
|
|
121
|
+
name=name,
|
|
122
|
+
description=description,
|
|
123
|
+
parameters=[
|
|
124
|
+
ParameterSchema(
|
|
125
|
+
name="task",
|
|
126
|
+
type="string",
|
|
127
|
+
description="The specific task for the agent to execute",
|
|
128
|
+
required=True,
|
|
129
|
+
),
|
|
130
|
+
ParameterSchema(
|
|
131
|
+
name="context",
|
|
132
|
+
type="string",
|
|
133
|
+
description="Context to provide to the agent before executing the task",
|
|
134
|
+
required=True,
|
|
135
|
+
),
|
|
136
|
+
],
|
|
137
|
+
category=ToolCategory.UTILITY,
|
|
138
|
+
)
|
|
139
|
+
self._provider = provider
|
|
140
|
+
self._model_name = model_name
|
|
141
|
+
self._interrupt_before_tool = interrupt_before_tool
|
|
142
|
+
self._tool_confirmation_callback = tool_confirmation_callback
|
|
143
|
+
self._direct_execution_callback = direct_execution_callback
|
|
144
|
+
self._tool_execution_result_callback = tool_execution_result_callback
|
|
145
|
+
|
|
146
|
+
def _load_aggregated_context(self, main_session_id: str) -> str | None:
|
|
147
|
+
"""
|
|
148
|
+
Load the aggregated checkpoint from executors directory if it exists.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
main_session_id: The main session ID
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Content of the aggregated checkpoint, or None if not found
|
|
155
|
+
"""
|
|
156
|
+
if main_session_id == "standalone":
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
home = Path.home()
|
|
160
|
+
aggregated_path = (
|
|
161
|
+
home
|
|
162
|
+
/ ".kader"
|
|
163
|
+
/ "memory"
|
|
164
|
+
/ "sessions"
|
|
165
|
+
/ main_session_id
|
|
166
|
+
/ "executors"
|
|
167
|
+
/ "checkpoint.md"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if aggregated_path.exists():
|
|
171
|
+
try:
|
|
172
|
+
return aggregated_path.read_text(encoding="utf-8")
|
|
173
|
+
except Exception:
|
|
174
|
+
return None
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
async def _aload_aggregated_context(self, main_session_id: str) -> str | None:
|
|
178
|
+
"""
|
|
179
|
+
Asynchronously load the aggregated checkpoint from executors directory.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
main_session_id: The main session ID
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Content of the aggregated checkpoint, or None if not found
|
|
186
|
+
"""
|
|
187
|
+
if main_session_id == "standalone":
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
home = Path.home()
|
|
191
|
+
aggregated_path = (
|
|
192
|
+
home
|
|
193
|
+
/ ".kader"
|
|
194
|
+
/ "memory"
|
|
195
|
+
/ "sessions"
|
|
196
|
+
/ main_session_id
|
|
197
|
+
/ "executors"
|
|
198
|
+
/ "checkpoint.md"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if aggregated_path.exists():
|
|
202
|
+
try:
|
|
203
|
+
return await aread_text(aggregated_path)
|
|
204
|
+
except Exception:
|
|
205
|
+
return None
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def execute(self, task: str, context: str) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Execute a task using a ReActAgent with isolated memory.
|
|
211
|
+
|
|
212
|
+
When interrupt_before_tool is True, the agent will pause before each tool
|
|
213
|
+
execution for user confirmation. The task only ends when the agent returns
|
|
214
|
+
its final response.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
task: The task to execute.
|
|
218
|
+
context: Context to add to memory before the task.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
A summary of what the agent accomplished.
|
|
222
|
+
"""
|
|
223
|
+
# Import here to avoid circular imports
|
|
224
|
+
from kader.agent.agents import ReActAgent
|
|
225
|
+
|
|
226
|
+
# Create a fresh memory manager for isolated context
|
|
227
|
+
# Persistence: ~/.kader/memory/sessions/<main-session-id>/executors/<agent-name>-<id>.json
|
|
228
|
+
execution_id = str(uuid.uuid4())
|
|
229
|
+
# Use propagated session ID or 'standalone' if not set
|
|
230
|
+
main_session_id = self._session_id if self._session_id else "standalone"
|
|
231
|
+
|
|
232
|
+
home = Path.home()
|
|
233
|
+
memory_dir = (
|
|
234
|
+
home
|
|
235
|
+
/ ".kader"
|
|
236
|
+
/ "memory"
|
|
237
|
+
/ "sessions"
|
|
238
|
+
/ main_session_id
|
|
239
|
+
/ "executors"
|
|
240
|
+
/ f"{self.name}-{execution_id}"
|
|
241
|
+
)
|
|
242
|
+
memory_file = memory_dir / "conversation.json"
|
|
243
|
+
|
|
244
|
+
memory = PersistentSlidingWindowConversationManager(
|
|
245
|
+
file_path=memory_file, window_size=20
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Load aggregated context from previous executors
|
|
249
|
+
aggregated_context = self._load_aggregated_context(main_session_id)
|
|
250
|
+
if aggregated_context:
|
|
251
|
+
full_context = f"## Previous Executor Context\n{aggregated_context}\n\n## Current Task Context\n{context}"
|
|
252
|
+
else:
|
|
253
|
+
full_context = context
|
|
254
|
+
|
|
255
|
+
# Add context to memory as user message
|
|
256
|
+
memory.add_message(Message.user(full_context))
|
|
257
|
+
|
|
258
|
+
# Get default tools (filesystem, web, command executor) - use cached version
|
|
259
|
+
from kader.tools import get_cached_default_registry
|
|
260
|
+
|
|
261
|
+
tools = get_cached_default_registry()
|
|
262
|
+
|
|
263
|
+
# Create ExecutorAgentPrompt with tool descriptions
|
|
264
|
+
system_prompt = ExecutorAgentPrompt(tools=tools.tools)
|
|
265
|
+
|
|
266
|
+
# Create the ReActAgent with separate memory and executor prompt
|
|
267
|
+
agent = ReActAgent(
|
|
268
|
+
name=f"{self.name}_worker",
|
|
269
|
+
tools=tools,
|
|
270
|
+
system_prompt=system_prompt,
|
|
271
|
+
provider=self._provider,
|
|
272
|
+
memory=memory,
|
|
273
|
+
model_name=self._model_name,
|
|
274
|
+
interrupt_before_tool=self._interrupt_before_tool,
|
|
275
|
+
tool_confirmation_callback=self._tool_confirmation_callback,
|
|
276
|
+
direct_execution_callback=self._direct_execution_callback,
|
|
277
|
+
tool_execution_result_callback=self._tool_execution_result_callback,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# Invoke the agent with the task
|
|
282
|
+
# The agent will handle tool interruptions internally
|
|
283
|
+
response = agent.invoke(task)
|
|
284
|
+
|
|
285
|
+
# Generate checkpoint and aggregate it
|
|
286
|
+
try:
|
|
287
|
+
checkpointer = Checkpointer()
|
|
288
|
+
checkpoint_path = checkpointer.generate_checkpoint(str(memory_file))
|
|
289
|
+
checkpoint_content = Path(checkpoint_path).read_text(encoding="utf-8")
|
|
290
|
+
|
|
291
|
+
# Aggregate the checkpoint into the main executors checkpoint
|
|
292
|
+
if main_session_id != "standalone":
|
|
293
|
+
aggregator = ContextAggregator(session_id=main_session_id)
|
|
294
|
+
# Use relative path from executors directory
|
|
295
|
+
relative_path = f"{self.name}-{execution_id}/checkpoint.md"
|
|
296
|
+
aggregator.aggregate(relative_path, subagent_name=self.name)
|
|
297
|
+
|
|
298
|
+
# Append the agent's response to the checkpoint content if it exists
|
|
299
|
+
response_content = None
|
|
300
|
+
if hasattr(response, "content"):
|
|
301
|
+
response_content = str(response.content)
|
|
302
|
+
elif isinstance(response, dict):
|
|
303
|
+
response_content = str(response.get("content", str(response)))
|
|
304
|
+
else:
|
|
305
|
+
response_content = str(response)
|
|
306
|
+
|
|
307
|
+
if response_content and response_content != "None":
|
|
308
|
+
checkpoint_content += f"\n\nResponse:\n{response_content}"
|
|
309
|
+
|
|
310
|
+
return checkpoint_content
|
|
311
|
+
except Exception:
|
|
312
|
+
# Fallback to raw response if checkpointing fails
|
|
313
|
+
if hasattr(response, "content"):
|
|
314
|
+
return str(response.content)
|
|
315
|
+
elif isinstance(response, dict):
|
|
316
|
+
return str(response.get("content", str(response)))
|
|
317
|
+
else:
|
|
318
|
+
return str(response)
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
return f"Agent execution failed: {str(e)}"
|
|
322
|
+
|
|
323
|
+
async def aexecute(self, task: str, context: str) -> str:
|
|
324
|
+
"""
|
|
325
|
+
Asynchronously execute a task using a ReActAgent.
|
|
326
|
+
|
|
327
|
+
When interrupt_before_tool is True, the agent will pause before each tool
|
|
328
|
+
execution for user confirmation. The task only ends when the agent returns
|
|
329
|
+
its final response.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
task: The task to execute.
|
|
333
|
+
context: Context to add to memory before the task.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
A summary of what the agent accomplished.
|
|
337
|
+
"""
|
|
338
|
+
# Import here to avoid circular imports
|
|
339
|
+
from kader.agent.agents import ReActAgent
|
|
340
|
+
|
|
341
|
+
# Create a fresh memory manager for isolated context
|
|
342
|
+
# Persistence: ~/.kader/memory/sessions/<main-session-id>/executors/<agent-name>-<id>.json
|
|
343
|
+
execution_id = str(uuid.uuid4())
|
|
344
|
+
# Use propagated session ID or 'standalone' if not set
|
|
345
|
+
main_session_id = self._session_id if self._session_id else "standalone"
|
|
346
|
+
|
|
347
|
+
home = Path.home()
|
|
348
|
+
memory_dir = (
|
|
349
|
+
home
|
|
350
|
+
/ ".kader"
|
|
351
|
+
/ "memory"
|
|
352
|
+
/ "sessions"
|
|
353
|
+
/ main_session_id
|
|
354
|
+
/ "executors"
|
|
355
|
+
/ f"{self.name}-{execution_id}"
|
|
356
|
+
)
|
|
357
|
+
memory_file = memory_dir / "conversation.json"
|
|
358
|
+
|
|
359
|
+
memory = PersistentSlidingWindowConversationManager(
|
|
360
|
+
file_path=memory_file, window_size=20
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Load aggregated context from previous executors (async)
|
|
364
|
+
aggregated_context = await self._aload_aggregated_context(main_session_id)
|
|
365
|
+
if aggregated_context:
|
|
366
|
+
full_context = f"## Previous Executor Context\n{aggregated_context}\n\n## Current Task Context\n{context}"
|
|
367
|
+
else:
|
|
368
|
+
full_context = context
|
|
369
|
+
|
|
370
|
+
# Add context to memory as user message
|
|
371
|
+
memory.add_message(Message.user(full_context))
|
|
372
|
+
|
|
373
|
+
# Get default tools (filesystem, web, command executor) - use cached version
|
|
374
|
+
from kader.tools import get_cached_default_registry
|
|
375
|
+
|
|
376
|
+
tools = get_cached_default_registry()
|
|
377
|
+
|
|
378
|
+
# Create ExecutorAgentPrompt with tool descriptions
|
|
379
|
+
system_prompt = ExecutorAgentPrompt(tools=tools.tools)
|
|
380
|
+
|
|
381
|
+
# Create the ReActAgent with separate memory and executor prompt
|
|
382
|
+
agent = ReActAgent(
|
|
383
|
+
name=f"{self.name}_worker",
|
|
384
|
+
tools=tools,
|
|
385
|
+
system_prompt=system_prompt,
|
|
386
|
+
provider=self._provider,
|
|
387
|
+
memory=memory,
|
|
388
|
+
model_name=self._model_name,
|
|
389
|
+
interrupt_before_tool=self._interrupt_before_tool,
|
|
390
|
+
tool_confirmation_callback=self._tool_confirmation_callback,
|
|
391
|
+
direct_execution_callback=self._direct_execution_callback,
|
|
392
|
+
tool_execution_result_callback=self._tool_execution_result_callback,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
# Invoke the agent asynchronously
|
|
397
|
+
# The agent will handle tool interruptions internally
|
|
398
|
+
response = await agent.ainvoke(task)
|
|
399
|
+
|
|
400
|
+
# Generate checkpoint and aggregate it
|
|
401
|
+
try:
|
|
402
|
+
checkpointer = Checkpointer()
|
|
403
|
+
checkpoint_path = await checkpointer.agenerate_checkpoint(
|
|
404
|
+
str(memory_file)
|
|
405
|
+
)
|
|
406
|
+
checkpoint_content = await aread_text(Path(checkpoint_path))
|
|
407
|
+
|
|
408
|
+
# Aggregate the checkpoint into the main executors checkpoint
|
|
409
|
+
if main_session_id != "standalone":
|
|
410
|
+
aggregator = ContextAggregator(session_id=main_session_id)
|
|
411
|
+
# Use relative path from executors directory
|
|
412
|
+
relative_path = f"{self.name}-{execution_id}/checkpoint.md"
|
|
413
|
+
await aggregator.aaggregate(relative_path, subagent_name=self.name)
|
|
414
|
+
|
|
415
|
+
# Append the agent's response to the checkpoint content if it exists
|
|
416
|
+
response_content = None
|
|
417
|
+
if hasattr(response, "content"):
|
|
418
|
+
response_content = str(response.content)
|
|
419
|
+
elif isinstance(response, dict):
|
|
420
|
+
response_content = str(response.get("content", str(response)))
|
|
421
|
+
else:
|
|
422
|
+
response_content = str(response)
|
|
423
|
+
|
|
424
|
+
if response_content and response_content != "None":
|
|
425
|
+
checkpoint_content += f"\n\nResponse:\n{response_content}"
|
|
426
|
+
|
|
427
|
+
return checkpoint_content
|
|
428
|
+
except Exception:
|
|
429
|
+
# Fallback to raw response if checkpointing fails
|
|
430
|
+
if hasattr(response, "content"):
|
|
431
|
+
return str(response.content)
|
|
432
|
+
elif isinstance(response, dict):
|
|
433
|
+
return str(response.get("content", str(response)))
|
|
434
|
+
else:
|
|
435
|
+
return str(response)
|
|
436
|
+
|
|
437
|
+
except Exception as e:
|
|
438
|
+
return f"Agent execution failed: {str(e)}"
|
|
439
|
+
|
|
440
|
+
def get_interruption_message(self, task: str, **kwargs: Any) -> str:
|
|
441
|
+
"""
|
|
442
|
+
Get a message describing the agent action for user confirmation.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
task: The task the agent will execute.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
A formatted string describing the action.
|
|
449
|
+
"""
|
|
450
|
+
# Truncate long tasks for readability
|
|
451
|
+
task_preview = task[:100] + "..." if len(task) > 100 else task
|
|
452
|
+
return f"execute {self.name}: {task_preview}"
|
kader/tools/filesys.py
CHANGED