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 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.tools import get_default_registry
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
- 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)
107
114
 
108
- def _create_agent(self, model_name: str) -> ReActAgent:
109
- """Create a new ReActAgent with the specified model."""
110
- registry = get_default_registry()
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
- 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")
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
- conversation.add_message("(+) Executing tool...", "assistant")
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._agent = self._create_agent(self._current_model)
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._agent.memory.clear()
435
- 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
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 ReActAgent."""
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 agent invoke in a thread
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
- None, lambda: self._agent.invoke(message)
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 and response.content:
502
- 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
+ )
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._agent.memory.clear()
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 agent memory and save
552
- 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
+ ]
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._agent.memory.clear()
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._agent.memory.add_message(msg)
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._agent.provider.total_cost
637
- usage = self._agent.provider.total_usage
638
- 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
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
@@ -3,7 +3,7 @@
3
3
  from kader.providers import OllamaProvider
4
4
 
5
5
  # Default model
6
- DEFAULT_MODEL = "qwen3-coder:480b-cloud"
6
+ DEFAULT_MODEL = "kimi-k2.5:cloud"
7
7
 
8
8
  HELP_TEXT = """## Kader CLI Commands
9
9
 
@@ -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__(self, content: str, role: str = "user") -> None:
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(self, content: str, role: str = "user") -> None:
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(multiplier=1, min=4, max=10),
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(multiplier=1, min=4, max=10),
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(multiplier=1, min=4, max=10),
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 get_default_registry
971
+ from kader.tools import get_cached_default_registry
909
972
 
910
- registry = get_default_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 BasicAssistancePrompt, PlanningAgentPrompt, ReActAgentPrompt
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
  ]
@@ -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)