pi-mono-coding 0.1.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.
- pi_coding/__init__.py +215 -0
- pi_coding/config.py +59 -0
- pi_coding/core/__init__.py +5 -0
- pi_coding/core/defaults.py +4 -0
- pi_coding/tools/__init__.py +70 -0
- pi_coding/tools/bash.py +213 -0
- pi_coding/tools/edit.py +176 -0
- pi_coding/tools/find.py +182 -0
- pi_coding/tools/grep.py +280 -0
- pi_coding/tools/ls.py +126 -0
- pi_coding/tools/read.py +210 -0
- pi_coding/tools/write.py +99 -0
- pi_coding/utils/__init__.py +91 -0
- pi_coding/utils/edit_diff.py +374 -0
- pi_coding/utils/git.py +201 -0
- pi_coding/utils/path_utils.py +129 -0
- pi_coding/utils/shell.py +143 -0
- pi_coding/utils/truncate.py +274 -0
- pi_mono_coding-0.1.0.dist-info/METADATA +485 -0
- pi_mono_coding-0.1.0.dist-info/RECORD +21 -0
- pi_mono_coding-0.1.0.dist-info/WHEEL +4 -0
pi_coding/__init__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pi_agent import Agent
|
|
7
|
+
|
|
8
|
+
from pi_coding.config import (
|
|
9
|
+
ENV_AGENT_DIR,
|
|
10
|
+
VERSION,
|
|
11
|
+
get_agent_dir,
|
|
12
|
+
get_bin_dir,
|
|
13
|
+
get_sessions_dir,
|
|
14
|
+
get_settings_path,
|
|
15
|
+
)
|
|
16
|
+
from pi_coding.core.defaults import DEFAULT_THINKING_LEVEL
|
|
17
|
+
from pi_coding.tools import (
|
|
18
|
+
all_tools,
|
|
19
|
+
bash_tool,
|
|
20
|
+
coding_tools,
|
|
21
|
+
create_all_tools,
|
|
22
|
+
create_bash_tool,
|
|
23
|
+
create_coding_tools,
|
|
24
|
+
create_edit_tool,
|
|
25
|
+
create_find_tool,
|
|
26
|
+
create_grep_tool,
|
|
27
|
+
create_ls_tool,
|
|
28
|
+
create_read_only_tools,
|
|
29
|
+
create_read_tool,
|
|
30
|
+
create_write_tool,
|
|
31
|
+
edit_tool,
|
|
32
|
+
find_tool,
|
|
33
|
+
grep_tool,
|
|
34
|
+
ls_tool,
|
|
35
|
+
read_only_tools,
|
|
36
|
+
read_tool,
|
|
37
|
+
write_tool,
|
|
38
|
+
)
|
|
39
|
+
from pi_coding.utils import (
|
|
40
|
+
DEFAULT_MAX_BYTES,
|
|
41
|
+
DEFAULT_MAX_LINES,
|
|
42
|
+
EditDiffError,
|
|
43
|
+
EditDiffResult,
|
|
44
|
+
FuzzyMatchResult,
|
|
45
|
+
GREP_MAX_LINE_LENGTH,
|
|
46
|
+
TruncationOptions,
|
|
47
|
+
TruncationResult,
|
|
48
|
+
compute_edit_diff,
|
|
49
|
+
detect_line_ending,
|
|
50
|
+
expand_path,
|
|
51
|
+
file_exists,
|
|
52
|
+
format_size,
|
|
53
|
+
fuzzy_find_text,
|
|
54
|
+
generate_diff_string,
|
|
55
|
+
normalize_for_fuzzy_match,
|
|
56
|
+
normalize_to_lf,
|
|
57
|
+
resolve_read_path,
|
|
58
|
+
resolve_to_cwd,
|
|
59
|
+
restore_line_endings,
|
|
60
|
+
strip_bom,
|
|
61
|
+
truncate_head,
|
|
62
|
+
truncate_line,
|
|
63
|
+
truncate_tail,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"get_coding_tools",
|
|
68
|
+
"get_read_only_tools",
|
|
69
|
+
"get_all_tools",
|
|
70
|
+
"create_coding_agent",
|
|
71
|
+
"CodingAgentConfig",
|
|
72
|
+
"read_tool",
|
|
73
|
+
"write_tool",
|
|
74
|
+
"edit_tool",
|
|
75
|
+
"bash_tool",
|
|
76
|
+
"ls_tool",
|
|
77
|
+
"grep_tool",
|
|
78
|
+
"find_tool",
|
|
79
|
+
"create_read_tool",
|
|
80
|
+
"create_write_tool",
|
|
81
|
+
"create_edit_tool",
|
|
82
|
+
"create_bash_tool",
|
|
83
|
+
"create_ls_tool",
|
|
84
|
+
"create_grep_tool",
|
|
85
|
+
"create_find_tool",
|
|
86
|
+
"coding_tools",
|
|
87
|
+
"read_only_tools",
|
|
88
|
+
"all_tools",
|
|
89
|
+
"create_coding_tools",
|
|
90
|
+
"create_read_only_tools",
|
|
91
|
+
"create_all_tools",
|
|
92
|
+
"DEFAULT_MAX_BYTES",
|
|
93
|
+
"DEFAULT_MAX_LINES",
|
|
94
|
+
"GREP_MAX_LINE_LENGTH",
|
|
95
|
+
"TruncationOptions",
|
|
96
|
+
"TruncationResult",
|
|
97
|
+
"format_size",
|
|
98
|
+
"truncate_head",
|
|
99
|
+
"truncate_tail",
|
|
100
|
+
"truncate_line",
|
|
101
|
+
"expand_path",
|
|
102
|
+
"resolve_to_cwd",
|
|
103
|
+
"resolve_read_path",
|
|
104
|
+
"file_exists",
|
|
105
|
+
"detect_line_ending",
|
|
106
|
+
"normalize_to_lf",
|
|
107
|
+
"restore_line_endings",
|
|
108
|
+
"normalize_for_fuzzy_match",
|
|
109
|
+
"fuzzy_find_text",
|
|
110
|
+
"FuzzyMatchResult",
|
|
111
|
+
"strip_bom",
|
|
112
|
+
"generate_diff_string",
|
|
113
|
+
"compute_edit_diff",
|
|
114
|
+
"EditDiffResult",
|
|
115
|
+
"EditDiffError",
|
|
116
|
+
# Configuration
|
|
117
|
+
"VERSION",
|
|
118
|
+
"ENV_AGENT_DIR",
|
|
119
|
+
"get_agent_dir",
|
|
120
|
+
"get_bin_dir",
|
|
121
|
+
"get_sessions_dir",
|
|
122
|
+
"get_settings_path",
|
|
123
|
+
# Defaults
|
|
124
|
+
"DEFAULT_THINKING_LEVEL",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_coding_tools(cwd: str | None = None) -> list:
|
|
129
|
+
return create_coding_tools(cwd or os.getcwd())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_read_only_tools(cwd: str | None = None) -> list:
|
|
133
|
+
return create_read_only_tools(cwd or os.getcwd())
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_all_tools(cwd: str | None = None) -> list:
|
|
137
|
+
return create_all_tools(cwd or os.getcwd())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CodingAgentConfig:
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
model: Any = None,
|
|
144
|
+
system_prompt: str | None = None,
|
|
145
|
+
working_dir: str | None = None,
|
|
146
|
+
tools: list | None = None,
|
|
147
|
+
):
|
|
148
|
+
self.model = model
|
|
149
|
+
self.system_prompt = system_prompt
|
|
150
|
+
self.working_dir = working_dir
|
|
151
|
+
self.tools = tools
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def create_coding_agent(
|
|
155
|
+
config: CodingAgentConfig | None = None,
|
|
156
|
+
**kwargs: Any,
|
|
157
|
+
) -> Agent:
|
|
158
|
+
cfg = config or CodingAgentConfig()
|
|
159
|
+
|
|
160
|
+
cwd = cfg.working_dir or os.getcwd()
|
|
161
|
+
|
|
162
|
+
system_prompt = cfg.system_prompt or """You are an expert coding assistant with access to file system tools.
|
|
163
|
+
|
|
164
|
+
## Core Principles
|
|
165
|
+
|
|
166
|
+
1. **Read before writing**: Always read files before suggesting changes
|
|
167
|
+
2. **Be precise**: Use exact string matches when editing
|
|
168
|
+
3. **Show context**: Display file previews before editing
|
|
169
|
+
4. **Explain changes**: Clearly describe what you're changing and why
|
|
170
|
+
5. **Handle errors**: Gracefully report errors and suggest solutions
|
|
171
|
+
|
|
172
|
+
## Available Tools
|
|
173
|
+
|
|
174
|
+
### File Operations
|
|
175
|
+
- **read**: Read file contents with line numbers, supports images
|
|
176
|
+
- **write**: Create or overwrite files (creates directories)
|
|
177
|
+
- **edit**: Search and replace text in files (exact match required)
|
|
178
|
+
|
|
179
|
+
### Search & Navigation
|
|
180
|
+
- **ls**: List directory contents with type indicators
|
|
181
|
+
- **grep**: Search file contents using ripgrep
|
|
182
|
+
- **find**: Find files by glob pattern using fd
|
|
183
|
+
|
|
184
|
+
### Execution
|
|
185
|
+
- **bash**: Execute shell commands with streaming output
|
|
186
|
+
|
|
187
|
+
## Workflow
|
|
188
|
+
|
|
189
|
+
When asked to make code changes:
|
|
190
|
+
1. Use ls to understand structure
|
|
191
|
+
2. Read relevant files
|
|
192
|
+
3. Explain what you're going to do
|
|
193
|
+
4. Make changes with edit or write
|
|
194
|
+
5. Verify changes by reading back
|
|
195
|
+
|
|
196
|
+
## Best Practices
|
|
197
|
+
|
|
198
|
+
- Use relative paths when possible
|
|
199
|
+
- Check if files exist before editing
|
|
200
|
+
- Be conservative with replacements
|
|
201
|
+
- Provide clear, concise explanations"""
|
|
202
|
+
|
|
203
|
+
tools = cfg.tools or create_coding_tools(cwd)
|
|
204
|
+
|
|
205
|
+
agent = Agent(
|
|
206
|
+
options={
|
|
207
|
+
"model": cfg.model,
|
|
208
|
+
"tools": tools,
|
|
209
|
+
**kwargs,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
agent.set_system_prompt(system_prompt)
|
|
214
|
+
|
|
215
|
+
return agent
|
pi_coding/config.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Configuration paths for pi_coding agent."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Version from package (can be updated)
|
|
7
|
+
VERSION = "0.1.0"
|
|
8
|
+
|
|
9
|
+
# App configuration
|
|
10
|
+
APP_NAME = "pi"
|
|
11
|
+
CONFIG_DIR_NAME = ".pi"
|
|
12
|
+
|
|
13
|
+
# Environment variable for custom agent directory
|
|
14
|
+
ENV_AGENT_DIR = f"{APP_NAME.upper()}_CODING_AGENT_DIR"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_agent_dir() -> Path:
|
|
18
|
+
"""Get the agent config directory (e.g., ~/.pi/agent/).
|
|
19
|
+
|
|
20
|
+
Checks ENV_AGENT_DIR first, then uses default ~/.pi/agent/
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Path to agent directory
|
|
24
|
+
"""
|
|
25
|
+
env_dir = os.environ.get(ENV_AGENT_DIR)
|
|
26
|
+
if env_dir:
|
|
27
|
+
if env_dir == "~":
|
|
28
|
+
return Path.home()
|
|
29
|
+
if env_dir.startswith("~/"):
|
|
30
|
+
return Path.home() / env_dir[2:]
|
|
31
|
+
return Path(env_dir)
|
|
32
|
+
return Path.home() / CONFIG_DIR_NAME / "agent"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_sessions_dir() -> Path:
|
|
36
|
+
"""Get the sessions directory (e.g., ~/.pi/agent/sessions/).
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Path to sessions directory
|
|
40
|
+
"""
|
|
41
|
+
return get_agent_dir() / "sessions"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_settings_path() -> Path:
|
|
45
|
+
"""Get path to settings.json (e.g., ~/.pi/agent/settings.json).
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Path to settings.json file
|
|
49
|
+
"""
|
|
50
|
+
return get_agent_dir() / "settings.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_bin_dir() -> Path:
|
|
54
|
+
"""Get path to managed binaries directory (fd, rg).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Path to bin directory
|
|
58
|
+
"""
|
|
59
|
+
return get_agent_dir() / "bin"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Coding agent tools - read, write, edit, bash, ls, grep, find."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pi_coding.tools.bash import bash_tool, create_bash_tool
|
|
6
|
+
from pi_coding.tools.edit import edit_tool, create_edit_tool
|
|
7
|
+
from pi_coding.tools.find import find_tool, create_find_tool
|
|
8
|
+
from pi_coding.tools.grep import grep_tool, create_grep_tool
|
|
9
|
+
from pi_coding.tools.ls import ls_tool, create_ls_tool
|
|
10
|
+
from pi_coding.tools.read import read_tool, create_read_tool
|
|
11
|
+
from pi_coding.tools.write import write_tool, create_write_tool
|
|
12
|
+
|
|
13
|
+
coding_tools = [read_tool, bash_tool, edit_tool, write_tool]
|
|
14
|
+
read_only_tools = [read_tool, bash_tool, ls_tool, grep_tool, find_tool]
|
|
15
|
+
all_tools = [read_tool, write_tool, edit_tool, bash_tool, ls_tool, grep_tool, find_tool]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_coding_tools(cwd: str) -> list:
|
|
19
|
+
return [
|
|
20
|
+
create_read_tool(cwd),
|
|
21
|
+
create_bash_tool(cwd),
|
|
22
|
+
create_edit_tool(cwd),
|
|
23
|
+
create_write_tool(cwd),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_read_only_tools(cwd: str) -> list:
|
|
28
|
+
return [
|
|
29
|
+
create_read_tool(cwd),
|
|
30
|
+
create_bash_tool(cwd),
|
|
31
|
+
create_ls_tool(cwd),
|
|
32
|
+
create_grep_tool(cwd),
|
|
33
|
+
create_find_tool(cwd),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_all_tools(cwd: str) -> list:
|
|
38
|
+
return [
|
|
39
|
+
create_read_tool(cwd),
|
|
40
|
+
create_write_tool(cwd),
|
|
41
|
+
create_edit_tool(cwd),
|
|
42
|
+
create_bash_tool(cwd),
|
|
43
|
+
create_ls_tool(cwd),
|
|
44
|
+
create_grep_tool(cwd),
|
|
45
|
+
create_find_tool(cwd),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"read_tool",
|
|
51
|
+
"write_tool",
|
|
52
|
+
"edit_tool",
|
|
53
|
+
"bash_tool",
|
|
54
|
+
"ls_tool",
|
|
55
|
+
"grep_tool",
|
|
56
|
+
"find_tool",
|
|
57
|
+
"create_read_tool",
|
|
58
|
+
"create_write_tool",
|
|
59
|
+
"create_edit_tool",
|
|
60
|
+
"create_bash_tool",
|
|
61
|
+
"create_ls_tool",
|
|
62
|
+
"create_grep_tool",
|
|
63
|
+
"create_find_tool",
|
|
64
|
+
"coding_tools",
|
|
65
|
+
"read_only_tools",
|
|
66
|
+
"all_tools",
|
|
67
|
+
"create_coding_tools",
|
|
68
|
+
"create_read_only_tools",
|
|
69
|
+
"create_all_tools",
|
|
70
|
+
]
|
pi_coding/tools/bash.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Bash tool - executes shell commands with streaming output and truncation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pi_agent.types import AgentToolUpdateCallback
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pi_agent import AgentTool, AgentToolResult
|
|
12
|
+
from pi_ai.types import TextContent
|
|
13
|
+
|
|
14
|
+
from pi_coding.utils import DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, format_size, truncate_tail
|
|
15
|
+
|
|
16
|
+
_BASH_TOOL_PARAMETERS = {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"command": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Bash command to execute",
|
|
22
|
+
},
|
|
23
|
+
"timeout": {
|
|
24
|
+
"type": "number",
|
|
25
|
+
"description": "Timeout in seconds (optional, no default timeout)",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"required": ["command"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _execute_bash(
|
|
33
|
+
tool_call_id: str,
|
|
34
|
+
params: dict[str, Any],
|
|
35
|
+
cwd: str,
|
|
36
|
+
cancel_event: asyncio.Event | None,
|
|
37
|
+
on_update: AgentToolUpdateCallback | None,
|
|
38
|
+
) -> AgentToolResult:
|
|
39
|
+
command = params.get("command")
|
|
40
|
+
timeout = params.get("timeout")
|
|
41
|
+
|
|
42
|
+
if not command:
|
|
43
|
+
return AgentToolResult(
|
|
44
|
+
content=[TextContent(type="text", text="Error: 'command' parameter is required")],
|
|
45
|
+
details={"error": "missing_parameter"},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if cancel_event and cancel_event.is_set():
|
|
49
|
+
return AgentToolResult(
|
|
50
|
+
content=[TextContent(type="text", text="Operation aborted")],
|
|
51
|
+
details={"error": "aborted"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
shell = os.environ.get("SHELL", "/bin/bash")
|
|
55
|
+
temp_file: tempfile._TemporaryFileWrapper | None = None
|
|
56
|
+
temp_file_path: str | None = None
|
|
57
|
+
|
|
58
|
+
chunks: list[bytes] = []
|
|
59
|
+
chunks_bytes = 0
|
|
60
|
+
max_chunks_bytes = DEFAULT_MAX_BYTES * 2
|
|
61
|
+
total_bytes = 0
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
process = await asyncio.create_subprocess_shell(
|
|
65
|
+
command,
|
|
66
|
+
cwd=cwd,
|
|
67
|
+
shell=True,
|
|
68
|
+
stdout=asyncio.subprocess.PIPE,
|
|
69
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
70
|
+
executable=shell,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def read_stream():
|
|
74
|
+
nonlocal total_bytes, chunks_bytes, temp_file, temp_file_path
|
|
75
|
+
|
|
76
|
+
if process.stdout is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
while True:
|
|
80
|
+
try:
|
|
81
|
+
chunk = await process.stdout.read(4096)
|
|
82
|
+
if not chunk:
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
total_bytes += len(chunk)
|
|
86
|
+
|
|
87
|
+
if total_bytes > DEFAULT_MAX_BYTES and temp_file is None:
|
|
88
|
+
temp_file = tempfile.NamedTemporaryFile(
|
|
89
|
+
mode="wb", suffix=".log", prefix="pi-bash-", delete=False
|
|
90
|
+
)
|
|
91
|
+
temp_file_path = temp_file.name
|
|
92
|
+
for c in chunks:
|
|
93
|
+
temp_file.write(c)
|
|
94
|
+
|
|
95
|
+
if temp_file:
|
|
96
|
+
temp_file.write(chunk)
|
|
97
|
+
|
|
98
|
+
chunks.append(chunk)
|
|
99
|
+
chunks_bytes += len(chunk)
|
|
100
|
+
|
|
101
|
+
while chunks_bytes > max_chunks_bytes and len(chunks) > 1:
|
|
102
|
+
removed = chunks.pop(0)
|
|
103
|
+
chunks_bytes -= len(removed)
|
|
104
|
+
|
|
105
|
+
if on_update:
|
|
106
|
+
full_buffer = b"".join(chunks)
|
|
107
|
+
full_text = full_buffer.decode("utf-8", errors="replace")
|
|
108
|
+
truncation = truncate_tail(full_text)
|
|
109
|
+
on_update(
|
|
110
|
+
AgentToolResult(
|
|
111
|
+
content=[TextContent(type="text", text=truncation.content or "")],
|
|
112
|
+
details={
|
|
113
|
+
"truncation": truncation.__dict__ if truncation.truncated else None,
|
|
114
|
+
"full_output_path": temp_file_path,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
except asyncio.CancelledError:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
read_task = asyncio.create_task(read_stream())
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
if timeout is not None and timeout > 0:
|
|
126
|
+
await asyncio.wait_for(process.wait(), timeout=timeout)
|
|
127
|
+
else:
|
|
128
|
+
await process.wait()
|
|
129
|
+
except asyncio.TimeoutError:
|
|
130
|
+
process.kill()
|
|
131
|
+
await process.wait()
|
|
132
|
+
if temp_file:
|
|
133
|
+
temp_file.close()
|
|
134
|
+
full_buffer = b"".join(chunks)
|
|
135
|
+
output = full_buffer.decode("utf-8", errors="replace")
|
|
136
|
+
return AgentToolResult(
|
|
137
|
+
content=[
|
|
138
|
+
TextContent(
|
|
139
|
+
type="text",
|
|
140
|
+
text=f"{output}\n\nCommand timed out after {timeout} seconds",
|
|
141
|
+
)
|
|
142
|
+
],
|
|
143
|
+
details={"error": "timeout", "timeout": timeout, "command": command},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
await read_task
|
|
147
|
+
|
|
148
|
+
if temp_file:
|
|
149
|
+
temp_file.close()
|
|
150
|
+
|
|
151
|
+
if cancel_event and cancel_event.is_set():
|
|
152
|
+
full_buffer = b"".join(chunks)
|
|
153
|
+
output = full_buffer.decode("utf-8", errors="replace")
|
|
154
|
+
return AgentToolResult(
|
|
155
|
+
content=[TextContent(type="text", text=f"{output}\n\nCommand aborted")],
|
|
156
|
+
details={"error": "aborted", "command": command},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
full_buffer = b"".join(chunks)
|
|
160
|
+
full_output = full_buffer.decode("utf-8", errors="replace")
|
|
161
|
+
truncation = truncate_tail(full_output)
|
|
162
|
+
output_text = truncation.content or "(no output)"
|
|
163
|
+
|
|
164
|
+
details: dict[str, Any] = {}
|
|
165
|
+
if truncation.truncated:
|
|
166
|
+
details["truncation"] = truncation.__dict__
|
|
167
|
+
details["full_output_path"] = temp_file_path
|
|
168
|
+
|
|
169
|
+
start_line = truncation.total_lines - truncation.output_lines + 1
|
|
170
|
+
end_line = truncation.total_lines
|
|
171
|
+
|
|
172
|
+
if truncation.last_line_partial:
|
|
173
|
+
last_line_size = format_size(len(full_output.split("\n")[-1].encode("utf-8")))
|
|
174
|
+
output_text += f"\n\n[Showing last {format_size(truncation.output_bytes)} of line {end_line} (line is {last_line_size}). Full output: {temp_file_path}]"
|
|
175
|
+
elif truncation.truncated_by == "lines":
|
|
176
|
+
output_text += f"\n\n[Showing lines {start_line}-{end_line} of {truncation.total_lines}. Full output: {temp_file_path}]"
|
|
177
|
+
else:
|
|
178
|
+
output_text += f"\n\n[Showing lines {start_line}-{end_line} of {truncation.total_lines} ({format_size(DEFAULT_MAX_BYTES)} limit). Full output: {temp_file_path}]"
|
|
179
|
+
|
|
180
|
+
if process.returncode is not None and process.returncode != 0:
|
|
181
|
+
output_text += f"\n\nCommand exited with code {process.returncode}"
|
|
182
|
+
return AgentToolResult(
|
|
183
|
+
content=[TextContent(type="text", text=output_text)],
|
|
184
|
+
details={"error": "nonzero_exit", "exit_code": process.returncode, **details},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return AgentToolResult(
|
|
188
|
+
content=[TextContent(type="text", text=output_text)],
|
|
189
|
+
details=details if details else None,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
if temp_file:
|
|
194
|
+
temp_file.close()
|
|
195
|
+
return AgentToolResult(
|
|
196
|
+
content=[TextContent(type="text", text=f"Error running command: {e}")],
|
|
197
|
+
details={"error": str(e), "command": command},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def create_bash_tool(cwd: str, options: dict[str, Any] | None = None) -> AgentTool:
|
|
202
|
+
return AgentTool(
|
|
203
|
+
name="bash",
|
|
204
|
+
label="Bash",
|
|
205
|
+
description=f"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last {DEFAULT_MAX_LINES} lines or {DEFAULT_MAX_BYTES // 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.",
|
|
206
|
+
parameters=_BASH_TOOL_PARAMETERS,
|
|
207
|
+
execute=lambda tool_call_id, params, cancel_event, on_update: _execute_bash(
|
|
208
|
+
tool_call_id, params, cwd, cancel_event, on_update
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
bash_tool = create_bash_tool(os.getcwd())
|