deepagents-cli 0.0.1__py3-none-any.whl → 0.0.3__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 deepagents-cli might be problematic. Click here for more details.

@@ -1,741 +0,0 @@
1
- """Middleware that exposes local filesystem tools to an agent.
2
-
3
- This mirrors the structure of `FilesystemMiddleware` but operates on the
4
- host/local filesystem (disk) rather than the in-memory/mock filesystem.
5
- It ports the tool behavior from `src/deepagents/local_fs_tools.py` into
6
- middleware-provided tools so they can be injected via AgentMiddleware.
7
- """
8
- # ruff: noqa: E501
9
-
10
- from __future__ import annotations
11
-
12
- from collections.abc import Awaitable, Callable
13
- from typing import Any, Optional, Union
14
-
15
- import os
16
- import pathlib
17
- import re
18
- import subprocess
19
-
20
- from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse
21
- from langchain.tools.tool_node import ToolCallRequest
22
- from langchain_core.messages import ToolMessage
23
- from langgraph.types import Command
24
- from langgraph.config import get_config
25
- from langchain_core.tools import tool
26
- from deepagents.middleware.filesystem import (
27
- _create_file_data,
28
- _format_content_with_line_numbers,
29
- FilesystemState,
30
- FILESYSTEM_SYSTEM_PROMPT,
31
- )
32
- from deepagents.middleware.common import TOO_LARGE_TOOL_MSG, FILESYSTEM_SYSTEM_PROMPT_GLOB_GREP_SUPPLEMENT
33
-
34
- from deepagents.prompts import (
35
- EDIT_DESCRIPTION,
36
- TOOL_DESCRIPTION,
37
- GLOB_DESCRIPTION,
38
- GREP_DESCRIPTION,
39
- WRITE_DESCRIPTION,
40
- )
41
-
42
- LOCAL_LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the specified directory on disk.
43
-
44
- Usage:
45
- - The ls tool will return a list of all files in the specified directory.
46
- - The path parameter accepts both absolute paths (starting with /) and relative paths
47
- - Relative paths are resolved relative to the current working directory
48
- - This is very useful for exploring the file system and finding the right file to read or edit.
49
- - You should almost ALWAYS use this tool before using the Read or Edit tools."""
50
-
51
- LOCAL_READ_FILE_TOOL_DESCRIPTION = TOOL_DESCRIPTION + "\n- You should ALWAYS make sure a file has been read before editing it."
52
- LOCAL_EDIT_FILE_TOOL_DESCRIPTION = EDIT_DESCRIPTION
53
-
54
-
55
- # -----------------------------
56
- # Path Resolution Helper
57
- # -----------------------------
58
-
59
- def _resolve_path(path: str, cwd: str, long_term_memory: bool = False) -> str:
60
- """Resolve relative paths against CWD, leave absolute paths unchanged.
61
-
62
- Special handling: /memories/* paths are redirected to ~/.deepagents/<agent_name>/
63
- agent_name is retrieved from the runtime config. If agent_name is None, /memories
64
- paths will return an error. This only works if long_term_memory=True.
65
- """
66
- if path.startswith("/memories"):
67
- if not long_term_memory:
68
- raise ValueError(
69
- "Long-term memory is disabled. "
70
- "/memories/ access requires long_term_memory=True."
71
- )
72
-
73
- # Get agent_name from config
74
- config = get_config()
75
- agent_name = config.get("configurable", {}).get("agent_name") if config else None
76
-
77
- if agent_name is None:
78
- raise ValueError(
79
- "Memory access is disabled when no agent name is provided. "
80
- "To use /memories/, run with --agent <name> to enable memory features."
81
- )
82
-
83
- agent_dir = pathlib.Path.home() / ".deepagents" / agent_name
84
- if path == "/memories":
85
- return str(agent_dir)
86
- else:
87
- relative_part = path[len("/memories/"):]
88
- return str(agent_dir / relative_part)
89
-
90
- if os.path.isabs(path):
91
- return path
92
- return str(pathlib.Path(cwd) / path)
93
-
94
-
95
- # -----------------------------
96
- # Tool Implementations (Local)
97
- # -----------------------------
98
-
99
- def _ls_impl(path: str = ".", cwd: str | None = None, long_term_memory: bool = False) -> list[str]:
100
- """List all files in the specified directory on disk."""
101
- try:
102
- if cwd:
103
- path = _resolve_path(path, cwd, long_term_memory)
104
- path_obj = pathlib.Path(path)
105
- if not path_obj.exists():
106
- return [f"Error: Path '{path}' does not exist"]
107
- if not path_obj.is_dir():
108
- return [f"Error: Path '{path}' is not a directory"]
109
-
110
- items: list[str] = []
111
- for item in path_obj.iterdir():
112
- items.append(str(item.name))
113
- return sorted(items)
114
- except Exception as e: # pragma: no cover - defensive
115
- return [f"Error listing directory: {str(e)}"]
116
-
117
-
118
- def _read_file_impl(
119
- file_path: str,
120
- offset: int = 0,
121
- limit: int = 2000,
122
- cwd: str | None = None,
123
- long_term_memory: bool = False,
124
- ) -> str:
125
- """Read a file from the local filesystem and return cat -n formatted content."""
126
- try:
127
- if cwd:
128
- file_path = _resolve_path(file_path, cwd, long_term_memory)
129
- path_obj = pathlib.Path(file_path)
130
-
131
- if not path_obj.exists():
132
- return f"Error: File '{file_path}' not found"
133
- if not path_obj.is_file():
134
- return f"Error: '{file_path}' is not a file"
135
-
136
- try:
137
- with open(path_obj, "r", encoding="utf-8") as f:
138
- content = f.read()
139
- except UnicodeDecodeError:
140
- # Fallback to binary read and lenient decode
141
- with open(path_obj, "rb") as f:
142
- content = f.read().decode("utf-8", errors="ignore")
143
-
144
- if not content or content.strip() == "":
145
- return "System reminder: File exists but has empty contents"
146
-
147
- lines = content.splitlines()
148
- start_idx = offset
149
- end_idx = min(start_idx + limit, len(lines))
150
-
151
- if start_idx >= len(lines):
152
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
153
-
154
- result_lines = []
155
- for i in range(start_idx, end_idx):
156
- line_content = lines[i]
157
- if len(line_content) > 2000:
158
- line_content = line_content[:2000]
159
- result_lines.append(f"{i + 1:6d}\t{line_content}")
160
-
161
- return "\n".join(result_lines)
162
-
163
- except Exception as e: # pragma: no cover - defensive
164
- return f"Error reading file: {str(e)}"
165
-
166
-
167
- def _write_file_impl(
168
- file_path: str,
169
- content: str,
170
- cwd: str | None = None,
171
- long_term_memory: bool = False,
172
- ) -> str:
173
- """Write content to a file on the local filesystem (creates parents)."""
174
- try:
175
- if cwd:
176
- file_path = _resolve_path(file_path, cwd, long_term_memory)
177
- path_obj = pathlib.Path(file_path)
178
- path_obj.parent.mkdir(parents=True, exist_ok=True)
179
- with open(path_obj, "w", encoding="utf-8") as f:
180
- f.write(content)
181
- return f"Successfully wrote to file '{file_path}'"
182
- except Exception as e: # pragma: no cover - defensive
183
- return f"Error writing file: {str(e)}"
184
-
185
-
186
- def _edit_file_impl(
187
- file_path: str,
188
- old_string: str,
189
- new_string: str,
190
- replace_all: bool = False,
191
- cwd: str | None = None,
192
- long_term_memory: bool = False,
193
- ) -> str:
194
- """Edit a file on disk by replacing old_string with new_string."""
195
- try:
196
- if cwd:
197
- file_path = _resolve_path(file_path, cwd, long_term_memory)
198
- path_obj = pathlib.Path(file_path)
199
- if not path_obj.exists():
200
- return f"Error: File '{file_path}' not found"
201
- if not path_obj.is_file():
202
- return f"Error: '{file_path}' is not a file"
203
-
204
- try:
205
- with open(path_obj, "r", encoding="utf-8") as f:
206
- content = f.read()
207
- except UnicodeDecodeError:
208
- return f"Error: File '{file_path}' contains non-UTF-8 content"
209
-
210
- if old_string not in content:
211
- return f"Error: String not found in file: '{old_string}'"
212
-
213
- if not replace_all:
214
- occurrences = content.count(old_string)
215
- if occurrences > 1:
216
- return (
217
- f"Error: String '{old_string}' appears {occurrences} times in file. "
218
- "Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
219
- )
220
- elif occurrences == 0:
221
- return f"Error: String not found in file: '{old_string}'"
222
-
223
- if replace_all:
224
- new_content = content.replace(old_string, new_string)
225
- else:
226
- new_content = content.replace(old_string, new_string, 1)
227
-
228
- with open(path_obj, "w", encoding="utf-8") as f:
229
- f.write(new_content)
230
-
231
- if replace_all:
232
- replacement_count = content.count(old_string)
233
- return f"Successfully replaced {replacement_count} instance(s) of the string in '{file_path}'"
234
- return f"Successfully replaced string in '{file_path}'"
235
-
236
- except Exception as e: # pragma: no cover - defensive
237
- return f"Error editing file: {str(e)}"
238
-
239
-
240
- def _glob_impl(
241
- pattern: str,
242
- path: str = ".",
243
- max_results: int = 100,
244
- include_dirs: bool = False,
245
- recursive: bool = True,
246
- cwd: str | None = None,
247
- long_term_memory: bool = False,
248
- ) -> str:
249
- """Find files and directories using glob patterns on local filesystem."""
250
- try:
251
- if cwd:
252
- path = _resolve_path(path, cwd, long_term_memory)
253
- path_obj = pathlib.Path(path)
254
- if not path_obj.exists():
255
- return f"Error: Path '{path}' does not exist"
256
- if not path_obj.is_dir():
257
- return f"Error: Path '{path}' is not a directory"
258
-
259
- results: list[str] = []
260
- try:
261
- matches = path_obj.rglob(pattern) if recursive else path_obj.glob(pattern)
262
- for match in matches:
263
- if len(results) >= max_results:
264
- break
265
- if match.is_file():
266
- results.append(str(match))
267
- elif match.is_dir() and include_dirs:
268
- results.append(f"{match}/")
269
- results.sort()
270
- except Exception as e:
271
- return f"Error processing glob pattern: {str(e)}"
272
-
273
- if not results:
274
- search_type = "recursive" if recursive else "non-recursive"
275
- dirs_note = " (including directories)" if include_dirs else ""
276
- return f"No matches found for pattern '{pattern}' in '{path}' ({search_type} search{dirs_note})"
277
-
278
- header = f"Found {len(results)} matches for pattern '{pattern}'"
279
- if len(results) >= max_results:
280
- header += f" (limited to {max_results} results)"
281
- header += ":\n\n"
282
- return header + "\n".join(results)
283
-
284
- except Exception as e: # pragma: no cover - defensive
285
- return f"Error in glob search: {str(e)}"
286
-
287
-
288
- def _grep_impl(
289
- pattern: str,
290
- files: Optional[Union[str, list[str]]] = None,
291
- path: Optional[str] = None,
292
- file_pattern: str = "*",
293
- max_results: int = 50,
294
- case_sensitive: bool = False,
295
- context_lines: int = 0,
296
- regex: bool = False,
297
- cwd: str | None = None,
298
- long_term_memory: bool = False,
299
- ) -> str:
300
- """Search for text patterns within files using ripgrep on local filesystem."""
301
- try:
302
- if not files and not path:
303
- return "Error: Must provide either 'files' parameter or 'path' parameter"
304
-
305
- if cwd:
306
- if files:
307
- if isinstance(files, str):
308
- files = _resolve_path(files, cwd, long_term_memory)
309
- else:
310
- files = [_resolve_path(f, cwd, long_term_memory) for f in files]
311
- if path:
312
- path = _resolve_path(path, cwd, long_term_memory)
313
-
314
- cmd: list[str] = ["rg"]
315
- if regex:
316
- cmd.extend(["-e", pattern])
317
- else:
318
- cmd.extend(["-F", pattern])
319
-
320
- if not case_sensitive:
321
- cmd.append("-i")
322
-
323
- if context_lines > 0:
324
- cmd.extend(["-C", str(context_lines)])
325
-
326
- if max_results > 0:
327
- cmd.extend(["-m", str(max_results)])
328
-
329
- if file_pattern != "*":
330
- cmd.extend(["-g", file_pattern])
331
-
332
- if files:
333
- if isinstance(files, str):
334
- cmd.append(files)
335
- else:
336
- cmd.extend(files)
337
- elif path:
338
- cmd.append(path)
339
-
340
- try:
341
- result = subprocess.run(
342
- cmd,
343
- capture_output=True,
344
- text=True,
345
- timeout=30,
346
- cwd=path if path and os.path.isdir(path) else None,
347
- )
348
- if result.returncode == 0:
349
- return result.stdout
350
- elif result.returncode == 1:
351
- pattern_desc = f"regex pattern '{pattern}'" if regex else f"text '{pattern}'"
352
- case_desc = " (case-sensitive)" if case_sensitive else " (case-insensitive)"
353
- return f"No matches found for {pattern_desc}{case_desc}"
354
- else:
355
- return f"Error running ripgrep: {result.stderr}"
356
- except subprocess.TimeoutExpired:
357
- return "Error: ripgrep search timed out"
358
- except FileNotFoundError:
359
- return "Error: ripgrep (rg) not found. Please install ripgrep to use this tool."
360
- except Exception as e:
361
- return f"Error running ripgrep: {str(e)}"
362
-
363
- except Exception as e: # pragma: no cover - defensive
364
- return f"Error in grep search: {str(e)}"
365
-
366
-
367
- # --------------------------------
368
- # Middleware: LocalFilesystemMiddleware
369
- # --------------------------------
370
-
371
- LOCAL_FILESYSTEM_SYSTEM_PROMPT = FILESYSTEM_SYSTEM_PROMPT + "\n" + FILESYSTEM_SYSTEM_PROMPT_GLOB_GREP_SUPPLEMENT
372
-
373
- # Skills discovery paths
374
- STANDARD_SKILL_PATHS = [
375
- "~/.deepagents/skills",
376
- "./.deepagents/skills",
377
- ]
378
-
379
-
380
- def _get_local_filesystem_tools(custom_tool_descriptions: dict[str, str] | None = None, cwd: str | None = None, long_term_memory: bool = False):
381
- """Return tool instances for local filesystem operations.
382
-
383
- agent_name is retrieved from runtime config via get_config() when tools are called.
384
- """
385
- # We already decorated read/write/edit/glob/grep with @tool including descriptions
386
- # Only `ls` needs a manual wrapper to attach a description.
387
- ls_description = (
388
- custom_tool_descriptions.get("ls") if custom_tool_descriptions else LOCAL_LIST_FILES_TOOL_DESCRIPTION
389
- )
390
-
391
- @tool(description=ls_description)
392
- def ls(path: str = ".") -> list[str]: # noqa: D401 - simple wrapper
393
- """List all files in the specified directory."""
394
- return _ls_impl(path, cwd=cwd, long_term_memory=long_term_memory)
395
-
396
- read_desc = (
397
- (custom_tool_descriptions or {}).get("read_file", LOCAL_READ_FILE_TOOL_DESCRIPTION)
398
- )
399
-
400
- @tool(description=read_desc)
401
- def read_file(file_path: str, offset: int = 0, limit: int = 2000) -> str:
402
- return _read_file_impl(file_path, offset, limit, cwd=cwd, long_term_memory=long_term_memory)
403
-
404
- write_desc = (
405
- (custom_tool_descriptions or {}).get("write_file", WRITE_DESCRIPTION)
406
- )
407
-
408
- @tool(description=write_desc)
409
- def write_file(file_path: str, content: str) -> str:
410
- return _write_file_impl(file_path, content, cwd=cwd, long_term_memory=long_term_memory)
411
-
412
- edit_desc = (
413
- (custom_tool_descriptions or {}).get("edit_file", LOCAL_EDIT_FILE_TOOL_DESCRIPTION)
414
- )
415
-
416
- @tool(description=edit_desc)
417
- def edit_file(
418
- file_path: str,
419
- old_string: str,
420
- new_string: str,
421
- replace_all: bool = False,
422
- ) -> str:
423
- return _edit_file_impl(file_path, old_string, new_string, replace_all, cwd=cwd, long_term_memory=long_term_memory)
424
-
425
- glob_desc = (
426
- (custom_tool_descriptions or {}).get("glob", GLOB_DESCRIPTION)
427
- )
428
-
429
- @tool(description=glob_desc)
430
- def glob(
431
- pattern: str,
432
- path: str = ".",
433
- max_results: int = 100,
434
- include_dirs: bool = False,
435
- recursive: bool = True,
436
- ) -> str:
437
- return _glob_impl(pattern, path, max_results, include_dirs, recursive, cwd=cwd, long_term_memory=long_term_memory)
438
-
439
- grep_desc = (
440
- (custom_tool_descriptions or {}).get("grep", GREP_DESCRIPTION)
441
- )
442
-
443
- @tool(description=grep_desc)
444
- def grep(
445
- pattern: str,
446
- files: Optional[Union[str, list[str]]] = None,
447
- path: Optional[str] = None,
448
- file_pattern: str = "*",
449
- max_results: int = 50,
450
- case_sensitive: bool = False,
451
- context_lines: int = 0,
452
- regex: bool = False,
453
- ) -> str:
454
- return _grep_impl(
455
- pattern,
456
- files=files,
457
- path=path,
458
- file_pattern=file_pattern,
459
- max_results=max_results,
460
- case_sensitive=case_sensitive,
461
- context_lines=context_lines,
462
- regex=regex,
463
- cwd=cwd,
464
- long_term_memory=long_term_memory,
465
- )
466
-
467
- return [ls, read_file, write_file, edit_file, glob, grep]
468
-
469
-
470
- class LocalFilesystemMiddleware(AgentMiddleware):
471
- """Middleware that injects local filesystem tools into an agent.
472
-
473
- Tools added:
474
- - ls
475
- - read_file
476
- - write_file
477
- - edit_file
478
- - glob
479
- - grep
480
-
481
- Skills are automatically discovered from:
482
- - ~/.deepagents/skills/ (personal skills)
483
- - ./.deepagents/skills/ (project skills)
484
- """
485
-
486
- state_schema = FilesystemState
487
-
488
- def _check_ripgrep_installed(self) -> None:
489
- """Check if ripgrep (rg) is installed on the system.
490
-
491
- Raises:
492
- RuntimeError: If ripgrep is not found on the system.
493
- """
494
- try:
495
- subprocess.run(
496
- ["rg", "--version"],
497
- capture_output=True,
498
- timeout=5,
499
- check=False,
500
- )
501
- except FileNotFoundError:
502
- raise RuntimeError(
503
- "ripgrep (rg) is not installed. The grep tool requires ripgrep to function. "
504
- "Please install it from https://github.com/BurntSushi/ripgrep#installation"
505
- )
506
- except Exception as e:
507
- raise RuntimeError(f"Error checking for ripgrep installation: {str(e)}")
508
-
509
- def __init__(
510
- self,
511
- *,
512
- system_prompt: str | None = None,
513
- custom_tool_descriptions: dict[str, str] | None = None,
514
- tool_token_limit_before_evict: int | None = 20000,
515
- cwd: str | None = None,
516
- long_term_memory: bool = False,
517
- ) -> None:
518
- self.cwd = cwd or os.getcwd()
519
- self.long_term_memory = long_term_memory
520
-
521
- # Check if ripgrep is installed
522
- self._check_ripgrep_installed()
523
-
524
- # Discover skills from standard locations
525
- self.skills = self._discover_skills()
526
-
527
- # Build system prompt
528
- cwd_prompt = f"\n\nCurrent working directory: {self.cwd}\n\nWhen using filesystem tools (ls, read_file, write_file, edit_file, glob, grep), relative paths will be resolved relative to this directory."
529
-
530
- # Add long-term memory documentation if enabled
531
- memory_prompt = ""
532
- if long_term_memory:
533
- memory_prompt = "\n\n## Long-term Memory\n\nYou can access long-term memory storage at /memories/. Files stored here persist across sessions and are saved to ~/.deepagents/<agent_name>/. You must use --agent <name> to enable this feature."
534
-
535
- base_prompt = system_prompt or LOCAL_FILESYSTEM_SYSTEM_PROMPT
536
- skills_prompt = self._build_skills_prompt()
537
-
538
- self.system_prompt = base_prompt + cwd_prompt + memory_prompt + skills_prompt
539
- self.tools = _get_local_filesystem_tools(custom_tool_descriptions, cwd=self.cwd, long_term_memory=long_term_memory)
540
- self.tool_token_limit_before_evict = tool_token_limit_before_evict
541
-
542
- def _discover_skills(self) -> list[dict[str, str]]:
543
- """Discover skills from standard filesystem locations.
544
-
545
- Returns:
546
- List of skill metadata dictionaries with keys: name, path, description, source.
547
- """
548
- from deepagents.skills import parse_skill_frontmatter
549
-
550
- discovered = {}
551
-
552
- for base_path in STANDARD_SKILL_PATHS:
553
- # Expand ~ to home directory
554
- expanded_path = os.path.expanduser(base_path)
555
-
556
- # Resolve relative paths against cwd
557
- if not os.path.isabs(expanded_path):
558
- expanded_path = os.path.join(self.cwd, expanded_path)
559
-
560
- if not os.path.exists(expanded_path):
561
- continue
562
-
563
- # Find all SKILL.md files
564
- try:
565
- skill_files = _glob_impl(
566
- pattern="**/SKILL.md",
567
- path=expanded_path,
568
- max_results=1000,
569
- recursive=True,
570
- )
571
-
572
- # Parse the glob output (skip header line)
573
- if "Found" not in skill_files:
574
- continue
575
-
576
- lines = skill_files.split('\n')
577
- skill_paths = [line for line in lines[2:] if line.strip()] # Skip header and empty
578
-
579
- for skill_path in skill_paths:
580
- try:
581
- # Read SKILL.md file
582
- content = _read_file_impl(skill_path, cwd=None)
583
- if content.startswith("Error:"):
584
- continue
585
-
586
- # Remove line numbers from cat -n format
587
- content_lines = []
588
- for line in content.split('\n'):
589
- # Format is " 1\tcontent"
590
- if '\t' in line:
591
- content_lines.append(line.split('\t', 1)[1])
592
- actual_content = '\n'.join(content_lines)
593
-
594
- # Parse YAML frontmatter
595
- frontmatter = parse_skill_frontmatter(actual_content)
596
- if not frontmatter.get('name'):
597
- continue
598
-
599
- skill_name = frontmatter['name']
600
- source = "project" if "./.deepagents" in base_path else "personal"
601
-
602
- # Project skills override personal skills
603
- discovered[skill_name] = {
604
- "name": skill_name,
605
- "path": skill_path,
606
- "description": frontmatter.get('description', ''),
607
- "version": frontmatter.get('version', ''),
608
- "source": source,
609
- }
610
- except Exception:
611
- # Skip skills that fail to parse
612
- continue
613
-
614
- except Exception:
615
- # Skip paths that fail to glob
616
- continue
617
-
618
- return list(discovered.values())
619
-
620
- def _build_skills_prompt(self) -> str:
621
- """Build the skills section of the system prompt.
622
-
623
- Returns:
624
- System prompt text describing available skills, or empty string if no skills.
625
- """
626
- if not self.skills:
627
- return ""
628
-
629
- prompt = "\n\n## Available Skills\n\nYou have access to the following skills:"
630
-
631
- for i, skill in enumerate(self.skills, 1):
632
- prompt += f"\n\n{i}. **{skill['name']}** ({skill['path']})"
633
- if skill['description']:
634
- prompt += f"\n - {skill['description']}"
635
- prompt += f"\n - Source: {skill['source']}"
636
-
637
- prompt += "\n\nTo use a skill, read its SKILL.md file using `read_file`. Skills may contain additional resources in scripts/, references/, and assets/ subdirectories."
638
-
639
- return prompt
640
-
641
- def wrap_model_call(
642
- self,
643
- request: ModelRequest,
644
- handler: Callable[[ModelRequest], ModelResponse],
645
- ) -> ModelResponse:
646
- if self.system_prompt is not None:
647
- request.system_prompt = (
648
- request.system_prompt + "\n\n" + self.system_prompt
649
- if request.system_prompt
650
- else self.system_prompt
651
- )
652
- return handler(request)
653
-
654
- async def awrap_model_call(
655
- self,
656
- request: ModelRequest,
657
- handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
658
- ) -> ModelResponse:
659
- if self.system_prompt is not None:
660
- request.system_prompt = (
661
- request.system_prompt + "\n\n" + self.system_prompt
662
- if request.system_prompt
663
- else self.system_prompt
664
- )
665
- return await handler(request)
666
-
667
- # --------- Token-eviction to filesystem state (like FilesystemMiddleware) ---------
668
- def _intercept_large_tool_result(self, tool_result: ToolMessage | Command) -> ToolMessage | Command:
669
- if isinstance(tool_result, ToolMessage) and isinstance(tool_result.content, str):
670
- content = tool_result.content
671
- if self.tool_token_limit_before_evict and len(content) > 4 * self.tool_token_limit_before_evict:
672
- file_path = f"/large_tool_results/{tool_result.tool_call_id}"
673
- file_data = _create_file_data(content)
674
- state_update = {
675
- "messages": [
676
- ToolMessage(
677
- TOO_LARGE_TOOL_MSG.format(
678
- tool_call_id=tool_result.tool_call_id,
679
- file_path=file_path,
680
- content_sample=_format_content_with_line_numbers(
681
- file_data["content"][:10], format_style="tab", start_line=1
682
- ),
683
- ),
684
- tool_call_id=tool_result.tool_call_id,
685
- )
686
- ],
687
- "files": {file_path: file_data},
688
- }
689
- return Command(update=state_update)
690
- elif isinstance(tool_result, Command):
691
- update = tool_result.update
692
- if update is None:
693
- return tool_result
694
- message_updates = update.get("messages", [])
695
- file_updates = update.get("files", {})
696
-
697
- edited_message_updates = []
698
- for message in message_updates:
699
- if self.tool_token_limit_before_evict and isinstance(message, ToolMessage) and isinstance(message.content, str):
700
- content = message.content
701
- if len(content) > 4 * self.tool_token_limit_before_evict:
702
- file_path = f"/large_tool_results/{message.tool_call_id}"
703
- file_data = _create_file_data(content)
704
- edited_message_updates.append(
705
- ToolMessage(
706
- TOO_LARGE_TOOL_MSG.format(
707
- tool_call_id=message.tool_call_id,
708
- file_path=file_path,
709
- content_sample=_format_content_with_line_numbers(
710
- file_data["content"][:10], format_style="tab", start_line=1
711
- ),
712
- ),
713
- tool_call_id=message.tool_call_id,
714
- )
715
- )
716
- file_updates[file_path] = file_data
717
- continue
718
- edited_message_updates.append(message)
719
- return Command(update={**update, "messages": edited_message_updates, "files": file_updates})
720
- return tool_result
721
-
722
- def wrap_tool_call(
723
- self,
724
- request: ToolCallRequest,
725
- handler: Callable[[ToolCallRequest], ToolMessage | Command],
726
- ) -> ToolMessage | Command:
727
- # Skip eviction for local filesystem tools
728
- if self.tool_token_limit_before_evict is None or request.tool_call["name"] in {"ls", "read_file", "write_file", "edit_file", "glob", "grep"}:
729
- return handler(request)
730
- tool_result = handler(request)
731
- return self._intercept_large_tool_result(tool_result)
732
-
733
- async def awrap_tool_call(
734
- self,
735
- request: ToolCallRequest,
736
- handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
737
- ) -> ToolMessage | Command:
738
- if self.tool_token_limit_before_evict is None or request.tool_call["name"] in {"ls", "read_file", "write_file", "edit_file", "glob", "grep"}:
739
- return await handler(request)
740
- tool_result = await handler(request)
741
- return self._intercept_large_tool_result(tool_result)