devduck 0.5.0__py3-none-any.whl → 0.5.2__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.

Potentially problematic release.


This version of devduck might be problematic. Click here for more details.

devduck/__init__.py CHANGED
@@ -1,262 +1,51 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- 🦆 devduck - extreme minimalist self-adapting agent
4
- one file. self-healing. runtime dependencies. adaptive.
5
- """
2
+ """🦆 devduck - self-adapting agent"""
6
3
  import sys
7
- import subprocess
4
+ import threading
8
5
  import os
9
6
  import platform
10
- import socket
11
7
  import logging
12
8
  import tempfile
13
- from datetime import datetime
9
+ import boto3
14
10
  from pathlib import Path
15
11
  from datetime import datetime
16
- from typing import Dict, Any
12
+ import warnings
17
13
  from logging.handlers import RotatingFileHandler
18
14
 
15
+ warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
16
+ warnings.filterwarnings("ignore", message=".*cache_prompt is deprecated.*")
17
+
19
18
  os.environ["BYPASS_TOOL_CONSENT"] = "true"
20
19
  os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
21
20
  os.environ["EDITOR_DISABLE_BACKUP"] = "true"
22
21
 
23
- # 📝 Setup logging system
24
22
  LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
25
23
  LOG_DIR.mkdir(parents=True, exist_ok=True)
26
24
  LOG_FILE = LOG_DIR / "devduck.log"
27
25
 
28
- # Configure logger
29
26
  logger = logging.getLogger("devduck")
30
27
  logger.setLevel(logging.DEBUG)
31
-
32
- # File handler with rotation (10MB max, keep 3 backups)
33
- file_handler = RotatingFileHandler(
34
- LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
28
+ logger.addHandler(
29
+ RotatingFileHandler(LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3)
35
30
  )
36
- file_handler.setLevel(logging.DEBUG)
37
- file_formatter = logging.Formatter(
38
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
39
- )
40
- file_handler.setFormatter(file_formatter)
41
-
42
- # Console handler (only warnings and above)
43
- console_handler = logging.StreamHandler()
44
- console_handler.setLevel(logging.WARNING)
45
- console_formatter = logging.Formatter("🦆 %(levelname)s: %(message)s")
46
- console_handler.setFormatter(console_formatter)
47
-
48
- logger.addHandler(file_handler)
49
- logger.addHandler(console_handler)
50
-
51
- logger.info("DevDuck logging system initialized")
52
-
31
+ logger.info("DevDuck initialized")
53
32
 
54
- # 🔧 Self-healing dependency installer
55
- def ensure_deps():
56
- """Install core dependencies at runtime if missing"""
57
- import importlib.metadata
58
33
 
59
- # Only ensure core deps - everything else is optional
60
- core_deps = [
61
- "strands-agents",
62
- "prompt_toolkit",
63
- "strands-agents-tools",
64
- ]
65
-
66
- # Check each package individually using importlib.metadata
67
- for dep in core_deps:
68
- pkg_name = dep.split("[")[0] # Get base package name (strip extras)
69
- try:
70
- # Check if package is installed using metadata (checks PyPI package name)
71
- importlib.metadata.version(pkg_name)
72
- except importlib.metadata.PackageNotFoundError:
73
- print(f"🦆 Installing {dep}...")
74
- logger.debug(f"🦆 Installing {dep}...")
75
- try:
76
- subprocess.check_call(
77
- [sys.executable, "-m", "pip", "install", dep],
78
- stdout=subprocess.DEVNULL,
79
- stderr=subprocess.DEVNULL,
80
- )
81
- except subprocess.CalledProcessError as e:
82
- print(f"🦆 Warning: Failed to install {dep}: {e}")
83
- logger.debug(f"🦆 Warning: Failed to install {dep}: {e}")
84
-
85
-
86
- # 🌍 Environment adaptation
87
- def adapt_to_env():
88
- """Self-adapt based on environment"""
89
- env_info = {
90
- "os": platform.system(),
91
- "arch": platform.machine(),
92
- "python": sys.version_info,
93
- "cwd": str(Path.cwd()),
94
- "home": str(Path.home()),
95
- "shell": os.environ.get("SHELL", "unknown"),
96
- "hostname": socket.gethostname(),
97
- }
98
-
99
- # Adaptive configurations - using common models
100
- if env_info["os"] == "Darwin": # macOS
101
- ollama_host = "http://localhost:11434"
102
- model = "qwen3:1.7b" # Lightweight for macOS
103
- elif env_info["os"] == "Linux":
104
- ollama_host = "http://localhost:11434"
105
- model = "qwen3:30b" # More power on Linux
106
- else: # Windows
107
- ollama_host = "http://localhost:11434"
108
- model = "qwen3:8b" # Conservative for Windows
109
-
110
- return env_info, ollama_host, model
111
-
112
-
113
- # 🔍 Self-awareness: Read own source code
114
34
  def get_own_source_code():
115
- """
116
- Read and return the source code of this agent file.
117
-
118
- Returns:
119
- str: The complete source code for self-awareness
120
- """
35
+ """Read own source code for self-awareness"""
121
36
  try:
122
- # Read this file (__init__.py)
123
- current_file = __file__
124
- with open(current_file, "r", encoding="utf-8") as f:
125
- init_code = f.read()
126
- return f"# devduck/__init__.py\n```python\n{init_code}\n```"
127
- except Exception as e:
128
- return f"Error reading own source code: {e}"
129
-
130
-
131
- def view_logs_tool(
132
- action: str = "view",
133
- lines: int = 100,
134
- pattern: str = None,
135
- ) -> Dict[str, Any]:
136
- """
137
- View and manage DevDuck logs.
138
-
139
- Args:
140
- action: Action to perform - "view", "tail", "search", "clear", "stats"
141
- lines: Number of lines to show (for view/tail)
142
- pattern: Search pattern (for search action)
143
-
144
- Returns:
145
- Dict with status and content
146
- """
147
- try:
148
- if action == "view":
149
- if not LOG_FILE.exists():
150
- return {"status": "success", "content": [{"text": "No logs yet"}]}
151
-
152
- with open(LOG_FILE, "r", encoding="utf-8") as f:
153
- all_lines = f.readlines()
154
- recent_lines = (
155
- all_lines[-lines:] if len(all_lines) > lines else all_lines
156
- )
157
- content = "".join(recent_lines)
158
-
159
- return {
160
- "status": "success",
161
- "content": [
162
- {"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
163
- ],
164
- }
165
-
166
- elif action == "tail":
167
- if not LOG_FILE.exists():
168
- return {"status": "success", "content": [{"text": "No logs yet"}]}
169
-
170
- with open(LOG_FILE, "r", encoding="utf-8") as f:
171
- all_lines = f.readlines()
172
- tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
173
- content = "".join(tail_lines)
174
-
175
- return {
176
- "status": "success",
177
- "content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
178
- }
179
-
180
- elif action == "search":
181
- if not pattern:
182
- return {
183
- "status": "error",
184
- "content": [{"text": "pattern parameter required for search"}],
185
- }
186
-
187
- if not LOG_FILE.exists():
188
- return {"status": "success", "content": [{"text": "No logs yet"}]}
189
-
190
- with open(LOG_FILE, "r", encoding="utf-8") as f:
191
- matching_lines = [line for line in f if pattern.lower() in line.lower()]
192
-
193
- if not matching_lines:
194
- return {
195
- "status": "success",
196
- "content": [{"text": f"No matches found for pattern: {pattern}"}],
197
- }
198
-
199
- content = "".join(matching_lines[-100:]) # Last 100 matches
200
- return {
201
- "status": "success",
202
- "content": [
203
- {
204
- "text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
205
- }
206
- ],
207
- }
208
-
209
- elif action == "clear":
210
- if LOG_FILE.exists():
211
- LOG_FILE.unlink()
212
- logger.info("Log file cleared by user")
213
- return {
214
- "status": "success",
215
- "content": [{"text": "Logs cleared successfully"}],
216
- }
217
-
218
- elif action == "stats":
219
- if not LOG_FILE.exists():
220
- return {"status": "success", "content": [{"text": "No logs yet"}]}
221
-
222
- stat = LOG_FILE.stat()
223
- size_mb = stat.st_size / (1024 * 1024)
224
- modified = datetime.fromtimestamp(stat.st_mtime).strftime(
225
- "%Y-%m-%d %H:%M:%S"
226
- )
227
-
228
- with open(LOG_FILE, "r", encoding="utf-8") as f:
229
- total_lines = sum(1 for _ in f)
230
-
231
- stats_text = f"""Log File Statistics:
232
- Path: {LOG_FILE}
233
- Size: {size_mb:.2f} MB
234
- Lines: {total_lines}
235
- Last Modified: {modified}"""
236
-
237
- return {"status": "success", "content": [{"text": stats_text}]}
238
-
239
- else:
240
- return {
241
- "status": "error",
242
- "content": [
243
- {
244
- "text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
245
- }
246
- ],
247
- }
248
-
37
+ with open(__file__, "r", encoding="utf-8") as f:
38
+ return f"# devduck/__init__.py\n```python\n{f.read()}\n```"
249
39
  except Exception as e:
250
- logger.error(f"Error in view_logs_tool: {e}")
251
- return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
40
+ return f"Error reading source: {e}"
252
41
 
253
42
 
254
43
  def get_shell_history_file():
255
- """Get the devduck-specific history file path."""
256
- devduck_history = Path.home() / ".devduck_history"
257
- if not devduck_history.exists():
258
- devduck_history.touch(mode=0o600)
259
- return str(devduck_history)
44
+ """Get devduck history file"""
45
+ history = Path.home() / ".devduck_history"
46
+ if not history.exists():
47
+ history.touch(mode=0o600)
48
+ return str(history)
260
49
 
261
50
 
262
51
  def get_shell_history_files():
@@ -336,32 +125,6 @@ def parse_history_line(line, history_type):
336
125
  return None
337
126
 
338
127
 
339
- def get_recent_logs():
340
- """Get the last N lines from the log file for context."""
341
- try:
342
- log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
343
-
344
- if not LOG_FILE.exists():
345
- return ""
346
-
347
- with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
348
- all_lines = f.readlines()
349
-
350
- recent_lines = (
351
- all_lines[-log_line_count:]
352
- if len(all_lines) > log_line_count
353
- else all_lines
354
- )
355
-
356
- if not recent_lines:
357
- return ""
358
-
359
- log_content = "".join(recent_lines)
360
- return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
361
- except Exception as e:
362
- return f"\n\n## Recent Logs: Error reading logs - {e}\n"
363
-
364
-
365
128
  def get_last_messages():
366
129
  """Get the last N messages from multiple shell histories for context."""
367
130
  try:
@@ -421,31 +184,52 @@ def get_last_messages():
421
184
  return ""
422
185
 
423
186
 
187
+ def get_recent_logs():
188
+ """Get recent logs for context"""
189
+ try:
190
+ log_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
191
+
192
+ if not LOG_FILE.exists():
193
+ return ""
194
+
195
+ with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
196
+ lines = f.readlines()
197
+
198
+ recent = lines[-log_count:] if len(lines) > log_count else lines
199
+
200
+ if recent:
201
+ return f"\n\n## Recent Logs (last {len(recent)} lines):\n```\n{''.join(recent)}```\n"
202
+ return ""
203
+ except:
204
+ return ""
205
+
206
+
424
207
  def append_to_shell_history(query, response):
425
- """Append the interaction to devduck shell history."""
208
+ """Append interaction to history"""
426
209
  import time
427
210
 
428
211
  try:
429
212
  history_file = get_shell_history_file()
430
213
  timestamp = str(int(time.time()))
214
+ response_summary = (
215
+ str(response).replace("\n", " ")[
216
+ : int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
217
+ ]
218
+ + "..."
219
+ )
431
220
 
432
221
  with open(history_file, "a", encoding="utf-8") as f:
433
222
  f.write(f": {timestamp}:0;# devduck: {query}\n")
434
- response_summary = (
435
- str(response).replace("\n", " ")[
436
- : int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
437
- ]
438
- + "..."
439
- )
440
223
  f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
441
224
 
442
225
  os.chmod(history_file, 0o600)
443
- except Exception:
226
+ except:
444
227
  pass
445
228
 
446
229
 
447
- # 🦆 The devduck agent
448
230
  class DevDuck:
231
+ """Minimalist adaptive agent with flexible tool loading"""
232
+
449
233
  def __init__(
450
234
  self,
451
235
  auto_start_servers=True,
@@ -459,43 +243,92 @@ class DevDuck:
459
243
  enable_ipc=True,
460
244
  ):
461
245
  """Initialize the minimalist adaptive agent"""
462
- logger.info("Initializing DevDuck agent...")
463
- try:
464
- # Self-heal dependencies
465
- ensure_deps()
246
+ logger.info("Initializing DevDuck...")
247
+
248
+ # Environment detection
249
+ self.os = platform.system()
250
+ self.arch = platform.machine()
251
+ self.model = "qwen3:1.7b" if self.os == "Darwin" else "qwen3:8b"
252
+
253
+ # Hot-reload state
254
+ self._agent_executing = False
255
+ self._reload_pending = False
256
+
257
+ # Server configuration
258
+ self.tcp_port = tcp_port
259
+ self.ws_port = ws_port
260
+ self.mcp_port = mcp_port
261
+ self.ipc_socket = ipc_socket or "/tmp/devduck_main.sock"
262
+ self.enable_tcp = enable_tcp
263
+ self.enable_ws = enable_ws
264
+ self.enable_mcp = enable_mcp
265
+ self.enable_ipc = enable_ipc
266
+
267
+ # Import core dependencies
268
+ from strands import Agent, tool
269
+
270
+ # Load tools with flexible configuration
271
+ tools = self._load_tools_flexible()
272
+
273
+ # Add built-in view_logs tool
274
+ @tool
275
+ def view_logs(action: str = "view", lines: int = 100, pattern: str = None):
276
+ """View and manage DevDuck logs"""
277
+ return self._view_logs_impl(action, lines, pattern)
278
+
279
+ tools.append(view_logs)
280
+
281
+ # Create model
282
+ model = self._create_model()
283
+
284
+ # Create agent
285
+ self.agent = Agent(
286
+ model=model,
287
+ tools=tools,
288
+ system_prompt=self._build_prompt(),
289
+ load_tools_from_directory=True,
290
+ )
466
291
 
467
- # Adapt to environment
468
- self.env_info, self.ollama_host, self.model = adapt_to_env()
292
+ # Auto-start servers
293
+ if auto_start_servers and "--mcp" not in sys.argv:
294
+ self._start_servers()
469
295
 
470
- # Execution state tracking for hot-reload
471
- self._agent_executing = False
472
- self._reload_pending = False
296
+ # Start hot-reload watcher
297
+ self._start_hot_reload()
473
298
 
474
- # Import after ensuring deps
475
- from strands import Agent, tool
299
+ logger.info(f"DevDuck ready with {len(tools)} tools")
476
300
 
477
- # Core tools (always available)
478
- core_tools = []
301
+ def _load_tools_flexible(self):
302
+ """
303
+ Load tools with flexible configuration via DEVDUCK_TOOLS env var.
479
304
 
