kader 0.1.5__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 -61
- cli/app.tcss +27 -382
- cli/utils.py +1 -6
- 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.5.dist-info → kader-1.0.0.dist-info}/METADATA +38 -1
- {kader-0.1.5.dist-info → kader-1.0.0.dist-info}/RECORD +27 -18
- {kader-0.1.5.dist-info → kader-1.0.0.dist-info}/WHEEL +0 -0
- {kader-0.1.5.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,18 +20,15 @@ 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,
|
|
31
31
|
HELP_TEXT,
|
|
32
|
-
THEME_NAMES,
|
|
33
32
|
)
|
|
34
33
|
from .widgets import ConversationView, InlineSelector, LoadingSpinner, ModelSelector
|
|
35
34
|
|
|
@@ -51,7 +50,6 @@ Type a message below to start chatting, or use one of the commands:
|
|
|
51
50
|
|
|
52
51
|
- `/help` - Show available commands
|
|
53
52
|
- `/models` - View available LLM models
|
|
54
|
-
- `/theme` - Change the color theme
|
|
55
53
|
- `/clear` - Clear the conversation
|
|
56
54
|
- `/save` - Save current session
|
|
57
55
|
- `/load` - Load a saved session
|
|
@@ -83,7 +81,6 @@ class KaderApp(App):
|
|
|
83
81
|
BINDINGS = [
|
|
84
82
|
Binding("ctrl+q", "quit", "Quit"),
|
|
85
83
|
Binding("ctrl+l", "clear", "Clear"),
|
|
86
|
-
Binding("ctrl+t", "cycle_theme", "Theme"),
|
|
87
84
|
Binding("ctrl+s", "save_session", "Save"),
|
|
88
85
|
Binding("ctrl+r", "refresh_tree", "Refresh"),
|
|
89
86
|
Binding("tab", "focus_next", "Next", show=False),
|
|
@@ -92,7 +89,6 @@ class KaderApp(App):
|
|
|
92
89
|
|
|
93
90
|
def __init__(self) -> None:
|
|
94
91
|
super().__init__()
|
|
95
|
-
self._current_theme_index = 0
|
|
96
92
|
self._is_processing = False
|
|
97
93
|
self._current_model = DEFAULT_MODEL
|
|
98
94
|
self._current_session_id: str | None = None
|
|
@@ -107,22 +103,79 @@ class KaderApp(App):
|
|
|
107
103
|
self._model_selector: Optional[ModelSelector] = None
|
|
108
104
|
self._update_info: Optional[str] = None # Latest version if update available
|
|
109
105
|
|
|
110
|
-
|
|
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)
|
|
111
114
|
|
|
112
|
-
def
|
|
113
|
-
"""Create a new
|
|
114
|
-
|
|
115
|
-
memory = SlidingWindowConversationManager(window_size=10)
|
|
116
|
-
return ReActAgent(
|
|
115
|
+
def _create_workflow(self, model_name: str) -> PlannerExecutorWorkflow:
|
|
116
|
+
"""Create a new PlannerExecutorWorkflow with the specified model."""
|
|
117
|
+
return PlannerExecutorWorkflow(
|
|
117
118
|
name="kader_cli",
|
|
118
|
-
tools=registry,
|
|
119
|
-
memory=memory,
|
|
120
119
|
model_name=model_name,
|
|
121
|
-
use_persistence=True,
|
|
122
120
|
interrupt_before_tool=True,
|
|
123
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
|
|
124
159
|
)
|
|
125
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
|
+
|
|
126
179
|
def _tool_confirmation_callback(self, message: str) -> tuple[bool, Optional[str]]:
|
|
127
180
|
"""
|
|
128
181
|
Callback for tool confirmation - called from agent thread.
|
|
@@ -139,7 +192,10 @@ class KaderApp(App):
|
|
|
139
192
|
|
|
140
193
|
# Wait for user response (blocking in agent thread)
|
|
141
194
|
# This is safe because we're in a background thread
|
|
142
|
-
|
|
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")
|
|
143
199
|
|
|
144
200
|
# Return the result
|
|
145
201
|
return self._confirmation_result
|
|
@@ -187,7 +243,8 @@ class KaderApp(App):
|
|
|
187
243
|
if event.confirmed:
|
|
188
244
|
if tool_message:
|
|
189
245
|
conversation.add_message(tool_message, "assistant")
|
|
190
|
-
|
|
246
|
+
# Show executing message - will be updated by result callback
|
|
247
|
+
conversation.add_message("[>] Executing tool...", "assistant")
|
|
191
248
|
# Restart spinner
|
|
192
249
|
try:
|
|
193
250
|
spinner = self.query_one(LoadingSpinner)
|
|
@@ -253,7 +310,7 @@ class KaderApp(App):
|
|
|
253
310
|
# Update model and recreate agent
|
|
254
311
|
old_model = self._current_model
|
|
255
312
|
self._current_model = event.model
|
|
256
|
-
self.
|
|
313
|
+
self._workflow = self._create_workflow(self._current_model)
|
|
257
314
|
|
|
258
315
|
conversation.add_message(
|
|
259
316
|
f"(+) Model changed from `{old_model}` to `{self._current_model}`",
|
|
@@ -433,16 +490,10 @@ Please resize your terminal."""
|
|
|
433
490
|
conversation.add_message(HELP_TEXT, "assistant")
|
|
434
491
|
elif cmd == "/models":
|
|
435
492
|
await self._show_model_selector(conversation)
|
|
436
|
-
elif cmd == "/theme":
|
|
437
|
-
self._cycle_theme()
|
|
438
|
-
theme_name = THEME_NAMES[self._current_theme_index]
|
|
439
|
-
conversation.add_message(
|
|
440
|
-
f"{{~}} Theme changed to **{theme_name}**!", "assistant"
|
|
441
|
-
)
|
|
442
493
|
elif cmd == "/clear":
|
|
443
494
|
conversation.clear_messages()
|
|
444
|
-
self.
|
|
445
|
-
self.
|
|
495
|
+
self._workflow.planner.memory.clear()
|
|
496
|
+
self._workflow.planner.provider.reset_tracking() # Reset usage/cost tracking
|
|
446
497
|
self._current_session_id = None
|
|
447
498
|
self.notify("Conversation cleared!", severity="information")
|
|
448
499
|
elif cmd == "/save":
|
|
@@ -472,7 +523,7 @@ Please resize your terminal."""
|
|
|
472
523
|
)
|
|
473
524
|
|
|
474
525
|
async def _handle_chat(self, message: str) -> None:
|
|
475
|
-
"""Handle regular chat messages with
|
|
526
|
+
"""Handle regular chat messages with PlannerExecutorWorkflow."""
|
|
476
527
|
if self._is_processing:
|
|
477
528
|
self.notify("Please wait for the current response...", severity="warning")
|
|
478
529
|
return
|
|
@@ -500,16 +551,21 @@ Please resize your terminal."""
|
|
|
500
551
|
spinner = self.query_one(LoadingSpinner)
|
|
501
552
|
|
|
502
553
|
try:
|
|
503
|
-
# Run the
|
|
554
|
+
# Run the workflow in a dedicated thread pool
|
|
504
555
|
loop = asyncio.get_event_loop()
|
|
505
556
|
response = await loop.run_in_executor(
|
|
506
|
-
|
|
557
|
+
self._agent_executor, lambda: self._workflow.run(message)
|
|
507
558
|
)
|
|
508
559
|
|
|
509
560
|
# Hide spinner and show response (this runs on main thread via await)
|
|
510
561
|
spinner.stop()
|
|
511
|
-
if response
|
|
512
|
-
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
|
+
)
|
|
513
569
|
|
|
514
570
|
except Exception as e:
|
|
515
571
|
spinner.stop()
|
|
@@ -522,34 +578,13 @@ Please resize your terminal."""
|
|
|
522
578
|
# Auto-refresh directory tree in case agent created/modified files
|
|
523
579
|
self._refresh_directory_tree()
|
|
524
580
|
|
|
525
|
-
def _cycle_theme(self) -> None:
|
|
526
|
-
"""Cycle through available themes."""
|
|
527
|
-
# Remove current theme class if it's not dark
|
|
528
|
-
current_theme = THEME_NAMES[self._current_theme_index]
|
|
529
|
-
if current_theme != "dark":
|
|
530
|
-
self.remove_class(f"theme-{current_theme}")
|
|
531
|
-
|
|
532
|
-
# Move to next theme
|
|
533
|
-
self._current_theme_index = (self._current_theme_index + 1) % len(THEME_NAMES)
|
|
534
|
-
new_theme = THEME_NAMES[self._current_theme_index]
|
|
535
|
-
|
|
536
|
-
# Apply new theme class (dark is default, no class needed)
|
|
537
|
-
if new_theme != "dark":
|
|
538
|
-
self.add_class(f"theme-{new_theme}")
|
|
539
|
-
|
|
540
581
|
def action_clear(self) -> None:
|
|
541
582
|
"""Clear the conversation (Ctrl+L)."""
|
|
542
583
|
conversation = self.query_one("#conversation-view", ConversationView)
|
|
543
584
|
conversation.clear_messages()
|
|
544
|
-
self.
|
|
585
|
+
self._workflow.planner.memory.clear()
|
|
545
586
|
self.notify("Conversation cleared!", severity="information")
|
|
546
587
|
|
|
547
|
-
def action_cycle_theme(self) -> None:
|
|
548
|
-
"""Cycle theme (Ctrl+T)."""
|
|
549
|
-
self._cycle_theme()
|
|
550
|
-
theme_name = THEME_NAMES[self._current_theme_index]
|
|
551
|
-
self.notify(f"Theme: {theme_name}", severity="information")
|
|
552
|
-
|
|
553
588
|
def action_save_session(self) -> None:
|
|
554
589
|
"""Save session (Ctrl+S)."""
|
|
555
590
|
conversation = self.query_one("#conversation-view", ConversationView)
|
|
@@ -579,8 +614,10 @@ Please resize your terminal."""
|
|
|
579
614
|
session = self._session_manager.create_session("kader_cli")
|
|
580
615
|
self._current_session_id = session.session_id
|
|
581
616
|
|
|
582
|
-
# Get messages from
|
|
583
|
-
messages = [
|
|
617
|
+
# Get messages from planner memory and save
|
|
618
|
+
messages = [
|
|
619
|
+
msg.message for msg in self._workflow.planner.memory.get_messages()
|
|
620
|
+
]
|
|
584
621
|
self._session_manager.save_conversation(self._current_session_id, messages)
|
|
585
622
|
|
|
586
623
|
conversation.add_message(
|
|
@@ -611,11 +648,11 @@ Please resize your terminal."""
|
|
|
611
648
|
|
|
612
649
|
# Clear current state
|
|
613
650
|
conversation.clear_messages()
|
|
614
|
-
self.
|
|
651
|
+
self._workflow.planner.memory.clear()
|
|
615
652
|
|
|
616
653
|
# Add loaded messages to memory and UI
|
|
617
654
|
for msg in messages:
|
|
618
|
-
self.
|
|
655
|
+
self._workflow.planner.memory.add_message(msg)
|
|
619
656
|
role = msg.get("role", "user")
|
|
620
657
|
content = msg.get("content", "")
|
|
621
658
|
if role in ["user", "assistant"] and content:
|
|
@@ -664,9 +701,9 @@ Please resize your terminal."""
|
|
|
664
701
|
"""Display LLM usage costs."""
|
|
665
702
|
try:
|
|
666
703
|
# Get cost and usage from the provider
|
|
667
|
-
cost = self.
|
|
668
|
-
usage = self.
|
|
669
|
-
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
|
|
670
707
|
|
|
671
708
|
lines = [
|
|
672
709
|
"## Usage Costs ($)\n",
|