emdash-core 0.1.37__py3-none-any.whl → 0.1.60__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.
Files changed (60) hide show
  1. emdash_core/agent/agents.py +9 -0
  2. emdash_core/agent/background.py +481 -0
  3. emdash_core/agent/inprocess_subagent.py +70 -1
  4. emdash_core/agent/mcp/config.py +78 -2
  5. emdash_core/agent/prompts/main_agent.py +53 -1
  6. emdash_core/agent/prompts/plan_mode.py +65 -44
  7. emdash_core/agent/prompts/subagents.py +73 -1
  8. emdash_core/agent/prompts/workflow.py +179 -28
  9. emdash_core/agent/providers/models.py +1 -1
  10. emdash_core/agent/providers/openai_provider.py +10 -0
  11. emdash_core/agent/research/researcher.py +154 -45
  12. emdash_core/agent/runner/agent_runner.py +145 -19
  13. emdash_core/agent/runner/sdk_runner.py +29 -2
  14. emdash_core/agent/skills.py +81 -1
  15. emdash_core/agent/toolkit.py +87 -11
  16. emdash_core/agent/tools/__init__.py +2 -0
  17. emdash_core/agent/tools/coding.py +344 -52
  18. emdash_core/agent/tools/lsp.py +361 -0
  19. emdash_core/agent/tools/skill.py +21 -1
  20. emdash_core/agent/tools/task.py +16 -19
  21. emdash_core/agent/tools/task_output.py +262 -32
  22. emdash_core/agent/verifier/__init__.py +11 -0
  23. emdash_core/agent/verifier/manager.py +295 -0
  24. emdash_core/agent/verifier/models.py +97 -0
  25. emdash_core/{swarm/worktree_manager.py → agent/worktree.py} +19 -1
  26. emdash_core/api/agent.py +297 -2
  27. emdash_core/api/research.py +3 -3
  28. emdash_core/api/router.py +0 -4
  29. emdash_core/context/longevity.py +197 -0
  30. emdash_core/context/providers/explored_areas.py +83 -39
  31. emdash_core/context/reranker.py +35 -144
  32. emdash_core/context/simple_reranker.py +500 -0
  33. emdash_core/context/tool_relevance.py +84 -0
  34. emdash_core/core/config.py +8 -0
  35. emdash_core/graph/__init__.py +8 -1
  36. emdash_core/graph/connection.py +24 -3
  37. emdash_core/graph/writer.py +7 -1
  38. emdash_core/models/agent.py +10 -0
  39. emdash_core/server.py +1 -6
  40. emdash_core/sse/stream.py +16 -1
  41. emdash_core/utils/__init__.py +0 -2
  42. emdash_core/utils/git.py +103 -0
  43. emdash_core/utils/image.py +147 -160
  44. {emdash_core-0.1.37.dist-info → emdash_core-0.1.60.dist-info}/METADATA +6 -6
  45. {emdash_core-0.1.37.dist-info → emdash_core-0.1.60.dist-info}/RECORD +47 -52
  46. emdash_core/api/swarm.py +0 -223
  47. emdash_core/db/__init__.py +0 -67
  48. emdash_core/db/auth.py +0 -134
  49. emdash_core/db/models.py +0 -91
  50. emdash_core/db/provider.py +0 -222
  51. emdash_core/db/providers/__init__.py +0 -5
  52. emdash_core/db/providers/supabase.py +0 -452
  53. emdash_core/swarm/__init__.py +0 -17
  54. emdash_core/swarm/merge_agent.py +0 -383
  55. emdash_core/swarm/session_manager.py +0 -274
  56. emdash_core/swarm/swarm_runner.py +0 -226
  57. emdash_core/swarm/task_definition.py +0 -137
  58. emdash_core/swarm/worker_spawner.py +0 -319
  59. {emdash_core-0.1.37.dist-info → emdash_core-0.1.60.dist-info}/WHEEL +0 -0
  60. {emdash_core-0.1.37.dist-info → emdash_core-0.1.60.dist-info}/entry_points.txt +0 -0