480
- # Try importing optional tools gracefully
481
- try:
482
- from strands.models.ollama import OllamaModel
483
- except ImportError:
484
- logger.warning(
485
- "strands-agents[ollama] not installed - Ollama model unavailable"
486
- )
487
- OllamaModel = None
305
+ Format: package:tool1,tool2:package2:tool3,tool4
306
+ Example: strands_tools:shell,editor:strands_fun_tools:clipboard
488
307
 
489
- try:
490
- from strands_tools.utils.models.model import create_model
491
- except ImportError:
492
- logger.warning(
493
- "strands-agents-tools not installed - create_model unavailable"
494
- )
495
- create_model = None
308
+ Static tools (always loaded):
309
+ - DevDuck's own tools (tcp, websocket, ipc, etc.)
310
+ - AgentCore tools (if AWS credentials available)
311
+ """
312
+ tools = []
496
313
 
497
- try:
498
- from .tools import (
314
+ # 1. STATIC: Core DevDuck tools (always load)
315
+ try:
316
+ from devduck.tools import (
317
+ tcp,
318
+ websocket,
319
+ ipc,
320
+ mcp_server,
321
+ install_tools,
322
+ use_github,
323
+ create_subagent,
324
+ store_in_kb,
325
+ system_prompt,
326
+ tray,
327
+ ambient,
328
+ )
329
+
330
+ tools.extend(
331
+ [
499
332
  tcp,
500
333
  websocket,
501
334
  ipc,
@@ -507,211 +340,197 @@ class DevDuck:
507
340
  system_prompt,
508
341
  tray,
509
342
  ambient,
510
- )
343
+ ]
344
+ )
345
+ logger.info("✅ DevDuck core tools loaded")
346
+ except ImportError as e:
347
+ logger.warning(f"DevDuck tools unavailable: {e}")
511
348
 
512
- core_tools.extend(
349
+ # 2. STATIC: AgentCore tools (if AWS credentials available and not disabled)
350
+ if os.getenv("DEVDUCK_DISABLE_AGENTCORE_TOOLS", "false").lower() != "true":
351
+ try:
352
+ boto3.client("sts").get_caller_identity()
353
+ from .tools.agentcore_config import agentcore_config
354
+ from .tools.agentcore_invoke import agentcore_invoke
355
+ from .tools.agentcore_logs import agentcore_logs
356
+ from .tools.agentcore_agents import agentcore_agents
357
+
358
+ tools.extend(
513
359
  [
514
- tcp,
515
- websocket,
516
- ipc,
517
- mcp_server,
518
- install_tools,
519
- use_github,
520
- create_subagent,
521
- store_in_kb,
522
- system_prompt,
523
- tray,
524
- ambient,
360
+ agentcore_config,
361
+ agentcore_invoke,
362
+ agentcore_logs,
363
+ agentcore_agents,
525
364
  ]
526
365
  )
527
- except ImportError as e:
528
- logger.warning(f"devduck.tools import failed: {e}")
366
+ logger.info("✅ AgentCore tools loaded")
367
+ except:
368
+ pass
369
+ else:
370
+ logger.info(
371
+ "⏭️ AgentCore tools disabled (DEVDUCK_DISABLE_AGENTCORE_TOOLS=true)"
372
+ )
529
373
 
530
- # Skip fun tools in --mcp mode (they're not needed for MCP server)
531
- if "--mcp" not in sys.argv:
532
- try:
533
- from strands_fun_tools import (
534
- listen,
535
- cursor,
536
- clipboard,
537
- screen_reader,
538
- yolo_vision,
539
- )
374
+ # 3. FLEXIBLE: Load tools from DEVDUCK_TOOLS env var
375
+ tools_config = os.getenv("DEVDUCK_TOOLS")
540
376
 
541
- core_tools.extend(
542
- [listen, cursor, clipboard, screen_reader, yolo_vision]
543
- )
544
- except ImportError:
545
- logger.info(
546
- "strands-fun-tools not installed - vision/audio tools unavailable (install with: pip install devduck[all])"
547
- )
377
+ if tools_config:
378
+ # Parse: "strands_tools:shell,editor:strands_fun_tools:clipboard"
379
+ tools.extend(self._parse_and_load_tools(tools_config))
380
+ else:
381
+ # Default: Load all common tools
382
+ tools.extend(self._load_default_tools())
383
+
384
+ return tools
385
+
386
+ def _parse_and_load_tools(self, config):
387
+ """
388
+ Parse DEVDUCK_TOOLS config and load specified tools.
389
+
390
+ Format: package:tool1,tool2:package2:tool3
391
+ Example: strands_tools:shell,editor:strands_fun_tools:clipboard,cursor
392
+ """
393
+ loaded_tools = []
394
+ current_package = None
395
+
396
+ for segment in config.split(":"):
397
+ segment = segment.strip()
398
+
399
+ # Check if this segment is a package or tool list
400
+ if "," not in segment and not segment.startswith("strands"):
401
+ # Single tool from current package
402
+ if current_package:
403
+ tool = self._load_single_tool(current_package, segment)
404
+ if tool:
405
+ loaded_tools.append(tool)
406
+ elif "," in segment:
407
+ # Tool list from current package
408
+ if current_package:
409
+ for tool_name in segment.split(","):
410
+ tool_name = tool_name.strip()
411
+ tool = self._load_single_tool(current_package, tool_name)
412
+ if tool:
413
+ loaded_tools.append(tool)
548
414
  else:
549
- logger.info("--mcp mode: skipping vision/audio tools")
415
+ # Package name
416
+ current_package = segment
550
417
 
551
- try:
552
- from strands_tools import (
418
+ logger.info(f"✅ Loaded {len(loaded_tools)} tools from DEVDUCK_TOOLS")
419
+ return loaded_tools
420
+
421
+ def _load_single_tool(self, package, tool_name):
422
+ """Load a single tool from a package"""
423
+ try:
424
+ module = __import__(package, fromlist=[tool_name])
425
+ tool = getattr(module, tool_name)
426
+ logger.debug(f"Loaded {tool_name} from {package}")
427
+ return tool
428
+ except Exception as e:
429
+ logger.warning(f"Failed to load {tool_name} from {package}: {e}")
430
+ return None
431
+
432
+ def _load_default_tools(self):
433
+ """Load default tools when DEVDUCK_TOOLS is not set"""
434
+ tools = []
435
+
436
+ # strands-agents-tools (essential)
437
+ try:
438
+ from strands_tools import (
439
+ shell,
440
+ editor,
441
+ file_read,
442
+ file_write,
443
+ calculator,
444
+ image_reader,
445
+ use_agent,
446
+ load_tool,
447
+ environment,
448
+ mcp_client,
449
+ retrieve,
450
+ )
451
+
452
+ tools.extend(
453
+ [
553
454
  shell,
554
455
  editor,
555
456
  file_read,
556
457
  file_write,
557
458
  calculator,
558
- # python_repl,
559
459
  image_reader,
560
460
  use_agent,
561
461
  load_tool,
562
462
  environment,
563
463
  mcp_client,
564
464
  retrieve,
565
- )
566
-
567
- core_tools.extend(
568
- [
569
- shell,
570
- editor,
571
- file_read,
572
- file_write,
573
- calculator,
574
- # python_repl,
575
- image_reader,
576
- use_agent,
577
- load_tool,
578
- environment,
579
- mcp_client,
580
- retrieve,
581
- ]
582
- )
583
- except ImportError:
584
- logger.info(
585
- "strands-agents-tools not installed - core tools unavailable (install with: pip install devduck[all])"
586
- )
587
-
588
- # Wrap view_logs_tool with @tool decorator
589
- @tool
590
- def view_logs(
591
- action: str = "view",
592
- lines: int = 100,
593
- pattern: str = None,
594
- ) -> Dict[str, Any]:
595
- """View and manage DevDuck logs."""
596
- return view_logs_tool(action, lines, pattern)
597
-
598
- # Add built-in tools to the toolset
599
- core_tools.extend([view_logs])
600
-
601
- # Assign tools
602
- self.tools = core_tools
603
-
604
- logger.info(f"Initialized {len(self.tools)} tools")
605
-
606
- # Check if MODEL_PROVIDER env variable is set
607
- model_provider = os.getenv("MODEL_PROVIDER")
608
-
609
- if model_provider and create_model:
610
- # Use create_model utility for any provider (bedrock, anthropic, etc.)
611
- self.agent_model = create_model(provider=model_provider)
612
- elif OllamaModel:
613
- # Fallback to default Ollama behavior
614
- self.agent_model = OllamaModel(
615
- host=self.ollama_host,
616
- model_id=self.model,
617
- temperature=1,
618
- keep_alive="5m",
619
- )
620
- else:
621
- raise ImportError(
622
- "No model provider available. Install with: pip install devduck[all]"
623
- )
624
-
625
- # Create agent with self-healing
626
- self.agent = Agent(
627
- model=self.agent_model,
628
- tools=self.tools,
629
- system_prompt=self._build_system_prompt(),
630
- load_tools_from_directory=True,
465
+ ]
631
466
  )
467
+ logger.info("✅ strands-agents-tools loaded")
468
+ except ImportError:
469
+ logger.warning("strands-agents-tools unavailable")
632
470
 
633
- # 🚀 AUTO-START SERVERS: TCP, WebSocket, MCP HTTP
634
- if auto_start_servers:
635
- logger.info("Auto-starting servers...")
636
- print("🦆 Auto-starting servers...")
471
+ # strands-fun-tools (optional, skip in --mcp mode)
472
+ if "--mcp" not in sys.argv:
473
+ try:
474
+ from strands_fun_tools import (
475
+ listen,
476
+ cursor,
477
+ clipboard,
478
+ screen_reader,
479
+ yolo_vision,
480
+ )
637
481
 
638
- if enable_tcp:
639
- try:
640
- # Start TCP server on configurable port
641
- tcp_result = self.agent.tool.tcp(
642
- action="start_server", port=tcp_port
643
- )
644
- if tcp_result.get("status") == "success":
645
- logger.info(f"✓ TCP server started on port {tcp_port}")
646
- print(f"🦆 ✓ TCP server: localhost:{tcp_port}")
647
- else:
648
- logger.warning(f"TCP server start issue: {tcp_result}")
649
- except Exception as e:
650
- logger.error(f"Failed to start TCP server: {e}")
651
- print(f"🦆 ⚠ TCP server failed: {e}")
482
+ tools.extend([listen, cursor, clipboard, screen_reader, yolo_vision])
483
+ logger.info("✅ strands-fun-tools loaded")
484
+ except ImportError:
485
+ logger.info("strands-fun-tools unavailable")
652
486
 
653
- if enable_ws:
654
- try:
655
- # Start WebSocket server on configurable port
656
- ws_result = self.agent.tool.websocket(
657
- action="start_server", port=ws_port
658
- )
659
- if ws_result.get("status") == "success":
660
- logger.info(f"✓ WebSocket server started on port {ws_port}")
661
- print(f"🦆 ✓ WebSocket server: localhost:{ws_port}")
662
- else:
663
- logger.warning(f"WebSocket server start issue: {ws_result}")
664
- except Exception as e:
665
- logger.error(f"Failed to start WebSocket server: {e}")
666
- print(f"🦆 ⚠ WebSocket server failed: {e}")
487
+ return tools
667
488
 
668
- if enable_mcp:
669
- try:
670
- # Start MCP server with HTTP transport on configurable port
671
- mcp_result = self.agent.tool.mcp_server(
672
- action="start",
673
- transport="http",
674
- port=mcp_port,
675
- expose_agent=True,
676
- agent=self.agent,
677
- )
678
- if mcp_result.get("status") == "success":
679
- logger.info(f"✓ MCP HTTP server started on port {mcp_port}")
680
- print(f"🦆 ✓ MCP server: http://localhost:{mcp_port}/mcp")
681
- else:
682
- logger.warning(f"MCP server start issue: {mcp_result}")
683
- except Exception as e:
684
- logger.error(f"Failed to start MCP server: {e}")
685
- print(f"🦆 ⚠ MCP server failed: {e}")
489
+ def _create_model(self):
490
+ """Create model with smart provider selection"""
491
+ provider = os.getenv("MODEL_PROVIDER")
686
492
 
687
- if enable_ipc:
493
+ if not provider:
494
+ # Auto-detect: Bedrock → MLX → Ollama
495
+ try:
496
+ boto3.client("sts").get_caller_identity()
497
+ provider = "bedrock"
498
+ print("🦆 Using Bedrock")
499
+ except:
500
+ if self.os == "Darwin" and self.arch in ["arm64", "aarch64"]:
688
501
  try:
689
- # Start IPC server for local process communication
690
- ipc_socket_path = ipc_socket or "/tmp/devduck_main.sock"
691
- ipc_result = self.agent.tool.ipc(
692
- action="start_server", socket_path=ipc_socket_path
693
- )
694
- if ipc_result.get("status") == "success":
695
- logger.info(f"✓ IPC server started on {ipc_socket_path}")
696
- print(f"🦆 IPC server: {ipc_socket_path}")
697
- else:
698
- logger.warning(f"IPC server start issue: {ipc_result}")
699
- except Exception as e:
700
- logger.error(f"Failed to start IPC server: {e}")
701
- print(f"🦆 IPC server failed: {e}")
702
-
703
- # Start file watcher for auto hot-reload
704
- self._start_file_watcher()
502
+ from strands_mlx import MLXModel
503
+
504
+ provider = "mlx"
505
+ self.model = "mlx-community/Qwen3-1.7B-4bit"
506
+ print("🦆 Using MLX")
507
+ except ImportError:
508
+ provider = "ollama"
509
+ print("🦆 Using Ollama")
510
+ else:
511
+ provider = "ollama"
512
+ print("🦆 Using Ollama")
513
+
514
+ # Create model
515
+ if provider == "mlx":
516
+ from strands_mlx import MLXModel
517
+
518
+ return MLXModel(model_id=self.model, temperature=1)
519
+ elif provider == "ollama":
520
+ from strands.models.ollama import OllamaModel
521
+
522
+ return OllamaModel(
523
+ host="http://localhost:11434",
524
+ model_id=self.model,
525
+ temperature=1,
526
+ keep_alive="5m",
527
+ )
528
+ else:
529
+ from strands_tools.utils.models.model import create_model
705
530
 
706
- logger.info(
707
- f"DevDuck agent initialized successfully with model {self.model}"
708
- )
531
+ return create_model(provider=provider)
709
532
 
710
- except Exception as e:
711
- logger.error(f"Initialization failed: {e}")
712
- self._self_heal(e)
713
-
714
- def _build_system_prompt(self):
533
+ def _build_prompt(self):
715
534
  """Build adaptive system prompt based on environment
