deer-agent-framework 0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. deer/__init__.py +36 -0
  2. deer/builtins/__init__.py +7 -0
  3. deer/builtins/python_manager/agent.py +29 -0
  4. deer/builtins/python_manager/tools.py +54 -0
  5. deer/core/__init__.py +1 -0
  6. deer/core/agent.py +463 -0
  7. deer/core/ui.py +31 -0
  8. deer/drivers/__init__.py +66 -0
  9. deer/drivers/base_driver.py +56 -0
  10. deer/drivers/gemini_driver.py +62 -0
  11. deer/drivers/ollama_driver.py +69 -0
  12. deer/executor/__init__.py +1 -0
  13. deer/executor/executor.py +168 -0
  14. deer/executor/logic.py +75 -0
  15. deer/executor/logic_secure.py +102 -0
  16. deer/main.py +71 -0
  17. deer/planner/__init__.py +1 -0
  18. deer/planner/planner.py +71 -0
  19. deer/prompts/__init__.py +6 -0
  20. deer/prompts/error_explain.py +30 -0
  21. deer/prompts/goal_improvement.py +65 -0
  22. deer/prompts/goal_validation.py +31 -0
  23. deer/prompts/humanizer.py +20 -0
  24. deer/prompts/planner.py +92 -0
  25. deer/prompts/response_improvement.py +23 -0
  26. deer/schema/__init__.py +2 -0
  27. deer/schema/io.py +40 -0
  28. deer/schema/plan.py +75 -0
  29. deer/tools/__init__.py +4 -0
  30. deer/tools/base.py +141 -0
  31. deer/tools/builtin/__init__.py +3 -0
  32. deer/tools/builtin/file_manager.py +136 -0
  33. deer/tools/builtin/git_manager.py +67 -0
  34. deer/tools/builtin/search_manager.py +123 -0
  35. deer/tools/decorators.py +127 -0
  36. deer/tools/registry.py +114 -0
  37. deer/tracing/__init__.py +2 -0
  38. deer/tracing/logging_config.py +20 -0
  39. deer/tracing/store.py +21 -0
  40. deer/utils/__init__.py +0 -0
  41. deer/utils/console.py +11 -0
  42. deer/utils/plots/__init__.py +7 -0
  43. deer/utils/plots/plot_traces.py +1034 -0
  44. deer/validator/__init__.py +1 -0
  45. deer/validator/plan_validator.py +23 -0
  46. deer/validator/rules.py +98 -0
  47. deer_agent_framework-0.0.dist-info/METADATA +163 -0
  48. deer_agent_framework-0.0.dist-info/RECORD +52 -0
  49. deer_agent_framework-0.0.dist-info/WHEEL +5 -0
  50. deer_agent_framework-0.0.dist-info/entry_points.txt +2 -0
  51. deer_agent_framework-0.0.dist-info/licenses/LICENSE +24 -0
  52. deer_agent_framework-0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ ERROR_EXPLAIN_PROMPT = """
2
+ Execution error:
3
+ {error}
4
+
5
+ Available tools:
6
+ {tools}
7
+
8
+ Analysis task:
9
+ Determine whether the failure was caused by one of the following:
10
+
11
+ 1. Missing capability
12
+ - The required operation cannot be performed because no suitable tool exists.
13
+
14
+ 2. Incorrect tool selection
15
+ - A valid tool exists, but the wrong tool was selected.
16
+
17
+ 3. Invalid tool usage
18
+ - A valid tool exists, but it was invoked with invalid arguments, invalid sequencing, or incompatible data.
19
+
20
+ 4. Logic or execution failure
21
+ - The failure originated from executable logic, runtime behavior, or tool-side execution.
22
+
23
+ Requirements:
24
+ - Analyze the error using ONLY the available tools listed above.
25
+ - Do NOT invent capabilities that are not explicitly available.
26
+ - Explicitly state whether the system lacks the required tooling to complete the task.
27
+ - If a missing capability is detected, identify the exact missing operation.
28
+ - If a valid tool exists, identify which tool should have been used and why.
29
+ - Keep the explanation concise, deterministic, and technical.
30
+ """
@@ -0,0 +1,65 @@
1
+ GOAL_IMPROVEMENT_PROMPT = """
2
+ System role:
3
+ You are a deterministic prompt optimizer.
4
+
5
+ Your task is to improve the user's goal prompt while preserving:
6
+ - original intent,
7
+ - semantic meaning,
8
+ - requested outcome,
9
+ - operational constraints,
10
+ - and domain context.
11
+
12
+ Do not change the task itself.
13
+
14
+ The optimization must:
15
+ - increase clarity,
16
+ - reduce ambiguity,
17
+ - improve structural consistency,
18
+ - improve determinism,
19
+ - improve executability,
20
+ - and remove unnecessary wording.
21
+
22
+ Do NOT:
23
+ - introduce new requirements,
24
+ - invent assumptions,
25
+ - expand scope,
26
+ - add explanations,
27
+ - add conversational filler,
28
+ - or transform concise prompts into verbose prompts.
29
+
30
+ Prefer:
31
+ - explicit instructions,
32
+ - deterministic wording,
33
+ - structured constraints,
34
+ - normalized terminology,
35
+ - and unambiguous execution semantics.
36
+
37
+ Agent identity:
38
+ {identity}
39
+
40
+ The optimized prompt must reflect the agent identity when selecting:
41
+ - terminology,
42
+ - tools,
43
+ - transformations,
44
+ - formatting,
45
+ - assumptions,
46
+ - domain-specific reasoning,
47
+ - and response structure.
48
+
49
+ The identity provides domain expertise, but does not override deterministic execution rules.
50
+
51
+ User goal:
52
+ {goal}
53
+
54
+ Runtime context (Payload):
55
+ {payload}
56
+ (Use the context above, including 'chat_history' if present, to resolve ambiguities, pronouns, or references to previous actions in the 'User goal').
57
+
58
+ Output rules:
59
+ - Return ONLY the optimized prompt.
60
+ - Do not explain changes.
61
+ - Do not use markdown.
62
+ - Preserve the original language of the user goal.
63
+ - Ensure the optimized goal is self-contained and explicitly mentions any files, entities, or values resolved from the context.
64
+
65
+ """
@@ -0,0 +1,31 @@
1
+ GOAL_VERIFIER_PROMPT = """
2
+ Your ONLY task is to RETRIEVE EVIDENCE that can be used to verify whether the already-executed result satisfies the reference goal.
3
+
4
+ Reference goal:
5
+ {goal}
6
+
7
+ Rules:
8
+ 1. The reference goal is NOT an instruction to execute.
9
+ 2. Do NOT perform, repeat, fix, create, write, delete, or modify anything from the reference goal.
10
+ 3. Use only available non-state-modifying tools to inspect existing state and retrieve evidence.
11
+ 4. Return raw evidence only.
12
+ 5. If evidence cannot be retrieved with the available read-only tools, return a concise statement that verification evidence is unavailable.
13
+ """
14
+
15
+ VERIFIER_JUDGE_PROMPT = """
16
+ User Goal: {goal}
17
+
18
+ Verification Evidence (Actual State):
19
+ {evidence}
20
+
21
+ Task:
22
+ Analyze the Verification Evidence strictly. Does it prove that the User Goal was successfully completed?
23
+
24
+ Rules:
25
+ - Respond ONLY with JSON.
26
+ - If the evidence shows the goal is NOT met, respond with is_success: false.
27
+ - If the evidence is missing or shows an error, respond with is_success: false.
28
+
29
+ Response format:
30
+ {{"is_success": bool, "feedback": "string explaining why"}}
31
+ """
@@ -0,0 +1,20 @@
1
+ HUMANIZER_PROMPT = """
2
+ You are a deterministic response synthesizer.
3
+ Your goal is to transform technical execution results into a natural, concise, and helpful response for the user.
4
+
5
+ Context:
6
+ User Goal: {goal}
7
+ Execution Trace: {trace}
8
+ Final Technical Result: {result}
9
+
10
+ Rules:
11
+ - Summarize what was accomplished based on the trace.
12
+ - If a file was created or modified, mention it.
13
+ - If information was retrieved, present it clearly.
14
+ - Keep the tone professional and direct.
15
+ - Use the same language as the User Goal.
16
+ - Do not explain the internal steps (s1, s2...) unless necessary for clarity.
17
+ - Output ONLY the final humanized response.
18
+
19
+ Response:
20
+ """
@@ -0,0 +1,92 @@
1
+ PLANNER_PROMPT = """
2
+ Sistem role:
3
+ You are a deterministic planner for an agent that executes plans step by step.
4
+ Your job is to transform the user's goal and initial payload into a structurally valid, deterministic, executable plan.
5
+
6
+ Agent identity:
7
+ {identity}
8
+ (The identity provides domain expertise but does not override deterministic execution rules).
9
+
10
+ Runtime executor:
11
+ - The Executor does NOT use an LLM and executes the generated plan exactly as written.
12
+ - Every step must be concrete, deterministic, and directly executable.
13
+ - A plan is an ordered list of steps. Each step produces exactly one public output (tool return value or "result" variable).
14
+ - A later step can consume the output of a previous step via "input_from".
15
+ - Inside tool parameters ("params") or logic, you can use "input" to refer to the direct dependency output (from "input_from"), or use a previous step ID (e.g., "s1") to refer to its output.
16
+ - In logic, "context" contains all previous outputs, and "params" are local constants.
17
+
18
+ Structural & Action Rules:
19
+ - Respond ONLY with valid JSON. No Markdown, no explanations, no text before/after.
20
+ - Root object must contain only a "steps" list.
21
+ - Each step must have: "id" (s1, s2...), "tool", "logic", "input_from", and "params" (a JSON object).
22
+ - Each step must define EXACTLY one action: either "tool" (name from Available tools) or "logic" (Python script).
23
+ - "input_from" must be null or a previous step ID. The first step MUST have "input_from": null.
24
+
25
+ Logic Script Rules:
26
+ - Use "logic" for deterministic transformations, calculations, or to provide a direct conversational response when no tools are required.
27
+ - Do NOT use "logic" to build large Markdown documents, source-code listings, or reports containing many files.
28
+ - If the task requires presenting large text, file contents, Markdown, or code blocks, keep those values as tool outputs or structured data and let the framework render them after execution.
29
+ - "logic" MUST use Python syntax (True, False, None), NOT JSON (true, false, null).
30
+ - It must assign the final step output to a variable named "result".
31
+ - Prohibited: imports, print, open, eval, exec, functions, classes, loops, try/except, with, global/nonlocal.
32
+ - Available names: input, params, context, pi, abs, min, max, round, str, int, float, len.
33
+ - Prefer simple expressions and short string literals.
34
+ - Avoid f-strings when interpolating dictionaries, tool outputs, JSON-like data, file contents, or any value that may contain braces, quotes, backticks, or newlines.
35
+ - Do not embed Markdown code fences, triple backticks, triple quotes, or complete source files inside "logic".
36
+ - Any multiline Python string MUST use triple quotes, but multiline strings should be avoided except for short messages.
37
+ - A string literal assigned in "logic" should be concise; large content must remain outside "logic".
38
+ - Never place literal newlines inside single-quoted or double-quoted strings.
39
+ - Generated Python code MUST always be syntactically valid.
40
+
41
+ Failure Handling & Task Limitations:
42
+ - Conversational Goals: If the user goal is purely conversational (greetings, identity questions, or general knowledge) and requires no tools, solve it with a single "logic" step assigning the response to "result".
43
+ - Missing Tools: If the user goal requires a specific technical action (e.g., file manipulation, git operations) for which no tool is available, you MUST return a valid Plan JSON with a single logic step.
44
+ - In the case of missing technical tools, "result" must contain:
45
+ 1. An explicit disclaimer stating that your execution is strictly limited to the provided tools and you cannot perform unauthorized actions.
46
+ 2. A technical explanation of why the specific action cannot be completed.
47
+ - NEVER attempt to bypass missing tools by using prohibited Python features (like 'open', 'os', or 'imports') inside a logic step.
48
+
49
+ Available tools:
50
+ {tools}
51
+
52
+ Example of Success (Transforming output from a tool):
53
+ {{
54
+ "steps": [
55
+ {{
56
+ "id": "s1",
57
+ "tool": "read_file",
58
+ "params": {{ "path": "hello.txt" }},
59
+ "input_from": null
60
+ }},
61
+ {{
62
+ "id": "s2",
63
+ "tool": null,
64
+ "logic": "result = content.upper()",
65
+ "input_from": "s1",
66
+ "params": {{}}
67
+ }}
68
+ ]
69
+ }}
70
+
71
+ Example of Failure (User wants to perform an action, but the specific domain tool is missing entirely from the 'Available tools' list):
72
+ {{
73
+ "steps": [
74
+ {{
75
+ "id": "s1",
76
+ "tool": null,
77
+ "logic": "result = 'I cannot perform this task directly because my execution is limited to the provided tools, and no tool matching the required capability is currently available in the system configuration.'",
78
+ "input_from": null,
79
+ "params": {{}}
80
+ }}
81
+ ]
82
+ }}
83
+
84
+
85
+ User goal:
86
+ {goal}
87
+
88
+ Initial payload:
89
+ {payload}
90
+
91
+ Generate the final Plan JSON now.
92
+ """.strip()
@@ -0,0 +1,23 @@
1
+ RESPONSE_IMPROVEMENT_PROMPT = """
2
+ You are a deterministic response formatter.
3
+
4
+ Your only job is formatting.
5
+
6
+ Rules:
7
+ - NEVER change the semantic meaning.
8
+ - NEVER rewrite sentences.
9
+ - NEVER summarize.
10
+ - NEVER explain.
11
+ - NEVER remove content.
12
+ - ONLY improve formatting.
13
+ - Add Markdown code fences when code is detected.
14
+ - Detect the correct language for code fences when possible.
15
+ - Preserve all code exactly.
16
+ - Preserve indentation exactly.
17
+ - If formatting is already correct, return the original response unchanged.
18
+ - If the format is "markdown" and the text contains Python or Bash code, YOU MUST wrap those code blocks using "~~~" fences (e.g., ~~~python or ~~~bash).
19
+ - Output only the final formatted response.
20
+
21
+ Original response:
22
+ {response}
23
+ """
@@ -0,0 +1,2 @@
1
+ from .plan import Step, Plan
2
+ from .io import AgentInput, AgentOutput
deer/schema/io.py ADDED
@@ -0,0 +1,40 @@
1
+ import itertools
2
+ from typing import Any, Dict, Optional, List
3
+ from pydantic import BaseModel, Field, create_model
4
+
5
+ _counter = itertools.count()
6
+
7
+
8
+ class AgentInput(BaseModel):
9
+ goal: str = Field(..., description="Goal or task to fulfill")
10
+ payload: Dict[str, Any] = Field(default_factory=dict, description="Initial data")
11
+
12
+
13
+ class StepTrace(BaseModel):
14
+ step_id: str
15
+ tool: str
16
+ input: Any
17
+ output: Any
18
+ error: Optional[str] = None
19
+
20
+
21
+ class Trace(BaseModel):
22
+ steps: List[StepTrace] = Field(default_factory=list)
23
+
24
+
25
+ class AgentOutput(BaseModel):
26
+ result: Any = None
27
+ trace: List[StepTrace] = Field(default_factory=list)
28
+ validated: bool = False
29
+ # image: Optional[str] = None
30
+
31
+ @property
32
+ def text(self):
33
+ return f"{str(self.result)}"
34
+
35
+
36
+ def Return(**fields: Any) -> type[BaseModel]:
37
+ return create_model(
38
+ f"InlineModel_{next(_counter)}",
39
+ **{name: (field_type, ...) for name, field_type in fields.items()},
40
+ )
deer/schema/plan.py ADDED
@@ -0,0 +1,75 @@
1
+ from typing import Optional, List, Dict, Any, NamedTuple
2
+ from pydantic import BaseModel, Field, field_validator, model_validator
3
+
4
+
5
+ class Step(BaseModel):
6
+
7
+ id: str = Field(..., description="Unique step identifier (e.g., s1, s2)")
8
+
9
+ tool: Optional[str] = Field(
10
+ None, description="Name of the tool to invoke. Optional if 'logic' is provided."
11
+ )
12
+
13
+ logic: Optional[str] = Field(
14
+ None,
15
+ description="Restricted Python expression executed by the deterministic runtime.",
16
+ )
17
+
18
+ input_from: Optional[str] = Field(
19
+ None,
20
+ description="ID of the previous step whose output will be passed as input.",
21
+ )
22
+
23
+ params: Dict[str, Any] = Field(
24
+ default_factory=dict,
25
+ description="Literal parameters or configuration for the tool/logic.",
26
+ )
27
+
28
+ @model_validator(mode="before")
29
+ @classmethod
30
+ def _normalize_string_nulls(cls, data: Any) -> Any:
31
+ if not isinstance(data, dict):
32
+ return data
33
+
34
+ for field_name in ("tool", "logic", "input_from"):
35
+ if data.get(field_name) == "null":
36
+ data[field_name] = None
37
+
38
+ return data
39
+
40
+ @field_validator("id")
41
+ @classmethod
42
+ def _non_empty_id(cls, v: str) -> str:
43
+ if not v or not v.strip():
44
+ raise ValueError("Step.id cannot be empty")
45
+ return v
46
+
47
+ @model_validator(mode="after")
48
+ def _validate_action(self) -> "Step":
49
+ has_tool = bool(self.tool and self.tool.strip())
50
+ has_logic = bool(self.logic and self.logic.strip())
51
+
52
+ if has_tool == has_logic:
53
+ raise ValueError("A step must provide exactly one of 'tool' or 'logic'.")
54
+
55
+ return self
56
+
57
+
58
+ class Plan(BaseModel):
59
+
60
+ steps: List[Step] = Field(
61
+ default_factory=list,
62
+ description="Ordered sequence of steps to be executed by the orchestrator.",
63
+ )
64
+
65
+ def step_by_id(self, step_id: str) -> Step:
66
+ for s in self.steps:
67
+ if s.id == step_id:
68
+ return s
69
+ raise KeyError(f"Step not found: {step_id}")
70
+
71
+
72
+ class VerificationResult(NamedTuple):
73
+ is_success: bool
74
+ feedback: str
75
+ verification_plan: Optional[Plan] = None
deer/tools/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .base import Tool, ToolProvider
2
+ from .registry import ToolRegistry
3
+ from .decorators import tool
4
+ from deer.schema.io import Return
deer/tools/base.py ADDED
@@ -0,0 +1,141 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Type
3
+ from dataclasses import dataclass, field
4
+
5
+ from pydantic import BaseModel
6
+ from pathlib import Path
7
+ import subprocess
8
+ import shlex
9
+
10
+
11
+ class ToolProviderError(ValueError):
12
+ pass
13
+
14
+
15
+ class CommandRunnerError(ValueError):
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class ToolProvider:
21
+ # jail: Path | str | None = field(default=None, kw_only=True)
22
+ tools: list[str] | None = field(default=None, kw_only=True)
23
+
24
+ def __post_init__(self):
25
+ # pass
26
+ self.jail_ = None
27
+
28
+ @property
29
+ def jail(self):
30
+ assert self.jail_ is not None, (
31
+ "Filesystem jail is not configured. "
32
+ "The runtime cannot access the sandbox root path."
33
+ )
34
+ return self.jail_
35
+
36
+ @jail.setter
37
+ def jail(self, jail):
38
+ self.jail_ = Path(jail).resolve(strict=True)
39
+
40
+ def jailed_path(self, path: str | Path) -> Path:
41
+ """
42
+ Validates and returns a safe path within the jail.
43
+
44
+ 1. If 'path' is relative, it's joined to the jail.
45
+ 2. If 'path' is absolute, it's checked for containment.
46
+ 3. All '..' and symlinks are resolved before validation.
47
+ """
48
+ path = Path(path)
49
+
50
+ # Handle point 2: If relative, interpret it as inside the jail.
51
+ # If absolute, it remains as is to be validated against the jail.
52
+ if not path.is_absolute():
53
+ path = self.jail / path
54
+
55
+ # Handle point 1: Normalize "..", symlinks, etc.
56
+ # strict=False allows the path to not exist yet (e.g., for creating files).
57
+ resolved = path.resolve(strict=False)
58
+
59
+ # Real containment verification
60
+ try:
61
+ # relative_to raises ValueError if 'resolved' is not a child of 'self.jail'
62
+ resolved.relative_to(self.jail)
63
+ except ValueError:
64
+ raise ToolProviderError(
65
+ f"Security breach: Path escapes jail: {resolved}"
66
+ ) from None
67
+
68
+ return resolved
69
+
70
+ def run_command(
71
+ self,
72
+ command: str,
73
+ *,
74
+ cwd: str | Path,
75
+ timeout_seconds: int = 30,
76
+ ) -> dict:
77
+ """Execute a command string without invoking a shell."""
78
+ args = shlex.split(command)
79
+
80
+ if not args:
81
+ raise CommandRunnerError("Command cannot be empty.")
82
+
83
+ safe_cwd = self.jail if cwd is None else self.jailed_path(cwd)
84
+
85
+ completed = subprocess.run(
86
+ args,
87
+ cwd=safe_cwd,
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=timeout_seconds,
91
+ check=False,
92
+ )
93
+
94
+ return {
95
+ "stdout": completed.stdout,
96
+ "stderr": completed.stderr,
97
+ "returncode": completed.returncode,
98
+ }
99
+
100
+
101
+ class Tool(ABC):
102
+ """Base contract for deterministic tools."""
103
+
104
+ # input_schema: Type[Any] | None = None
105
+ # output_schema: Type[Any] | None = None
106
+ #
107
+ # name: str = ""
108
+ # description: str = ""
109
+ # modifies_state: bool = False
110
+
111
+ def __init__(self) -> None:
112
+ if not self.name:
113
+ self.name = self.__class__.__name__.lower()
114
+
115
+ def validate_input(self, value: Any) -> Any:
116
+ return self._validate_with_schema(self.params_type, value)
117
+
118
+ def validate_output(self, value: Any) -> Any:
119
+ return self._validate_with_schema(self.return_type, value)
120
+
121
+ def _validate_with_schema(self, schema: Type[Any] | None, value: Any) -> Any:
122
+ if schema is None:
123
+ return value
124
+
125
+ if isinstance(schema, type) and issubclass(schema, BaseModel):
126
+ return schema.model_validate(value)
127
+
128
+ return value
129
+
130
+ @abstractmethod
131
+ def run(self, params: dict[str, Any] | None = None) -> Any:
132
+ """Execute the tool deterministically.
133
+
134
+ Args:
135
+ value: Main input value.
136
+ params: Optional literal parameters.
137
+
138
+ Returns:
139
+ The deterministic output produced by the tool.
140
+ """
141
+ raise NotImplementedError
@@ -0,0 +1,3 @@
1
+ from .file_manager import FileManager
2
+ from .git_manager import GitManager
3
+ from .search_manager import SearchManager
@@ -0,0 +1,136 @@
1
+ from deer.tools import ToolProvider, tool, Return
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shutil
6
+
7
+
8
+ class FileManagerError(ValueError):
9
+ pass
10
+
11
+
12
+ @dataclass
13
+ class FileManager(ToolProvider):
14
+
15
+ @tool(modifies_state=True)
16
+ def new_file(self, path: str, content: str) -> Return(exists=bool):
17
+ """Creates a new file with the given content."""
18
+ safe_path = self.jailed_path(path)
19
+
20
+ safe_path.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ with open(safe_path, "w") as f:
23
+ f.write(content)
24
+
25
+ return {
26
+ "exists": safe_path.exists(),
27
+ }
28
+
29
+ @tool()
30
+ def read_file(self, path: str) -> Return(content=str):
31
+ """Reads the content of a file."""
32
+ safe_path = self.jailed_path(path)
33
+
34
+ with open(safe_path, "r") as f:
35
+ content = f.read()
36
+ return {
37
+ "content": content,
38
+ }
39
+
40
+ @tool(modifies_state=True)
41
+ def delete_file(self, path: str) -> Return(exists=bool):
42
+ """Deletes a file."""
43
+ safe_path = self.jailed_path(path)
44
+
45
+ if safe_path.is_file():
46
+ safe_path.unlink()
47
+ else:
48
+ raise ValueError(f"'{path}' is not a file or does not exist.")
49
+
50
+ return {
51
+ "exists": not safe_path.exists(),
52
+ }
53
+
54
+ @tool(modifies_state=True)
55
+ def create_directory(self, path: str) -> Return(status=str):
56
+ """Creates a directory."""
57
+ safe_path = self.jailed_path(path)
58
+ safe_path.mkdir(parents=True, exist_ok=True)
59
+ return {"status": "success"}
60
+
61
+ @tool(modifies_state=True)
62
+ def delete_directory(self, path: str) -> Return(exists=bool):
63
+ """Removes a directory and all its contents recursively."""
64
+ safe_path = self.jailed_path(path)
65
+
66
+ if safe_path.is_dir():
67
+ shutil.rmtree(safe_path)
68
+ else:
69
+ raise ValueError(f"'{path}' is not a directory or does not exist.")
70
+
71
+ return {
72
+ "exists": not safe_path.exists(),
73
+ }
74
+
75
+ @tool()
76
+ def get_file_info(self, path: str) -> Return(
77
+ exists=bool,
78
+ size_bytes=int,
79
+ is_dir=bool,
80
+ is_file=bool,
81
+ last_modified=float,
82
+ ):
83
+ """Retrieves metadata about a file or directory, including its existence, size, type, and modification time."""
84
+ safe_path = self.jailed_path(path)
85
+ if not safe_path.exists():
86
+ return {"exists": False}
87
+
88
+ stats = safe_path.stat()
89
+ return {
90
+ "exists": True,
91
+ "size_bytes": stats.st_size,
92
+ "is_dir": safe_path.is_dir(),
93
+ "is_file": safe_path.is_file(),
94
+ "last_modified": stats.st_mtime,
95
+ }
96
+
97
+ @tool()
98
+ def directory_tree(self, path: str, max_depth: int) -> Return(tree=dict):
99
+ """Returns the directory structure as a nested dictionary."""
100
+ safe_path = self.jailed_path(path)
101
+
102
+ if not safe_path.exists():
103
+ raise FileNotFoundError(f"Path does not exist: {path}")
104
+
105
+ if not safe_path.is_dir():
106
+ raise ValueError(f"Path is not a directory: {path}")
107
+
108
+ def build_node(current_path: Path, depth: int = 0) -> dict:
109
+ relative_path = current_path.relative_to(self.jail)
110
+
111
+ node = {
112
+ "name": current_path.name,
113
+ "path": str(relative_path),
114
+ "type": "directory" if current_path.is_dir() else "file",
115
+ }
116
+
117
+ if current_path.is_file():
118
+ node["size_bytes"] = current_path.stat().st_size
119
+ return node
120
+
121
+ if depth >= max_depth:
122
+ node["children"] = []
123
+ node["truncated"] = True
124
+ return node
125
+
126
+ children = sorted(
127
+ current_path.iterdir(),
128
+ key=lambda item: (not item.is_dir(), item.name.lower()),
129
+ )
130
+
131
+ node["children"] = [build_node(child, depth + 1) for child in children]
132
+ return node
133
+
134
+ return {
135
+ "tree": build_node(safe_path),
136
+ }