@@ -81,6 +81,9 @@ class CustomAgent:
81
81
  system_prompt: Custom system prompt
82
82
  tools: List of tools to enable
83
83
  mcp_servers: MCP server configurations for this agent
84
+ rules: List of rule names to apply (references .emdash/rules/)
85
+ skills: List of skill names to enable (references .emdash/skills/)
86
+ verifiers: List of verifier names to use (references .emdash/verifiers.json)
84
87
  examples: Example interactions
85
88
  file_path: Source file path
86
89
  """
@@ -91,6 +94,9 @@ class CustomAgent:
91
94
  system_prompt: str = ""
92
95
  tools: list[str] = field(default_factory=list)
93
96
  mcp_servers: list[AgentMCPServerConfig] = field(default_factory=list)
97
+ rules: list[str] = field(default_factory=list)
98
+ skills: list[str] = field(default_factory=list)
99
+ verifiers: list[str] = field(default_factory=list)
94
100
  examples: list[dict] = field(default_factory=list)
95
101
  file_path: Optional[Path] = None
96
102
 
@@ -200,6 +206,9 @@ def _parse_agent_file(file_path: Path) -> Optional[CustomAgent]:
200
206
  system_prompt=system_prompt,
201
207
  tools=frontmatter.get("tools", []),
202
208
  mcp_servers=mcp_servers,
209
+ rules=frontmatter.get("rules", []),
210
+ skills=frontmatter.get("skills", []),
211
+ verifiers=frontmatter.get("verifiers", []),
203
212
  examples=examples,
204
213
  file_path=file_path,
205
214
  )
@@ -0,0 +1,481 @@
1
+ """Background task management for shell commands and sub-agents.
2
+
3
+ This module provides a centralized manager for tracking background tasks,
4
+ checking for completions, and generating notifications to inject into
5
+ the agent's context.
6
+
7
+ Inspired by Claude Code's background task system with notification-based
8
+ completion handling.
9
+ """
10
+
11
+ import subprocess
12
+ import threading
13
+ import time
14
+ import uuid
15
+ from concurrent.futures import Future, ThreadPoolExecutor
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Optional
20
+
21
+ from ..utils.logger import log
22
+
23
+
24
+ class TaskType(Enum):
25
+ """Type of background task."""
26
+ SHELL = "shell"
27
+ SUBAGENT = "subagent"
28
+
29
+
30
+ class TaskStatus(Enum):
31
+ """Status of a background task."""
32
+ RUNNING = "running"
33
+ COMPLETED = "completed"
34
+ FAILED = "failed"
35
+ KILLED = "killed"
36
+
37
+
38
+ @dataclass
39
+ class BackgroundTask:
40
+ """Represents a background task (shell command or sub-agent)."""
41
+
42
+ task_id: str
43
+ task_type: TaskType
44
+ description: str
45
+ started_at: float = field(default_factory=time.time)
46
+
47
+ # Shell-specific
48
+ process: Optional[subprocess.Popen] = None
49
+ command: Optional[str] = None
50
+
51
+ # Sub-agent specific
52
+ future: Optional[Future] = None
53
+ agent_type: Optional[str] = None
54
+
55
+ # Output capture
56
+ output_file: Optional[Path] = None
57
+ stdout: str = ""
58
+ stderr: str = ""
59
+
60
+ # Status
61
+ status: TaskStatus = TaskStatus.RUNNING
62
+ exit_code: Optional[int] = None
63
+ result: Optional[Any] = None
64
+ error: Optional[str] = None
65
+ completed_at: Optional[float] = None
66
+
67
+ # Whether the agent has been notified of completion
68
+ notified: bool = False
69
+
70
+ def to_dict(self) -> dict:
71
+ """Convert to dictionary for serialization."""
72
+ return {
73
+ "task_id": self.task_id,
74
+ "task_type": self.task_type.value,
75
+ "description": self.description,
76
+ "status": self.status.value,
77
+ "exit_code": self.exit_code,
78
+ "started_at": self.started_at,
79
+ "completed_at": self.completed_at,
80
+ "command": self.command,
81
+ "agent_type": self.agent_type,
82
+ "stdout": self.stdout[-5000:] if self.stdout else "",
83
+ "stderr": self.stderr[-2000:] if self.stderr else "",
84
+ "error": self.error,
85
+ }
86
+
87
+
88
+ class BackgroundTaskManager:
89
+ """Manages background tasks with notification-based completion.
90
+
91
+ This is a singleton that tracks all background tasks (shell commands
92
+ and sub-agents), monitors their completion, and provides notifications
93
+ to inject into the agent's context.
94
+
95
+ Usage:
96
+ manager = BackgroundTaskManager.get_instance()
97
+
98
+ # Start a shell command
99
+ task_id = manager.start_shell("npm test", description="Run tests")
100
+
101
+ # Check for completed tasks to notify agent
102
+ notifications = manager.get_pending_notifications()
103
+
104
+ # Get task status
105
+ task = manager.get_task(task_id)
106
+
107
+ # Kill a task
108
+ manager.kill_task(task_id)
109
+ """
110
+
111
+ _instance: Optional["BackgroundTaskManager"] = None
112
+ _lock = threading.Lock()
113
+
114
+ def __init__(self):
115
+ """Initialize the manager."""
116
+ self._tasks: dict[str, BackgroundTask] = {}
117
+ self._executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix="bg-task-")
118
+ self._monitor_thread: Optional[threading.Thread] = None
119
+ self._stop_monitor = threading.Event()
120
+ self._start_monitor()
121
+
122
+ @classmethod
123
+ def get_instance(cls) -> "BackgroundTaskManager":
124
+ """Get the singleton instance."""
125
+ if cls._instance is None:
126
+ with cls._lock:
127
+ if cls._instance is None:
128
+ cls._instance = cls()
129
+ return cls._instance
130
+
131
+ @classmethod
132
+ def reset_instance(cls) -> None:
133
+ """Reset the singleton (for testing)."""
134
+ with cls._lock:
135
+ if cls._instance is not None:
136
+ cls._instance.shutdown()
137
+ cls._instance = None
138
+
139
+ def _start_monitor(self) -> None:
140
+ """Start the background monitor thread."""
141
+ if self._monitor_thread is not None and self._monitor_thread.is_alive():
142
+ return
143
+
144
+ self._stop_monitor.clear()
145
+ self._monitor_thread = threading.Thread(
146
+ target=self._monitor_loop,
147
+ name="bg-task-monitor",
148
+ daemon=True,
149
+ )
150
+ self._monitor_thread.start()
151
+
152
+ def _monitor_loop(self) -> None:
153
+ """Monitor running tasks for completion."""
154
+ while not self._stop_monitor.is_set():
155
+ try:
156
+ self._check_tasks()
157
+ except Exception as e:
158
+ log.warning(f"Error in background task monitor: {e}")
159
+
160
+ # Check every 500ms
161
+ self._stop_monitor.wait(0.5)
162
+
163
+ def _check_tasks(self) -> None:
164
+ """Check all running tasks for completion."""
165
+ for task in list(self._tasks.values()):
166
+ if task.status != TaskStatus.RUNNING:
167
+ continue
168
+
169
+ if task.task_type == TaskType.SHELL:
170
+ self._check_shell_task(task)
171
+ elif task.task_type == TaskType.SUBAGENT:
172
+ self._check_subagent_task(task)
173
+
174
+ def _check_shell_task(self, task: BackgroundTask) -> None:
175
+ """Check if a shell task has completed."""
176
+ if task.process is None:
177
+ return
178
+
179
+ poll = task.process.poll()
180
+ if poll is not None:
181
+ # Process completed
182
+ task.exit_code = poll
183
+ task.completed_at = time.time()
184
+
185
+ # Capture any remaining output
186
+ try:
187
+ stdout, stderr = task.process.communicate(timeout=1)
188
+ task.stdout += stdout
189
+ task.stderr += stderr
190
+ except Exception:
191
+ pass
192
+
193
+ if poll == 0:
194
+ task.status = TaskStatus.COMPLETED
195
+ else:
196
+ task.status = TaskStatus.FAILED
197
+ task.error = f"Command exited with code {poll}"
198
+
199
+ log.info(f"Shell task {task.task_id} completed with exit code {poll}")
200
+
201
+ def _check_subagent_task(self, task: BackgroundTask) -> None:
202
+ """Check if a sub-agent task has completed."""
203
+ if task.future is None:
204
+ return
205
+
206
+ if task.future.done():
207
+ task.completed_at = time.time()
208
+
209
+ try:
210
+ result = task.future.result(timeout=0)
211
+ task.result = result
212
+ task.status = TaskStatus.COMPLETED
213
+ task.exit_code = 0
214
+ log.info(f"Sub-agent task {task.task_id} completed successfully")
215
+ except Exception as e:
216
+ task.status = TaskStatus.FAILED
217
+ task.error = str(e)
218
+ task.exit_code = 1
219
+ log.warning(f"Sub-agent task {task.task_id} failed: {e}")
220
+
221
+ def start_shell(
222
+ self,
223
+ command: str,
224
+ description: str = "",
225
+ cwd: Optional[Path] = None,
226
+ timeout: Optional[int] = None,
227
+ ) -> str:
228
+ """Start a shell command in the background.
229
+
230
+ Args:
231
+ command: Shell command to execute
232
+ description: Human-readable description
233
+ cwd: Working directory (defaults to current)
234
+ timeout: Optional timeout in seconds (not enforced, just metadata)
235
+
236
+ Returns:
237
+ Task ID for tracking
238
+ """
239
+ task_id = f"shell_{uuid.uuid4().hex[:8]}"
240
+
241
+ log.info(f"Starting background shell task {task_id}: {command[:50]}...")
242
+
243
+ # Start process with pipes for output capture
244
+ process = subprocess.Popen(
245
+ command,
246
+ shell=True,
247
+ stdout=subprocess.PIPE,
248
+ stderr=subprocess.PIPE,
249
+ text=True,
250
+ cwd=cwd,
251
+ )
252
+
253
+ task = BackgroundTask(
254
+ task_id=task_id,
255
+ task_type=TaskType.SHELL,
256
+ description=description or command[:50],
257
+ process=process,
258
+ command=command,
259
+ )
260
+
261
+ # Start output reader threads
262
+ self._start_output_reader(task)
263
+
264
+ self._tasks[task_id] = task
265
+ return task_id
266
+
267
+ def _start_output_reader(self, task: BackgroundTask) -> None:
268
+ """Start threads to read stdout/stderr without blocking."""
269
+ def read_stream(stream, attr_name):
270
+ try:
271
+ for line in stream:
272
+ current = getattr(task, attr_name)
273
+ setattr(task, attr_name, current + line)
274
+ except Exception:
275
+ pass
276
+
277
+ if task.process and task.process.stdout:
278
+ threading.Thread(
279
+ target=read_stream,
280
+ args=(task.process.stdout, "stdout"),
281
+ daemon=True,
282
+ ).start()
283
+
284
+ if task.process and task.process.stderr:
285
+ threading.Thread(
286
+ target=read_stream,
287
+ args=(task.process.stderr, "stderr"),
288
+ daemon=True,
289
+ ).start()
290
+
291
+ def start_subagent(
292
+ self,
293
+ future: Future,
294
+ agent_type: str,
295
+ description: str = "",
296
+ ) -> str:
297
+ """Register a sub-agent task for tracking.
298
+
299
+ Args:
300
+ future: Future from async sub-agent execution
301
+ agent_type: Type of sub-agent (Explore, Plan, etc.)
302
+ description: Human-readable description
303
+
304
+ Returns:
305
+ Task ID for tracking
306
+ """
307
+ task_id = f"agent_{uuid.uuid4().hex[:8]}"
308
+
309
+ log.info(f"Registering background sub-agent {task_id}: {agent_type}")
310
+
311
+ task = BackgroundTask(
312
+ task_id=task_id,
313
+ task_type=TaskType.SUBAGENT,
314
+ description=description,
315
+ future=future,
316
+ agent_type=agent_type,
317
+ )
318
+
319
+ self._tasks[task_id] = task
320
+ return task_id
321
+
322
+ def get_task(self, task_id: str) -> Optional[BackgroundTask]:
323
+ """Get a task by ID."""
324
+ return self._tasks.get(task_id)
325
+
326
+ def get_all_tasks(self) -> list[BackgroundTask]:
327
+ """Get all tasks."""
328
+ return list(self._tasks.values())
329
+
330
+ def get_running_tasks(self) -> list[BackgroundTask]:
331
+ """Get all currently running tasks."""
332
+ return [t for t in self._tasks.values() if t.status == TaskStatus.RUNNING]
333
+
334
+ def get_pending_notifications(self) -> list[BackgroundTask]:
335
+ """Get completed tasks that haven't been notified yet.
336
+
337
+ Returns:
338
+ List of tasks that completed since last check.
339
+ Marks them as notified so they won't be returned again.
340
+ """
341
+ notifications = []
342
+
343
+ for task in self._tasks.values():
344
+ if task.status != TaskStatus.RUNNING and not task.notified:
345
+ notifications.append(task)
346
+ task.notified = True
347
+
348
+ return notifications
349
+
350
+ def format_notification(self, task: BackgroundTask) -> str:
351
+ """Format a task completion as a notification message.
352
+
353
+ Args:
354
+ task: Completed task
355
+
356
+ Returns:
357
+ Formatted notification string for injection into context
358
+ """
359
+ status_str = "completed successfully" if task.status == TaskStatus.COMPLETED else "failed"
360
+
361
+ if task.task_type == TaskType.SHELL:
362
+ msg = f"[Background shell task {task.task_id} {status_str}]"
363
+ msg += f"\nCommand: {task.command}"
364
+ msg += f"\nExit code: {task.exit_code}"
365
+
366
+ if task.stdout:
367
+ # Truncate long output
368
+ stdout = task.stdout[-3000:] if len(task.stdout) > 3000 else task.stdout
369
+ if len(task.stdout) > 3000:
370
+ stdout = "...(truncated)\n" + stdout
371
+ msg += f"\n\nStdout:\n{stdout}"
372
+
373
+ if task.stderr:
374
+ stderr = task.stderr[-1500:] if len(task.stderr) > 1500 else task.stderr
375
+ if len(task.stderr) > 1500:
376
+ stderr = "...(truncated)\n" + stderr
377
+ msg += f"\n\nStderr:\n{stderr}"
378
+
379
+ else: # SUBAGENT
380
+ msg = f"[Background sub-agent {task.task_id} ({task.agent_type}) {status_str}]"
381
+
382
+ if task.error:
383
+ msg += f"\nError: {task.error}"
384
+ elif task.result:
385
+ # Include summary from sub-agent result
386
+ if hasattr(task.result, "summary"):
387
+ msg += f"\n\nSummary:\n{task.result.summary}"
388
+ elif isinstance(task.result, dict) and "summary" in task.result:
389
+ msg += f"\n\nSummary:\n{task.result['summary']}"
390
+
391
+ msg += f"\n\nUse task_output(task_id='{task.task_id}') for full details."
392
+
393
+ return msg
394
+
395
+ def kill_task(self, task_id: str) -> bool:
396
+ """Kill a running task.
397
+
398
+ Args:
399
+ task_id: Task to kill
400
+
401
+ Returns:
402
+ True if task was killed, False if not found or already completed
403
+ """
404
+ task = self._tasks.get(task_id)
405
+ if task is None:
406
+ return False
407
+
408
+ if task.status != TaskStatus.RUNNING:
409
+ return False
410
+
411
+ log.info(f"Killing background task {task_id}")
412
+
413
+ if task.task_type == TaskType.SHELL and task.process:
414
+ try:
415
+ task.process.terminate()
416
+ # Give it a moment to terminate gracefully
417
+ try:
418
+ task.process.wait(timeout=2)
419
+ except subprocess.TimeoutExpired:
420
+ task.process.kill()
421
+
422
+ task.status = TaskStatus.KILLED
423
+ task.exit_code = -15 # SIGTERM
424
+ task.completed_at = time.time()
425
+ return True
426
+ except Exception as e:
427
+ log.warning(f"Failed to kill shell task {task_id}: {e}")
428
+ return False
429
+
430
+ elif task.task_type == TaskType.SUBAGENT and task.future:
431
+ # Can't really kill a future, but we can mark it
432
+ task.future.cancel()
433
+ task.status = TaskStatus.KILLED
434
+ task.completed_at = time.time()
435
+ return True
436
+
437
+ return False
438
+
439
+ def cleanup_old_tasks(self, max_age_seconds: int = 3600) -> int:
440
+ """Remove old completed tasks.
441
+
442
+ Args:
443
+ max_age_seconds: Remove tasks older than this (default 1 hour)
444
+
445
+ Returns:
446
+ Number of tasks removed
447
+ """
448
+ now = time.time()
449
+ to_remove = []
450
+
451
+ for task_id, task in self._tasks.items():
452
+ if task.status == TaskStatus.RUNNING:
453
+ continue
454
+
455
+ if task.completed_at and (now - task.completed_at) > max_age_seconds:
456
+ to_remove.append(task_id)
457
+
458
+ for task_id in to_remove:
459
+ del self._tasks[task_id]
460
+
461
+ if to_remove:
462
+ log.debug(f"Cleaned up {len(to_remove)} old background tasks")
463
+
464
+ return len(to_remove)
465
+
466
+ def shutdown(self) -> None:
467
+ """Shutdown the manager and clean up resources."""
468
+ log.info("Shutting down BackgroundTaskManager")
469
+
470
+ # Stop monitor thread
471
+ self._stop_monitor.set()
472
+ if self._monitor_thread:
473
+ self._monitor_thread.join(timeout=2)
474
+
475
+ # Kill any running tasks
476
+ for task in list(self._tasks.values()):
477
+ if task.status == TaskStatus.RUNNING:
478
+ self.kill_task(task.task_id)
479
+
480
+ # Shutdown executor
481
+ self._executor.shutdown(wait=False)
@@ -39,8 +39,13 @@ class SubAgentResult:
39
39
  iterations: int
40
40
  tools_used: list[str]
41
41
  execution_time: float
42
+ exploration_steps: list[dict] = None # Detailed exploration steps
42
43
  error: Optional[str] = None
43
44
 
45
+ def __post_init__(self):
46
+ if self.exploration_steps is None:
47
+ self.exploration_steps = []
48
+
44
49
  def to_dict(self) -> dict:
45
50
  return asdict(self)
46
51
 
@@ -102,6 +107,7 @@ class InProcessSubAgent:
102
107
  # Tracking
103
108
  self.files_explored: set[str] = set()
104
109
  self.tools_used: list[str] = []
110
+ self.exploration_steps: list[dict] = [] # Detailed step tracking
105
111
 
106
112
  def _inject_thoroughness(self, prompt: str) -> str:
107
113
  """Inject thoroughness level into the system prompt."""
@@ -146,6 +152,7 @@ class InProcessSubAgent:
146
152
  event_map = {
147
153
  "tool_start": EventType.TOOL_START,
148
154
  "tool_result": EventType.TOOL_RESULT,
155
+ "thinking": EventType.THINKING,
149
156
  }
150
157
 
151
158
  if event_type in event_map:
@@ -279,9 +286,11 @@ Now, your task:
279
286
  if assistant_msg:
280
287
  messages.append(assistant_msg)
281
288
 
282
- # Save content
289
+ # Save content and emit thinking event
283
290
  if response.content:
284
291
  last_content = response.content
292
+ # Emit thinking event for UI
293
+ self._emit("thinking", content=response.content)
285
294
 
286
295
  # Check if done
287
296
  if not response.tool_calls:
@@ -316,6 +325,15 @@ Now, your task:
316
325
  summary=summary,
317
326
  )
318
327
 
328
+ # Track exploration step with details
329
+ step = {
330
+ "tool": tool_call.name,
331
+ "params": self._sanitize_params(args),
332
+ "success": result.success,
333
+ "summary": self._extract_result_summary(tool_call.name, args, result),
334
+ }
335
+ self.exploration_steps.append(step)
336
+
319
337
  # Add tool result to messages (truncated to avoid context overflow)
320
338
  tool_output = json.dumps(result.to_dict(), indent=2)
321
339
  tool_output = truncate_tool_output(tool_output, max_tokens=15000)
@@ -351,6 +369,7 @@ Now, your task:
351
369
  iterations=iterations,
352
370
  tools_used=list(set(self.tools_used)),
353
371
  execution_time=execution_time,
372
+ exploration_steps=self.exploration_steps[-30:], # Last 30 steps
354
373
  error=error,
355
374
  )
356
375
 
@@ -367,6 +386,56 @@ Now, your task:
367
386
  pass
368
387
  return findings[-10:]
369
388
 
389
+ def _sanitize_params(self, args: dict) -> dict:
390
+ """Sanitize params for logging - truncate long values."""
391
+ sanitized = {}
392
+ for key, value in args.items():
393
+ if isinstance(value, str) and len(value) > 200:
394
+ sanitized[key] = value[:200] + "..."
395
+ else:
396
+ sanitized[key] = value
397
+ return sanitized
398
+
399
+ def _extract_result_summary(self, tool_name: str, args: dict, result) -> str:
400
+ """Extract a meaningful summary from tool result based on tool type."""
401
+ if not result.success:
402
+ return f"Failed: {result.error or 'unknown error'}"
403
+
404
+ data = result.data or {}
405
+
406
+ # Tool-specific summaries
407
+ if tool_name == "read_file":
408
+ path = args.get("path", args.get("file_path", ""))
409
+ lines = data.get("line_count", data.get("lines", "?"))
410
+ return f"Read {path} ({lines} lines)"
411
+
412
+ elif tool_name == "glob":
413
+ matches = data.get("matches", data.get("files", []))
414
+ pattern = args.get("pattern", "")
415
+ return f"Found {len(matches)} files matching '{pattern}'"
416
+
417
+ elif tool_name == "grep":
418
+ matches = data.get("matches", [])
419
+ pattern = args.get("pattern", "")
420
+ return f"Found {len(matches)} matches for '{pattern}'"
421
+
422
+ elif tool_name == "semantic_search":
423
+ results = data.get("results", [])
424
+ query = args.get("query", "")[:50]
425
+ return f"Found {len(results)} results for '{query}'"
426
+
427
+ elif tool_name == "list_files":
428
+ files = data.get("files", data.get("entries", []))
429
+ path = args.get("path", "")
430
+ return f"Listed {len(files)} items in {path}"
431
+
432
+ else:
433
+ # Generic summary
434
+ if isinstance(data, dict):
435
+ keys = list(data.keys())[:3]
436
+ return f"Returned: {', '.join(keys)}" if keys else "Success"
437
+ return str(data)[:100] if data else "Success"
438
+
370
439
 
371
440
  # Thread pool for parallel execution
372
441
  _executor: Optional[ThreadPoolExecutor] = None