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 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.tools import get_default_registry
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
- self._agent = self._create_agent(self._current_model)
106
+ # Dedicated thread pool for agent invocation (isolated from default pool)
107
+ self._agent_executor = ThreadPoolExecutor(
108
+ max_workers=2, thread_name_prefix="kader_agent"
109
+ )
110
+ # Ensure executor is properly shut down on exit
111
+ atexit.register(self._agent_executor.shutdown, wait=False)
112
+
113
+ self._workflow = self._create_workflow(self._current_model)
111
114
 
112
- def _create_agent(self, model_name: str) -> ReActAgent:
113
- """Create a new ReActAgent with the specified model."""
114
- registry = get_default_registry()
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
- self._confirmation_event.wait()
195
+ # Timeout after 5 minutes to prevent indefinite blocking
196
+ if not self._confirmation_event.wait(timeout=300):
197
+ # Timeout occurred - decline tool execution gracefully
198
+ return (False, "Tool confirmation timed out after 5 minutes")
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
- conversation.add_message("(+) Executing tool...", "assistant")
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._agent = self._create_agent(self._current_model)
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._agent.memory.clear()
445
- self._agent.provider.reset_tracking() # Reset usage/cost tracking
495
+ self._workflow.planner.memory.clear()
496
+ self._workflow.planner.provider.reset_tracking() # Reset usage/cost tracking
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 ReActAgent."""
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 agent invoke in a thread
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
- None, lambda: self._agent.invoke(message)
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 and response.content:
512
- conversation.add_message(response.content, "assistant")
562
+ if response:
563
+ conversation.add_message(
564
+ response,
565
+ "assistant",
566
+ model_name=self._workflow.planner.provider.model,
567
+ usage_cost=self._workflow.planner.provider.total_cost.total_cost,
568
+ )
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._agent.memory.clear()
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 agent memory and save
583
- messages = [msg.message for msg in self._agent.memory.get_messages()]
617
+ # Get messages from planner memory and save
618
+ messages = [
619
+ msg.message for msg in self._workflow.planner.memory.get_messages()
620
+ ]
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._agent.memory.clear()
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._agent.memory.add_message(msg)
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._agent.provider.total_cost
668
- usage = self._agent.provider.total_usage
669
- model = self._agent.provider.model
704
+ cost = self._workflow.planner.provider.total_cost
705
+ usage = self._workflow.planner.provider.total_usage
706
+ model = self._workflow.planner.provider.model
670
707
 
671
708
  lines = [
672
709
  "## Usage Costs ($)\n",