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
cli/app.py
CHANGED
|
@@ -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",
|
cli/app.tcss
CHANGED
|
@@ -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 {
|
cli/utils.py
CHANGED
cli/widgets/conversation.py
CHANGED
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
"""Conversation display widget for Kader CLI."""
|
|
2
2
|
|
|
3
3
|
from textual.app import ComposeResult
|
|
4
|
-
from textual.containers import VerticalScroll
|
|
4
|
+
from textual.containers import Horizontal, VerticalScroll
|
|
5
5
|
from textual.widgets import Markdown, Static
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Message(Static):
|
|
9
9
|
"""A single message in the conversation."""
|
|
10
10
|
|
|
11
|
-
def __init__(
|
|
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:
|
|
12
18
|
super().__init__()
|
|
13
19
|
self.content = content
|
|
14
20
|
self.role = role
|
|
21
|
+
self.model_name = model_name
|
|
22
|
+
self.usage_cost = usage_cost
|
|
15
23
|
self.add_class(f"message-{role}")
|
|
16
24
|
|
|
17
25
|
def compose(self) -> ComposeResult:
|
|
18
26
|
prefix = "(**) **You:**" if self.role == "user" else "(^^) **Kader:**"
|
|
19
27
|
yield Markdown(f"{prefix}\n\n{self.content}")
|
|
20
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
|
+
|
|
21
41
|
|
|
22
42
|
class ConversationView(VerticalScroll):
|
|
23
43
|
"""Scrollable conversation history with markdown rendering."""
|
|
@@ -41,11 +61,37 @@ class ConversationView(VerticalScroll):
|
|
|
41
61
|
background: $surface-darken-1;
|
|
42
62
|
border-left: thick $success;
|
|
43
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
|
+
}
|
|
44
84
|
"""
|
|
45
85
|
|
|
46
|
-
def add_message(
|
|
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:
|
|
47
93
|
"""Add a message to the conversation."""
|
|
48
|
-
message = Message(content, role)
|
|
94
|
+
message = Message(content, role, model_name, usage_cost)
|
|
49
95
|
self.mount(message)
|
|
50
96
|
self.scroll_end(animate=True)
|
|
51
97
|
|
kader/__init__.py
CHANGED
|
@@ -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
|
]
|
kader/agent/agents.py
CHANGED
|
@@ -31,6 +31,8 @@ class ReActAgent(BaseAgent):
|
|
|
31
31
|
use_persistence: bool = False,
|
|
32
32
|
interrupt_before_tool: bool = True,
|
|
33
33
|
tool_confirmation_callback: Optional[callable] = None,
|
|
34
|
+
direct_execution_callback: Optional[callable] = None,
|
|
35
|
+
tool_execution_result_callback: Optional[callable] = None,
|
|
34
36
|
) -> None:
|
|
35
37
|
# Resolve tools for prompt context if necessary
|
|
36
38
|
# The base agent handles tool registration, but for the prompt template
|
|
@@ -67,6 +69,8 @@ class ReActAgent(BaseAgent):
|
|
|
67
69
|
use_persistence=use_persistence,
|
|
68
70
|
interrupt_before_tool=interrupt_before_tool,
|
|
69
71
|
tool_confirmation_callback=tool_confirmation_callback,
|
|
72
|
+
direct_execution_callback=direct_execution_callback,
|
|
73
|
+
tool_execution_result_callback=tool_execution_result_callback,
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
|
|
@@ -90,6 +94,8 @@ class PlanningAgent(BaseAgent):
|
|
|
90
94
|
use_persistence: bool = False,
|
|
91
95
|
interrupt_before_tool: bool = True,
|
|
92
96
|
tool_confirmation_callback: Optional[callable] = None,
|
|
97
|
+
direct_execution_callback: Optional[callable] = None,
|
|
98
|
+
tool_execution_result_callback: Optional[callable] = None,
|
|
93
99
|
) -> None:
|
|
94
100
|
# Ensure TodoTool is available
|
|
95
101
|
_todo_tool = TodoTool()
|
|
@@ -123,4 +129,6 @@ class PlanningAgent(BaseAgent):
|
|
|
123
129
|
use_persistence=use_persistence,
|
|
124
130
|
interrupt_before_tool=interrupt_before_tool,
|
|
125
131
|
tool_confirmation_callback=tool_confirmation_callback,
|
|
132
|
+
direct_execution_callback=direct_execution_callback,
|
|
133
|
+
tool_execution_result_callback=tool_execution_result_callback,
|
|
126
134
|
)
|
kader/agent/base.py
CHANGED
|
@@ -48,11 +48,15 @@ class BaseAgent:
|
|
|
48
48
|
provider: Optional[BaseLLMProvider] = None,
|
|
49
49
|
memory: Optional[ConversationManager] = None,
|
|
50
50
|
retry_attempts: int = 3,
|
|
51
|
+
retry_wait_min: int = 1,
|
|
52
|
+
retry_wait_max: int = 5,
|
|
51
53
|
model_name: str = "qwen3-coder:480b-cloud",
|
|
52
54
|
session_id: Optional[str] = None,
|
|
53
55
|
use_persistence: bool = False,
|
|
54
56
|
interrupt_before_tool: bool = True,
|
|
55
57
|
tool_confirmation_callback: Optional[callable] = None,
|
|
58
|
+
direct_execution_callback: Optional[callable] = None,
|
|
59
|
+
tool_execution_result_callback: Optional[callable] = None,
|
|
56
60
|
) -> None:
|
|
57
61
|
"""
|
|
58
62
|
Initialize the Base Agent.
|
|
@@ -75,8 +79,12 @@ class BaseAgent:
|
|
|
75
79
|
self.name = name
|
|
76
80
|
self.system_prompt = system_prompt
|
|
77
81
|
self.retry_attempts = retry_attempts
|
|
82
|
+
self.retry_wait_min = retry_wait_min
|
|
83
|
+
self.retry_wait_max = retry_wait_max
|
|
78
84
|
self.interrupt_before_tool = interrupt_before_tool
|
|
79
85
|
self.tool_confirmation_callback = tool_confirmation_callback
|
|
86
|
+
self.direct_execution_callback = direct_execution_callback
|
|
87
|
+
self.tool_execution_result_callback = tool_execution_result_callback
|
|
80
88
|
|
|
81
89
|
# Persistence Configuration
|
|
82
90
|
self.session_id = session_id
|
|
@@ -339,6 +347,31 @@ class BaseAgent:
|
|
|
339
347
|
if llm_content and len(llm_content) > 0:
|
|
340
348
|
display_str = f"{llm_content}\n\n{display_str}"
|
|
341
349
|
|
|
350
|
+
# Extract tool name for direct execution check
|
|
351
|
+
fn_info = tool_call_dict.get("function", {})
|
|
352
|
+
if not fn_info and "name" in tool_call_dict:
|
|
353
|
+
fn_info = tool_call_dict
|
|
354
|
+
tool_name = fn_info.get("name", "")
|
|
355
|
+
|
|
356
|
+
# List of tools to execute directly (show message but don't ask for confirmation)
|
|
357
|
+
direct_execution_tools = {
|
|
358
|
+
"read_file",
|
|
359
|
+
"glob",
|
|
360
|
+
"grep",
|
|
361
|
+
"read_directory",
|
|
362
|
+
"read_dir",
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Direct execution for specific tools - applies regardless of callback
|
|
366
|
+
if tool_name in direct_execution_tools:
|
|
367
|
+
# Notify via direct_execution_callback if available (for CLI/TUI display)
|
|
368
|
+
if self.direct_execution_callback:
|
|
369
|
+
self.direct_execution_callback(display_str, tool_name)
|
|
370
|
+
else:
|
|
371
|
+
# Fallback: print to console
|
|
372
|
+
print(display_str)
|
|
373
|
+
return True, None
|
|
374
|
+
|
|
342
375
|
# Use callback if provided (e.g., for GUI/TUI)
|
|
343
376
|
if self.tool_confirmation_callback:
|
|
344
377
|
return self.tool_confirmation_callback(display_str)
|
|
@@ -435,6 +468,18 @@ class BaseAgent:
|
|
|
435
468
|
# Execute tool
|
|
436
469
|
tool_result = self._tool_registry.run(tool_call)
|
|
437
470
|
|
|
471
|
+
# Notify about tool execution result if callback available
|
|
472
|
+
if self.tool_execution_result_callback:
|
|
473
|
+
# Handle both enum and string status
|
|
474
|
+
status = tool_result.status
|
|
475
|
+
status_value = (
|
|
476
|
+
status.value if hasattr(status, "value") else str(status)
|
|
477
|
+
)
|
|
478
|
+
success = status_value == "success"
|
|
479
|
+
self.tool_execution_result_callback(
|
|
480
|
+
tool_call.name, success, tool_result.content
|
|
481
|
+
)
|
|
482
|
+
|
|
438
483
|
# add result to memory
|
|
439
484
|
# But here we just return messages, caller handles memory add
|
|
440
485
|
tool_msg = Message.tool(
|
|
@@ -482,6 +527,18 @@ class BaseAgent:
|
|
|
482
527
|
# Execute tool async
|
|
483
528
|
tool_result = await self._tool_registry.arun(tool_call)
|
|
484
529
|
|
|
530
|
+
# Notify about tool execution result if callback available
|
|
531
|
+
if self.tool_execution_result_callback:
|
|
532
|
+
# Handle both enum and string status
|
|
533
|
+
status = tool_result.status
|
|
534
|
+
status_value = (
|
|
535
|
+
status.value if hasattr(status, "value") else str(status)
|
|
536
|
+
)
|
|
537
|
+
success = status_value == "success"
|
|
538
|
+
self.tool_execution_result_callback(
|
|
539
|
+
tool_call.name, success, tool_result.content
|
|
540
|
+
)
|
|
541
|
+
|
|
485
542
|
tool_msg = Message.tool(
|
|
486
543
|
tool_call_id=tool_result.tool_call_id, content=tool_result.content
|
|
487
544
|
)
|
|
@@ -509,7 +566,9 @@ class BaseAgent:
|
|
|
509
566
|
|
|
510
567
|
runner = Retrying(
|
|
511
568
|
stop=stop_after_attempt(self.retry_attempts),
|
|
512
|
-
wait=wait_exponential(
|
|
569
|
+
wait=wait_exponential(
|
|
570
|
+
multiplier=1, min=self.retry_wait_min, max=self.retry_wait_max
|
|
571
|
+
),
|
|
513
572
|
reraise=True,
|
|
514
573
|
)
|
|
515
574
|
|
|
@@ -652,7 +711,9 @@ class BaseAgent:
|
|
|
652
711
|
|
|
653
712
|
runner = Retrying(
|
|
654
713
|
stop=stop_after_attempt(self.retry_attempts),
|
|
655
|
-
wait=wait_exponential(
|
|
714
|
+
wait=wait_exponential(
|
|
715
|
+
multiplier=1, min=self.retry_wait_min, max=self.retry_wait_max
|
|
716
|
+
),
|
|
656
717
|
reraise=True,
|
|
657
718
|
)
|
|
658
719
|
|
|
@@ -689,7 +750,9 @@ class BaseAgent:
|
|
|
689
750
|
|
|
690
751
|
runner = AsyncRetrying(
|
|
691
752
|
stop=stop_after_attempt(self.retry_attempts),
|
|
692
|
-
wait=wait_exponential(
|
|
753
|
+
wait=wait_exponential(
|
|
754
|
+
multiplier=1, min=self.retry_wait_min, max=self.retry_wait_max
|
|
755
|
+
),
|
|
693
756
|
reraise=True,
|
|
694
757
|
)
|
|
695
758
|
|
|
@@ -905,9 +968,9 @@ class BaseAgent:
|
|
|
905
968
|
if registry is None:
|
|
906
969
|
# Lazy import to avoid circular dependencies if any
|
|
907
970
|
try:
|
|
908
|
-
from kader.tools import
|
|
971
|
+
from kader.tools import get_cached_default_registry
|
|
909
972
|
|
|
910
|
-
registry =
|
|
973
|
+
registry = get_cached_default_registry()
|
|
911
974
|
except ImportError:
|
|
912
975
|
pass
|
|
913
976
|
|
kader/memory/types.py
CHANGED
|
@@ -114,3 +114,63 @@ def load_json(path: Path) -> dict[str, Any]:
|
|
|
114
114
|
return {}
|
|
115
115
|
with open(path, "r", encoding="utf-8") as f:
|
|
116
116
|
return decode_bytes_values(json.load(f))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- Async File I/O Utilities ---
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def aread_text(path: Path, encoding: str = "utf-8") -> str:
|
|
123
|
+
"""Asynchronously read text from a file.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
path: Path to the file
|
|
127
|
+
encoding: File encoding (default: utf-8)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
File contents as string
|
|
131
|
+
"""
|
|
132
|
+
import aiofiles
|
|
133
|
+
|
|
134
|
+
async with aiofiles.open(path, "r", encoding=encoding) as f:
|
|
135
|
+
return await f.read()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def awrite_text(path: Path, content: str, encoding: str = "utf-8") -> None:
|
|
139
|
+
"""Asynchronously write text to a file.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
path: Path to the file
|
|
143
|
+
content: Text content to write
|
|
144
|
+
encoding: File encoding (default: utf-8)
|
|
145
|
+
"""
|
|
146
|
+
import aiofiles
|
|
147
|
+
|
|
148
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
async with aiofiles.open(path, "w", encoding=encoding) as f:
|
|
150
|
+
await f.write(content)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def asave_json(path: Path, data: dict[str, Any]) -> None:
|
|
154
|
+
"""Asynchronously save data to a JSON file.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Path to the JSON file
|
|
158
|
+
data: Data to save
|
|
159
|
+
"""
|
|
160
|
+
content = json.dumps(encode_bytes_values(data), indent=2, ensure_ascii=False)
|
|
161
|
+
await awrite_text(path, content)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def aload_json(path: Path) -> dict[str, Any]:
|
|
165
|
+
"""Asynchronously load data from a JSON file.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
path: Path to the JSON file
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Loaded data, or empty dict if file doesn't exist
|
|
172
|
+
"""
|
|
173
|
+
if not path.exists():
|
|
174
|
+
return {}
|
|
175
|
+
content = await aread_text(path)
|
|
176
|
+
return decode_bytes_values(json.loads(content))
|
kader/prompts/__init__.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
from .agent_prompts import
|
|
1
|
+
from .agent_prompts import (
|
|
2
|
+
BasicAssistancePrompt,
|
|
3
|
+
ExecutorAgentPrompt,
|
|
4
|
+
KaderPlannerPrompt,
|
|
5
|
+
PlanningAgentPrompt,
|
|
6
|
+
ReActAgentPrompt,
|
|
7
|
+
)
|
|
2
8
|
from .base import PromptBase
|
|
3
9
|
|
|
4
10
|
__all__ = [
|
|
@@ -6,4 +12,6 @@ __all__ = [
|
|
|
6
12
|
"BasicAssistancePrompt",
|
|
7
13
|
"ReActAgentPrompt",
|
|
8
14
|
"PlanningAgentPrompt",
|
|
15
|
+
"KaderPlannerPrompt",
|
|
16
|
+
"ExecutorAgentPrompt",
|
|
9
17
|
]
|
kader/prompts/agent_prompts.py
CHANGED
|
@@ -25,3 +25,31 @@ class PlanningAgentPrompt(PromptBase):
|
|
|
25
25
|
|
|
26
26
|
def __init__(self, **kwargs: Any) -> None:
|
|
27
27
|
super().__init__(template_path="planning_agent.j2", **kwargs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KaderPlannerPrompt(PromptBase):
|
|
31
|
+
"""
|
|
32
|
+
Prompt for Kader Planner Agent.
|
|
33
|
+
|
|
34
|
+
Enhanced planning prompt with specific instructions for:
|
|
35
|
+
- Using Agent as a Tool with proper task/context parameters
|
|
36
|
+
- Tracking completed actions to avoid repetition
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
40
|
+
super().__init__(template_path="kader_planner.j2", **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExecutorAgentPrompt(PromptBase):
|
|
44
|
+
"""
|
|
45
|
+
Prompt for Executor Agent (sub-agents in PlannerExecutorWorkflow).
|
|
46
|
+
|
|
47
|
+
Emphasizes:
|
|
48
|
+
- Careful thinking before each action
|
|
49
|
+
- Safe execution with error handling
|
|
50
|
+
- Detailed step-by-step reporting of what was done
|
|
51
|
+
- Structured final answer with files created, summary, and issues
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
55
|
+
super().__init__(template_path="executor_agent.j2", **kwargs)
|