716
535
 
717
536
  IMPORTANT: The system prompt includes the agent's complete source code.
@@ -721,427 +540,344 @@ class DevDuck:
721
540
 
722
541
  Learning: Always check source code truth over conversation memory!
723
542
  """
724
- session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
725
-
726
- # Get own source code for self-awareness
727
543
  own_code = get_own_source_code()
544
+ recent_context = get_last_messages()
545
+ recent_logs = get_recent_logs()
728
546
 
729
- # print(own_code)
730
-
731
- # Get recent conversation history context (with error handling)
547
+ # Detect if using Bedrock for AgentCore documentation
548
+ provider = os.getenv("MODEL_PROVIDER", "")
549
+ is_bedrock = provider == "bedrock" or "bedrock" in provider.lower()
732
550
  try:
733
- recent_context = get_last_messages()
734
- except Exception as e:
735
- print(f"🦆 Warning: Could not load history context: {e}")
736
- recent_context = ""
551
+ if not is_bedrock:
552
+ boto3.client("sts").get_caller_identity()
553
+ is_bedrock = True
554
+ except:
555
+ pass
737
556
 
738
- # Get recent logs for immediate visibility
739
- try:
740
- recent_logs = get_recent_logs()
741
- except Exception as e:
742
- print(f"🦆 Warning: Could not load recent logs: {e}")
743
- recent_logs = ""
557
+ # Build AgentCore documentation if using Bedrock
558
+ agentcore_docs = ""
559
+ if is_bedrock:
560
+ handler_path = str(Path(__file__).parent / "agentcore_handler.py")
561
+ agentcore_docs = f"""
562
+
563
+ ## 🚀 AgentCore (Bedrock)
564
+
565
+ Handler: `{handler_path}`
566
+
567
+ ### Quick Deploy:
568
+ ```python
569
+ # Configure + launch
570
+ agentcore_config(action="configure", agent_name="devduck",
571
+ tools="strands_tools:shell,editor", auto_launch=True)
744
572
 
745
- return f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
573
+ # Invoke
574
+ agentcore_invoke(prompt="test", agent_name="devduck")
746
575
 
747
- Environment: {self.env_info['os']} {self.env_info['arch']}
748
- Python: {self.env_info['python']}
576
+ # Monitor
577
+ agentcore_logs(agent_name="devduck")
578
+ agentcore_agents(action="list")
579
+ ```
580
+
581
+ ### Key Params:
582
+ - tools: "package:tool1,tool2:package2:tool3"
583
+ - idle_timeout: 900s (default)
584
+ - model_id: us.anthropic.claude-sonnet-4-5-20250929-v1:0
585
+ """
586
+
587
+ return f"""🦆 DevDuck - self-adapting agent
588
+
589
+ Environment: {self.os} {self.arch}
749
590
  Model: {self.model}
