tau-coding-agent 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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import glob as _glob
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from tau.tool.types import (
|
|
10
|
+
Tool, ToolKind, ToolExecutionMode,
|
|
11
|
+
ToolInvocation, ToolResult,
|
|
12
|
+
ToolExecutionUpdateCallback, AbortSignal, ToolContext,
|
|
13
|
+
)
|
|
14
|
+
from tau.tool.render import call_line
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _render_glob_call(args: dict, _streaming: bool) -> list[str]:
|
|
18
|
+
return call_line("glob", args.get("pattern", ""))
|
|
19
|
+
|
|
20
|
+
_MAX_RESULTS = 1000
|
|
21
|
+
_PREVIEW_LINES = 5
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _render_glob_result(content: str, opts: Any) -> list[str]:
|
|
25
|
+
from tau.tui.ansi import DIM, RESET
|
|
26
|
+
metadata = opts.metadata or {}
|
|
27
|
+
match_count = metadata.get("match_count", 0)
|
|
28
|
+
truncated = metadata.get("truncated", False)
|
|
29
|
+
|
|
30
|
+
if match_count == 0:
|
|
31
|
+
return ["No files matched"]
|
|
32
|
+
|
|
33
|
+
file_word = "file" if match_count == 1 else "files"
|
|
34
|
+
summary = f"Found {match_count} {file_word}"
|
|
35
|
+
if truncated:
|
|
36
|
+
summary += f" {DIM}(truncated){RESET}"
|
|
37
|
+
|
|
38
|
+
lines = [l for l in content.splitlines() if l and not l.startswith("[")]
|
|
39
|
+
result = [summary]
|
|
40
|
+
|
|
41
|
+
show = lines if opts.expanded else lines[:_PREVIEW_LINES]
|
|
42
|
+
for path in show:
|
|
43
|
+
result.append(path)
|
|
44
|
+
|
|
45
|
+
if opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
46
|
+
result.append(f"{DIM} (ctrl+o to collapse){RESET}")
|
|
47
|
+
elif not opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
48
|
+
result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GlobParams(BaseModel):
|
|
54
|
+
"""Parameters for the glob tool."""
|
|
55
|
+
pattern: str = Field(description="Glob pattern (e.g. 'src/**/*.py').")
|
|
56
|
+
path: str = Field(default="", description="Base directory to search from. Defaults to the agent's cwd.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GlobTool(Tool):
|
|
60
|
+
"""Tool for finding files matching glob patterns."""
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
name="glob",
|
|
64
|
+
description=(
|
|
65
|
+
"Find files matching a glob pattern. Returns absolute paths, one per line, "
|
|
66
|
+
f"up to {_MAX_RESULTS} results. Supports ** for recursive matching."
|
|
67
|
+
),
|
|
68
|
+
schema=GlobParams,
|
|
69
|
+
kind=ToolKind.Read,
|
|
70
|
+
execution_mode=ToolExecutionMode.Parallel,
|
|
71
|
+
render_result=_render_glob_result,
|
|
72
|
+
render_call=_render_glob_call,
|
|
73
|
+
render_shell="default",
|
|
74
|
+
prompt_guidelines="Use to discover files by pattern before reading or editing them.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def get_display_name(self, args: dict[str, Any]) -> str:
|
|
78
|
+
"""Get a short display name for the glob operation."""
|
|
79
|
+
return args.get("pattern", "glob")
|
|
80
|
+
|
|
81
|
+
async def execute(
|
|
82
|
+
self,
|
|
83
|
+
invocation: ToolInvocation,
|
|
84
|
+
tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
|
|
85
|
+
signal: Optional[AbortSignal] = None,
|
|
86
|
+
context: Optional[ToolContext] = None,
|
|
87
|
+
) -> ToolResult:
|
|
88
|
+
"""Execute the glob pattern matching operation."""
|
|
89
|
+
params = GlobParams.model_validate(invocation.params)
|
|
90
|
+
base = Path(params.path or invocation.cwd or ".").resolve()
|
|
91
|
+
|
|
92
|
+
if not base.is_dir():
|
|
93
|
+
return ToolResult.error(invocation.id, f"Base path is not a directory: {base}")
|
|
94
|
+
|
|
95
|
+
matches = sorted(_glob.glob(str(base / params.pattern), recursive=True))
|
|
96
|
+
truncated = len(matches) > _MAX_RESULTS
|
|
97
|
+
matches = matches[:_MAX_RESULTS]
|
|
98
|
+
|
|
99
|
+
metadata = {
|
|
100
|
+
"pattern": params.pattern,
|
|
101
|
+
"match_count": len(matches),
|
|
102
|
+
"truncated": truncated,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if not matches:
|
|
106
|
+
return ToolResult.ok(invocation.id, f"No files matched pattern: {params.pattern}", metadata=metadata)
|
|
107
|
+
|
|
108
|
+
result = "\n".join(matches)
|
|
109
|
+
if truncated:
|
|
110
|
+
result += f"\n\n[Results truncated at {_MAX_RESULTS}. Narrow your pattern.]"
|
|
111
|
+
|
|
112
|
+
return ToolResult.ok(invocation.id, result, metadata=metadata)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from tau.tool.types import (
|
|
10
|
+
Tool, ToolKind, ToolExecutionMode,
|
|
11
|
+
ToolInvocation, ToolResult,
|
|
12
|
+
ToolExecutionUpdateCallback, AbortSignal, ToolContext,
|
|
13
|
+
)
|
|
14
|
+
from tau.tool.render import call_line
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _render_grep_call(args: dict, _streaming: bool) -> list[str]:
|
|
18
|
+
pattern = args.get("pattern", "")
|
|
19
|
+
path = args.get("path", "")
|
|
20
|
+
return call_line("grep", pattern, path)
|
|
21
|
+
|
|
22
|
+
_MAX_MATCHES = 500
|
|
23
|
+
_PREVIEW_LINES = 5
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _render_grep_result(content: str, opts: Any) -> list[str]:
|
|
27
|
+
from tau.tui.ansi import DIM, RESET
|
|
28
|
+
metadata = opts.metadata or {}
|
|
29
|
+
match_count = metadata.get("match_count", 0)
|
|
30
|
+
files_searched = metadata.get("files_searched", 0)
|
|
31
|
+
truncated = metadata.get("truncated", False)
|
|
32
|
+
|
|
33
|
+
if match_count == 0:
|
|
34
|
+
return ["No matches found"]
|
|
35
|
+
|
|
36
|
+
file_word = "file" if files_searched == 1 else "files"
|
|
37
|
+
match_word = "match" if match_count == 1 else "matches"
|
|
38
|
+
summary = f"Found {match_count} {match_word} in {files_searched} {file_word}"
|
|
39
|
+
if truncated:
|
|
40
|
+
summary += f" {DIM}(truncated){RESET}"
|
|
41
|
+
|
|
42
|
+
lines = [l for l in content.splitlines() if ":" in l]
|
|
43
|
+
result = [summary]
|
|
44
|
+
|
|
45
|
+
show = lines if opts.expanded else lines[:_PREVIEW_LINES]
|
|
46
|
+
for line in show:
|
|
47
|
+
file_part, _, rest = line.partition(":")
|
|
48
|
+
lineno, _, text = rest.partition(": ")
|
|
49
|
+
result.append(f"{DIM}{file_part}:{lineno.strip()}{RESET} {text}")
|
|
50
|
+
|
|
51
|
+
if opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
52
|
+
result.append(f"{DIM} (ctrl+o to collapse){RESET}")
|
|
53
|
+
elif not opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
54
|
+
result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GrepParams(BaseModel):
|
|
60
|
+
"""Parameters for the grep tool."""
|
|
61
|
+
pattern: str = Field(description="Regular expression to search for.")
|
|
62
|
+
path: str = Field(default="", description="File or directory to search. Defaults to the agent's cwd.")
|
|
63
|
+
include: str = Field(default="", description="Glob pattern to filter files (e.g. '*.py'). Only used when path is a directory.")
|
|
64
|
+
case_sensitive: bool = Field(default=True, description="Whether the pattern is case-sensitive.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GrepTool(Tool):
|
|
68
|
+
"""Tool for searching files by regex pattern."""
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
super().__init__(
|
|
71
|
+
name="grep",
|
|
72
|
+
description=(
|
|
73
|
+
"Search for a regex pattern in files. Returns matches as 'file:line: content', "
|
|
74
|
+
f"up to {_MAX_MATCHES} matches. When path is a directory, searches recursively."
|
|
75
|
+
),
|
|
76
|
+
schema=GrepParams,
|
|
77
|
+
kind=ToolKind.Read,
|
|
78
|
+
execution_mode=ToolExecutionMode.Parallel,
|
|
79
|
+
render_result=_render_grep_result,
|
|
80
|
+
render_call=_render_grep_call,
|
|
81
|
+
render_shell="default",
|
|
82
|
+
prompt_guidelines="Prefer over read when searching for a symbol, function, or pattern across the codebase.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def get_display_name(self, args: dict[str, Any]) -> str:
|
|
86
|
+
"""Get a short display name for the grep operation."""
|
|
87
|
+
return args.get("pattern", "grep")
|
|
88
|
+
|
|
89
|
+
async def execute(
|
|
90
|
+
self,
|
|
91
|
+
invocation: ToolInvocation,
|
|
92
|
+
tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
|
|
93
|
+
signal: Optional[AbortSignal] = None,
|
|
94
|
+
context: Optional[ToolContext] = None,
|
|
95
|
+
) -> ToolResult:
|
|
96
|
+
"""Execute the regex pattern search operation."""
|
|
97
|
+
params = GrepParams.model_validate(invocation.params)
|
|
98
|
+
|
|
99
|
+
flags = 0 if params.case_sensitive else re.IGNORECASE
|
|
100
|
+
try:
|
|
101
|
+
regex = re.compile(params.pattern, flags)
|
|
102
|
+
except re.error as e:
|
|
103
|
+
return ToolResult.error(invocation.id, f"Invalid regex pattern: {e}")
|
|
104
|
+
|
|
105
|
+
target = Path(params.path or invocation.cwd or ".").resolve()
|
|
106
|
+
files: list[Path] = []
|
|
107
|
+
|
|
108
|
+
if target.is_file():
|
|
109
|
+
files = [target]
|
|
110
|
+
elif target.is_dir():
|
|
111
|
+
glob_pat = f"**/{params.include}" if params.include else "**/*"
|
|
112
|
+
files = [p for p in target.glob(glob_pat) if p.is_file()]
|
|
113
|
+
else:
|
|
114
|
+
return ToolResult.error(invocation.id, f"Path not found: {target}")
|
|
115
|
+
|
|
116
|
+
matches: list[str] = []
|
|
117
|
+
truncated = False
|
|
118
|
+
|
|
119
|
+
for file in sorted(files):
|
|
120
|
+
if truncated:
|
|
121
|
+
break
|
|
122
|
+
try:
|
|
123
|
+
for lineno, line in enumerate(file.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
124
|
+
if regex.search(line):
|
|
125
|
+
matches.append(f"{file}:{lineno}: {line}")
|
|
126
|
+
if len(matches) >= _MAX_MATCHES:
|
|
127
|
+
truncated = True
|
|
128
|
+
break
|
|
129
|
+
except OSError:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
metadata = {
|
|
133
|
+
"pattern": params.pattern,
|
|
134
|
+
"files_searched": len(files),
|
|
135
|
+
"match_count": len(matches),
|
|
136
|
+
"truncated": truncated,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if not matches:
|
|
140
|
+
return ToolResult.ok(invocation.id, f"No matches for pattern: {params.pattern}", metadata=metadata)
|
|
141
|
+
|
|
142
|
+
result = "\n".join(matches)
|
|
143
|
+
if truncated:
|
|
144
|
+
result += f"\n\n[Results truncated at {_MAX_MATCHES} matches.]"
|
|
145
|
+
|
|
146
|
+
return ToolResult.ok(invocation.id, result, metadata=metadata)
|
tau/builtins/tools/ls.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from tau.tool.types import (
|
|
9
|
+
Tool, ToolKind, ToolExecutionMode,
|
|
10
|
+
ToolInvocation, ToolResult,
|
|
11
|
+
ToolExecutionUpdateCallback, AbortSignal, ToolContext,
|
|
12
|
+
)
|
|
13
|
+
from tau.tool.render import call_line
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _render_ls_call(args: dict, _streaming: bool) -> list[str]:
|
|
17
|
+
return call_line("ls", args.get("path", ""))
|
|
18
|
+
|
|
19
|
+
_PREVIEW_LINES = 5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LsParams(BaseModel):
|
|
23
|
+
"""Parameters for the ls tool."""
|
|
24
|
+
path: str = Field(default="", description="Directory path to list. Defaults to the agent's cwd.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_ls_result(content: str, opts: Any) -> list[str]:
|
|
28
|
+
from tau.tui.ansi import DIM, RESET
|
|
29
|
+
metadata = opts.metadata or {}
|
|
30
|
+
path = metadata.get("path", "")
|
|
31
|
+
file_count = metadata.get("file_count", 0)
|
|
32
|
+
dir_count = metadata.get("dir_count", 0)
|
|
33
|
+
entries = metadata.get("entries", [])
|
|
34
|
+
|
|
35
|
+
parts = []
|
|
36
|
+
if dir_count:
|
|
37
|
+
parts.append(f"{dir_count} {'dir' if dir_count == 1 else 'dirs'}")
|
|
38
|
+
if file_count:
|
|
39
|
+
parts.append(f"{file_count} {'file' if file_count == 1 else 'files'}")
|
|
40
|
+
summary = f"Found {', '.join(parts)}" if parts else (path or "empty directory")
|
|
41
|
+
result = [summary]
|
|
42
|
+
|
|
43
|
+
if not entries:
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
show = entries if opts.expanded else entries[:_PREVIEW_LINES]
|
|
47
|
+
for entry in show:
|
|
48
|
+
name = entry["name"]
|
|
49
|
+
is_dir = entry["is_dir"]
|
|
50
|
+
size_str = entry.get("size_str", "")
|
|
51
|
+
if is_dir:
|
|
52
|
+
result.append(f"{name}/")
|
|
53
|
+
else:
|
|
54
|
+
tail = f" {DIM}{size_str}{RESET}" if size_str else ""
|
|
55
|
+
result.append(f"{name}{tail}")
|
|
56
|
+
|
|
57
|
+
if opts.expanded and len(entries) > _PREVIEW_LINES:
|
|
58
|
+
result.append(f"{DIM} (ctrl+o to collapse){RESET}")
|
|
59
|
+
elif not opts.expanded and len(entries) > _PREVIEW_LINES:
|
|
60
|
+
result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LsTool(Tool):
|
|
66
|
+
"""Tool for listing directory contents."""
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
super().__init__(
|
|
69
|
+
name="ls",
|
|
70
|
+
description="List the contents of a directory, showing files and subdirectories.",
|
|
71
|
+
schema=LsParams,
|
|
72
|
+
kind=ToolKind.Read,
|
|
73
|
+
execution_mode=ToolExecutionMode.Parallel,
|
|
74
|
+
render_result=_render_ls_result,
|
|
75
|
+
render_call=_render_ls_call,
|
|
76
|
+
render_shell="default",
|
|
77
|
+
prompt_guidelines="Use to get an overview of a directory before diving into files. Use glob for targeted file discovery.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def get_display_name(self, args: dict[str, Any]) -> str:
|
|
81
|
+
return args.get("path", ".") or "."
|
|
82
|
+
|
|
83
|
+
async def execute(
|
|
84
|
+
self,
|
|
85
|
+
invocation: ToolInvocation,
|
|
86
|
+
tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
|
|
87
|
+
signal: Optional[AbortSignal] = None,
|
|
88
|
+
context: Optional[ToolContext] = None,
|
|
89
|
+
) -> ToolResult:
|
|
90
|
+
params = LsParams.model_validate(invocation.params)
|
|
91
|
+
target = Path(params.path or invocation.cwd or ".").resolve()
|
|
92
|
+
|
|
93
|
+
if not target.exists():
|
|
94
|
+
return ToolResult.error(invocation.id, f"Path not found: {target}")
|
|
95
|
+
if not target.is_dir():
|
|
96
|
+
return ToolResult.error(invocation.id, f"Not a directory: {target}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
raw_entries = sorted(target.iterdir(), key=lambda p: (p.is_file(), p.name.lower()))
|
|
100
|
+
except PermissionError:
|
|
101
|
+
return ToolResult.error(invocation.id, f"Permission denied: {target}")
|
|
102
|
+
|
|
103
|
+
file_count = dir_count = 0
|
|
104
|
+
entries = []
|
|
105
|
+
lines = [f"{target}/"]
|
|
106
|
+
for entry in raw_entries:
|
|
107
|
+
is_dir = entry.is_dir()
|
|
108
|
+
if is_dir:
|
|
109
|
+
dir_count += 1
|
|
110
|
+
else:
|
|
111
|
+
file_count += 1
|
|
112
|
+
try:
|
|
113
|
+
size_str = _human_size(entry.stat().st_size) if entry.is_file() else ""
|
|
114
|
+
except OSError:
|
|
115
|
+
size_str = ""
|
|
116
|
+
entries.append({"name": entry.name, "is_dir": is_dir, "size_str": size_str})
|
|
117
|
+
suffix = "/" if is_dir else ""
|
|
118
|
+
lines.append(f" {entry.name}{suffix}{f' {size_str}' if size_str else ''}")
|
|
119
|
+
|
|
120
|
+
metadata = {
|
|
121
|
+
"path": str(target),
|
|
122
|
+
"file_count": file_count,
|
|
123
|
+
"dir_count": dir_count,
|
|
124
|
+
"entries": entries,
|
|
125
|
+
}
|
|
126
|
+
return ToolResult.ok(invocation.id, "\n".join(lines), metadata=metadata)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _human_size(n: int) -> str:
|
|
130
|
+
"""Convert byte count to human-readable format (B, KB, MB, GB, TB)."""
|
|
131
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
132
|
+
if n < 1024:
|
|
133
|
+
return f"{n}{unit}" if unit == "B" else f"{n:.1f}{unit}"
|
|
134
|
+
n //= 1024
|
|
135
|
+
return f"{n:.1f}TB"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from tau.tool.types import (
|
|
9
|
+
Tool, ToolKind, ToolExecutionMode,
|
|
10
|
+
ToolInvocation, ToolResult,
|
|
11
|
+
ToolExecutionUpdateCallback, AbortSignal, ToolContext,
|
|
12
|
+
)
|
|
13
|
+
from tau.tool.render import call_line
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _render_read_call(args: dict, _streaming: bool) -> list[str]:
|
|
17
|
+
return call_line("read", args.get("path", ""))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReadParams(BaseModel):
|
|
21
|
+
"""Parameters for the read tool."""
|
|
22
|
+
path: str = Field(description="Absolute path to the file to read.")
|
|
23
|
+
offset: int = Field(default=0, ge=0, description="Line number to start reading from (0-based).")
|
|
24
|
+
limit: int = Field(default=2000, ge=1, description="Maximum number of lines to read.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_PREVIEW_LINES = 5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _render_read_result(content: str, opts: Any) -> list[str]:
|
|
31
|
+
from tau.tui.ansi import DIM, RESET
|
|
32
|
+
metadata = opts.metadata or {}
|
|
33
|
+
lines_returned = metadata.get("lines_returned", 0)
|
|
34
|
+
truncated = metadata.get("truncated", False)
|
|
35
|
+
|
|
36
|
+
line_word = "line" if lines_returned == 1 else "lines"
|
|
37
|
+
result = [f"Read {lines_returned} {line_word}"]
|
|
38
|
+
|
|
39
|
+
parsed = []
|
|
40
|
+
for raw in content.splitlines():
|
|
41
|
+
if "\t" in raw:
|
|
42
|
+
num, _, text = raw.partition("\t")
|
|
43
|
+
parsed.append((num.strip(), text))
|
|
44
|
+
|
|
45
|
+
if not parsed:
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
show = parsed if opts.expanded else parsed[:_PREVIEW_LINES]
|
|
49
|
+
for num, text in show:
|
|
50
|
+
result.append(f"{DIM}{num}{RESET} {text}")
|
|
51
|
+
|
|
52
|
+
if opts.expanded and (len(parsed) > _PREVIEW_LINES or truncated):
|
|
53
|
+
result.append(f"{DIM} (ctrl+o to collapse){RESET}")
|
|
54
|
+
elif not opts.expanded and (len(parsed) > _PREVIEW_LINES or truncated):
|
|
55
|
+
result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
|
|
56
|
+
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ReadTool(Tool):
|
|
61
|
+
"""Tool for reading file contents with line numbers."""
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
super().__init__(
|
|
64
|
+
name="read",
|
|
65
|
+
description=(
|
|
66
|
+
"Read the contents of a file. Returns lines with 1-based line numbers in the format "
|
|
67
|
+
"'<n>\\t<content>'. Use offset and limit to read large files in chunks."
|
|
68
|
+
),
|
|
69
|
+
schema=ReadParams,
|
|
70
|
+
kind=ToolKind.Read,
|
|
71
|
+
execution_mode=ToolExecutionMode.Parallel,
|
|
72
|
+
render_result=_render_read_result,
|
|
73
|
+
render_call=_render_read_call,
|
|
74
|
+
render_shell="default",
|
|
75
|
+
prompt_guidelines="Use grep first to locate the relevant section, then read with offset/limit instead of loading the entire file.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_display_name(self, args: dict[str, Any]) -> str:
|
|
79
|
+
"""Get a short display name for the read operation."""
|
|
80
|
+
return args.get("path", "read")
|
|
81
|
+
|
|
82
|
+
async def execute(
|
|
83
|
+
self,
|
|
84
|
+
invocation: ToolInvocation,
|
|
85
|
+
tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
|
|
86
|
+
signal: Optional[AbortSignal] = None,
|
|
87
|
+
context: Optional[ToolContext] = None,
|
|
88
|
+
) -> ToolResult:
|
|
89
|
+
"""Execute the file read operation."""
|
|
90
|
+
params = ReadParams.model_validate(invocation.params)
|
|
91
|
+
path = Path(params.path)
|
|
92
|
+
|
|
93
|
+
if not path.exists():
|
|
94
|
+
return ToolResult.error(invocation.id, f"File not found: {params.path}")
|
|
95
|
+
if not path.is_file():
|
|
96
|
+
return ToolResult.error(invocation.id, f"Not a file: {params.path}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
100
|
+
except OSError as e:
|
|
101
|
+
return ToolResult.error(invocation.id, f"Cannot read file: {e}")
|
|
102
|
+
|
|
103
|
+
total = len(lines)
|
|
104
|
+
start = params.offset
|
|
105
|
+
end = min(start + params.limit, total)
|
|
106
|
+
chunk = lines[start:end]
|
|
107
|
+
|
|
108
|
+
numbered = "\n".join(f"{start + i + 1}\t{line}" for i, line in enumerate(chunk))
|
|
109
|
+
|
|
110
|
+
footer = ""
|
|
111
|
+
truncated = end < total
|
|
112
|
+
if truncated:
|
|
113
|
+
footer = f"\n\n[Showing lines {start + 1}–{end} of {total}. Use offset={end} to read more.]"
|
|
114
|
+
|
|
115
|
+
metadata = {
|
|
116
|
+
"file_path": str(path),
|
|
117
|
+
"total_lines": total,
|
|
118
|
+
"lines_returned": len(chunk),
|
|
119
|
+
"offset": start,
|
|
120
|
+
"truncated": truncated,
|
|
121
|
+
}
|
|
122
|
+
return ToolResult.ok(invocation.id, numbered + footer, metadata=metadata)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from tau.tool.types import (
|
|
9
|
+
Tool, ToolKind, ToolExecutionMode,
|
|
10
|
+
ToolInvocation, ToolResult,
|
|
11
|
+
ToolExecutionUpdateCallback, AbortSignal, ToolContext,
|
|
12
|
+
)
|
|
13
|
+
from tau.tool.render import call_line
|
|
14
|
+
|
|
15
|
+
_DEFAULT_TIMEOUT = 120
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _render_terminal_call(args: dict, _streaming: bool) -> list[str]:
|
|
19
|
+
return call_line("terminal", args.get("command", ""))
|
|
20
|
+
_PREVIEW_LINES = 5
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _render_terminal_result(content: str, opts: Any) -> list[str]:
|
|
24
|
+
from tau.tui.ansi import RED, DIM, RESET
|
|
25
|
+
metadata = opts.metadata or {}
|
|
26
|
+
exit_code = metadata.get("exit_code", 0)
|
|
27
|
+
timed_out = metadata.get("timed_out", False)
|
|
28
|
+
failed = timed_out or exit_code != 0
|
|
29
|
+
|
|
30
|
+
if timed_out:
|
|
31
|
+
return [f"{RED}Timed out{RESET}"]
|
|
32
|
+
|
|
33
|
+
lines = content.splitlines() if content else []
|
|
34
|
+
if not lines:
|
|
35
|
+
return [f"{RED}Failed (exit {exit_code}, no output){RESET}" if failed else "(no output)"]
|
|
36
|
+
|
|
37
|
+
def fmt(line: str) -> str:
|
|
38
|
+
return f"{RED}{line}{RESET}" if failed else line
|
|
39
|
+
|
|
40
|
+
show = lines if opts.expanded else lines[:_PREVIEW_LINES]
|
|
41
|
+
result = [fmt(show[0])]
|
|
42
|
+
for line in show[1:]:
|
|
43
|
+
result.append(fmt(line))
|
|
44
|
+
|
|
45
|
+
if opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
46
|
+
result.append(f"{DIM} (ctrl+o to collapse){RESET}")
|
|
47
|
+
elif not opts.expanded and len(lines) > _PREVIEW_LINES:
|
|
48
|
+
result.append(f"{DIM} ··· (ctrl+o to expand){RESET}")
|
|
49
|
+
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TerminalParams(BaseModel):
|
|
54
|
+
"""Parameters for terminal command execution."""
|
|
55
|
+
command: str = Field(description="Shell command to execute.")
|
|
56
|
+
timeout: int = Field(
|
|
57
|
+
default=_DEFAULT_TIMEOUT,
|
|
58
|
+
ge=1,
|
|
59
|
+
le=600,
|
|
60
|
+
description=f"Timeout in seconds (default {_DEFAULT_TIMEOUT}, max 600).",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TerminalTool(Tool):
|
|
65
|
+
"""Tool for executing shell commands."""
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
super().__init__(
|
|
68
|
+
name="terminal",
|
|
69
|
+
description=(
|
|
70
|
+
"Execute a shell command and return its combined stdout+stderr output. "
|
|
71
|
+
"Commands run in the agent's working directory. "
|
|
72
|
+
"Avoid interactive commands or those that require user input."
|
|
73
|
+
),
|
|
74
|
+
schema=TerminalParams,
|
|
75
|
+
kind=ToolKind.Execute,
|
|
76
|
+
render_result=_render_terminal_result,
|
|
77
|
+
render_call=_render_terminal_call,
|
|
78
|
+
render_shell="default",
|
|
79
|
+
prompt_guidelines="Run tests or the build after making code changes to verify correctness.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def get_display_name(self, args: dict[str, Any]) -> str:
|
|
83
|
+
"""Get a short display name for the command."""
|
|
84
|
+
cmd = args.get("command", "")
|
|
85
|
+
return cmd[:60] + ("…" if len(cmd) > 60 else "")
|
|
86
|
+
|
|
87
|
+
async def execute(
|
|
88
|
+
self,
|
|
89
|
+
invocation: ToolInvocation,
|
|
90
|
+
tool_execution_update_callback: Optional[ToolExecutionUpdateCallback] = None,
|
|
91
|
+
signal: Optional[AbortSignal] = None,
|
|
92
|
+
context: Optional[ToolContext] = None,
|
|
93
|
+
) -> ToolResult:
|
|
94
|
+
"""Execute a shell command and return the output."""
|
|
95
|
+
params = TerminalParams.model_validate(invocation.params)
|
|
96
|
+
cwd = invocation.cwd or None
|
|
97
|
+
|
|
98
|
+
sm = context.settings if context is not None else None
|
|
99
|
+
shell_path = sm.get_shell_path() if sm is not None else None
|
|
100
|
+
shell_prefix = sm.get_shell_command_prefix() if sm is not None else None
|
|
101
|
+
|
|
102
|
+
command = params.command
|
|
103
|
+
if shell_prefix:
|
|
104
|
+
command = f"{shell_prefix}\n{command}"
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
if shell_path:
|
|
108
|
+
proc = await asyncio.create_subprocess_exec(
|
|
109
|
+
shell_path, "-c", command,
|
|
110
|
+
stdout=asyncio.subprocess.PIPE,
|
|
111
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
112
|
+
cwd=cwd,
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
proc = await asyncio.create_subprocess_shell(
|
|
116
|
+
command,
|
|
117
|
+
stdout=asyncio.subprocess.PIPE,
|
|
118
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
119
|
+
cwd=cwd,
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=params.timeout)
|
|
123
|
+
except asyncio.TimeoutError:
|
|
124
|
+
proc.kill()
|
|
125
|
+
await proc.communicate()
|
|
126
|
+
return ToolResult.error(
|
|
127
|
+
invocation.id,
|
|
128
|
+
f"Command timed out after {params.timeout}s: {params.command}",
|
|
129
|
+
metadata={"command": params.command, "exit_code": -1, "timed_out": True, "output_length": 0},
|
|
130
|
+
)
|
|
131
|
+
except OSError as e:
|
|
132
|
+
return ToolResult.error(invocation.id, f"Failed to start command: {e}")
|
|
133
|
+
|
|
134
|
+
output = stdout.decode("utf-8", errors="replace")
|
|
135
|
+
rc = proc.returncode or 0
|
|
136
|
+
metadata = {
|
|
137
|
+
"command": params.command,
|
|
138
|
+
"exit_code": rc,
|
|
139
|
+
"timed_out": False,
|
|
140
|
+
"output_length": len(output),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if rc != 0:
|
|
144
|
+
return ToolResult(
|
|
145
|
+
id=invocation.id,
|
|
146
|
+
content=output or f"(exit code {rc}, no output)",
|
|
147
|
+
is_error=True,
|
|
148
|
+
metadata=metadata,
|
|
149
|
+
)
|
|
150
|
+
return ToolResult.ok(invocation.id, output or "(no output)", metadata=metadata)
|