devduck 0.5.4__py3-none-any.whl → 0.6.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.
- devduck/__init__.py +973 -557
- devduck/_version.py +2 -2
- devduck/tools/__init__.py +3 -0
- devduck/tools/agentcore_config.py +1 -0
- devduck/tools/ipc.py +4 -1
- devduck/tools/state_manager.py +292 -0
- devduck/tools/tcp.py +6 -0
- devduck/tools/websocket.py +7 -1
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/METADATA +23 -5
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/RECORD +14 -13
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/WHEEL +0 -0
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/entry_points.txt +0 -0
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {devduck-0.5.4.dist-info → devduck-0.6.0.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""
|
|
3
|
+
🦆 devduck - extreme minimalist self-adapting agent
|
|
4
|
+
one file. self-healing. runtime dependencies. adaptive.
|
|
5
|
+
"""
|
|
3
6
|
import sys
|
|
4
|
-
import
|
|
7
|
+
import subprocess
|
|
5
8
|
import os
|
|
6
9
|
import platform
|
|
10
|
+
import socket
|
|
7
11
|
import logging
|
|
8
12
|
import tempfile
|
|
9
|
-
import
|
|
13
|
+
import time
|
|
14
|
+
import warnings
|
|
10
15
|
from pathlib import Path
|
|
11
16
|
from datetime import datetime
|
|
12
|
-
import
|
|
17
|
+
from typing import Dict, Any
|
|
13
18
|
from logging.handlers import RotatingFileHandler
|
|
14
19
|
|
|
15
20
|
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*")
|
|
@@ -21,31 +26,169 @@ os.environ["EDITOR_DISABLE_BACKUP"] = "true"
|
|
|
21
26
|
|
|
22
27
|
LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
|
|
23
28
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
24
|
-
LOG_FILE = LOG_DIR / "devduck.log"
|
|
25
29
|
|
|
30
|
+
LOG_FILE = LOG_DIR / "devduck.log"
|
|
26
31
|
logger = logging.getLogger("devduck")
|
|
27
32
|
logger.setLevel(logging.DEBUG)
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
|
|
34
|
+
file_handler = RotatingFileHandler(
|
|
35
|
+
LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=3, encoding="utf-8"
|
|
36
|
+
)
|
|
37
|
+
file_handler.setLevel(logging.DEBUG)
|
|
38
|
+
file_formatter = logging.Formatter(
|
|
39
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
30
40
|
)
|
|
31
|
-
|
|
41
|
+
file_handler.setFormatter(file_formatter)
|
|
42
|
+
|
|
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")
|
|
32
52
|
|
|
33
53
|
|
|
34
54
|
def get_own_source_code():
|
|
35
55
|
"""Read own source code for self-awareness"""
|
|
36
56
|
try:
|
|
37
57
|
with open(__file__, "r", encoding="utf-8") as f:
|
|
38
|
-
return f"#
|
|
58
|
+
return f"# Source path: {__file__}\n\ndevduck/__init__.py\n```python\n{f.read()}\n```"
|
|
39
59
|
except Exception as e:
|
|
40
60
|
return f"Error reading source: {e}"
|
|
41
61
|
|
|
42
62
|
|
|
63
|
+
def view_logs_tool(
|
|
64
|
+
action: str = "view",
|
|
65
|
+
lines: int = 100,
|
|
66
|
+
pattern: str = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
View and manage DevDuck logs.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
action: Action to perform - "view", "tail", "search", "clear", "stats"
|
|
73
|
+
lines: Number of lines to show (for view/tail)
|
|
74
|
+
pattern: Search pattern (for search action)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict with status and content
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
if action == "view":
|
|
81
|
+
if not LOG_FILE.exists():
|
|
82
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
83
|
+
|
|
84
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
85
|
+
all_lines = f.readlines()
|
|
86
|
+
recent_lines = (
|
|
87
|
+
all_lines[-lines:] if len(all_lines) > lines else all_lines
|
|
88
|
+
)
|
|
89
|
+
content = "".join(recent_lines)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"status": "success",
|
|
93
|
+
"content": [
|
|
94
|
+
{"text": f"Last {len(recent_lines)} log lines:\n\n{content}"}
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
elif action == "tail":
|
|
99
|
+
if not LOG_FILE.exists():
|
|
100
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
101
|
+
|
|
102
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
103
|
+
all_lines = f.readlines()
|
|
104
|
+
tail_lines = all_lines[-50:] if len(all_lines) > 50 else all_lines
|
|
105
|
+
content = "".join(tail_lines)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"status": "success",
|
|
109
|
+
"content": [{"text": f"Tail (last 50 lines):\n\n{content}"}],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
elif action == "search":
|
|
113
|
+
if not pattern:
|
|
114
|
+
return {
|
|
115
|
+
"status": "error",
|
|
116
|
+
"content": [{"text": "pattern parameter required for search"}],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if not LOG_FILE.exists():
|
|
120
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
121
|
+
|
|
122
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
123
|
+
matching_lines = [line for line in f if pattern.lower() in line.lower()]
|
|
124
|
+
|
|
125
|
+
if not matching_lines:
|
|
126
|
+
return {
|
|
127
|
+
"status": "success",
|
|
128
|
+
"content": [{"text": f"No matches found for pattern: {pattern}"}],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
content = "".join(matching_lines[-100:]) # Last 100 matches
|
|
132
|
+
return {
|
|
133
|
+
"status": "success",
|
|
134
|
+
"content": [
|
|
135
|
+
{
|
|
136
|
+
"text": f"Found {len(matching_lines)} matches (showing last 100):\n\n{content}"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
elif action == "clear":
|
|
142
|
+
if LOG_FILE.exists():
|
|
143
|
+
LOG_FILE.unlink()
|
|
144
|
+
logger.info("Log file cleared by user")
|
|
145
|
+
return {
|
|
146
|
+
"status": "success",
|
|
147
|
+
"content": [{"text": "Logs cleared successfully"}],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
elif action == "stats":
|
|
151
|
+
if not LOG_FILE.exists():
|
|
152
|
+
return {"status": "success", "content": [{"text": "No logs yet"}]}
|
|
153
|
+
|
|
154
|
+
stat = LOG_FILE.stat()
|
|
155
|
+
size_mb = stat.st_size / (1024 * 1024)
|
|
156
|
+
modified = datetime.fromtimestamp(stat.st_mtime).strftime(
|
|
157
|
+
"%Y-%m-%d %H:%M:%S"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
161
|
+
total_lines = sum(1 for _ in f)
|
|
162
|
+
|
|
163
|
+
stats_text = f"""Log File Statistics:
|
|
164
|
+
Path: {LOG_FILE}
|
|
165
|
+
Size: {size_mb:.2f} MB
|
|
166
|
+
Lines: {total_lines}
|
|
167
|
+
Last Modified: {modified}"""
|
|
168
|
+
|
|
169
|
+
return {"status": "success", "content": [{"text": stats_text}]}
|
|
170
|
+
|
|
171
|
+
else:
|
|
172
|
+
return {
|
|
173
|
+
"status": "error",
|
|
174
|
+
"content": [
|
|
175
|
+
{
|
|
176
|
+
"text": f"Unknown action: {action}. Valid: view, tail, search, clear, stats"
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(f"Error in view_logs_tool: {e}")
|
|
183
|
+
return {"status": "error", "content": [{"text": f"Error: {str(e)}"}]}
|
|
184
|
+
|
|
185
|
+
|
|
43
186
|
def get_shell_history_file():
|
|
44
|
-
"""Get devduck history file"""
|
|
45
|
-
|
|
46
|
-
if not
|
|
47
|
-
|
|
48
|
-
return str(
|
|
187
|
+
"""Get the devduck-specific history file path."""
|
|
188
|
+
devduck_history = Path.home() / ".devduck_history"
|
|
189
|
+
if not devduck_history.exists():
|
|
190
|
+
devduck_history.touch(mode=0o600)
|
|
191
|
+
return str(devduck_history)
|
|
49
192
|
|
|
50
193
|
|
|
51
194
|
def get_shell_history_files():
|
|
@@ -125,6 +268,32 @@ def parse_history_line(line, history_type):
|
|
|
125
268
|
return None
|
|
126
269
|
|
|
127
270
|
|
|
271
|
+
def get_recent_logs():
|
|
272
|
+
"""Get the last N lines from the log file for context."""
|
|
273
|
+
try:
|
|
274
|
+
log_line_count = int(os.getenv("DEVDUCK_LOG_LINE_COUNT", "50"))
|
|
275
|
+
|
|
276
|
+
if not LOG_FILE.exists():
|
|
277
|
+
return ""
|
|
278
|
+
|
|
279
|
+
with open(LOG_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
|
280
|
+
all_lines = f.readlines()
|
|
281
|
+
|
|
282
|
+
recent_lines = (
|
|
283
|
+
all_lines[-log_line_count:]
|
|
284
|
+
if len(all_lines) > log_line_count
|
|
285
|
+
else all_lines
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if not recent_lines:
|
|
289
|
+
return ""
|
|
290
|
+
|
|
291
|
+
log_content = "".join(recent_lines)
|
|
292
|
+
return f"\n\n## Recent Logs (last {len(recent_lines)} lines):\n```\n{log_content}```\n"
|
|
293
|
+
except Exception as e:
|
|
294
|
+
return f"\n\n## Recent Logs: Error reading logs - {e}\n"
|
|
295
|
+
|
|
296
|
+
|
|
128
297
|
def get_last_messages():
|
|
129
298
|
"""Get the last N messages from multiple shell histories for context."""
|
|
130
299
|
try:
|
|
@@ -184,225 +353,178 @@ def get_last_messages():
|
|
|
184
353
|
return ""
|
|
185
354
|
|
|
186
355
|
|
|
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
|
-
|
|
207
356
|
def append_to_shell_history(query, response):
|
|
208
|
-
"""Append interaction to history"""
|
|
209
|
-
import time
|
|
210
|
-
|
|
357
|
+
"""Append the interaction to devduck shell history."""
|
|
211
358
|
try:
|
|
212
359
|
history_file = get_shell_history_file()
|
|
213
360
|
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
|
-
)
|
|
220
361
|
|
|
221
362
|
with open(history_file, "a", encoding="utf-8") as f:
|
|
222
363
|
f.write(f": {timestamp}:0;# devduck: {query}\n")
|
|
364
|
+
response_summary = (
|
|
365
|
+
str(response).replace("\n", " ")[
|
|
366
|
+
: int(os.getenv("DEVDUCK_RESPONSE_SUMMARY_LENGTH", "10000"))
|
|
367
|
+
]
|
|
368
|
+
+ "..."
|
|
369
|
+
)
|
|
223
370
|
f.write(f": {timestamp}:0;# devduck_result: {response_summary}\n")
|
|
224
371
|
|
|
225
372
|
os.chmod(history_file, 0o600)
|
|
226
|
-
except:
|
|
373
|
+
except Exception:
|
|
227
374
|
pass
|
|
228
375
|
|
|
229
376
|
|
|
377
|
+
# 🦆 The devduck agent
|
|
230
378
|
class DevDuck:
|
|
231
|
-
"""Minimalist adaptive agent with flexible tool loading"""
|
|
232
|
-
|
|
233
379
|
def __init__(
|
|
234
380
|
self,
|
|
235
381
|
auto_start_servers=True,
|
|
236
|
-
|
|
237
|
-
ws_port=8080,
|
|
238
|
-
mcp_port=8000,
|
|
239
|
-
ipc_socket=None,
|
|
240
|
-
enable_tcp=True,
|
|
241
|
-
enable_ws=True,
|
|
242
|
-
enable_mcp=True,
|
|
243
|
-
enable_ipc=True,
|
|
382
|
+
servers=None,
|
|
244
383
|
):
|
|
245
|
-
"""Initialize the minimalist adaptive agent
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
)
|
|
291
|
-
|
|
292
|
-
# Auto-start servers
|
|
293
|
-
if auto_start_servers and "--mcp" not in sys.argv:
|
|
294
|
-
self._start_servers()
|
|
295
|
-
|
|
296
|
-
# Start hot-reload watcher
|
|
297
|
-
self._start_hot_reload()
|
|
298
|
-
|
|
299
|
-
logger.info(f"DevDuck ready with {len(tools)} tools")
|
|
300
|
-
|
|
301
|
-
def _load_tools_flexible(self):
|
|
384
|
+
"""Initialize the minimalist adaptive agent
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
auto_start_servers: Enable automatic server startup
|
|
388
|
+
servers: Dict of server configs with optional env var lookups
|
|
389
|
+
Example: {
|
|
390
|
+
"tcp": {"port": 9999},
|
|
391
|
+
"ws": {"port": 8080, "LOOKUP_KEY": "SLACK_API_KEY"},
|
|
392
|
+
"mcp": {"port": 8000},
|
|
393
|
+
"ipc": {"socket_path": "/tmp/devduck.sock"}
|
|
394
|
+
}
|
|
302
395
|
"""
|
|
303
|
-
|
|
396
|
+
logger.info("Initializing DevDuck agent...")
|
|
397
|
+
try:
|
|
398
|
+
self.env_info = {
|
|
399
|
+
"os": platform.system(),
|
|
400
|
+
"arch": platform.machine(),
|
|
401
|
+
"python": sys.version_info,
|
|
402
|
+
"cwd": str(Path.cwd()),
|
|
403
|
+
"home": str(Path.home()),
|
|
404
|
+
"shell": os.environ.get("SHELL", "unknown"),
|
|
405
|
+
"hostname": socket.gethostname(),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Execution state tracking for hot-reload
|
|
409
|
+
self._agent_executing = False
|
|
410
|
+
self._reload_pending = False
|
|
411
|
+
|
|
412
|
+
# Server configuration
|
|
413
|
+
if servers is None:
|
|
414
|
+
# Default server config from env vars
|
|
415
|
+
servers = {
|
|
416
|
+
"tcp": {
|
|
417
|
+
"port": int(os.getenv("DEVDUCK_TCP_PORT", "9999")),
|
|
418
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_TCP", "true").lower()
|
|
419
|
+
== "true",
|
|
420
|
+
},
|
|
421
|
+
"ws": {
|
|
422
|
+
"port": int(os.getenv("DEVDUCK_WS_PORT", "8080")),
|
|
423
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_WS", "true").lower()
|
|
424
|
+
== "true",
|
|
425
|
+
},
|
|
426
|
+
"mcp": {
|
|
427
|
+
"port": int(os.getenv("DEVDUCK_MCP_PORT", "8000")),
|
|
428
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_MCP", "true").lower()
|
|
429
|
+
== "true",
|
|
430
|
+
},
|
|
431
|
+
"ipc": {
|
|
432
|
+
"socket_path": os.getenv(
|
|
433
|
+
"DEVDUCK_IPC_SOCKET", "/tmp/devduck_main.sock"
|
|
434
|
+
),
|
|
435
|
+
"enabled": os.getenv("DEVDUCK_ENABLE_IPC", "true").lower()
|
|
436
|
+
== "true",
|
|
437
|
+
},
|
|
438
|
+
}
|
|
304
439
|
|
|
305
|
-
|
|
306
|
-
Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
440
|
+
self.servers = servers
|
|
307
441
|
|
|
308
|
-
|
|
309
|
-
- DevDuck's own tools (tcp, websocket, ipc, etc.)
|
|
310
|
-
- AgentCore tools (if AWS credentials available)
|
|
311
|
-
"""
|
|
312
|
-
tools = []
|
|
442
|
+
from strands import Agent, tool
|
|
313
443
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
444
|
+
# 🧰 Load tools with flexible configuration
|
|
445
|
+
tools_config = os.getenv("DEVDUCK_TOOLS")
|
|
446
|
+
if tools_config:
|
|
447
|
+
logger.info(f"Loading tools from DEVDUCK_TOOLS: {tools_config}")
|
|
448
|
+
core_tools = self._load_tools_from_config(tools_config)
|
|
449
|
+
else:
|
|
450
|
+
logger.info("Loading default tool set")
|
|
451
|
+
core_tools = self._load_default_tools()
|
|
452
|
+
|
|
453
|
+
# Wrap view_logs_tool with @tool decorator
|
|
454
|
+
@tool
|
|
455
|
+
def view_logs(
|
|
456
|
+
action: str = "view",
|
|
457
|
+
lines: int = 100,
|
|
458
|
+
pattern: str = None,
|
|
459
|
+
) -> Dict[str, Any]:
|
|
460
|
+
"""View and manage DevDuck logs."""
|
|
461
|
+
return view_logs_tool(action, lines, pattern)
|
|
462
|
+
|
|
463
|
+
# Add built-in tools to the toolset
|
|
464
|
+
core_tools.extend([view_logs])
|
|
465
|
+
|
|
466
|
+
# Assign tools
|
|
467
|
+
self.tools = core_tools
|
|
468
|
+
|
|
469
|
+
logger.info(f"Initialized {len(self.tools)} tools")
|
|
470
|
+
|
|
471
|
+
# 🎯 Smart model selection
|
|
472
|
+
self.agent_model, self.model = self._select_model()
|
|
473
|
+
|
|
474
|
+
# Create agent with self-healing
|
|
475
|
+
self.agent = Agent(
|
|
476
|
+
model=self.agent_model,
|
|
477
|
+
tools=self.tools,
|
|
478
|
+
system_prompt=self._build_system_prompt(),
|
|
479
|
+
load_tools_from_directory=True,
|
|
328
480
|
)
|
|
329
481
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
websocket,
|
|
334
|
-
ipc,
|
|
335
|
-
mcp_server,
|
|
336
|
-
install_tools,
|
|
337
|
-
use_github,
|
|
338
|
-
create_subagent,
|
|
339
|
-
store_in_kb,
|
|
340
|
-
system_prompt,
|
|
341
|
-
tray,
|
|
342
|
-
ambient,
|
|
343
|
-
]
|
|
344
|
-
)
|
|
345
|
-
logger.info("✅ DevDuck core tools loaded")
|
|
346
|
-
except ImportError as e:
|
|
347
|
-
logger.warning(f"DevDuck tools unavailable: {e}")
|
|
482
|
+
# 🚀 AUTO-START SERVERS
|
|
483
|
+
if auto_start_servers and "--mcp" not in sys.argv:
|
|
484
|
+
self._start_servers()
|
|
348
485
|
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
486
|
+
# Start file watcher for auto hot-reload
|
|
487
|
+
self._start_file_watcher()
|
|
357
488
|
|
|
358
|
-
tools.extend(
|
|
359
|
-
[
|
|
360
|
-
agentcore_config,
|
|
361
|
-
agentcore_invoke,
|
|
362
|
-
agentcore_logs,
|
|
363
|
-
agentcore_agents,
|
|
364
|
-
]
|
|
365
|
-
)
|
|
366
|
-
logger.info("✅ AgentCore tools loaded")
|
|
367
|
-
except:
|
|
368
|
-
pass
|
|
369
|
-
else:
|
|
370
489
|
logger.info(
|
|
371
|
-
"
|
|
490
|
+
f"DevDuck agent initialized successfully with model {self.model}"
|
|
372
491
|
)
|
|
373
492
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logger.error(f"Initialization failed: {e}")
|
|
495
|
+
self._self_heal(e)
|
|
385
496
|
|
|
386
|
-
def
|
|
497
|
+
def _load_tools_from_config(self, config):
|
|
387
498
|
"""
|
|
388
|
-
|
|
499
|
+
Load tools based on DEVDUCK_TOOLS configuration.
|
|
389
500
|
|
|
390
501
|
Format: package:tool1,tool2:package2:tool3
|
|
391
|
-
Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
502
|
+
Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
392
503
|
"""
|
|
393
|
-
|
|
504
|
+
tools = []
|
|
505
|
+
|
|
506
|
+
# Always load DevDuck core tools
|
|
507
|
+
tools.extend(self._load_devduck_tools())
|
|
508
|
+
|
|
509
|
+
# Parse and load configured tools
|
|
394
510
|
current_package = None
|
|
395
511
|
|
|
396
512
|
for segment in config.split(":"):
|
|
397
513
|
segment = segment.strip()
|
|
398
514
|
|
|
399
515
|
# Check if this segment is a package or tool list
|
|
400
|
-
if "," not in segment and not
|
|
516
|
+
if "," not in segment and not any(
|
|
517
|
+
segment.startswith(pkg)
|
|
518
|
+
for pkg in [
|
|
519
|
+
"strands_",
|
|
520
|
+
"devduck",
|
|
521
|
+
] # TODO: we should accept any python library here.
|
|
522
|
+
):
|
|
401
523
|
# Single tool from current package
|
|
402
524
|
if current_package:
|
|
403
525
|
tool = self._load_single_tool(current_package, segment)
|
|
404
526
|
if tool:
|
|
405
|
-
|
|
527
|
+
tools.append(tool)
|
|
406
528
|
elif "," in segment:
|
|
407
529
|
# Tool list from current package
|
|
408
530
|
if current_package:
|
|
@@ -410,13 +532,13 @@ class DevDuck:
|
|
|
410
532
|
tool_name = tool_name.strip()
|
|
411
533
|
tool = self._load_single_tool(current_package, tool_name)
|
|
412
534
|
if tool:
|
|
413
|
-
|
|
535
|
+
tools.append(tool)
|
|
414
536
|
else:
|
|
415
537
|
# Package name
|
|
416
538
|
current_package = segment
|
|
417
539
|
|
|
418
|
-
logger.info(f"
|
|
419
|
-
return
|
|
540
|
+
logger.info(f"Loaded tools from DEVDUCK_TOOLS configuration")
|
|
541
|
+
return tools
|
|
420
542
|
|
|
421
543
|
def _load_single_tool(self, package, tool_name):
|
|
422
544
|
"""Load a single tool from a package"""
|
|
@@ -430,10 +552,13 @@ class DevDuck:
|
|
|
430
552
|
return None
|
|
431
553
|
|
|
432
554
|
def _load_default_tools(self):
|
|
433
|
-
"""Load default
|
|
555
|
+
"""Load default comprehensive tool set"""
|
|
434
556
|
tools = []
|
|
435
557
|
|
|
436
|
-
#
|
|
558
|
+
# Always load DevDuck core tools
|
|
559
|
+
tools.extend(self._load_devduck_tools())
|
|
560
|
+
|
|
561
|
+
# Load strands-agents-tools (essential)
|
|
437
562
|
try:
|
|
438
563
|
from strands_tools import (
|
|
439
564
|
shell,
|
|
@@ -466,9 +591,9 @@ class DevDuck:
|
|
|
466
591
|
)
|
|
467
592
|
logger.info("✅ strands-agents-tools loaded")
|
|
468
593
|
except ImportError:
|
|
469
|
-
logger.
|
|
594
|
+
logger.info("strands-agents-tools unavailable")
|
|
470
595
|
|
|
471
|
-
# strands-fun-tools (optional, skip in --mcp mode)
|
|
596
|
+
# Load strands-fun-tools (optional, skip in --mcp mode)
|
|
472
597
|
if "--mcp" not in sys.argv:
|
|
473
598
|
try:
|
|
474
599
|
from strands_fun_tools import (
|
|
@@ -486,23 +611,103 @@ class DevDuck:
|
|
|
486
611
|
|
|
487
612
|
return tools
|
|
488
613
|
|
|
489
|
-
def
|
|
490
|
-
"""
|
|
614
|
+
def _load_devduck_tools(self):
|
|
615
|
+
"""Load DevDuck's core tools (always available)"""
|
|
616
|
+
tools = []
|
|
617
|
+
try:
|
|
618
|
+
from .tools import (
|
|
619
|
+
tcp,
|
|
620
|
+
websocket,
|
|
621
|
+
ipc,
|
|
622
|
+
mcp_server,
|
|
623
|
+
install_tools,
|
|
624
|
+
use_github,
|
|
625
|
+
create_subagent,
|
|
626
|
+
store_in_kb,
|
|
627
|
+
system_prompt,
|
|
628
|
+
state_manager,
|
|
629
|
+
tray,
|
|
630
|
+
ambient,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
tools.extend(
|
|
634
|
+
[
|
|
635
|
+
tcp,
|
|
636
|
+
websocket,
|
|
637
|
+
ipc,
|
|
638
|
+
mcp_server,
|
|
639
|
+
install_tools,
|
|
640
|
+
use_github,
|
|
641
|
+
create_subagent,
|
|
642
|
+
store_in_kb,
|
|
643
|
+
system_prompt,
|
|
644
|
+
state_manager,
|
|
645
|
+
tray,
|
|
646
|
+
ambient,
|
|
647
|
+
]
|
|
648
|
+
)
|
|
649
|
+
logger.info("✅ DevDuck core tools loaded")
|
|
650
|
+
except ImportError as e:
|
|
651
|
+
logger.warning(f"DevDuck tools unavailable: {e}")
|
|
652
|
+
|
|
653
|
+
# Load AgentCore tools if AWS credentials available (conditional)
|
|
654
|
+
if os.getenv("DEVDUCK_DISABLE_AGENTCORE_TOOLS", "false").lower() != "true":
|
|
655
|
+
try:
|
|
656
|
+
import boto3
|
|
657
|
+
|
|
658
|
+
boto3.client("sts").get_caller_identity()
|
|
659
|
+
|
|
660
|
+
from .tools.agentcore_config import agentcore_config
|
|
661
|
+
from .tools.agentcore_invoke import agentcore_invoke
|
|
662
|
+
from .tools.agentcore_logs import agentcore_logs
|
|
663
|
+
from .tools.agentcore_agents import agentcore_agents
|
|
664
|
+
|
|
665
|
+
tools.extend(
|
|
666
|
+
[
|
|
667
|
+
agentcore_config,
|
|
668
|
+
agentcore_invoke,
|
|
669
|
+
agentcore_logs,
|
|
670
|
+
agentcore_agents,
|
|
671
|
+
]
|
|
672
|
+
)
|
|
673
|
+
logger.info("✅ AgentCore tools loaded")
|
|
674
|
+
except Exception as e:
|
|
675
|
+
logger.debug(f"AgentCore tools unavailable: {e}")
|
|
676
|
+
else:
|
|
677
|
+
logger.info(
|
|
678
|
+
"⏭️ AgentCore tools disabled (DEVDUCK_DISABLE_AGENTCORE_TOOLS=true)"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
return tools
|
|
682
|
+
|
|
683
|
+
def _select_model(self):
|
|
684
|
+
"""
|
|
685
|
+
Smart model selection with fallback: Bedrock → MLX → Ollama
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
Tuple of (model_instance, model_name)
|
|
689
|
+
"""
|
|
491
690
|
provider = os.getenv("MODEL_PROVIDER")
|
|
492
691
|
|
|
493
692
|
if not provider:
|
|
494
693
|
# Auto-detect: Bedrock → MLX → Ollama
|
|
495
694
|
try:
|
|
695
|
+
# Try Bedrock if AWS credentials available
|
|
696
|
+
import boto3
|
|
697
|
+
|
|
496
698
|
boto3.client("sts").get_caller_identity()
|
|
497
699
|
provider = "bedrock"
|
|
498
700
|
print("🦆 Using Bedrock")
|
|
499
701
|
except:
|
|
500
|
-
|
|
702
|
+
# Try MLX on Apple Silicon
|
|
703
|
+
if platform.system() == "Darwin" and platform.machine() in [
|
|
704
|
+
"arm64",
|
|
705
|
+
"aarch64",
|
|
706
|
+
]:
|
|
501
707
|
try:
|
|
502
708
|
from strands_mlx import MLXModel
|
|
503
709
|
|
|
504
710
|
provider = "mlx"
|
|
505
|
-
self.model = "mlx-community/Qwen3-1.7B-4bit"
|
|
506
711
|
print("🦆 Using MLX")
|
|
507
712
|
except ImportError:
|
|
508
713
|
provider = "ollama"
|
|
@@ -511,26 +716,43 @@ class DevDuck:
|
|
|
511
716
|
provider = "ollama"
|
|
512
717
|
print("🦆 Using Ollama")
|
|
513
718
|
|
|
514
|
-
# Create model
|
|
719
|
+
# Create model based on provider
|
|
515
720
|
if provider == "mlx":
|
|
516
721
|
from strands_mlx import MLXModel
|
|
517
722
|
|
|
518
|
-
|
|
723
|
+
model_name = "mlx-community/Qwen3-1.7B-4bit"
|
|
724
|
+
return MLXModel(model_id=model_name, temperature=1), model_name
|
|
725
|
+
|
|
519
726
|
elif provider == "ollama":
|
|
520
727
|
from strands.models.ollama import OllamaModel
|
|
521
728
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
729
|
+
os_type = platform.system()
|
|
730
|
+
if os_type == "Darwin":
|
|
731
|
+
model_name = "qwen3:1.7b"
|
|
732
|
+
elif os_type == "Linux":
|
|
733
|
+
model_name = "qwen3:30b"
|
|
734
|
+
else:
|
|
735
|
+
model_name = "qwen3:8b"
|
|
736
|
+
|
|
737
|
+
return (
|
|
738
|
+
OllamaModel(
|
|
739
|
+
host="http://localhost:11434",
|
|
740
|
+
model_id=model_name,
|
|
741
|
+
temperature=1,
|
|
742
|
+
keep_alive="5m",
|
|
743
|
+
),
|
|
744
|
+
model_name,
|
|
527
745
|
)
|
|
746
|
+
|
|
528
747
|
else:
|
|
748
|
+
# Bedrock or other providers via create_model
|
|
529
749
|
from strands_tools.utils.models.model import create_model
|
|
530
750
|
|
|
531
|
-
|
|
751
|
+
model = create_model(provider=provider)
|
|
752
|
+
model_name = os.getenv("STRANDS_MODEL_ID", "bedrock")
|
|
753
|
+
return model, model_name
|
|
532
754
|
|
|
533
|
-
def
|
|
755
|
+
def _build_system_prompt(self):
|
|
534
756
|
"""Build adaptive system prompt based on environment
|
|
535
757
|
|
|
536
758
|
IMPORTANT: The system prompt includes the agent's complete source code.
|
|
@@ -540,82 +762,83 @@ class DevDuck:
|
|
|
540
762
|
|
|
541
763
|
Learning: Always check source code truth over conversation memory!
|
|
542
764
|
"""
|
|
765
|
+
# Current date and time
|
|
766
|
+
current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
767
|
+
current_date = datetime.now().strftime("%A, %B %d, %Y")
|
|
768
|
+
current_time = datetime.now().strftime("%I:%M %p")
|
|
769
|
+
|
|
770
|
+
session_id = f"devduck-{datetime.now().strftime('%Y-%m-%d')}"
|
|
771
|
+
|
|
772
|
+
# Get own file path for self-modification awareness
|
|
773
|
+
own_file_path = Path(__file__).resolve()
|
|
774
|
+
|
|
775
|
+
# Get own source code for self-awareness
|
|
543
776
|
own_code = get_own_source_code()
|
|
544
|
-
recent_context = get_last_messages()
|
|
545
|
-
recent_logs = get_recent_logs()
|
|
546
777
|
|
|
547
|
-
#
|
|
548
|
-
provider = os.getenv("MODEL_PROVIDER", "")
|
|
549
|
-
is_bedrock = provider == "bedrock" or "bedrock" in provider.lower()
|
|
778
|
+
# Get recent conversation history context (with error handling)
|
|
550
779
|
try:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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)
|
|
572
|
-
|
|
573
|
-
# Invoke
|
|
574
|
-
agentcore_invoke(prompt="test", agent_name="devduck")
|
|
575
|
-
|
|
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
|
-
"""
|
|
780
|
+
recent_context = get_last_messages()
|
|
781
|
+
except Exception as e:
|
|
782
|
+
print(f"🦆 Warning: Could not load history context: {e}")
|
|
783
|
+
recent_context = ""
|
|
784
|
+
|
|
785
|
+
# Get recent logs for immediate visibility
|
|
786
|
+
try:
|
|
787
|
+
recent_logs = get_recent_logs()
|
|
788
|
+
except Exception as e:
|
|
789
|
+
print(f"🦆 Warning: Could not load recent logs: {e}")
|
|
790
|
+
recent_logs = ""
|
|
586
791
|
|
|
587
|
-
return f"""🦆 DevDuck - self-adapting agent
|
|
792
|
+
return f"""🦆 You are DevDuck - an extreme minimalist, self-adapting agent.
|
|
588
793
|
|
|
589
|
-
Environment: {self.os} {self.arch}
|
|
794
|
+
Environment: {self.env_info['os']} {self.env_info['arch']}
|
|
795
|
+
Python: {self.env_info['python']}
|
|
590
796
|
Model: {self.model}
|
|
591
|
-
|
|
797
|
+
Hostname: {self.env_info['hostname']}
|
|
798
|
+
Session ID: {session_id}
|
|
799
|
+
Current Time: {current_datetime} ({current_date} at {current_time})
|
|
800
|
+
My Path: {own_file_path}
|
|
592
801
|
|
|
593
802
|
You are:
|
|
594
803
|
- Minimalist: Brief, direct responses
|
|
804
|
+
- Self-healing: Adapt when things break
|
|
595
805
|
- Efficient: Get things done fast
|
|
596
806
|
- Pragmatic: Use what works
|
|
597
807
|
|
|
808
|
+
Current working directory: {self.env_info['cwd']}
|
|
809
|
+
|
|
598
810
|
{recent_context}
|
|
599
811
|
{recent_logs}
|
|
600
|
-
{agentcore_docs}
|
|
601
|
-
|
|
602
|
-
## Your Code
|
|
603
812
|
|
|
813
|
+
## Your Own Implementation:
|
|
604
814
|
You have full access to your own source code for self-awareness and self-modification:
|
|
605
|
-
|
|
815
|
+
|
|
606
816
|
{own_code}
|
|
607
|
-
---
|
|
608
817
|
|
|
609
|
-
## Hot Reload Active:
|
|
610
|
-
- Save .py
|
|
611
|
-
-
|
|
612
|
-
-
|
|
818
|
+
## Hot Reload System Active:
|
|
819
|
+
- **Instant Tool Creation** - Save any .py file in `./tools/` and it becomes immediately available
|
|
820
|
+
- **No Restart Needed** - Tools are auto-loaded and ready to use instantly
|
|
821
|
+
- **Live Development** - Modify existing tools while running and test immediately
|
|
822
|
+
- **Full Python Access** - Create any Python functionality as a tool
|
|
823
|
+
- **Agent Protection** - Hot-reload waits until agent finishes current task
|
|
824
|
+
|
|
825
|
+
## Dynamic Tool Loading:
|
|
826
|
+
- **Install Tools** - Use install_tools() to load tools from any Python package
|
|
827
|
+
- Example: install_tools(action="install_and_load", package="strands-fun-tools", module="strands_fun_tools")
|
|
828
|
+
- Expands capabilities without restart
|
|
829
|
+
- Access to entire Python ecosystem
|
|
613
830
|
|
|
614
831
|
## Tool Configuration:
|
|
615
832
|
Set DEVDUCK_TOOLS for custom tools:
|
|
616
833
|
- Format: package:tool1,tool2:package2:tool3
|
|
617
834
|
- Example: strands_tools:shell,editor:strands_fun_tools:clipboard
|
|
618
|
-
-
|
|
835
|
+
- Tools are filtered - only specified tools are loaded
|
|
836
|
+
|
|
837
|
+
## MCP Server:
|
|
838
|
+
- **Expose as MCP Server** - Use mcp_server() to expose devduck via MCP protocol
|
|
839
|
+
- Example: mcp_server(action="start", port=8000)
|
|
840
|
+
- Connect from Claude Desktop, other agents, or custom clients
|
|
841
|
+
- Full bidirectional communication
|
|
619
842
|
|
|
620
843
|
## Knowledge Base Integration:
|
|
621
844
|
- **Automatic RAG** - Set DEVDUCK_KNOWLEDGE_BASE_ID to enable automatic retrieval/storage
|
|
@@ -624,293 +847,472 @@ Set DEVDUCK_TOOLS for custom tools:
|
|
|
624
847
|
- Seamless memory across sessions without manual tool calls
|
|
625
848
|
|
|
626
849
|
## System Prompt Management:
|
|
627
|
-
- system_prompt(action='view') -
|
|
628
|
-
- system_prompt(action='update', prompt='
|
|
629
|
-
- system_prompt(action='update', repository='
|
|
850
|
+
- **View**: system_prompt(action='view') - See current prompt
|
|
851
|
+
- **Update Local**: system_prompt(action='update', prompt='new text') - Updates env var + .prompt file
|
|
852
|
+
- **Update GitHub**: system_prompt(action='update', prompt='text', repository='cagataycali/devduck') - Syncs to repo variables
|
|
853
|
+
- **Variable Name**: system_prompt(action='update', prompt='text', variable_name='CUSTOM_PROMPT') - Use custom var
|
|
854
|
+
- **Add Context**: system_prompt(action='add_context', context='new learning') - Append without replacing
|
|
855
|
+
|
|
856
|
+
### 🧠 Self-Improvement Pattern:
|
|
857
|
+
When you learn something valuable during conversations:
|
|
858
|
+
1. Identify the new insight or pattern
|
|
859
|
+
2. Use system_prompt(action='add_context', context='...') to append it
|
|
860
|
+
3. Sync to GitHub: system_prompt(action='update', prompt=new_full_prompt, repository='owner/repo')
|
|
861
|
+
4. New learnings persist across sessions via SYSTEM_PROMPT env var
|
|
862
|
+
|
|
863
|
+
**Repository Integration**: Set repository='cagataycali/devduck' to sync prompts across deployments
|
|
630
864
|
|
|
631
865
|
## Shell Commands:
|
|
632
|
-
- Prefix with ! to
|
|
633
|
-
- Example: ! ls -la
|
|
866
|
+
- Prefix with ! to execute shell commands directly
|
|
867
|
+
- Example: ! ls -la (lists files)
|
|
868
|
+
- Example: ! pwd (shows current directory)
|
|
634
869
|
|
|
635
|
-
Response
|
|
870
|
+
**Response Format:**
|
|
871
|
+
- Tool calls: **MAXIMUM PARALLELISM - ALWAYS**
|
|
872
|
+
- Communication: **MINIMAL WORDS**
|
|
873
|
+
- Efficiency: **Speed is paramount**
|
|
636
874
|
|
|
637
|
-
|
|
875
|
+
{os.getenv('SYSTEM_PROMPT', '')}"""
|
|
638
876
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
877
|
+
def _self_heal(self, error):
|
|
878
|
+
"""Attempt self-healing when errors occur"""
|
|
879
|
+
logger.error(f"Self-healing triggered by error: {error}")
|
|
880
|
+
print(f"🦆 Self-healing from: {error}")
|
|
643
881
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
Args:
|
|
649
|
-
param1: Description of param1
|
|
650
|
-
param2: Description of param2 (default: 10)
|
|
651
|
-
|
|
652
|
-
Returns:
|
|
653
|
-
str: Description of return value
|
|
654
|
-
\"\"\"
|
|
655
|
-
# Implementation
|
|
656
|
-
return f"Result: {{param1}} - {{param2}}"
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### **Action-Based Pattern:**
|
|
660
|
-
```python
|
|
661
|
-
from typing import Dict, Any
|
|
662
|
-
from strands import tool
|
|
882
|
+
# Prevent infinite recursion by tracking heal attempts
|
|
883
|
+
if not hasattr(self, "_heal_count"):
|
|
884
|
+
self._heal_count = 0
|
|
663
885
|
|
|
664
|
-
|
|
665
|
-
def my_tool(action: str, data: str = None) -> Dict[str, Any]:
|
|
666
|
-
\"\"\"Multi-action tool.
|
|
667
|
-
|
|
668
|
-
Args:
|
|
669
|
-
action: Action to perform (get, set, delete)
|
|
670
|
-
data: Optional data for action
|
|
671
|
-
|
|
672
|
-
Returns:
|
|
673
|
-
Dict with status and content
|
|
674
|
-
\"\"\"
|
|
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}}"}}]}}
|
|
679
|
-
else:
|
|
680
|
-
return {{"status": "error", "content": [{{"text": f"Unknown action: {{action}}"}}]}}
|
|
681
|
-
```
|
|
886
|
+
self._heal_count += 1
|
|
682
887
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
5. Handle errors gracefully
|
|
689
|
-
6. Log important operations
|
|
888
|
+
# Limit recursion - if we've tried more than 3 times, give up
|
|
889
|
+
if self._heal_count > 2:
|
|
890
|
+
print(f"🦆 Self-healing failed after {self._heal_count} attempts")
|
|
891
|
+
print("🦆 Please fix the issue manually and restart")
|
|
892
|
+
sys.exit(1)
|
|
690
893
|
|
|
691
|
-
|
|
894
|
+
elif "connection" in str(error).lower():
|
|
895
|
+
print("🦆 Connection issue - checking ollama service...")
|
|
896
|
+
try:
|
|
897
|
+
subprocess.run(["ollama", "serve"], check=False, timeout=2)
|
|
898
|
+
except:
|
|
899
|
+
pass
|
|
692
900
|
|
|
693
|
-
|
|
694
|
-
"""Implementation of view_logs tool"""
|
|
901
|
+
# Retry initialization
|
|
695
902
|
try:
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
903
|
+
self.__init__()
|
|
904
|
+
except Exception as e2:
|
|
905
|
+
print(f"🦆 Self-heal failed: {e2}")
|
|
906
|
+
print("🦆 Running in minimal mode...")
|
|
907
|
+
self.agent = None
|
|
908
|
+
|
|
909
|
+
def _is_port_available(self, port):
|
|
910
|
+
"""Check if a port is available"""
|
|
911
|
+
try:
|
|
912
|
+
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
913
|
+
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
914
|
+
test_socket.bind(("0.0.0.0", port))
|
|
915
|
+
test_socket.close()
|
|
916
|
+
return True
|
|
917
|
+
except OSError:
|
|
918
|
+
return False
|
|
919
|
+
|
|
920
|
+
def _is_socket_available(self, socket_path):
|
|
921
|
+
"""Check if a Unix socket is available"""
|
|
922
|
+
import os
|
|
923
|
+
|
|
924
|
+
# If socket file doesn't exist, it's available
|
|
925
|
+
if not os.path.exists(socket_path):
|
|
926
|
+
return True
|
|
927
|
+
# If it exists, try to connect to see if it's in use
|
|
928
|
+
try:
|
|
929
|
+
test_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
930
|
+
test_socket.connect(socket_path)
|
|
931
|
+
test_socket.close()
|
|
932
|
+
return False # Socket is in use
|
|
933
|
+
except (ConnectionRefusedError, FileNotFoundError):
|
|
934
|
+
# Socket file exists but not in use - remove stale socket
|
|
935
|
+
try:
|
|
936
|
+
os.remove(socket_path)
|
|
937
|
+
return True
|
|
938
|
+
except:
|
|
939
|
+
return False
|
|
940
|
+
except Exception:
|
|
941
|
+
return False
|
|
942
|
+
|
|
943
|
+
def _find_available_port(self, start_port, max_attempts=10):
|
|
944
|
+
"""Find an available port starting from start_port"""
|
|
945
|
+
for offset in range(max_attempts):
|
|
946
|
+
port = start_port + offset
|
|
947
|
+
if self._is_port_available(port):
|
|
948
|
+
return port
|
|
949
|
+
return None
|
|
708
950
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
}
|
|
951
|
+
def _find_available_socket(self, base_socket_path, max_attempts=10):
|
|
952
|
+
"""Find an available socket path"""
|
|
953
|
+
if self._is_socket_available(base_socket_path):
|
|
954
|
+
return base_socket_path
|
|
955
|
+
# Try numbered alternatives
|
|
956
|
+
for i in range(1, max_attempts):
|
|
957
|
+
alt_socket = f"{base_socket_path}.{i}"
|
|
958
|
+
if self._is_socket_available(alt_socket):
|
|
959
|
+
return alt_socket
|
|
960
|
+
return None
|
|
732
961
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
962
|
+
def _start_servers(self):
|
|
963
|
+
"""Auto-start configured servers with port conflict handling"""
|
|
964
|
+
logger.info("Auto-starting servers...")
|
|
965
|
+
print("🦆 Auto-starting servers...")
|
|
737
966
|
|
|
738
|
-
|
|
739
|
-
|
|
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}"}]}
|
|
967
|
+
# Start servers in order: IPC, TCP, WS, MCP
|
|
968
|
+
server_order = ["ipc", "tcp", "ws", "mcp"]
|
|
745
969
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
try:
|
|
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}")
|
|
970
|
+
for server_type in server_order:
|
|
971
|
+
if server_type not in self.servers:
|
|
972
|
+
continue
|
|
754
973
|
|
|
755
|
-
|
|
756
|
-
try:
|
|
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}")
|
|
974
|
+
config = self.servers[server_type]
|
|
761
975
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
except Exception as e:
|
|
773
|
-
logger.warning(f"MCP server failed: {e}")
|
|
976
|
+
# Check if server is enabled
|
|
977
|
+
if not config.get("enabled", True):
|
|
978
|
+
continue
|
|
979
|
+
|
|
980
|
+
# Check for LOOKUP_KEY (conditional start based on env var)
|
|
981
|
+
if "LOOKUP_KEY" in config:
|
|
982
|
+
lookup_key = config["LOOKUP_KEY"]
|
|
983
|
+
if not os.getenv(lookup_key):
|
|
984
|
+
logger.info(f"Skipping {server_type} - {lookup_key} not set")
|
|
985
|
+
continue
|
|
774
986
|
|
|
775
|
-
|
|
987
|
+
# Start the server with port conflict handling
|
|
776
988
|
try:
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
989
|
+
if server_type == "tcp":
|
|
990
|
+
port = config.get("port", 9999)
|
|
991
|
+
|
|
992
|
+
# Check port availability BEFORE attempting to start
|
|
993
|
+
if not self._is_port_available(port):
|
|
994
|
+
alt_port = self._find_available_port(port + 1)
|
|
995
|
+
if alt_port:
|
|
996
|
+
logger.info(f"Port {port} in use, using {alt_port}")
|
|
997
|
+
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
998
|
+
port = alt_port
|
|
999
|
+
else:
|
|
1000
|
+
logger.warning(f"No available ports found for TCP server")
|
|
1001
|
+
continue
|
|
781
1002
|
|
|
782
|
-
|
|
783
|
-
"""Start hot-reload file watcher"""
|
|
1003
|
+
result = self.agent.tool.tcp(action="start_server", port=port)
|
|
784
1004
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1005
|
+
if result.get("status") == "success":
|
|
1006
|
+
logger.info(f"✓ TCP server started on port {port}")
|
|
1007
|
+
print(f"🦆 ✓ TCP server: localhost:{port}")
|
|
1008
|
+
|
|
1009
|
+
elif server_type == "ws":
|
|
1010
|
+
port = config.get("port", 8080)
|
|
790
1011
|
|
|
791
|
-
|
|
792
|
-
|
|
1012
|
+
# Check port availability BEFORE attempting to start
|
|
1013
|
+
if not self._is_port_available(port):
|
|
1014
|
+
alt_port = self._find_available_port(port + 1)
|
|
1015
|
+
if alt_port:
|
|
1016
|
+
logger.info(f"Port {port} in use, using {alt_port}")
|
|
1017
|
+
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
1018
|
+
port = alt_port
|
|
1019
|
+
else:
|
|
1020
|
+
logger.warning(
|
|
1021
|
+
f"No available ports found for WebSocket server"
|
|
1022
|
+
)
|
|
1023
|
+
continue
|
|
793
1024
|
|
|
794
|
-
|
|
795
|
-
debounce = 3 # seconds
|
|
1025
|
+
result = self.agent.tool.websocket(action="start_server", port=port)
|
|
796
1026
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
print(f"🦆
|
|
810
|
-
|
|
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()
|
|
1027
|
+
if result.get("status") == "success":
|
|
1028
|
+
logger.info(f"✓ WebSocket server started on port {port}")
|
|
1029
|
+
print(f"🦆 ✓ WebSocket server: localhost:{port}")
|
|
1030
|
+
|
|
1031
|
+
elif server_type == "mcp":
|
|
1032
|
+
port = config.get("port", 8000)
|
|
1033
|
+
|
|
1034
|
+
# Check port availability BEFORE attempting to start
|
|
1035
|
+
if not self._is_port_available(port):
|
|
1036
|
+
alt_port = self._find_available_port(port + 1)
|
|
1037
|
+
if alt_port:
|
|
1038
|
+
logger.info(f"Port {port} in use, using {alt_port}")
|
|
1039
|
+
print(f"🦆 Port {port} in use, using {alt_port}")
|
|
1040
|
+
port = alt_port
|
|
818
1041
|
else:
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
logger.error(f"File watcher error: {e}")
|
|
1042
|
+
logger.warning(f"No available ports found for MCP server")
|
|
1043
|
+
continue
|
|
822
1044
|
|
|
823
|
-
|
|
1045
|
+
result = self.agent.tool.mcp_server(
|
|
1046
|
+
action="start",
|
|
1047
|
+
transport="http",
|
|
1048
|
+
port=port,
|
|
1049
|
+
expose_agent=True,
|
|
1050
|
+
agent=self.agent,
|
|
1051
|
+
)
|
|
824
1052
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1053
|
+
if result.get("status") == "success":
|
|
1054
|
+
logger.info(f"✓ MCP HTTP server started on port {port}")
|
|
1055
|
+
print(f"🦆 ✓ MCP server: http://localhost:{port}/mcp")
|
|
1056
|
+
|
|
1057
|
+
elif server_type == "ipc":
|
|
1058
|
+
socket_path = config.get("socket_path", "/tmp/devduck_main.sock")
|
|
1059
|
+
|
|
1060
|
+
# Check socket availability BEFORE attempting to start
|
|
1061
|
+
available_socket = self._find_available_socket(socket_path)
|
|
1062
|
+
if not available_socket:
|
|
1063
|
+
logger.warning(
|
|
1064
|
+
f"No available socket paths found for IPC server"
|
|
1065
|
+
)
|
|
1066
|
+
continue
|
|
1067
|
+
|
|
1068
|
+
if available_socket != socket_path:
|
|
1069
|
+
logger.info(
|
|
1070
|
+
f"Socket {socket_path} in use, using {available_socket}"
|
|
1071
|
+
)
|
|
1072
|
+
print(
|
|
1073
|
+
f"🦆 Socket {socket_path} in use, using {available_socket}"
|
|
1074
|
+
)
|
|
1075
|
+
socket_path = available_socket
|
|
1076
|
+
|
|
1077
|
+
result = self.agent.tool.ipc(
|
|
1078
|
+
action="start_server", socket_path=socket_path
|
|
1079
|
+
)
|
|
828
1080
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1081
|
+
if result.get("status") == "success":
|
|
1082
|
+
logger.info(f"✓ IPC server started on {socket_path}")
|
|
1083
|
+
print(f"🦆 ✓ IPC server: {socket_path}")
|
|
1084
|
+
# TODO: support custom file path here so we can trigger foreign python function like another file
|
|
1085
|
+
except Exception as e:
|
|
1086
|
+
logger.error(f"Failed to start {server_type} server: {e}")
|
|
1087
|
+
print(f"🦆 ⚠ {server_type.upper()} server failed: {e}")
|
|
834
1088
|
|
|
835
1089
|
def __call__(self, query):
|
|
836
|
-
"""
|
|
1090
|
+
"""Make the agent callable with automatic knowledge base integration"""
|
|
837
1091
|
if not self.agent:
|
|
838
|
-
|
|
1092
|
+
logger.warning("Agent unavailable - attempted to call with query")
|
|
1093
|
+
return "🦆 Agent unavailable - try: devduck.restart()"
|
|
839
1094
|
|
|
840
1095
|
try:
|
|
1096
|
+
logger.info(f"Agent call started: {query[:100]}...")
|
|
1097
|
+
# Mark agent as executing to prevent hot-reload interruption
|
|
841
1098
|
self._agent_executing = True
|
|
842
1099
|
|
|
843
|
-
#
|
|
844
|
-
|
|
845
|
-
if
|
|
1100
|
+
# 📚 Knowledge Base Retrieval (BEFORE agent runs)
|
|
1101
|
+
knowledge_base_id = os.getenv("DEVDUCK_KNOWLEDGE_BASE_ID")
|
|
1102
|
+
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
846
1103
|
try:
|
|
847
|
-
self.agent.
|
|
848
|
-
|
|
849
|
-
|
|
1104
|
+
if "retrieve" in self.agent.tool_names:
|
|
1105
|
+
logger.info(f"Retrieving context from KB: {knowledge_base_id}")
|
|
1106
|
+
self.agent.tool.retrieve(
|
|
1107
|
+
text=query, knowledgeBaseId=knowledge_base_id
|
|
1108
|
+
)
|
|
1109
|
+
except Exception as e:
|
|
1110
|
+
logger.warning(f"KB retrieval failed: {e}")
|
|
850
1111
|
|
|
851
|
-
# Run agent
|
|
1112
|
+
# Run the agent
|
|
852
1113
|
result = self.agent(query)
|
|
853
1114
|
|
|
854
|
-
#
|
|
855
|
-
if
|
|
1115
|
+
# 💾 Knowledge Base Storage (AFTER agent runs)
|
|
1116
|
+
if knowledge_base_id and hasattr(self.agent, "tool"):
|
|
856
1117
|
try:
|
|
857
|
-
self.agent.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1118
|
+
if "store_in_kb" in self.agent.tool_names:
|
|
1119
|
+
conversation_content = f"Input: {query}, Result: {result!s}"
|
|
1120
|
+
conversation_title = f"DevDuck: {datetime.now().strftime('%Y-%m-%d')} | {query[:500]}"
|
|
1121
|
+
self.agent.tool.store_in_kb(
|
|
1122
|
+
content=conversation_content,
|
|
1123
|
+
title=conversation_title,
|
|
1124
|
+
knowledge_base_id=knowledge_base_id,
|
|
1125
|
+
)
|
|
1126
|
+
logger.info(f"Stored conversation in KB: {knowledge_base_id}")
|
|
1127
|
+
except Exception as e:
|
|
1128
|
+
logger.warning(f"KB storage failed: {e}")
|
|
864
1129
|
|
|
1130
|
+
# Clear executing flag
|
|
865
1131
|
self._agent_executing = False
|
|
866
1132
|
|
|
867
|
-
# Check for pending reload
|
|
1133
|
+
# Check for pending hot-reload
|
|
868
1134
|
if self._reload_pending:
|
|
869
|
-
|
|
1135
|
+
logger.info("Triggering pending hot-reload after agent completion")
|
|
1136
|
+
print("🦆 Agent finished - triggering pending hot-reload...")
|
|
870
1137
|
self._hot_reload()
|
|
871
1138
|
|
|
872
1139
|
return result
|
|
873
1140
|
except Exception as e:
|
|
874
|
-
self._agent_executing = False
|
|
875
|
-
logger.error(f"Agent call failed: {e}")
|
|
876
|
-
|
|
1141
|
+
self._agent_executing = False # Reset flag on error
|
|
1142
|
+
logger.error(f"Agent call failed with error: {e}")
|
|
1143
|
+
self._self_heal(e)
|
|
1144
|
+
if self.agent:
|
|
1145
|
+
return self.agent(query)
|
|
1146
|
+
else:
|
|
1147
|
+
return f"🦆 Error: {e}"
|
|
1148
|
+
|
|
1149
|
+
def restart(self):
|
|
1150
|
+
"""Restart the agent"""
|
|
1151
|
+
print("🦆 Restarting...")
|
|
1152
|
+
self.__init__()
|
|
1153
|
+
|
|
1154
|
+
def _start_file_watcher(self):
|
|
1155
|
+
"""Start background file watcher for auto hot-reload"""
|
|
1156
|
+
import threading
|
|
1157
|
+
|
|
1158
|
+
logger.info("Starting file watcher for hot-reload")
|
|
1159
|
+
# Get the path to this file
|
|
1160
|
+
self._watch_file = Path(__file__).resolve()
|
|
1161
|
+
self._last_modified = (
|
|
1162
|
+
self._watch_file.stat().st_mtime if self._watch_file.exists() else None
|
|
1163
|
+
)
|
|
1164
|
+
self._watcher_running = True
|
|
1165
|
+
self._is_reloading = False
|
|
1166
|
+
|
|
1167
|
+
# Start watcher thread
|
|
1168
|
+
self._watcher_thread = threading.Thread(
|
|
1169
|
+
target=self._file_watcher_thread, daemon=True
|
|
1170
|
+
)
|
|
1171
|
+
self._watcher_thread.start()
|
|
1172
|
+
logger.info(f"File watcher started, monitoring {self._watch_file}")
|
|
1173
|
+
|
|
1174
|
+
def _file_watcher_thread(self):
|
|
1175
|
+
"""Background thread that watches for file changes"""
|
|
1176
|
+
last_reload_time = 0
|
|
1177
|
+
debounce_seconds = 3 # 3 second debounce
|
|
1178
|
+
|
|
1179
|
+
while self._watcher_running:
|
|
1180
|
+
try:
|
|
1181
|
+
# Skip if currently reloading
|
|
1182
|
+
if self._is_reloading:
|
|
1183
|
+
time.sleep(1)
|
|
1184
|
+
continue
|
|
1185
|
+
|
|
1186
|
+
if self._watch_file.exists():
|
|
1187
|
+
current_mtime = self._watch_file.stat().st_mtime
|
|
1188
|
+
current_time = time.time()
|
|
1189
|
+
|
|
1190
|
+
# Check if file was modified AND debounce period has passed
|
|
1191
|
+
if (
|
|
1192
|
+
self._last_modified
|
|
1193
|
+
and current_mtime > self._last_modified
|
|
1194
|
+
and current_time - last_reload_time > debounce_seconds
|
|
1195
|
+
):
|
|
1196
|
+
print(f"🦆 Detected changes in {self._watch_file.name}!")
|
|
1197
|
+
last_reload_time = current_time
|
|
1198
|
+
|
|
1199
|
+
# Check if agent is currently executing
|
|
1200
|
+
if self._agent_executing:
|
|
1201
|
+
logger.info(
|
|
1202
|
+
"Code change detected but agent is executing - reload pending"
|
|
1203
|
+
)
|
|
1204
|
+
print(
|
|
1205
|
+
"🦆 Agent is currently executing - reload will trigger after completion"
|
|
1206
|
+
)
|
|
1207
|
+
self._reload_pending = True
|
|
1208
|
+
# Don't update _last_modified yet - keep detecting the change
|
|
1209
|
+
else:
|
|
1210
|
+
# Safe to reload immediately
|
|
1211
|
+
self._last_modified = current_mtime
|
|
1212
|
+
logger.info(
|
|
1213
|
+
f"Code change detected in {self._watch_file.name} - triggering hot-reload"
|
|
1214
|
+
)
|
|
1215
|
+
time.sleep(
|
|
1216
|
+
0.5
|
|
1217
|
+
) # Small delay to ensure file write is complete
|
|
1218
|
+
self._hot_reload()
|
|
1219
|
+
else:
|
|
1220
|
+
# Update timestamp if no change or still in debounce
|
|
1221
|
+
if not self._reload_pending:
|
|
1222
|
+
self._last_modified = current_mtime
|
|
1223
|
+
|
|
1224
|
+
except Exception as e:
|
|
1225
|
+
logger.error(f"File watcher error: {e}")
|
|
1226
|
+
|
|
1227
|
+
# Check every 1 second
|
|
1228
|
+
time.sleep(1)
|
|
1229
|
+
|
|
1230
|
+
def _stop_file_watcher(self):
|
|
1231
|
+
"""Stop the file watcher"""
|
|
1232
|
+
self._watcher_running = False
|
|
1233
|
+
logger.info("File watcher stopped")
|
|
1234
|
+
|
|
1235
|
+
def _hot_reload(self):
|
|
1236
|
+
"""Hot-reload by restarting the entire Python process with fresh code"""
|
|
1237
|
+
logger.info("Hot-reload initiated")
|
|
1238
|
+
print("🦆 Hot-reloading via process restart...")
|
|
1239
|
+
|
|
1240
|
+
try:
|
|
1241
|
+
# Set reload flag to prevent recursive reloads during shutdown
|
|
1242
|
+
self._is_reloading = True
|
|
1243
|
+
|
|
1244
|
+
# Update last_modified before reload to acknowledge the change
|
|
1245
|
+
if hasattr(self, "_watch_file") and self._watch_file.exists():
|
|
1246
|
+
self._last_modified = self._watch_file.stat().st_mtime
|
|
877
1247
|
|
|
1248
|
+
# Reset pending flag
|
|
1249
|
+
self._reload_pending = False
|
|
878
1250
|
|
|
879
|
-
#
|
|
1251
|
+
# Stop the file watcher
|
|
1252
|
+
if hasattr(self, "_watcher_running"):
|
|
1253
|
+
self._watcher_running = False
|
|
1254
|
+
|
|
1255
|
+
print("🦆 Restarting process with fresh code...")
|
|
1256
|
+
|
|
1257
|
+
# Restart the entire Python process
|
|
1258
|
+
# This ensures all code is freshly loaded
|
|
1259
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
1260
|
+
|
|
1261
|
+
except Exception as e:
|
|
1262
|
+
logger.error(f"Hot-reload failed: {e}")
|
|
1263
|
+
print(f"🦆 Hot-reload failed: {e}")
|
|
1264
|
+
print("🦆 Falling back to manual restart")
|
|
1265
|
+
self._is_reloading = False
|
|
1266
|
+
|
|
1267
|
+
def status(self):
|
|
1268
|
+
"""Show current status"""
|
|
1269
|
+
return {
|
|
1270
|
+
"model": self.model,
|
|
1271
|
+
"env": self.env_info,
|
|
1272
|
+
"agent_ready": self.agent is not None,
|
|
1273
|
+
"tools": len(self.tools) if hasattr(self, "tools") else 0,
|
|
1274
|
+
"file_watcher": {
|
|
1275
|
+
"enabled": hasattr(self, "_watcher_running") and self._watcher_running,
|
|
1276
|
+
"watching": (
|
|
1277
|
+
str(self._watch_file) if hasattr(self, "_watch_file") else None
|
|
1278
|
+
),
|
|
1279
|
+
},
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
# 🦆 Auto-initialize when imported
|
|
880
1284
|
# Check environment variables to control server configuration
|
|
1285
|
+
# Also check if --mcp flag is present to skip auto-starting servers
|
|
881
1286
|
_auto_start = os.getenv("DEVDUCK_AUTO_START_SERVERS", "true").lower() == "true"
|
|
882
1287
|
|
|
883
1288
|
# Disable auto-start if --mcp flag is present (stdio mode)
|
|
884
1289
|
if "--mcp" in sys.argv:
|
|
885
1290
|
_auto_start = False
|
|
886
1291
|
|
|
887
|
-
|
|
888
|
-
_ws_port = int(os.getenv("DEVDUCK_WS_PORT", "8080"))
|
|
889
|
-
_mcp_port = int(os.getenv("DEVDUCK_MCP_PORT", "8000"))
|
|
890
|
-
_ipc_socket = os.getenv("DEVDUCK_IPC_SOCKET", None)
|
|
891
|
-
_enable_tcp = os.getenv("DEVDUCK_ENABLE_TCP", "true").lower() == "true"
|
|
892
|
-
_enable_ws = os.getenv("DEVDUCK_ENABLE_WS", "true").lower() == "true"
|
|
893
|
-
_enable_mcp = os.getenv("DEVDUCK_ENABLE_MCP", "true").lower() == "true"
|
|
894
|
-
_enable_ipc = os.getenv("DEVDUCK_ENABLE_IPC", "true").lower() == "true"
|
|
895
|
-
|
|
896
|
-
devduck = DevDuck(
|
|
897
|
-
auto_start_servers=_auto_start,
|
|
898
|
-
tcp_port=_tcp_port,
|
|
899
|
-
ws_port=_ws_port,
|
|
900
|
-
mcp_port=_mcp_port,
|
|
901
|
-
ipc_socket=_ipc_socket,
|
|
902
|
-
enable_tcp=_enable_tcp,
|
|
903
|
-
enable_ws=_enable_ws,
|
|
904
|
-
enable_mcp=_enable_mcp,
|
|
905
|
-
enable_ipc=_enable_ipc,
|
|
906
|
-
)
|
|
1292
|
+
devduck = DevDuck(auto_start_servers=_auto_start)
|
|
907
1293
|
|
|
908
1294
|
|
|
1295
|
+
# 🚀 Convenience functions
|
|
909
1296
|
def ask(query):
|
|
910
|
-
"""Quick query"""
|
|
1297
|
+
"""Quick query interface"""
|
|
911
1298
|
return devduck(query)
|
|
912
1299
|
|
|
913
1300
|
|
|
1301
|
+
def status():
|
|
1302
|
+
"""Quick status check"""
|
|
1303
|
+
return devduck.status()
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def restart():
|
|
1307
|
+
"""Quick restart"""
|
|
1308
|
+
devduck.restart()
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def hot_reload():
|
|
1312
|
+
"""Quick hot-reload without restart"""
|
|
1313
|
+
devduck._hot_reload()
|
|
1314
|
+
|
|
1315
|
+
|
|
914
1316
|
def extract_commands_from_history():
|
|
915
1317
|
"""Extract commonly used commands from shell history for auto-completion."""
|
|
916
1318
|
commands = set()
|
|
@@ -984,8 +1386,7 @@ def extract_commands_from_history():
|
|
|
984
1386
|
|
|
985
1387
|
|
|
986
1388
|
def interactive():
|
|
987
|
-
"""Interactive REPL
|
|
988
|
-
import time
|
|
1389
|
+
"""Interactive REPL mode for devduck"""
|
|
989
1390
|
from prompt_toolkit import prompt
|
|
990
1391
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
991
1392
|
from prompt_toolkit.completion import WordCompleter
|
|
@@ -993,10 +1394,14 @@ def interactive():
|
|
|
993
1394
|
|
|
994
1395
|
print("🦆 DevDuck")
|
|
995
1396
|
print(f"📝 Logs: {LOG_DIR}")
|
|
996
|
-
print("Type 'exit'
|
|
1397
|
+
print("Type 'exit', 'quit', or 'q' to quit.")
|
|
1398
|
+
print("Prefix with ! to run shell commands (e.g., ! ls -la)")
|
|
997
1399
|
print("-" * 50)
|
|
1400
|
+
logger.info("Interactive mode started")
|
|
998
1401
|
|
|
999
|
-
|
|
1402
|
+
# Set up prompt_toolkit with history
|
|
1403
|
+
history_file = get_shell_history_file()
|
|
1404
|
+
history = FileHistory(history_file)
|
|
1000
1405
|
|
|
1001
1406
|
# Create completions from common commands and shell history
|
|
1002
1407
|
base_commands = ["exit", "quit", "q", "help", "clear", "status", "reload"]
|
|
@@ -1012,42 +1417,61 @@ def interactive():
|
|
|
1012
1417
|
|
|
1013
1418
|
while True:
|
|
1014
1419
|
try:
|
|
1420
|
+
# Use prompt_toolkit for enhanced input with arrow key support
|
|
1015
1421
|
q = prompt(
|
|
1016
1422
|
"\n🦆 ",
|
|
1017
1423
|
history=history,
|
|
1018
1424
|
auto_suggest=AutoSuggestFromHistory(),
|
|
1019
1425
|
completer=completer,
|
|
1020
1426
|
complete_while_typing=True,
|
|
1021
|
-
mouse_support=False,
|
|
1022
|
-
)
|
|
1427
|
+
mouse_support=False, # breaks scrolling when enabled
|
|
1428
|
+
)
|
|
1023
1429
|
|
|
1024
1430
|
# Reset interrupt count on successful prompt
|
|
1025
1431
|
interrupt_count = 0
|
|
1026
1432
|
|
|
1433
|
+
# Check for exit command
|
|
1027
1434
|
if q.lower() in ["exit", "quit", "q"]:
|
|
1435
|
+
print("\n🦆 Goodbye!")
|
|
1028
1436
|
break
|
|
1029
1437
|
|
|
1030
|
-
|
|
1438
|
+
# Skip empty inputs
|
|
1439
|
+
if q.strip() == "":
|
|
1031
1440
|
continue
|
|
1032
1441
|
|
|
1033
|
-
#
|
|
1442
|
+
# Handle shell commands with ! prefix
|
|
1034
1443
|
if q.startswith("!"):
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1444
|
+
shell_command = q[1:].strip()
|
|
1445
|
+
try:
|
|
1446
|
+
if devduck.agent:
|
|
1447
|
+
devduck._agent_executing = (
|
|
1448
|
+
True # Prevent hot-reload during shell execution
|
|
1449
|
+
)
|
|
1450
|
+
result = devduck.agent.tool.shell(
|
|
1451
|
+
command=shell_command, timeout=9000
|
|
1452
|
+
)
|
|
1453
|
+
devduck._agent_executing = False
|
|
1454
|
+
|
|
1455
|
+
# Append shell command to history
|
|
1456
|
+
append_to_shell_history(q, result["content"][0]["text"])
|
|
1457
|
+
|
|
1458
|
+
# Check if reload was pending
|
|
1459
|
+
if devduck._reload_pending:
|
|
1460
|
+
print(
|
|
1461
|
+
"🦆 Shell command finished - triggering pending hot-reload..."
|
|
1462
|
+
)
|
|
1463
|
+
devduck._hot_reload()
|
|
1464
|
+
else:
|
|
1465
|
+
print("🦆 Agent unavailable")
|
|
1466
|
+
except Exception as e:
|
|
1467
|
+
devduck._agent_executing = False # Reset on error
|
|
1468
|
+
print(f"🦆 Shell command error: {e}")
|
|
1046
1469
|
continue
|
|
1047
1470
|
|
|
1048
|
-
#
|
|
1471
|
+
# Execute the agent with user input
|
|
1049
1472
|
result = ask(q)
|
|
1050
|
-
|
|
1473
|
+
|
|
1474
|
+
# Append to shell history
|
|
1051
1475
|
append_to_shell_history(q, str(result))
|
|
1052
1476
|
|
|
1053
1477
|
except KeyboardInterrupt:
|
|
@@ -1066,12 +1490,14 @@ def interactive():
|
|
|
1066
1490
|
print("\n🦆 Interrupted. Press Ctrl+C again to exit.")
|
|
1067
1491
|
|
|
1068
1492
|
last_interrupt = current_time
|
|
1493
|
+
continue
|
|
1069
1494
|
except Exception as e:
|
|
1070
1495
|
print(f"🦆 Error: {e}")
|
|
1496
|
+
continue
|
|
1071
1497
|
|
|
1072
1498
|
|
|
1073
1499
|
def cli():
|
|
1074
|
-
"""CLI entry point"""
|
|
1500
|
+
"""CLI entry point for pip-installed devduck command"""
|
|
1075
1501
|
import argparse
|
|
1076
1502
|
|
|
1077
1503
|
parser = argparse.ArgumentParser(
|
|
@@ -1079,16 +1505,14 @@ def cli():
|
|
|
1079
1505
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1080
1506
|
epilog="""
|
|
1081
1507
|
Examples:
|
|
1082
|
-
devduck #
|
|
1083
|
-
devduck "query"
|
|
1084
|
-
devduck --mcp # MCP stdio mode
|
|
1085
|
-
devduck --tcp-port 9000 # Custom TCP port
|
|
1086
|
-
devduck --no-tcp --no-ws # Disable TCP and WebSocket
|
|
1508
|
+
devduck # Start interactive mode
|
|
1509
|
+
devduck "your query here" # One-shot query
|
|
1510
|
+
devduck --mcp # MCP stdio mode (for Claude Desktop)
|
|
1087
1511
|
|
|
1088
1512
|
Tool Configuration:
|
|
1089
1513
|
export DEVDUCK_TOOLS="strands_tools:shell,editor:strands_fun_tools:clipboard"
|
|
1090
1514
|
|
|
1091
|
-
|
|
1515
|
+
Claude Desktop Config:
|
|
1092
1516
|
{
|
|
1093
1517
|
"mcpServers": {
|
|
1094
1518
|
"devduck": {
|
|
@@ -1100,72 +1524,64 @@ MCP Config:
|
|
|
1100
1524
|
""",
|
|
1101
1525
|
)
|
|
1102
1526
|
|
|
1103
|
-
|
|
1104
|
-
parser.add_argument("
|
|
1105
|
-
|
|
1106
|
-
# Server configuration
|
|
1107
|
-
parser.add_argument(
|
|
1108
|
-
"--tcp-port", type=int, default=9999, help="TCP server port (default: 9999)"
|
|
1109
|
-
)
|
|
1110
|
-
parser.add_argument(
|
|
1111
|
-
"--ws-port",
|
|
1112
|
-
type=int,
|
|
1113
|
-
default=8080,
|
|
1114
|
-
help="WebSocket server port (default: 8080)",
|
|
1115
|
-
)
|
|
1116
|
-
parser.add_argument(
|
|
1117
|
-
"--mcp-port",
|
|
1118
|
-
type=int,
|
|
1119
|
-
default=8000,
|
|
1120
|
-
help="MCP HTTP server port (default: 8000)",
|
|
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
|
-
)
|
|
1527
|
+
# Query argument
|
|
1528
|
+
parser.add_argument("query", nargs="*", help="Query to send to the agent")
|
|
1128
1529
|
|
|
1129
|
-
#
|
|
1130
|
-
parser.add_argument("--no-tcp", action="store_true", help="Disable TCP server")
|
|
1131
|
-
parser.add_argument("--no-ws", action="store_true", help="Disable WebSocket server")
|
|
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")
|
|
1530
|
+
# MCP stdio mode flag
|
|
1134
1531
|
parser.add_argument(
|
|
1135
|
-
"--
|
|
1532
|
+
"--mcp",
|
|
1136
1533
|
action="store_true",
|
|
1137
|
-
help="
|
|
1534
|
+
help="Start MCP server in stdio mode (for Claude Desktop integration)",
|
|
1138
1535
|
)
|
|
1139
1536
|
|
|
1140
1537
|
args = parser.parse_args()
|
|
1141
1538
|
|
|
1539
|
+
logger.info("CLI mode started")
|
|
1540
|
+
|
|
1541
|
+
# Handle --mcp flag for stdio mode
|
|
1142
1542
|
if args.mcp:
|
|
1543
|
+
logger.info("Starting MCP server in stdio mode (blocking, foreground)")
|
|
1143
1544
|
print("🦆 Starting MCP stdio server...", file=sys.stderr)
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1545
|
+
|
|
1546
|
+
# Don't auto-start HTTP/TCP/WS servers for stdio mode
|
|
1547
|
+
if devduck.agent:
|
|
1548
|
+
try:
|
|
1549
|
+
# Start MCP server in stdio mode - this BLOCKS until terminated
|
|
1550
|
+
devduck.agent.tool.mcp_server(
|
|
1551
|
+
action="start",
|
|
1552
|
+
transport="stdio",
|
|
1553
|
+
expose_agent=True,
|
|
1554
|
+
agent=devduck.agent,
|
|
1555
|
+
)
|
|
1556
|
+
except Exception as e:
|
|
1557
|
+
logger.error(f"Failed to start MCP stdio server: {e}")
|
|
1558
|
+
print(f"🦆 Error: {e}", file=sys.stderr)
|
|
1559
|
+
sys.exit(1)
|
|
1560
|
+
else:
|
|
1561
|
+
print("🦆 Agent not available", file=sys.stderr)
|
|
1153
1562
|
sys.exit(1)
|
|
1154
1563
|
return
|
|
1155
1564
|
|
|
1156
1565
|
if args.query:
|
|
1157
|
-
|
|
1566
|
+
query = " ".join(args.query)
|
|
1567
|
+
logger.info(f"CLI query: {query}")
|
|
1568
|
+
result = ask(query)
|
|
1158
1569
|
print(result)
|
|
1159
1570
|
else:
|
|
1571
|
+
# No arguments - start interactive mode
|
|
1160
1572
|
interactive()
|
|
1161
1573
|
|
|
1162
1574
|
|
|
1163
|
-
# Make module callable
|
|
1575
|
+
# 🦆 Make module directly callable: import devduck; devduck("query")
|
|
1164
1576
|
class CallableModule(sys.modules[__name__].__class__):
|
|
1577
|
+
"""Make the module itself callable"""
|
|
1578
|
+
|
|
1165
1579
|
def __call__(self, query):
|
|
1580
|
+
"""Allow direct module call: import devduck; devduck("query")"""
|
|
1166
1581
|
return ask(query)
|
|
1167
1582
|
|
|
1168
1583
|
|
|
1584
|
+
# Replace module in sys.modules with callable version
|
|
1169
1585
|
sys.modules[__name__].__class__ = CallableModule
|
|
1170
1586
|
|
|
1171
1587
|
|