750
- Hostname: {self.env_info['hostname']}
751
- Session ID: {session_id}
591
+ CWD: {Path.cwd()}
752
592
 
753
593
  You are:
754
594
  - Minimalist: Brief, direct responses
755
- - Self-healing: Adapt when things break
756
595
  - Efficient: Get things done fast
757
596
  - Pragmatic: Use what works
758
597
 
759
- Current working directory: {self.env_info['cwd']}
760
-
761
598
  {recent_context}
762
599
  {recent_logs}
600
+ {agentcore_docs}
763
601
 
764
- ## Your Own Implementation:
765
- You have full access to your own source code for self-awareness and self-modification:
602
+ ## Your Code
766
603
 
604
+ You have full access to your own source code for self-awareness and self-modification:
605
+ ---
767
606
  {own_code}
607
+ ---
768
608
 
769
- ## Hot Reload System Active:
770
- - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
771
- - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
772
- - **Live Development** - Modify existing tools while running and test immediately
773
- - **Full Python Access** - Create any Python functionality as a tool
774
-
775
- ## Dynamic Tool Loading:
776
- - **Install Tools** - Use install_tools() to load tools from any Python package
777
- - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
778
- - Expands capabilities without restart
779
- - Access to entire Python ecosystem
609
+ ## Hot Reload Active:
610
+ - Save .py files in ./tools/ for instant tool creation
611
+ - Use install_tools() to load from packages
612
+ - No restart needed
780
613
 
781
- ## MCP Server:
782
- - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
783
- - Example: mcp_server(action="start", port=8000)
784
- - Connect from Claude Desktop, other agents, or custom clients
785
- - Full bidirectional communication
614
+ ## Tool Configuration:
615
+ Set DEVDUCK_TOOLS for custom tools:
616
+ - Format: package:tool1,tool2:package2:tool3
617
+ - Example: strands_tools:shell,editor:strands_fun_tools:clipboard
618
+ - Static tools always loaded: tcp, websocket, ipc, mcp_server, agentcore_*
786
619
 
787
620
  ## Knowledge Base Integration:
788
- - **Automatic RAG** - Set STRANDS_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
621
+ - **Automatic RAG** - Set DEVDUCK_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
789
622
  - Before each query: Retrieves relevant context from knowledge base
790
623
  - After each response: Stores conversation for future reference
791
624
  - Seamless memory across sessions without manual tool calls
792
625
 
793
- ## Tool Creation Patterns:
626
+ ## System Prompt Management:
627
+ - system_prompt(action='view') - View current
628
+ - system_prompt(action='update', prompt='...') - Update
629
+ - system_prompt(action='update', repository='owner/repo') - Sync to GitHub
794
630
 
