vial-ml 0.1.0__tar.gz

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.
vial_ml-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.3
2
+ Name: vial-ml
3
+ Version: 0.1.0
4
+ Summary: Surgical code isolation for AI agents
5
+ Author: Momchil Georgiev
6
+ Author-email: Momchil Georgiev <40865235+momchilgeorgiev@users.noreply.github.com>
7
+ Requires-Dist: typer>=0.25.1
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Vial
12
+
13
+ Surgical code isolation for AI agents.
14
+
15
+ Vial extracts a single function or class from a large Python file into a clean, focused workspace file so an agent only sees — and edits — exactly what it needs to.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install vial
21
+ # or with uv:
22
+ uv add vial
23
+ ```
24
+
25
+ ## CLI
26
+
27
+ ```bash
28
+ # Isolate a function
29
+ vial extract billing.py process_payment
30
+
31
+ # Agent edits .vial_workspace/process_payment_isolated.py ...
32
+
33
+ # Stitch it back (validates syntax first)
34
+ vial merge
35
+ ```
36
+
37
+ ## Python API
38
+
39
+ ```python
40
+ from vial import Vial
41
+
42
+ v = Vial()
43
+ isolated_path = v.extract("billing.py", "process_payment")
44
+ # agent edits isolated_path ...
45
+ v.merge()
46
+ ```
47
+
48
+ ## Why
49
+
50
+ When an agent reads a 2,000-line file to fix a 20-line function it wastes tokens, risks editing the wrong code, and produces complex diffs that frequently fail. Vial reduces the context to only what matters, cutting token usage by 71–88% on typical files.
51
+
52
+ ## How it works
53
+
54
+ 1. **Extract** — AST-based line-bound finder locates the exact start/end of the target. A protected context header (imports + module-level globals, commented out) is prepended so the agent has the context it needs without being able to accidentally modify it.
55
+ 2. **Edit** — The agent modifies only the isolated file.
56
+ 3. **Merge** — Vial validates the modified code's syntax, then splices it back into the original file at the exact line range. Surrounding code is never touched.
57
+
58
+ All session state lives in a `metadata.json` file inside the workspace directory.
@@ -0,0 +1,48 @@
1
+ # Vial
2
+
3
+ Surgical code isolation for AI agents.
4
+
5
+ Vial extracts a single function or class from a large Python file into a clean, focused workspace file so an agent only sees — and edits — exactly what it needs to.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install vial
11
+ # or with uv:
12
+ uv add vial
13
+ ```
14
+
15
+ ## CLI
16
+
17
+ ```bash
18
+ # Isolate a function
19
+ vial extract billing.py process_payment
20
+
21
+ # Agent edits .vial_workspace/process_payment_isolated.py ...
22
+
23
+ # Stitch it back (validates syntax first)
24
+ vial merge
25
+ ```
26
+
27
+ ## Python API
28
+
29
+ ```python
30
+ from vial import Vial
31
+
32
+ v = Vial()
33
+ isolated_path = v.extract("billing.py", "process_payment")
34
+ # agent edits isolated_path ...
35
+ v.merge()
36
+ ```
37
+
38
+ ## Why
39
+
40
+ When an agent reads a 2,000-line file to fix a 20-line function it wastes tokens, risks editing the wrong code, and produces complex diffs that frequently fail. Vial reduces the context to only what matters, cutting token usage by 71–88% on typical files.
41
+
42
+ ## How it works
43
+
44
+ 1. **Extract** — AST-based line-bound finder locates the exact start/end of the target. A protected context header (imports + module-level globals, commented out) is prepended so the agent has the context it needs without being able to accidentally modify it.
45
+ 2. **Edit** — The agent modifies only the isolated file.
46
+ 3. **Merge** — Vial validates the modified code's syntax, then splices it back into the original file at the exact line range. Surrounding code is never touched.
47
+
48
+ All session state lives in a `metadata.json` file inside the workspace directory.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "vial-ml"
3
+ version = "0.1.0"
4
+ description = "Surgical code isolation for AI agents"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Momchil Georgiev", email = "40865235+momchilgeorgiev@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "typer>=0.25.1",
12
+ ]
13
+
14
+ [project.scripts]
15
+ vial = "vial.cli:app"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.8.14,<0.9.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv.build-backend]
22
+ module-name = "vial"
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+
27
+ [tool.coverage.run]
28
+ source = ["src/vial"]
29
+ omit = ["*/cli.py"]
30
+
31
+ [tool.coverage.report]
32
+ show_missing = true
33
+ fail_under = 85
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=9.0.3",
38
+ "pytest-cov>=7.1.0",
39
+ ]
@@ -0,0 +1,61 @@
1
+ from pathlib import Path
2
+ from vial.workspace import Workspace
3
+ from vial.extractor import find_target
4
+ from vial.context import extract_context
5
+ from vial.merger import validate_syntax, strip_header, splice_back
6
+
7
+ HEADER_DELIMITER = "#" + "=" * 40 + " END OF CONTEXT " + "=" * 40
8
+
9
+
10
+ class Vial:
11
+ def __init__(self, workspace_dir: str | Path = ".vial_workspace"):
12
+ self.workspace = Workspace(workspace_dir)
13
+
14
+ def extract(self, source_filepath: str | Path, target_name: str) -> str:
15
+ """Extract target_name from source_filepath into an isolated workspace file."""
16
+ source = Path(source_filepath)
17
+ source_code = source.read_text()
18
+
19
+ result = find_target(source_code, target_name)
20
+ context = extract_context(source_code)
21
+
22
+ header = "# Context from original file (read-only — do not modify this block):\n"
23
+ if context:
24
+ header += "\n".join(f"# {line}" for line in context.splitlines()) + "\n"
25
+ header += HEADER_DELIMITER + "\n"
26
+
27
+ isolated_path = self.workspace.isolated_path(target_name)
28
+ isolated_path.write_text(header + result.code)
29
+
30
+ self.workspace.save_metadata({
31
+ "source_filepath": str(source.resolve()),
32
+ "target_name": target_name,
33
+ "start_line": result.start_line,
34
+ "end_line": result.end_line,
35
+ "isolated_filepath": str(isolated_path.resolve()),
36
+ })
37
+
38
+ return str(isolated_path)
39
+
40
+ def merge(self) -> None:
41
+ """Validate and merge the modified isolated file back into the source."""
42
+ meta = self.workspace.load_metadata()
43
+ isolated = Path(meta["isolated_filepath"])
44
+ content = isolated.read_text()
45
+
46
+ modified_code = strip_header(content, HEADER_DELIMITER)
47
+
48
+ is_valid, error = validate_syntax(modified_code)
49
+ if not is_valid:
50
+ raise ValueError(
51
+ f"Merge aborted — agent produced invalid Python code:\n{error}"
52
+ )
53
+
54
+ splice_back(
55
+ source_filepath=Path(meta["source_filepath"]),
56
+ modified_code=modified_code,
57
+ start_line=meta["start_line"],
58
+ end_line=meta["end_line"],
59
+ )
60
+
61
+ self.workspace.cleanup()
@@ -0,0 +1,36 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from vial import Vial
4
+
5
+ app = typer.Typer(help="Vial — surgical code isolation for AI agents.")
6
+
7
+
8
+ @app.command()
9
+ def extract(
10
+ source: Path = typer.Argument(..., help="Path to the Python source file."),
11
+ target: str = typer.Argument(..., help="Name of the function or class to extract."),
12
+ workspace: Path = typer.Option(".vial_workspace", help="Workspace directory."),
13
+ ):
14
+ """Extract a named function or class into an isolated workspace file."""
15
+ try:
16
+ v = Vial(workspace_dir=workspace)
17
+ isolated = v.extract(source, target)
18
+ typer.echo(f"Isolated '{target}' → {isolated}")
19
+ except (ValueError, FileNotFoundError) as e:
20
+ typer.echo(f"Error: {e}", err=True)
21
+ raise typer.Exit(code=1)
22
+
23
+
24
+ @app.command()
25
+ def merge(
26
+ workspace: Path = typer.Option(".vial_workspace", help="Workspace directory."),
27
+ ):
28
+ """Validate and merge the modified isolated file back into the source."""
29
+ try:
30
+ v = Vial(workspace_dir=workspace)
31
+ meta = v.workspace.load_metadata()
32
+ v.merge()
33
+ typer.echo(f"Merged '{meta['target_name']}' back into {meta['source_filepath']}")
34
+ except (ValueError, FileNotFoundError) as e:
35
+ typer.echo(f"Error: {e}", err=True)
36
+ raise typer.Exit(code=1)
@@ -0,0 +1,18 @@
1
+ import ast
2
+
3
+
4
+ def extract_context(source_code: str) -> str:
5
+ """Return import statements and top-level assignments as a single string."""
6
+ if not source_code.strip():
7
+ return ""
8
+
9
+ tree = ast.parse(source_code)
10
+ lines = source_code.splitlines()
11
+ collected: list[str] = []
12
+
13
+ for node in tree.body:
14
+ if isinstance(node, (ast.Import, ast.ImportFrom, ast.Assign, ast.AnnAssign)):
15
+ chunk = lines[node.lineno - 1 : node.end_lineno]
16
+ collected.extend(chunk)
17
+
18
+ return "\n".join(collected)
@@ -0,0 +1,34 @@
1
+ import ast
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class ExtractionResult:
7
+ target_name: str
8
+ start_line: int # 0-indexed, inclusive
9
+ end_line: int # 0-indexed, exclusive
10
+ code: str
11
+
12
+
13
+ def find_target(source_code: str, target_name: str) -> ExtractionResult:
14
+ """Locate a named function or class and return its bounds and source."""
15
+ tree = ast.parse(source_code)
16
+ lines = source_code.splitlines()
17
+
18
+ for node in ast.walk(tree):
19
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
20
+ if node.name == target_name:
21
+ start = node.lineno - 1
22
+ end = node.end_lineno
23
+ code = "\n".join(lines[start:end])
24
+ return ExtractionResult(
25
+ target_name=target_name,
26
+ start_line=start,
27
+ end_line=end,
28
+ code=code,
29
+ )
30
+
31
+ raise ValueError(
32
+ f"'{target_name}' not found in source. "
33
+ "Check the name and make sure it is a top-level function or class."
34
+ )
@@ -0,0 +1,34 @@
1
+ import ast
2
+ from pathlib import Path
3
+
4
+
5
+ def validate_syntax(code: str) -> tuple[bool, str]:
6
+ """Returns (True, '') if code is valid Python, else (False, error_message)."""
7
+ try:
8
+ ast.parse(code)
9
+ return True, ""
10
+ except SyntaxError as e:
11
+ return False, f"SyntaxError on line {e.lineno}: {e.msg}\n{e.text or ''}"
12
+
13
+
14
+ def strip_header(content: str, delimiter: str) -> str:
15
+ """Remove the read-only context header block above the delimiter."""
16
+ if delimiter in content:
17
+ _, code = content.split(delimiter, 1)
18
+ return code.strip("\n")
19
+ return content.strip("\n")
20
+
21
+
22
+ def splice_back(
23
+ source_filepath: Path,
24
+ modified_code: str,
25
+ start_line: int,
26
+ end_line: int,
27
+ ) -> Path:
28
+ """Replace lines [start_line:end_line] in source_filepath with modified_code."""
29
+ original_lines = source_filepath.read_text().splitlines()
30
+ modified_lines = modified_code.splitlines()
31
+
32
+ new_lines = original_lines[:start_line] + modified_lines + original_lines[end_line:]
33
+ source_filepath.write_text("\n".join(new_lines) + "\n")
34
+ return source_filepath
@@ -0,0 +1,31 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+
5
+ class Workspace:
6
+ METADATA_FILENAME = "metadata.json"
7
+
8
+ def __init__(self, directory: Path | str = ".vial_workspace"):
9
+ self.dir = Path(directory)
10
+ self.dir.mkdir(parents=True, exist_ok=True)
11
+ self.metadata_path = self.dir / self.METADATA_FILENAME
12
+
13
+ def isolated_path(self, target_name: str) -> Path:
14
+ return self.dir / f"{target_name}_isolated.py"
15
+
16
+ def save_metadata(self, meta: dict) -> None:
17
+ self.metadata_path.write_text(json.dumps(meta, indent=2))
18
+
19
+ def load_metadata(self) -> dict:
20
+ if not self.metadata_path.exists():
21
+ raise FileNotFoundError(
22
+ f"No active session found in {self.dir}. Run `vial extract` first."
23
+ )
24
+ return json.loads(self.metadata_path.read_text())
25
+
26
+ def cleanup(self) -> None:
27
+ meta = self.load_metadata()
28
+ isolated = Path(meta["isolated_filepath"])
29
+ if isolated.exists():
30
+ isolated.unlink()
31
+ self.metadata_path.unlink()