pari-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.
- pari/__init__.py +3 -0
- pari/agent/__init__.py +14 -0
- pari/agent/loop.py +35 -0
- pari/agent/reflection.py +39 -0
- pari/agent/runtime.py +91 -0
- pari/agent/subagent.py +34 -0
- pari/cli.py +132 -0
- pari/config.py +23 -0
- pari/context/__init__.py +15 -0
- pari/context/cache.py +15 -0
- pari/context/chunks.py +40 -0
- pari/context/compaction.py +21 -0
- pari/context/epoch.py +32 -0
- pari/context/warmer.py +32 -0
- pari/edit/__init__.py +20 -0
- pari/edit/cascade.py +22 -0
- pari/edit/diff.py +8 -0
- pari/edit/dmp.py +15 -0
- pari/edit/exact.py +37 -0
- pari/edit/git_osr.py +14 -0
- pari/edit/indenter.py +12 -0
- pari/eval/__init__.py +14 -0
- pari/eval/judge.py +21 -0
- pari/eval/logging.py +19 -0
- pari/eval/metrics.py +22 -0
- pari/eval/runner.py +34 -0
- pari/fs/__init__.py +16 -0
- pari/fs/atomic.py +53 -0
- pari/fs/path.py +28 -0
- pari/fs/queue.py +22 -0
- pari/intelligence/__init__.py +15 -0
- pari/intelligence/ast.py +25 -0
- pari/intelligence/cache.py +21 -0
- pari/intelligence/ppr.py +21 -0
- pari/intelligence/repomap.py +21 -0
- pari/intelligence/search.py +33 -0
- pari/output/__init__.py +14 -0
- pari/output/parser.py +14 -0
- pari/output/spooler.py +33 -0
- pari/output/truncation.py +20 -0
- pari/providers/__init__.py +24 -0
- pari/providers/anthropic.py +23 -0
- pari/providers/base.py +56 -0
- pari/providers/deepseek.py +24 -0
- pari/providers/mock.py +46 -0
- pari/providers/ollama.py +25 -0
- pari/providers/openai.py +23 -0
- pari/routing/__init__.py +16 -0
- pari/routing/anti_verbosity.py +13 -0
- pari/routing/classifier.py +24 -0
- pari/routing/router.py +23 -0
- pari/sandbox/__init__.py +16 -0
- pari/sandbox/base.py +74 -0
- pari/sandbox/bwrap.py +39 -0
- pari/sandbox/dispatcher.py +31 -0
- pari/sandbox/e2b.py +32 -0
- pari/sandbox/process.py +38 -0
- pari/sandbox/seatbelt.py +32 -0
- pari/session/__init__.py +15 -0
- pari/session/checkpoint.py +24 -0
- pari/session/dag.py +34 -0
- pari/session/events.py +50 -0
- pari/session/logger.py +41 -0
- pari/session/undo.py +99 -0
- pari/tools/__init__.py +14 -0
- pari/tools/base.py +35 -0
- pari/tools/bash.py +24 -0
- pari/tools/fs.py +53 -0
- pari/tools/registry.py +36 -0
- pari/tools/search.py +38 -0
- pari/tui/__init__.py +28 -0
- pari/tui/app.py +355 -0
- pari/tui/commands.py +45 -0
- pari/tui/diff_view.py +17 -0
- pari/tui/status.py +65 -0
- pari_cli-0.1.0.dist-info/METADATA +167 -0
- pari_cli-0.1.0.dist-info/RECORD +81 -0
- pari_cli-0.1.0.dist-info/WHEEL +5 -0
- pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
- pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- pari_cli-0.1.0.dist-info/top_level.txt +1 -0
pari/edit/indenter.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""PARI Layer 5: Relative Indentation Normalizer.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
LLMs frequently emit replacement blocks whose leading indentation is offset by
|
|
5
|
+
2 or 4 spaces relative to the target site.
|
|
6
|
+
The RelativeIndenter calculates the delta between the indentation of the first line
|
|
7
|
+
of the search block and the replacement block, normalizing subsequent lines.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def normalize_relative_indentation(search_block: str, replace_block: str) -> str:
|
|
11
|
+
"""Normalize indentation of replacement block to match search block site."""
|
|
12
|
+
raise NotImplementedError
|
pari/eval/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PARI Layer 10: Evaluation & Observability Harness.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes the universal problem of "vibe coding" (subjective prompt tweaking without objective regression tests).
|
|
5
|
+
Provides a deterministic local micro-eval suite ($3-$5 per run) and structured observability.
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- runner: Micro-eval benchmark runner executing deterministic test cases
|
|
9
|
+
- judge: Cross-model verification judge (Graptile principle: never evaluate with same model family)
|
|
10
|
+
- metrics: Quantitative benchmarks (pass rates, token costs, turn counts)
|
|
11
|
+
- logging: Structured JSON logging (structlog format) for full execution telemetry
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Placeholder imports will be exported once implementations are added
|
pari/eval/judge.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PARI Layer 10: Cross-Model Verification Judge.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Implements the Graptile Principle:
|
|
5
|
+
By default, PARI-generated code is evaluated by a different model family than the one
|
|
6
|
+
that generated it (e.g. Claude generation evaluated by GPT-4o-mini).
|
|
7
|
+
Prevents self-evaluation confirmation bias.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrossModelJudge:
|
|
14
|
+
"""Evaluates patch correctness using an independent model family."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, judge_model: str = "gpt-4o-mini"):
|
|
17
|
+
self.judge_model = judge_model
|
|
18
|
+
|
|
19
|
+
async def evaluate_patch(self, problem_statement: str, patch_diff: str) -> Dict[str, Any]:
|
|
20
|
+
"""Ask independent judge model to evaluate if patch correctly resolves the issue."""
|
|
21
|
+
raise NotImplementedError
|
pari/eval/logging.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""PARI Layer 10: Structured Observability Logging.
|
|
2
|
+
|
|
3
|
+
Emits structured JSON events for monitoring, analysis, and debugging.
|
|
4
|
+
Logs events such as tool_call, edit_applied, sandbox_exec, turn_complete.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StructuredLogger:
|
|
12
|
+
"""Emits newline-delimited JSON logs to stdout or a designated log file."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, log_file: Optional[Path] = None):
|
|
15
|
+
self.log_file = log_file
|
|
16
|
+
|
|
17
|
+
def emit(self, event_name: str, **kwargs: Any):
|
|
18
|
+
"""Emit structured telemetry event."""
|
|
19
|
+
raise NotImplementedError
|
pari/eval/metrics.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PARI Layer 10: Quantitative Evaluation Metrics.
|
|
2
|
+
|
|
3
|
+
Computes pass rate, mean cost per resolved task, mean turns, and duration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class EvalMetrics:
|
|
12
|
+
total_tasks: int
|
|
13
|
+
passed_tasks: int
|
|
14
|
+
pass_rate: float
|
|
15
|
+
total_cost_usd: float
|
|
16
|
+
mean_cost_per_task: float
|
|
17
|
+
mean_turns: float
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def calculate_eval_metrics(results: List[Dict[str, Any]]) -> EvalMetrics:
|
|
21
|
+
"""Compute aggregate benchmark metrics across test runs."""
|
|
22
|
+
raise NotImplementedError
|
pari/eval/runner.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""PARI Layer 10: Micro-Eval Benchmark Runner.
|
|
2
|
+
|
|
3
|
+
Runs a local micro-benchmark suite ($3-$5 per run) against isolated git worktrees
|
|
4
|
+
and executes deterministic pytest suites to compute exact pass rates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TestCase:
|
|
14
|
+
id: str
|
|
15
|
+
issue_description: str
|
|
16
|
+
test_command: str
|
|
17
|
+
base_commit: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class EvalReport:
|
|
22
|
+
metrics: Any
|
|
23
|
+
case_results: List[Dict[str, Any]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MicroEvalRunner:
|
|
27
|
+
"""Executes evaluation benchmarks across test cases."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, workspace: Path):
|
|
30
|
+
self.workspace = workspace.resolve()
|
|
31
|
+
|
|
32
|
+
def run_suite(self, test_cases: List[TestCase]) -> EvalReport:
|
|
33
|
+
"""Run benchmark suite and compile report."""
|
|
34
|
+
raise NotImplementedError
|
pari/fs/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""PARI Layer 6: Concurrency and File Safety Engine.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F3 (File write race conditions tearing files during parallel operations)
|
|
5
|
+
and path traversal / prefix collision vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- path: Canonical path resolution via Path.resolve().relative_to()
|
|
9
|
+
- atomic: Atomic write using temporary file + os.replace() + directory os.fsync()
|
|
10
|
+
- queue: FileMutationQueue maintaining per-canonical-realpath asyncio locks
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pari.fs.path import resolve_safe_path
|
|
14
|
+
from pari.fs.atomic import atomic_write, read_file
|
|
15
|
+
|
|
16
|
+
__all__ = ["resolve_safe_path", "atomic_write", "read_file"]
|
pari/fs/atomic.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""PARI Layer 6: Atomic File Write and Sync Protocol.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F1 & F3.
|
|
5
|
+
Writes content to a temporary file in the same directory (guaranteeing same filesystem device),
|
|
6
|
+
calls os.fsync() on the file descriptor, atomically replaces the target via os.replace(),
|
|
7
|
+
and flushes the parent directory entry with fsync on POSIX.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def atomic_write(target_path: Path, content: str) -> None:
|
|
16
|
+
"""Atomically write content to target_path, preventing partial/torn writes."""
|
|
17
|
+
target = target_path.resolve()
|
|
18
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
# Write to a temporary file in the same directory (same filesystem device)
|
|
21
|
+
fd, temp_path = tempfile.mkstemp(dir=target.parent, prefix=".pari_tmp_")
|
|
22
|
+
try:
|
|
23
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
24
|
+
f.write(content)
|
|
25
|
+
f.flush()
|
|
26
|
+
os.fsync(f.fileno())
|
|
27
|
+
|
|
28
|
+
# Atomic replacement
|
|
29
|
+
os.replace(temp_path, target)
|
|
30
|
+
|
|
31
|
+
# Sync parent directory on POSIX
|
|
32
|
+
if os.name == "posix":
|
|
33
|
+
try:
|
|
34
|
+
parent_fd = os.open(str(target.parent), os.O_RDONLY | os.O_DIRECTORY)
|
|
35
|
+
try:
|
|
36
|
+
os.fsync(parent_fd)
|
|
37
|
+
finally:
|
|
38
|
+
os.close(parent_fd)
|
|
39
|
+
except (OSError, PermissionError):
|
|
40
|
+
pass
|
|
41
|
+
except Exception:
|
|
42
|
+
if os.path.exists(temp_path):
|
|
43
|
+
try:
|
|
44
|
+
os.remove(temp_path)
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_file(file_path: Path) -> str:
|
|
51
|
+
"""Read full text content of a file."""
|
|
52
|
+
return file_path.resolve().read_text(encoding="utf-8")
|
|
53
|
+
|
pari/fs/path.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""PARI Layer 6: Safe Canonical Path Resolution.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Prevents path traversal attacks (e.g. ../../etc/shadow) and path prefix collisions
|
|
5
|
+
(e.g., /workspace_evil matching /workspace in string-prefix checks).
|
|
6
|
+
Enforces that resolved canonical paths reside strictly within authorized workspace roots.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_safe_path(workspace_root: Union[str, Path], target_path: Union[str, Path]) -> Path:
|
|
14
|
+
"""Resolve a target path safely relative to the workspace root."""
|
|
15
|
+
root = Path(workspace_root).resolve()
|
|
16
|
+
target = Path(target_path)
|
|
17
|
+
if not target.is_absolute():
|
|
18
|
+
resolved = (root / target).resolve()
|
|
19
|
+
else:
|
|
20
|
+
resolved = target.resolve()
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
resolved.relative_to(root)
|
|
24
|
+
except ValueError:
|
|
25
|
+
raise PermissionError(f"Path traversal rejected: '{target_path}' is outside workspace '{workspace_root}'")
|
|
26
|
+
|
|
27
|
+
return resolved
|
|
28
|
+
|
pari/fs/queue.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PARI Layer 6: File Mutation Queue.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Pi's file-mutation-queue.ts.
|
|
5
|
+
Maintains a registry of asyncio.Lock primitives keyed by canonical realpath.
|
|
6
|
+
Serializes concurrent writes to the same physical file while allowing parallel
|
|
7
|
+
writes to distinct files.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileMutationQueue:
|
|
15
|
+
"""Serializes concurrent write mutations per canonical filesystem path."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
async def get_lock(self, file_path: Path) -> asyncio.Lock:
|
|
21
|
+
"""Get canonical per-path lock."""
|
|
22
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PARI Layer 3: Codebase Intelligence & Navigation Layer.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Pi F3 (Blind file traversal loops requiring 8+ grep turns) and
|
|
5
|
+
Aider F4 (Personalized PageRank hub distortion from generic utility sinks).
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- search: Fast file globbing & ripgrep substring search (v1 primary navigation)
|
|
9
|
+
- ast: Tree-sitter AST symbol extractor across Python, TypeScript, Go, Rust
|
|
10
|
+
- ppr: Personalized PageRank graph ranking with hub suppression (>50 inbound refs dampened)
|
|
11
|
+
- repomap: 2,048-token binary-search packing for the static context repo map
|
|
12
|
+
- cache: Worktree-scoped SQLite symbol cache preventing lock collisions
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Placeholder imports will be exported once implementations are added
|
pari/intelligence/ast.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""PARI Layer 3: Tree-sitter AST Symbol Extractor.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Extracts high-signal structural definitions (classes, functions, methods, imports)
|
|
5
|
+
from source code without reading full file bodies into prompt context.
|
|
6
|
+
Degrades gracefully to regex/heuristic parsing when tree-sitter bindings are not installed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SymbolInfo:
|
|
16
|
+
file_path: str
|
|
17
|
+
symbol_name: str
|
|
18
|
+
kind: str # "class", "function", "method", "variable"
|
|
19
|
+
line_number: int
|
|
20
|
+
signature: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_symbols(file_path: Path, content: str) -> List[SymbolInfo]:
|
|
24
|
+
"""Extract symbols from source code."""
|
|
25
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PARI Layer 3: Worktree-Scoped Symbol Cache.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Aider F6 (Diskcache SQLite lock collisions across git worktrees).
|
|
5
|
+
Scans and caches AST tags with SQLite database files scoped explicitly to the
|
|
6
|
+
unique worktree root path, eliminating multi-session database locking errors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorktreeSymbolCache:
|
|
14
|
+
"""Manages cached symbol index keyed specifically to workspace path and file mtimes."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, workspace: Path, cache_dir: Optional[Path] = None):
|
|
17
|
+
self.workspace = workspace.resolve()
|
|
18
|
+
|
|
19
|
+
def get_db_path(self) -> Path:
|
|
20
|
+
"""Return path to workspace-isolated SQLite cache file."""
|
|
21
|
+
raise NotImplementedError
|
pari/intelligence/ppr.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PARI Layer 3: Personalized PageRank with Hub Suppression.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Aider F4 (PageRank hub distortion).
|
|
5
|
+
In naive PageRank, generic utilities like `utils.py` or `helpers.py` accumulate
|
|
6
|
+
hundreds of inbound edges and crowd out crucial business logic in a 2,048-token budget.
|
|
7
|
+
PARI applies a hub suppression dampener: any node with >50 inbound references
|
|
8
|
+
has its rank dynamically dampened, prioritizing domain-specific classes and interfaces.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Dict, List, Set, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_personalized_pagerank(
|
|
15
|
+
graph_edges: List[Tuple[str, str]],
|
|
16
|
+
personalized_targets: Set[str],
|
|
17
|
+
damping_factor: float = 0.85,
|
|
18
|
+
hub_threshold: int = 50,
|
|
19
|
+
) -> Dict[str, float]:
|
|
20
|
+
"""Compute personalized PageRank scores with dampening for high-indegree utility hubs."""
|
|
21
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PARI Layer 3: Repo Map Generator & Token Packer.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Packs codebase structural symbols into a strict token budget (default 2,048 tokens)
|
|
5
|
+
using binary-search selection based on PPR rankings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RepoMapGenerator:
|
|
13
|
+
"""Generates condensed repository map from parsed source symbols."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, workspace: Path, max_tokens: int = 2048):
|
|
16
|
+
self.workspace = workspace.resolve()
|
|
17
|
+
self.max_tokens = max_tokens
|
|
18
|
+
|
|
19
|
+
def generate(self, focus_files: Optional[List[str]] = None) -> str:
|
|
20
|
+
"""Scan workspace, extract symbols, and format condensed outline."""
|
|
21
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""PARI Layer 3: Substring & Globbing Search Engine (v1 Baseline Navigation).
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Provides fast, deterministic code search without requiring C-extension compilation.
|
|
5
|
+
Enforces ignore boundaries (.git, .venv, node_modules, build artifacts).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GrepMatch:
|
|
15
|
+
file_path: str
|
|
16
|
+
line_number: int
|
|
17
|
+
line_content: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_files(workspace_root: Path, pattern: str = "*", max_results: int = 100) -> List[str]:
|
|
21
|
+
"""Find files in workspace matching a glob pattern, respecting ignore rules."""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def grep_search(
|
|
26
|
+
workspace_root: Path,
|
|
27
|
+
query: str,
|
|
28
|
+
is_regex: bool = False,
|
|
29
|
+
path_filter: str = "*",
|
|
30
|
+
max_matches: int = 50,
|
|
31
|
+
) -> List[GrepMatch]:
|
|
32
|
+
"""Search for query across files in workspace, returning matches with line numbers."""
|
|
33
|
+
raise NotImplementedError
|
pari/output/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PARI Layer 7: Output Handling and Context Hygiene.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes the #1 failure mode in the Failure Mode Atlas: Unbounded stdout poisoning.
|
|
5
|
+
Prevents large tool outputs (e.g. cat massive.log, npm install, pytest) from flooding
|
|
6
|
+
the model's context window and displacing core instructions.
|
|
7
|
+
|
|
8
|
+
Subsystems:
|
|
9
|
+
- spooler: 50KB / 2k-line disk spooler storing raw outputs in sidecar files
|
|
10
|
+
- truncation: Head/tail extraction preserving stack traces and return headers
|
|
11
|
+
- parser: Structured test failure extractors (pytest, jest, cargo)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Placeholder imports will be exported once implementations are added
|
pari/output/parser.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PARI Layer 7: Structured Test Output Parser.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Extracts high-signal test failure summaries from raw test output (pytest, jest, cargo),
|
|
5
|
+
allowing the model to immediately grasp failing assertions without digesting
|
|
6
|
+
100s of lines of framework internals.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_test_failures(raw_output: str) -> List[str]:
|
|
13
|
+
"""Parse failure lines from test runner output."""
|
|
14
|
+
raise NotImplementedError
|
pari/output/spooler.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""PARI Layer 7: Managed Output Disk Spooler.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Codex F4 (RSS bloat on large outputs) & OpenCode 50KB output cap.
|
|
5
|
+
Streams subprocess output into a temporary disk sidecar file.
|
|
6
|
+
If output exceeds max_bytes (default 50KB) or max_lines (default 2,000 lines),
|
|
7
|
+
it generates a sanitized head/tail summary for prompt injection while preserving
|
|
8
|
+
the full output on disk for on-demand inspection via /spool command.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SpoolResult:
|
|
18
|
+
context_text: str
|
|
19
|
+
sidecar_path: Path
|
|
20
|
+
total_bytes: int
|
|
21
|
+
total_lines: int
|
|
22
|
+
was_truncated: bool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ManagedOutputSpooler:
|
|
26
|
+
"""Spools process output to disk sidecars and extracts safe context summaries."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, spool_dir: Optional[Path] = None, max_bytes: int = 50 * 1024):
|
|
29
|
+
self.max_bytes = max_bytes
|
|
30
|
+
|
|
31
|
+
def spool(self, raw_output: str, command_tag: str = "cmd") -> SpoolResult:
|
|
32
|
+
"""Spool output to disk sidecar and return truncated context text if large."""
|
|
33
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PARI Layer 7: Head/Tail Truncation with Context Preservation.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
When output exceeds the context budget:
|
|
5
|
+
1. Keeps the first N lines (command launch, build headers).
|
|
6
|
+
2. Keeps the last M lines (stack traces, exit assertions, summary errors).
|
|
7
|
+
3. Injects a clear omission notice linking to the disk sidecar containing full raw output.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_head_tail(
|
|
14
|
+
text: str,
|
|
15
|
+
head_lines: int = 50,
|
|
16
|
+
tail_lines: int = 100,
|
|
17
|
+
sidecar_path: str = "",
|
|
18
|
+
) -> Tuple[str, bool]:
|
|
19
|
+
"""Truncate text by extracting head and tail sections if line count is high."""
|
|
20
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PARI Multi-Provider LLM Integration Layer.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Antigravity F5 (Hard Google API lock-in) and Claude Code Anthropic exclusivity.
|
|
5
|
+
Provides unified model abstraction across Anthropic Claude, OpenAI, DeepSeek, local Ollama,
|
|
6
|
+
and an offline fallback provider for local testing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from pari.providers.base import LLMProvider, ProviderMessage, ModelResponse, ToolCall
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_provider(model_name: Optional[str] = None) -> LLMProvider:
|
|
14
|
+
"""Instantiate the best available LLM provider based on model name and environment."""
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"LLMProvider",
|
|
20
|
+
"ProviderMessage",
|
|
21
|
+
"ModelResponse",
|
|
22
|
+
"ToolCall",
|
|
23
|
+
"get_provider",
|
|
24
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PARI Providers: Anthropic Claude Adapter.
|
|
2
|
+
|
|
3
|
+
Supports Claude 3.7 Sonnet, Claude 3.5 Sonnet, and Claude 3.5 Haiku with prompt caching.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnthropicProvider(LLMProvider):
|
|
11
|
+
"""Adapter for Anthropic Messages API."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, model_name: str = "claude-3-7-sonnet-20250219", api_key: Optional[str] = None):
|
|
14
|
+
super().__init__(model_name, api_key)
|
|
15
|
+
|
|
16
|
+
async def complete(
|
|
17
|
+
self,
|
|
18
|
+
messages: List[ProviderMessage],
|
|
19
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
20
|
+
temperature: float = 0.0,
|
|
21
|
+
) -> ModelResponse:
|
|
22
|
+
"""Call Anthropic Messages API with prompt caching."""
|
|
23
|
+
raise NotImplementedError
|
pari/providers/base.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""PARI Providers: Base LLM Interface.
|
|
2
|
+
|
|
3
|
+
Defines the contract for calling LLM inference APIs with tool calling and prompt caching.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ToolCall:
|
|
13
|
+
id: str
|
|
14
|
+
name: str
|
|
15
|
+
arguments: Dict[str, Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ProviderMessage:
|
|
20
|
+
role: str # "system", "user", "assistant", "tool"
|
|
21
|
+
content: str
|
|
22
|
+
tool_calls: Optional[List[ToolCall]] = None
|
|
23
|
+
tool_call_id: Optional[str] = None
|
|
24
|
+
cache_control: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ModelResponse:
|
|
29
|
+
text: str
|
|
30
|
+
tool_calls: List[ToolCall] = field(default_factory=list)
|
|
31
|
+
tokens_in: int = 0
|
|
32
|
+
tokens_out: int = 0
|
|
33
|
+
cached_tokens: int = 0
|
|
34
|
+
stop_reason: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def has_tool_calls(self) -> bool:
|
|
38
|
+
return len(self.tool_calls) > 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LLMProvider(ABC):
|
|
42
|
+
"""Abstract base class for all LLM backend adapters."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, model_name: str, api_key: Optional[str] = None):
|
|
45
|
+
self.model_name = model_name
|
|
46
|
+
self.api_key = api_key
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def complete(
|
|
50
|
+
self,
|
|
51
|
+
messages: List[ProviderMessage],
|
|
52
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
53
|
+
temperature: float = 0.0,
|
|
54
|
+
) -> ModelResponse:
|
|
55
|
+
"""Execute a completion request."""
|
|
56
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""PARI Providers: DeepSeek Adapter.
|
|
2
|
+
|
|
3
|
+
Supports DeepSeek V3 and DeepSeek R1 via OpenAI-compatible API endpoint.
|
|
4
|
+
High performance, cost-effective editor tier model ($0.27/M input, $1.10/M output).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeepSeekProvider(LLMProvider):
|
|
12
|
+
"""Adapter for DeepSeek API using OpenAI-compatible interface."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model_name: str = "deepseek-chat", api_key: Optional[str] = None):
|
|
15
|
+
super().__init__(model_name, api_key)
|
|
16
|
+
|
|
17
|
+
async def complete(
|
|
18
|
+
self,
|
|
19
|
+
messages: List[ProviderMessage],
|
|
20
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
21
|
+
temperature: float = 0.0,
|
|
22
|
+
) -> ModelResponse:
|
|
23
|
+
"""Call DeepSeek chat API."""
|
|
24
|
+
raise NotImplementedError
|
pari/providers/mock.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""PARI Providers: Offline / Mock Provider for local testing and dry-runs.
|
|
2
|
+
|
|
3
|
+
Enables offline testing of the agent loop, tool execution, session recovery,
|
|
4
|
+
and diff review without requiring remote cloud API tokens.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MockOfflineProvider(LLMProvider):
|
|
12
|
+
"""Fallback provider when no cloud API keys are configured."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model_name: str = "mock-offline"):
|
|
15
|
+
super().__init__(model_name, api_key="offline")
|
|
16
|
+
|
|
17
|
+
async def complete(
|
|
18
|
+
self,
|
|
19
|
+
messages: List[ProviderMessage],
|
|
20
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
|
21
|
+
temperature: float = 0.0,
|
|
22
|
+
) -> ModelResponse:
|
|
23
|
+
"""Simulate completion for local testing."""
|
|
24
|
+
last_prompt = messages[-1].content if messages else ""
|
|
25
|
+
lower_prompt = last_prompt.lower()
|
|
26
|
+
|
|
27
|
+
# If user asks to find python files, simulate a tool call or answer
|
|
28
|
+
if "find" in lower_prompt or "python" in lower_prompt:
|
|
29
|
+
return ModelResponse(
|
|
30
|
+
text="PARI Agent: Found python files in workspace.",
|
|
31
|
+
tool_calls=[],
|
|
32
|
+
tokens_in=120,
|
|
33
|
+
tokens_out=40,
|
|
34
|
+
cached_tokens=0,
|
|
35
|
+
stop_reason="end_turn",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return ModelResponse(
|
|
39
|
+
text=f"PARI Agent response to: {last_prompt}",
|
|
40
|
+
tool_calls=[],
|
|
41
|
+
tokens_in=80,
|
|
42
|
+
tokens_out=30,
|
|
43
|
+
cached_tokens=0,
|
|
44
|
+
stop_reason="end_turn",
|
|
45
|
+
)
|
|
46
|
+
|