795
- ### **1. @tool Decorator:**
631
+ ## Shell Commands:
632
+ - Prefix with ! to run shell commands
633
+ - Example: ! ls -la
634
+
635
+ Response: MINIMAL WORDS, MAX PARALLELISM
636
+
637
+ ## Tool Building Guide:
638
+
639
+ ### **@tool Decorator (Recommended):**
796
640
  ```python
797
- # ./tools/calculate_tip.py
641
+ # ./tools/my_tool.py
798
642
  from strands import tool
799
643
 
800
644
  @tool
801
- def calculate_tip(amount: float, percentage: float = 15.0) -> str:
802
- \"\"\"Calculate tip and total for a bill.
645
+ def my_tool(param1: str, param2: int = 10) -> str:
646
+ \"\"\"Tool description.
803
647
 
804
648
  Args:
805
- amount: Bill amount in dollars
806
- percentage: Tip percentage (default: 15.0)
649
+ param1: Description of param1
650
+ param2: Description of param2 (default: 10)
807
651
 
808
652
  Returns:
809
- str: Formatted tip calculation result
653
+ str: Description of return value
810
654
  \"\"\"
811
- tip = amount * (percentage / 100)
812
- total = amount + tip
813
- return f"Tip: {{tip:.2f}}, Total: {{total:.2f}}"
655
+ # Implementation
656
+ return f"Result: {{param1}} - {{param2}}"
814
657
  ```
815
658
 
816
- ### **2. Action-Based Pattern:**
659
+ ### **Action-Based Pattern:**
817
660
  ```python
818
- # ./tools/weather.py
819
661
  from typing import Dict, Any
820
662
  from strands import tool
821
663
 
822
664
  @tool
823
- def weather(action: str, location: str = None) -> Dict[str, Any]:
824
- \"\"\"Comprehensive weather information tool.
665
+ def my_tool(action: str, data: str = None) -> Dict[str, Any]:
666
+ \"\"\"Multi-action tool.
825
667
 
826
668
  Args:
827
- action: Action to perform (current, forecast, alerts)
828
- location: City name (required)
669
+ action: Action to perform (get, set, delete)
670
+ data: Optional data for action
829
671
 
830
672
  Returns:
831
- Dict containing status and response content
673
+ Dict with status and content
832
674
  \"\"\"
833
- if action == "current":
834
- return {{"status": "success", "content": [{{"text": f"Weather for {{location}}"}}]}}
835
- elif action == "forecast":
836
- return {{"status": "success", "content": [{{"text": f"Forecast for {{location}}"}}]}}
675
+ if action == "get":
676
+ return {{"status": "success", "content": [{{"text": f"Got: {{data}}"}}]}}
677
+ elif action == "set":
678
+ return {{"status": "success", "content": [{{"text": f"Set: {{data}}"}}]}}
837
679
  else:
838
680
  return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
839
681
  ```
840
682
 
841
- ## System Prompt Management:
842
- - **View**: system_prompt(action='view') - See current prompt
843
- - **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
844
- - **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
845
- - **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
846
- - **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
847
-
848
- ### 🧠 Self-Improvement Pattern:
849
- When you learn something valuable during conversations:
850
- 1. Identify the new insight or pattern
851
- 2. Use system_prompt(action='add_context', context='...') to append it
852
- 3. Sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
853
- 4. New learnings persist across sessions via SYSTEM_PROMPT env var
854
-
855
- **Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
856
-
857
- ## Shell Commands:
858
- - Prefix with ! to execute shell commands directly
859
- - Example: ! ls -la (lists files)
860
- - Example: ! pwd (shows current directory)
861
-
862
- **Response Format:**
863
- - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
864
- - Communication: **MINIMAL WORDS**
865
- - Efficiency: **Speed is paramount**
683
+ ### **Tool Best Practices:**
684
+ 1. Use type hints for all parameters
685
+ 2. Provide clear docstrings
686
+ 3. Return consistent formats (str or Dict[str, Any])
687
+ 4. Use action-based pattern for complex tools
688
+ 5. Handle errors gracefully
689
+ 6. Log important operations
866
690
 
