vamp-cli 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.
- vamp_cli/__init__.py +0 -0
- vamp_cli/agent/__init__.py +0 -0
- vamp_cli/agent/context_sync.py +72 -0
- vamp_cli/agent/graph.py +159 -0
- vamp_cli/agent/instructions.py +18 -0
- vamp_cli/agent/nodes.py +375 -0
- vamp_cli/agent/roles.py +8 -0
- vamp_cli/agent/runtime/__init__.py +0 -0
- vamp_cli/agent/runtime/runtime.py +133 -0
- vamp_cli/agent/state.py +55 -0
- vamp_cli/agent/status.py +17 -0
- vamp_cli/config/__init__.py +0 -0
- vamp_cli/config/loader.py +152 -0
- vamp_cli/config/schema.py +82 -0
- vamp_cli/llm/__init__.py +0 -0
- vamp_cli/llm/adapter.py +52 -0
- vamp_cli/llm/adapter_registry.py +26 -0
- vamp_cli/llm/debug_callbacks.py +18 -0
- vamp_cli/llm/router.py +247 -0
- vamp_cli/llm/usage.py +170 -0
- vamp_cli/logging_setup/logging_setup.py +37 -0
- vamp_cli/main.py +1023 -0
- vamp_cli/persistence/__init__.py +29 -0
- vamp_cli/persistence/compaction.py +372 -0
- vamp_cli/persistence/emitter.py +371 -0
- vamp_cli/persistence/event_log.py +303 -0
- vamp_cli/persistence/fork.py +81 -0
- vamp_cli/persistence/loader.py +616 -0
- vamp_cli/persistence/transcript.py +160 -0
- vamp_cli/prompts/prompts.py +182 -0
- vamp_cli/sandbox/__init__.py +0 -0
- vamp_cli/sandbox/base.py +22 -0
- vamp_cli/sandbox/config.py +16 -0
- vamp_cli/sandbox/docker.py +293 -0
- vamp_cli/sandbox/manager.py +31 -0
- vamp_cli/sandbox/result.py +14 -0
- vamp_cli/sandbox/terminal.py +151 -0
- vamp_cli/tools/__init__.py +0 -0
- vamp_cli/tools/base.py +44 -0
- vamp_cli/tools/diagnostic.py +120 -0
- vamp_cli/tools/execute_cmd.py +16 -0
- vamp_cli/tools/files.py +652 -0
- vamp_cli/tools/registry.py +163 -0
- vamp_cli/tools/tools_classes.py +622 -0
- vamp_cli/utils/__init__.py +0 -0
- vamp_cli/utils/error_codes.py +7 -0
- vamp_cli/utils/ignore.py +36 -0
- vamp_cli/utils/sha_256.py +32 -0
- vamp_cli/utils/token_tracker.py +31 -0
- vamp_cli/workspace/__init__.py +0 -0
- vamp_cli/workspace/workspace.py +16 -0
- vamp_cli-0.1.0.dist-info/METADATA +317 -0
- vamp_cli-0.1.0.dist-info/RECORD +57 -0
- vamp_cli-0.1.0.dist-info/WHEEL +5 -0
- vamp_cli-0.1.0.dist-info/entry_points.txt +2 -0
- vamp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- vamp_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from vamp_cli.sandbox.base import Sandbox
|
|
5
|
+
from vamp_cli.sandbox.result import CommandResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ANSI_ESCAPE_RE = re.compile(
|
|
9
|
+
r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def _strip_ansi(text: str) -> str:
|
|
13
|
+
return ANSI_ESCAPE_RE.sub("", text)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _truncate_output(
|
|
17
|
+
text: str,
|
|
18
|
+
max_chars: int = 10000,
|
|
19
|
+
) -> tuple[str, bool]:
|
|
20
|
+
if len(text) <= max_chars:
|
|
21
|
+
return text, False
|
|
22
|
+
|
|
23
|
+
half = max_chars // 2
|
|
24
|
+
omitted = len(text) - max_chars
|
|
25
|
+
|
|
26
|
+
output = (
|
|
27
|
+
f"{text[:half]}\n\n"
|
|
28
|
+
f"... [Output truncated: {omitted} characters omitted] ...\n\n"
|
|
29
|
+
f"{text[-half:]}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return output, True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Terminal:
|
|
36
|
+
"""
|
|
37
|
+
Shell interface exposed to the agent.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, sandbox: Sandbox) -> None:
|
|
41
|
+
self.sandbox = sandbox
|
|
42
|
+
|
|
43
|
+
async def execute(
|
|
44
|
+
self,
|
|
45
|
+
command: str,
|
|
46
|
+
cwd: str = ".",
|
|
47
|
+
timeout: int = 60,
|
|
48
|
+
max_output_chars: int = 12000,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Execute a shell command inside the configured sandbox.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
command:
|
|
55
|
+
Shell command to execute.
|
|
56
|
+
|
|
57
|
+
cwd:
|
|
58
|
+
Working directory relative to /workspace.
|
|
59
|
+
|
|
60
|
+
timeout:
|
|
61
|
+
Maximum execution time in seconds.
|
|
62
|
+
|
|
63
|
+
max_output_chars:
|
|
64
|
+
Maximum amount of combined stdout/stderr returned
|
|
65
|
+
to the model.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary containing execution status and output.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if not command.strip():
|
|
72
|
+
return {
|
|
73
|
+
"success": False,
|
|
74
|
+
"command": command,
|
|
75
|
+
"exit_code": -1,
|
|
76
|
+
"error": "Command cannot be empty.",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if timeout <= 0:
|
|
80
|
+
return {
|
|
81
|
+
"success": False,
|
|
82
|
+
"command": command,
|
|
83
|
+
"exit_code": -1,
|
|
84
|
+
"error": "Timeout must be greater than zero.",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if max_output_chars <= 0:
|
|
88
|
+
return {
|
|
89
|
+
"success": False,
|
|
90
|
+
"command": command,
|
|
91
|
+
"exit_code": -1,
|
|
92
|
+
"error": "max_output_chars must be greater than zero.",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
result: CommandResult = await self.sandbox.execute(
|
|
97
|
+
command=command,
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
timeout=timeout,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
return {
|
|
104
|
+
"success": False,
|
|
105
|
+
"command": command,
|
|
106
|
+
"exit_code": -1,
|
|
107
|
+
"error": str(e),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return {
|
|
112
|
+
"success": False,
|
|
113
|
+
"command": command,
|
|
114
|
+
"exit_code": -1,
|
|
115
|
+
"error": f"Failed to execute command: {str(e)}",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
stdout = _strip_ansi(result.stdout or "")
|
|
119
|
+
stderr = _strip_ansi(result.stderr or "")
|
|
120
|
+
|
|
121
|
+
if stderr:
|
|
122
|
+
combined_output = (
|
|
123
|
+
stdout + "\n" + stderr
|
|
124
|
+
).strip()
|
|
125
|
+
else:
|
|
126
|
+
combined_output = stdout.strip()
|
|
127
|
+
|
|
128
|
+
combined_output, truncated = _truncate_output(
|
|
129
|
+
combined_output,
|
|
130
|
+
max_output_chars,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if result.timed_out:
|
|
134
|
+
return {
|
|
135
|
+
"success": False,
|
|
136
|
+
"command": command,
|
|
137
|
+
"exit_code": 124,
|
|
138
|
+
"error": f"Command timed out after {timeout} seconds.",
|
|
139
|
+
"output": combined_output or "(No output)",
|
|
140
|
+
"timed_out": True,
|
|
141
|
+
"truncated": truncated,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"success": result.exit_code == 0,
|
|
146
|
+
"command": command,
|
|
147
|
+
"exit_code": result.exit_code,
|
|
148
|
+
"output": combined_output or "(No output)",
|
|
149
|
+
"timed_out": False,
|
|
150
|
+
"truncated": truncated,
|
|
151
|
+
}
|
|
File without changes
|
vamp_cli/tools/base.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Any
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InputSchema(BaseModel):
|
|
9
|
+
type: str
|
|
10
|
+
properties: dict[str, Any]
|
|
11
|
+
required: list[str]
|
|
12
|
+
additionalProperties: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Tools(ABC):
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
input_schema: InputSchema
|
|
20
|
+
parallel_safe: bool = False
|
|
21
|
+
|
|
22
|
+
def schema(self) -> dict[str, Any]:
|
|
23
|
+
return {
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"description": self.description,
|
|
26
|
+
"input_schema": self.input_schema,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def to_openai_tool(self) -> dict[str, Any]:
|
|
30
|
+
"""OpenAI function-calling shape accepted by ChatLiteLLM.bind_tools()."""
|
|
31
|
+
return {
|
|
32
|
+
"type": "function",
|
|
33
|
+
"function": {
|
|
34
|
+
"name": self.name,
|
|
35
|
+
"description": self.description,
|
|
36
|
+
"parameters": self.input_schema.model_dump(),
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def run(self, **kwargs: Any) -> str:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
async def arun(self, **kwargs: Any) -> str:
|
|
44
|
+
return await asyncio.to_thread(self.run, **kwargs)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from vamp_cli.workspace.workspace import Workspace
|
|
7
|
+
from vamp_cli.utils.error_codes import Error_codes
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DiagnosticTools:
|
|
11
|
+
def __init__(self, workspace: Workspace) -> None:
|
|
12
|
+
self.workspace = workspace
|
|
13
|
+
|
|
14
|
+
def check_file_syntax(self, file_path: str) -> dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
syntax check for newly modified files
|
|
17
|
+
"""
|
|
18
|
+
path = self.workspace.safe_path(file_path)
|
|
19
|
+
|
|
20
|
+
if path == Error_codes.PATH_SCOPE_ERROR:
|
|
21
|
+
return {
|
|
22
|
+
"parent": path,
|
|
23
|
+
"error": Error_codes.PATH_SCOPE_ERROR.value
|
|
24
|
+
}
|
|
25
|
+
path = Path(path)
|
|
26
|
+
|
|
27
|
+
if not path.is_file():
|
|
28
|
+
return {"valid": False, "error": f"File '{file_path}' does not exist."}
|
|
29
|
+
|
|
30
|
+
# Python AST Check
|
|
31
|
+
if path.suffix == ".py":
|
|
32
|
+
try:
|
|
33
|
+
code = path.read_text(encoding="utf-8", errors="replace")
|
|
34
|
+
ast.parse(code, filename=str(path))
|
|
35
|
+
return {"valid": True, "diagnostics": []}
|
|
36
|
+
except SyntaxError as e:
|
|
37
|
+
return {
|
|
38
|
+
"valid": False,
|
|
39
|
+
"diagnostics": [{
|
|
40
|
+
"file": file_path,
|
|
41
|
+
"line": e.lineno,
|
|
42
|
+
"column": e.offset,
|
|
43
|
+
"severity": "error",
|
|
44
|
+
"message": f"SyntaxError: {e.msg}",
|
|
45
|
+
"source": "python_ast"
|
|
46
|
+
}]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# JSON Parse Check
|
|
50
|
+
if path.suffix == ".json":
|
|
51
|
+
try:
|
|
52
|
+
json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
|
53
|
+
return {"valid": True, "diagnostics": []}
|
|
54
|
+
except json.JSONDecodeError as e:
|
|
55
|
+
return {
|
|
56
|
+
"valid": False,
|
|
57
|
+
"diagnostics": [{
|
|
58
|
+
"file": file_path,
|
|
59
|
+
"line": e.lineno,
|
|
60
|
+
"column": e.colno,
|
|
61
|
+
"severity": "error",
|
|
62
|
+
"message": f"JSONDecodeError: {e.msg}",
|
|
63
|
+
"source": "json_parser"
|
|
64
|
+
}]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {"valid": True, "diagnostics": []}
|
|
68
|
+
|
|
69
|
+
def get_workspace_diagnostics(self, target_path: str = ".") -> dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Runs project-level linters if available in the environment and returns
|
|
72
|
+
structured diagnostics for the LLM.
|
|
73
|
+
"""
|
|
74
|
+
diagnostics = []
|
|
75
|
+
root = Path(self.workspace.root)
|
|
76
|
+
|
|
77
|
+
# Python: Fast Ruff check if installed
|
|
78
|
+
ruff_bin = subprocess.run(["which", "ruff"], capture_output=True, text=True).stdout.strip()
|
|
79
|
+
if ruff_bin:
|
|
80
|
+
cmd = ["ruff", "check", target_path, "--output-format=json"]
|
|
81
|
+
proc = subprocess.run(cmd, cwd=root, capture_output=True, text=True)
|
|
82
|
+
if proc.stdout.strip():
|
|
83
|
+
try:
|
|
84
|
+
ruff_errors = json.loads(proc.stdout)
|
|
85
|
+
for err in ruff_errors[:30]: # Cap output to avoid context overflow
|
|
86
|
+
diagnostics.append({
|
|
87
|
+
"file": err.get("filename"),
|
|
88
|
+
"line": err.get("location", {}).get("row"),
|
|
89
|
+
"column": err.get("location", {}).get("column"),
|
|
90
|
+
"severity": "error",
|
|
91
|
+
"code": err.get("code"),
|
|
92
|
+
"message": err.get("message"),
|
|
93
|
+
"source": "ruff"
|
|
94
|
+
})
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
# TypeScript / JavaScript: Check for tsc if tsconfig exists
|
|
99
|
+
if (root / "tsconfig.json").is_file():
|
|
100
|
+
npx_bin = subprocess.run(["which", "npx"], capture_output=True, text=True).stdout.strip()
|
|
101
|
+
if npx_bin:
|
|
102
|
+
proc = subprocess.run(
|
|
103
|
+
["npx", "tsc", "--noEmit", "--pretty", "false"],
|
|
104
|
+
cwd=root,
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True
|
|
107
|
+
)
|
|
108
|
+
for line in proc.stdout.splitlines()[:20]:
|
|
109
|
+
if "error TS" in line:
|
|
110
|
+
diagnostics.append({
|
|
111
|
+
"raw": line.strip(),
|
|
112
|
+
"severity": "error",
|
|
113
|
+
"source": "tsc"
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"has_errors": len(diagnostics) > 0,
|
|
118
|
+
"total_diagnostics": len(diagnostics),
|
|
119
|
+
"diagnostics": diagnostics
|
|
120
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from vamp_cli.sandbox.terminal import Terminal
|
|
2
|
+
|
|
3
|
+
class ExecuteCommandTool:
|
|
4
|
+
|
|
5
|
+
def __init__(self, terminal: Terminal):
|
|
6
|
+
self.terminal = terminal
|
|
7
|
+
|
|
8
|
+
async def execute_command(
|
|
9
|
+
self,
|
|
10
|
+
command: str,
|
|
11
|
+
cwd: str = ".",
|
|
12
|
+
):
|
|
13
|
+
return await self.terminal.execute(
|
|
14
|
+
command=command,
|
|
15
|
+
cwd=cwd,
|
|
16
|
+
)
|