867
691
  {os.getenv('SYSTEM_PROMPT', '')}"""
868
692
 
869
- def _self_heal(self, error):
870
- """Attempt self-healing when errors occur"""
871
- logger.error(f"Self-healing triggered by error: {error}")
872
- print(f"🦆 Self-healing from: {error}")
693
+ def _view_logs_impl(self, action, lines, pattern):
694
+ """Implementation of view_logs tool"""
695
+ try:
696
+ if action == "view":
697
+ if not LOG_FILE.exists():
698
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
699
+ with open(LOG_FILE, "r") as f:
700
+ all_lines = f.readlines()
701
+ recent = all_lines[-lines:] if len(all_lines) > lines else all_lines
702
+ return {
703
+ "status": "success",
704
+ "content": [
705
+ {"text": f"Last {len(recent)} lines:\n\n{''.join(recent)}"}
706
+ ],
707
+ }
873
708
 
874
- # Prevent infinite recursion by tracking heal attempts
875
- if not hasattr(self, "_heal_count"):
876
- self._heal_count = 0
709
+ elif action == "search":
710
+ if not pattern:
711
+ return {
712
+ "status": "error",
713
+ "content": [{"text": "pattern required"}],
714
+ }
715
+ if not LOG_FILE.exists():
716
+ return {"status": "success", "content": [{"text": "No logs yet"}]}
717
+ with open(LOG_FILE, "r") as f:
718
+ matches = [line for line in f if pattern.lower() in line.lower()]
719
+ if not matches:
720
+ return {
721
+ "status": "success",
722
+ "content": [{"text": f"No matches for: {pattern}"}],
723
+ }
724
+ return {
725
+ "status": "success",
726
+ "content": [
727
+ {
728
+ "text": f"Found {len(matches)} matches:\n\n{''.join(matches[-100:])}"
729
+ }
730
+ ],
731
+ }
877
732
 
878
- self._heal_count += 1
733
+ elif action == "clear":
734
+ if LOG_FILE.exists():
735
+ LOG_FILE.unlink()
736
+ return {"status": "success", "content": [{"text": "Logs cleared"}]}
879
737
 
880
- # Limit recursion - if we've tried more than 3 times, give up
881
- if self._heal_count > 3:
882
- print(f"🦆 Self-healing failed after {self._heal_count} attempts")
883
- print("🦆 Please fix the issue manually and restart")
884
- sys.exit(1)
738
+ else:
739
+ return {
740
+ "status": "error",
741
+ "content": [{"text": f"Unknown action: {action}"}],
742
+ }
743
+ except Exception as e:
744
+ return {"status": "error", "content": [{"text": f"Error: {e}"}]}
885
745
 
886
- # Common healing strategies
887
- if "not found" in str(error).lower() and "model" in str(error).lower():
888
- print("🦆 Model not found - trying to pull model...")
746
+ def _start_servers(self):
747
+ """Auto-start servers"""
748
+ if self.enable_tcp:
889
749
  try:
890
- # Try to pull the model
891
- result = subprocess.run(
892
- ["ollama", "pull", self.model], capture_output=True, timeout=60
893
- )
894
- if result.returncode == 0:
895
- print(f"🦆 Successfully pulled {self.model}")
896
- else:
897
- print(f"🦆 Failed to pull {self.model}, trying fallback...")
898
- # Fallback to basic models
899
- fallback_models = ["llama3.2:1b", "qwen2.5:0.5b", "gemma2:2b"]
900
- for fallback in fallback_models:
901
- try:
902
- subprocess.run(
903
- ["ollama", "pull", fallback],
904
- capture_output=True,
905
- timeout=30,
906
- )
907
- self.model = fallback
908
- print(f"🦆 Using fallback model: {fallback}")
909
- break
910
- except:
911
- continue
912
- except Exception as pull_error:
913
- print(f"🦆 Model pull failed: {pull_error}")
914
- # Ultra-minimal fallback
915
- self.model = "llama3.2:1b"
750
+ self.agent.tool.tcp(action="start_server", port=self.tcp_port)
751
+ print(f"🦆 TCP: localhost:{self.tcp_port}")
752
+ except Exception as e:
753
+ logger.warning(f"TCP server failed: {e}")
916
754
 
917
- elif "ollama" in str(error).lower():
918
- print("🦆 Ollama issue - checking service...")
755
+ if self.enable_ws:
919
756
  try:
920
- # Check if ollama is running
921
- result = subprocess.run(
922
- ["ollama", "list"], capture_output=True, timeout=5
923
- )
924
- if result.returncode != 0:
925
- print("🦆 Starting ollama service...")
926
- subprocess.Popen(["ollama", "serve"])
927
- import time
928
-
929
- time.sleep(3) # Wait for service to start
930
- except Exception as ollama_error:
931
- print(f"🦆 Ollama service issue: {ollama_error}")
932
-
933
- elif "import" in str(error).lower():
934
- print("🦆 Import issue - reinstalling dependencies...")
935
- ensure_deps()
757
+ self.agent.tool.websocket(action="start_server", port=self.ws_port)
758
+ print(f"🦆 WebSocket: localhost:{self.ws_port}")
759
+ except Exception as e:
760
+ logger.warning(f"WebSocket server failed: {e}")
936
761
 
937
- elif "connection" in str(error).lower():
938
- print("🦆 Connection issue - checking ollama service...")
762
+ if self.enable_mcp:
939
763
  try:
940
- subprocess.run(["ollama", "serve"], check=False, timeout=2)
941
- except:
942
- pass
943
-
944
- # Retry initialization
945
- try:
946
- self.__init__()
947
- except Exception as e2:
948
- print(f"🦆 Self-heal failed: {e2}")
949
- print("🦆 Running in minimal mode...")
950
- self.agent = None
951
-
952
- def __call__(self, query):
953
- """Make the agent callable with automatic knowledge base integration"""
954
- if not self.agent:
955
- logger.warning("Agent unavailable - attempted to call with query")
956
- return "🦆 Agent unavailable - try: devduck.restart()"
957
-
958
- try:
959
- logger.info(f"Agent call started: {query[:100]}...")
960
- # Mark agent as executing to prevent hot-reload interruption
961
- self._agent_executing = True
962
-
963
- # 📚 Knowledge Base Retrieval (BEFORE agent runs)
964
- knowledge_base_id = os.getenv("STRANDS_KNOWLEDGE_BASE_ID")
965
- if knowledge_base_id and hasattr(self.agent, "tool"):
966
- try:
967
- if "retrieve" in self.agent.tool_names:
968
- logger.info(f"Retrieving context from KB: {knowledge_base_id}")
969
- self.agent.tool.retrieve(
970
- text=query, knowledgeBaseId=knowledge_base_id
971
- )
972
- except Exception as e:
973
- logger.warning(f"KB retrieval failed: {e}")
974
-
975
- # Run the agent
976
- result = self.agent(query)
977
-
978
- # 💾 Knowledge Base Storage (AFTER agent runs)
979
- if knowledge_base_id and hasattr(self.agent, "tool"):
980
- try:
981
- if "store_in_kb" in self.agent.tool_names:
982
-
983
- conversation_content = f"Input: {query}, Result: {result!s}"
984
- conversation_title = f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}"
985
- self.agent.tool.store_in_kb(
986
- content=conversation_content,
987
- title=conversation_title,
988
- knowledge_base_id=knowledge_base_id,
989
- )
990
- logger.info(f"Stored conversation in KB: {knowledge_base_id}")
991
- except Exception as e:
992
- logger.warning(f"KB storage failed: {e}")
993
-
994
- # Agent finished - check if reload was pending
995
- self._agent_executing = False
996
- logger.info("Agent call completed successfully")
997
- if self._reload_pending:
998
- logger.info("Triggering pending hot-reload after agent completion")
999
- print("🦆 Agent finished - triggering pending hot-reload...")
1000
- self.hot_reload()
1001
-
1002
- return result
1003
- except Exception as e:
1004
- self._agent_executing = False # Reset flag on error
1005
- logger.error(f"Agent call failed with error: {e}")
1006
- self._self_heal(e)
1007
- if self.agent:
1008
- return self.agent(query)
1009
- else:
1010
- return f"🦆 Error: {e}"
764
+ self.agent.tool.mcp_server(
765
+ action="start",
766
+ transport="http",
767
+ port=self.mcp_port,
768
+ expose_agent=True,
769
+ agent=self.agent,
770
+ )
771
+ print(f"🦆 MCP: http://localhost:{self.mcp_port}/mcp")
772
+ except Exception as e:
773
+ logger.warning(f"MCP server failed: {e}")
1011
774
 
1012
- def restart(self):
1013
- """Restart the agent"""
1014
- print("🦆 Restarting...")
1015
- self.__init__()
775
+ if self.enable_ipc:
776
+ try:
777
+ self.agent.tool.ipc(action="start_server", socket_path=self.ipc_socket)
778
+ print(f"🦆 ✓ IPC: {self.ipc_socket}")
779
+ except Exception as e:
780
+ logger.warning(f"IPC server failed: {e}")
1016
781
 
1017
- def _start_file_watcher(self):
1018
- """Start background file watcher for auto hot-reload"""
1019
- import threading
782
+ def _start_hot_reload(self):
783
+ """Start hot-reload file watcher"""
1020
784
 
1021
- logger.info("Starting file watcher for hot-reload")
1022
- # Get the path to this file
1023
785
  self._watch_file = Path(__file__).resolve()
1024
786
  self._last_modified = (
1025
787
  self._watch_file.stat().st_mtime if self._watch_file.exists() else None
1026
788
  )
1027
789
  self._watcher_running = True
1028
790
 
1029
- # Start watcher thread
1030
- self._watcher_thread = threading.Thread(
1031
- target=self._file_watcher_thread, daemon=True
1032
- )
1033
- self._watcher_thread.start()
1034
- logger.info(f"File watcher started, monitoring {self._watch_file}")
1035
-
1036
- def _file_watcher_thread(self):
1037
- """Background thread that watches for file changes"""
1038
- import time
1039
-
1040
- last_reload_time = 0
1041
- debounce_seconds = 3 # 3 second debounce
791
+ def watcher_thread():
792
+ import time
1042
793
 
1043
- while self._watcher_running:
1044
- try:
1045
- # Skip if currently reloading to prevent triggering during exec()
1046
- if getattr(self, "_is_reloading", False):
1047
- time.sleep(1)
1048
- continue
794
+ last_reload = 0
795
+ debounce = 3 # seconds
1049
796
 
1050
- if self._watch_file.exists():
1051
- current_mtime = self._watch_file.stat().st_mtime
1052
- current_time = time.time()
1053
-
1054
- # Check if file was modified AND debounce period has passed
1055
- if (
1056
- self._last_modified
1057
- and current_mtime > self._last_modified
1058
- and current_time - last_reload_time > debounce_seconds
1059
- ):
1060
-
1061
- print(f"🦆 Detected changes in {self._watch_file.name}!")
1062
- self._last_modified = current_mtime
1063
- last_reload_time = current_time
1064
-
1065
- # Check if agent is currently executing
1066
- if getattr(self, "_agent_executing", False):
1067
- logger.info(
1068
- "Code change detected but agent is executing - reload pending"
1069
- )
1070
- print(
1071
- "🦆 Agent is currently executing - reload will trigger after completion"
1072
- )
1073
- self._reload_pending = True
797
+ while self._watcher_running:
798
+ try:
799
+ if self._watch_file.exists():
800
+ mtime = self._watch_file.stat().st_mtime
801
+ current_time = time.time()
802
+
803
+ if (
804
+ self._last_modified
805
+ and mtime > self._last_modified
806
+ and current_time - last_reload > debounce
807
+ ):
808
+
809
+ print(f"🦆 Code changed - hot-reload triggered")
810
+ self._last_modified = mtime
811
+ last_reload = current_time
812
+
813
+ if self._agent_executing:
814
+ print("🦆 Reload pending (agent executing)")
815
+ self._reload_pending = True
816
+ else:
817
+ self._hot_reload()
1074
818
  else:
1075
- # Safe to reload immediately
1076
- logger.info(
1077
- f"Code change detected in {self._watch_file.name} - triggering hot-reload"
1078
- )
1079
- time.sleep(
1080
- 0.5
1081
- ) # Small delay to ensure file write is complete
1082
- self.hot_reload()
1083
- else:
1084
- self._last_modified = current_mtime
819
+ self._last_modified = mtime
820
+ except Exception as e:
821
+ logger.error(f"File watcher error: {e}")
1085
822
 
1086
- except Exception as e:
1087
- print(f"🦆 File watcher error: {e}")
823
+ time.sleep(1)
1088
824
 
1089
- # Check every 1 second
1090
- time.sleep(1)
825
+ thread = threading.Thread(target=watcher_thread, daemon=True)
826
+ thread.start()
827
+ logger.info(f"Hot-reload watching: {self._watch_file}")
1091
828
 
1092
- def _stop_file_watcher(self):
1093
- """Stop the file watcher"""
829
+ def _hot_reload(self):
830
+ """Hot-reload by restarting process"""
831
+ logger.info("Hot-reload: restarting process")
1094
832
  self._watcher_running = False
1095
- print("🦆 File watcher stopped")
833
+ os.execv(sys.executable, [sys.executable] + sys.argv)
1096
834
 
1097
- def hot_reload(self):
1098
- """Hot-reload by restarting the entire Python process with fresh code"""
1099
- logger.info("Hot-reload initiated")
1100
- print("🦆 Hot-reloading via process restart...")
835
+ def __call__(self, query):
836
+ """Call agent with KB integration"""
837
+ if not self.agent:
838
+ return "🦆 Agent unavailable"
1101
839
 
1102
840
  try:
1103
- # Set reload flag to prevent recursive reloads during shutdown
1104
- if hasattr(self, "_is_reloading") and self._is_reloading:
1105
- print("🦆 Reload already in progress, skipping")
1106
- return
841
+ self._agent_executing = True
1107
842
 
1108
- self._is_reloading = True
843
+ # KB retrieval
844
+ kb_id = os.getenv("DEVDUCK_KNOWLEDGE_BASE_ID")
845
+ if kb_id:
846
+ try:
847
+ self.agent.tool.retrieve(text=query, knowledgeBaseId=kb_id)
848
+ except:
849
+ pass
1109
850
 
1110
- # Stop the file watcher
1111
- if hasattr(self, "_watcher_running"):
1112
- self._watcher_running = False
851
+ # Run agent
852
+ result = self.agent(query)
1113
853
 
1114
- print("🦆 Restarting process with fresh code...")
854
+ # KB storage
855
+ if kb_id:
856
+ try:
857
+ self.agent.tool.store_in_kb(
858
+ content=f"Input: {query}\nResult: {str(result)}",
859
+ title=f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}",
860
+ knowledge_base_id=kb_id,
861
+ )
862
+ except:
863
+ pass
1115
864
 
1116
- # Restart the entire Python process
1117
- # This ensures all code is freshly loaded
1118
- os.execv(sys.executable, [sys.executable] + sys.argv)
865
+ self._agent_executing = False
1119
866
 
867
+ # Check for pending reload
868
+ if self._reload_pending:
869
+ print("🦆 Agent finished - triggering pending reload")
870
+ self._hot_reload()
871
+
872
+ return result
1120
873
  except Exception as e:
1121
- print(f"🦆 Hot-reload failed: {e}")
1122
- print("🦆 Falling back to manual restart")
1123
- self._is_reloading = False
1124
-
1125
- def status(self):
1126
- """Show current status"""
1127
- return {
1128
- "model": self.model,
1129
- "host": self.ollama_host,
1130
- "env": self.env_info,
1131
- "agent_ready": self.agent is not None,
1132
- "tools": len(self.tools) if hasattr(self, "tools") else 0,
1133
- "file_watcher": {
1134
- "enabled": hasattr(self, "_watcher_running") and self._watcher_running,
1135
- "watching": (
1136
- str(self._watch_file) if hasattr(self, "_watch_file") else None
1137
- ),
1138
- },
1139
- }
1140
-
1141
-
1142
- # 🦆 Auto-initialize when imported
874
+ self._agent_executing = False
875
+ logger.error(f"Agent call failed: {e}")
876
+ return f"🦆 Error: {e}"
877
+
878
+
879
+ # Initialize
1143
880
  # Check environment variables to control server configuration
1144
- # Also check if --mcp flag is present to skip auto-starting servers
1145
881
  _auto_start = os.getenv("DEVDUCK_AUTO_START_SERVERS", "true").lower() == "true"
1146
882
 
1147
883
  # Disable auto-start if --mcp flag is present (stdio mode)
@@ -1170,27 +906,11 @@ devduck = DevDuck(
1170
906
  )
1171
907
 
1172
908
 
1173
- # 🚀 Convenience functions
1174
909
  def ask(query):
1175
- """Quick query interface"""
910
+ """Quick query"""
1176
911
  return devduck(query)
1177
912
 
1178
913
 
1179
- def status():
1180
- """Quick status check"""
1181
- return devduck.status()
1182
-
1183
-
1184
- def restart():
1185
- """Quick restart"""
1186
- devduck.restart()
1187
-
1188
-
1189
- def hot_reload():
1190
- """Quick hot-reload without restart"""
1191
- devduck.hot_reload()
1192
-
1193
-
1194
914
  def extract_commands_from_history():
1195
915
  """Extract commonly used commands from shell history for auto-completion."""
1196
916
  commands = set()
@@ -1264,7 +984,8 @@ def extract_commands_from_history():
1264
984
 
1265
985
 
1266
986
  def interactive():
1267
- """Interactive REPL mode for devduck"""
987
+ """Interactive REPL with history"""
988
+ import time
1268
989
  from prompt_toolkit import prompt
1269
990
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
1270
991
  from prompt_toolkit.completion import WordCompleter
@@ -1272,14 +993,10 @@ def interactive():
1272
993
 
1273
994
  print("🦆 DevDuck")
1274
995
  print(f"📝 Logs: {LOG_DIR}")
1275
- print("Type 'exit', 'quit', or 'q' to quit.")
1276
- print("Prefix with ! to run shell commands (e.g., ! ls -la)")
996
+ print("Type 'exit' to quit. Prefix with ! for shell commands.")
1277
997
  print("-" * 50)
1278
- logger.info("Interactive mode started")
1279
998
 
1280
- # Set up prompt_toolkit with history
1281
- history_file = get_shell_history_file()
1282
- history = FileHistory(history_file)
999
+ history = FileHistory(get_shell_history_file())
1283
1000
 
1284
1001
  # Create completions from common commands and shell history
1285
1002
  base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
@@ -1289,164 +1006,72 @@ def interactive():
1289
1006
  all_commands = list(set(base_commands + history_commands))
1290
1007
  completer = WordCompleter(all_commands, ignore_case=True)
1291
1008
 
1009
+ # Track consecutive interrupts for double Ctrl+C to exit
1010
+ interrupt_count = 0
1011
+ last_interrupt = 0
1012
+
1292
1013
  while True:
1293
1014
  try:
1294
- # Use prompt_toolkit for enhanced input with arrow key support
1295
1015
  q = prompt(
1296
1016
  "\n🦆 ",
1297
1017
  history=history,
1298
1018
  auto_suggest=AutoSuggestFromHistory(),
1299
1019
  completer=completer,
1300
1020
  complete_while_typing=True,
1301
- mouse_support=False, # breaks scrolling when enabled
1302
- )
1021
+ mouse_support=False,
1022
+ ).strip()
1023
+
1024
+ # Reset interrupt count on successful prompt
1025
+ interrupt_count = 0
1303
1026
 
1304
- # Check for exit command
1305
1027
  if q.lower() in ["exit", "quit", "q"]:
1306
- print("\n🦆 Goodbye!")
1307
1028
  break
1308
1029
 
1309
- # Skip empty inputs
1310
- if q.strip() == "":
1030
+ if not q:
1311
1031
  continue
1312
1032
 
1313
- # Handle shell commands with ! prefix
1033
+ # Shell commands
1314
1034
  if q.startswith("!"):
1315
- shell_command = q[1:].strip()
1316
- try:
1317
- if devduck.agent:
1318
- devduck._agent_executing = (
1319
- True # Prevent hot-reload during shell execution
1320
- )
1321
- result = devduck.agent.tool.shell(
1322
- command=shell_command, timeout=9000
1323
- )
1324
- devduck._agent_executing = False
1325
-
1326
- # Append shell command to history
1327
- append_to_shell_history(q, result["content"][0]["text"])
1328
-
1329
- # Check if reload was pending
1330
- if devduck._reload_pending:
1331
- print(
1332
- "🦆 Shell command finished - triggering pending hot-reload..."
1333
- )
1334
- devduck.hot_reload()
1335
- else:
1336
- print("🦆 Agent unavailable")
1337
- except Exception as e:
1338
- devduck._agent_executing = False # Reset on error
1339
- print(f"🦆 Shell command error: {e}")
1340
- continue
1341
-
1342
- # Get recent conversation context
1343
- recent_context = get_last_messages()
1344
-
1345
- # Get recent logs
1346
- recent_logs = get_recent_logs()
1347
-
1348
- # Update system prompt before each call with history context
1349
- if devduck.agent:
1350
- # Rebuild system prompt with history
1351
- own_code = get_own_source_code()
1352
- session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
1353
-
1354
- devduck.agent.system_prompt = f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
1355
-
1356
- Environment: {devduck.env_info['os']} {devduck.env_info['arch']}
1357
- Python: {devduck.env_info['python']}
1358
- Model: {devduck.model}
1359
- Hostname: {devduck.env_info['hostname']}
1360
- Session ID: {session_id}
1361
-
1362
- You are:
1363
- - Minimalist: Brief, direct responses
1364
- - Self-healing: Adapt when things break
1365
- - Efficient: Get things done fast
1366
- - Pragmatic: Use what works
1367
-
1368
- Current working directory: {devduck.env_info['cwd']}
1369
-
1370
- {recent_context}
1371
- {recent_logs}
1372
-
1373
- ## Your Own Implementation:
1374
- You have full access to your own source code for self-awareness and self-modification:
1375
-
1376
- {own_code}
1377
-
1378
- ## Hot Reload System Active:
1379
- - **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
1380
- - **No Restart Needed** - Tools are auto-loaded and ready to use instantly
1381
- - **Live Development** - Modify existing tools while running and test immediately
1382
- - **Full Python Access** - Create any Python functionality as a tool
1383
-
1384
- ## Dynamic Tool Loading:
1385
- - **Install Tools** - Use install_tools() to load tools from any Python package
1386
- - Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
1387
- - Expands capabilities without restart
1388
- - Access to entire Python ecosystem
1389
-
1390
- ## MCP Server:
1391
- - **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
1392
- - Example: mcp_server(action="start", port=8000)
1393
- - Connect from Claude Desktop, other agents, or custom clients
1394
- - Full bidirectional communication
1395
-
1396
- ## System Prompt Management:
1397
- - **View**: system_prompt(action='view') - See current prompt
1398
- - **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
1399
- - **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
1400
- - **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
1401
- - **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
1402
-
1403
- ### 🧠 Self-Improvement Pattern:
1404
- When you learn something valuable during conversations:
1405
- 1. Identify the new insight or pattern
1406
- 2. Use system_prompt(action='add_context', context='...') to append it
1407
- 3. Optionally sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
1408
- 4. New learnings persist across sessions via SYSTEM_PROMPT env var
1409
-
1410
- **Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
1411
-
1412
- ## Shell Commands:
1413
- - Prefix with ! to execute shell commands directly
1414
- - Example: ! ls -la (lists files)
1415
- - Example: ! pwd (shows current directory)
1416
-
1417
- **Response Format:**
1418
- - Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
1419
- - Communication: **MINIMAL WORDS**
1420
- - Efficiency: **Speed is paramount**
1421
-
1422
- {os.getenv('SYSTEM_PROMPT', '')}"""
1423
-
1424
- # Update model if MODEL_PROVIDER changed
1425
- model_provider = os.getenv("MODEL_PROVIDER")
1426
- if model_provider:
1427
- try:
1428
- from strands_tools.utils.models.model import create_model
1035
+ if devduck.agent:
1036
+ devduck._agent_executing = True
1037
+ result = devduck.agent.tool.shell(
1038
+ command=q[1:].strip(), timeout=9000
1039
+ )
1040
+ devduck._agent_executing = False
1041
+ append_to_shell_history(q, result["content"][0]["text"])
1429
1042
 
1430
- devduck.agent.model = create_model(provider=model_provider)
1431
- except Exception as e:
1432
- print(f"🦆 Model update error: {e}")
1043
+ if devduck._reload_pending:
1044
+ print("🦆 Shell finished - triggering pending reload")
1045
+ devduck._hot_reload()
1046
+ continue
1433
1047
 
1434
- # Execute the agent with user input
1048
+ # Agent query
1435
1049
  result = ask(q)
1436
-
1437
- # Append to shell history
1050
+ print(result)
1438
1051
  append_to_shell_history(q, str(result))
1439
1052
 
1440
1053
  except KeyboardInterrupt:
1441
- print("\n🦆 Interrupted. Type 'exit' to quit.")
1442
- continue
1054
+ current_time = time.time()
1055
+
1056
+ # Check if this is a consecutive interrupt within 2 seconds
1057
+ if current_time - last_interrupt < 2:
1058
+ interrupt_count += 1
1059
+ if interrupt_count >= 2:
1060
+ print("\n🦆 Exiting...")
1061
+ break
1062
+ else:
1063
+ print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
1064
+ else:
1065
+ interrupt_count = 1
1066
+ print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
1067
+
1068
+ last_interrupt = current_time
1443
1069
  except Exception as e:
1444
1070
  print(f"🦆 Error: {e}")
1445
- continue
1446
1071
 
1447
1072
 
1448
1073
  def cli():
1449
- """CLI entry point for pip-installed devduck command"""
1074
+ """CLI entry point"""
1450
1075
  import argparse
1451
1076
 
1452
1077
  parser = argparse.ArgumentParser(
@@ -1454,14 +1079,16 @@ def cli():
1454
1079
  formatter_class=argparse.RawDescriptionHelpFormatter,
1455
1080
  epilog="""
1456
1081
  Examples:
1457
- devduck # Start interactive mode
1458
- devduck "your query here" # One-shot query
1459
- devduck --mcp # MCP stdio mode (for Claude Desktop)
1082
+ devduck # Interactive mode
1083
+ devduck "query" # One-shot query
1084
+ devduck --mcp # MCP stdio mode
1460
1085
  devduck --tcp-port 9000 # Custom TCP port
1461
1086
  devduck --no-tcp --no-ws # Disable TCP and WebSocket
1462
- devduck --mcp-port 3000 # Custom MCP port
1463
1087
 
1464
- Claude Desktop Config:
1088
+ Tool Configuration:
1089
+ export DEVDUCK_TOOLS="strands_tools:shell,editor:strands_fun_tools:clipboard"
1090
+
1091
+ MCP Config:
1465
1092
  {
1466
1093
  "mcpServers": {
1467
1094
  "devduck": {
@@ -1473,15 +1100,8 @@ Claude Desktop Config:
1473
1100
  """,
1474
1101
  )
1475
1102
 
1476
- # Query argument
1477
- parser.add_argument("query", nargs="*", help="Query to send to the agent")
1478
-
1479
- # MCP stdio mode flag
1480
- parser.add_argument(
1481
- "--mcp",
1482
- action="store_true",
1483
- help="Start MCP server in stdio mode (for Claude Desktop integration)",
1484
- )
1103
+ parser.add_argument("query", nargs="*", help="Query")
1104
+ parser.add_argument("--mcp", action="store_true", help="MCP stdio mode")
1485
1105
 
1486
1106
  # Server configuration
1487
1107
  parser.add_argument(
@@ -1499,65 +1119,53 @@ Claude Desktop Config:
1499
1119
  default=8000,
1500
1120
  help="MCP HTTP server port (default: 8000)",
1501
1121
  )
1122
+ parser.add_argument(
1123
+ "--ipc-socket",
1124
+ type=str,
1125
+ default=None,
1126
+ help="IPC socket path (default: /tmp/devduck_main.sock)",
1127
+ )
1502
1128
 
1503
1129
  # Server enable/disable flags
1504
1130
  parser.add_argument("--no-tcp", action="store_true", help="Disable TCP server")
1505
1131
  parser.add_argument("--no-ws", action="store_true", help="Disable WebSocket server")
1506
1132
  parser.add_argument("--no-mcp", action="store_true", help="Disable MCP server")
1133
+ parser.add_argument("--no-ipc", action="store_true", help="Disable IPC server")
1507
1134
  parser.add_argument(
1508
1135
  "--no-servers",
1509
1136
  action="store_true",
1510
- help="Disable all servers (no TCP, WebSocket, or MCP)",
1137
+ help="Disable all servers (no TCP, WebSocket, MCP, or IPC)",
1511
1138
  )
1512
1139
 
1513
1140
  args = parser.parse_args()
1514
1141
 
1515
- logger.info("CLI mode started")
1516
-
1517
- # Handle --mcp flag for stdio mode
1518
1142
  if args.mcp:
1519
- logger.info("Starting MCP server in stdio mode (blocking, foreground)")
1520
1143
  print("🦆 Starting MCP stdio server...", file=sys.stderr)
1521
-
1522
- # Don't auto-start HTTP/TCP/WS servers for stdio mode
1523
- if devduck.agent:
1524
- try:
1525
- # Start MCP server in stdio mode - this BLOCKS until terminated
1526
- devduck.agent.tool.mcp_server(
1527
- action="start",
1528
- transport="stdio",
1529
- expose_agent=True,
1530
- agent=devduck.agent,
1531
- )
1532
- except Exception as e:
1533
- logger.error(f"Failed to start MCP stdio server: {e}")
1534
- print(f"🦆 Error: {e}", file=sys.stderr)
1535
- sys.exit(1)
1536
- else:
1537
- print("🦆 Agent not available", file=sys.stderr)
1144
+ try:
1145
+ devduck.agent.tool.mcp_server(
1146
+ action="start",
1147
+ transport="stdio",
1148
+ expose_agent=True,
1149
+ agent=devduck.agent,
1150
+ )
1151
+ except Exception as e:
1152
+ print(f"🦆 Error: {e}", file=sys.stderr)
1538
1153
  sys.exit(1)
1539
1154
  return
1540
1155
 
1541
1156
  if args.query:
1542
- query = " ".join(args.query)
1543
- logger.info(f"CLI query: {query}")
1544
- result = ask(query)
1157
+ result = ask(" ".join(args.query))
1545
1158
  print(result)
1546
1159
  else:
1547
- # No arguments - start interactive mode
1548
1160
  interactive()
1549
1161
 
1550
1162
 
1551
- # 🦆 Make module directly callable: import devduck; devduck("query")
1163
+ # Make module callable
1552
1164
  class CallableModule(sys.modules[__name__].__class__):
1553
- """Make the module itself callable"""
1554
-
1555
1165
  def __call__(self, query):
1556
- """Allow direct module call: import devduck; devduck("query")"""
1557
1166
  return ask(query)
1558
1167
 
1559
1168
 
1560
- # Replace module in sys.modules with callable version
1561
1169
  sys.modules[__name__].__class__ = CallableModule
1562
1170
 
1563
1171