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/__init__.py
ADDED
pari/agent/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PARI Layer 4: The Execution Loop (Agent Runtime).
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Implements the double-while execution loop with stop_reason == "length" truncation guard,
|
|
5
|
+
bounded reflection retry limits, and lightweight asyncio fiber-based subagents.
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- loop: Core double-while agent loop with continuation handling
|
|
9
|
+
- runtime: Turn execution, tool orchestration, and state transitions
|
|
10
|
+
- reflection: Structured error injection with max 3 reflection retries
|
|
11
|
+
- subagent: Fiber-based read-only research subagents (closing Pi F2 RAM bloat)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Placeholder imports will be exported once implementations are added
|
pari/agent/loop.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""PARI Layer 4: Double-While Execution Loop.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Outer loop: Session conversation interaction (waiting for user input).
|
|
5
|
+
Inner loop: Autonomous tool-calling turns until task is resolved or user feedback needed.
|
|
6
|
+
Continuation Guard: If the model's stop_reason == "length" (e.g. output truncated),
|
|
7
|
+
automatically prompts a continuation message rather than executing broken partial syntax.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TurnResult:
|
|
16
|
+
response_text: str
|
|
17
|
+
tool_calls_executed: int
|
|
18
|
+
tokens_used: int
|
|
19
|
+
requires_user_input: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentExecutionLoop:
|
|
23
|
+
"""Manages the autonomous double-while execution loop."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, max_autonomous_turns: int = 25):
|
|
26
|
+
self.max_autonomous_turns = max_autonomous_turns
|
|
27
|
+
|
|
28
|
+
async def run_turn(
|
|
29
|
+
self,
|
|
30
|
+
prompt: str,
|
|
31
|
+
step_fn: Callable[[str, int], Any],
|
|
32
|
+
is_length_truncated: Callable[[Any], bool],
|
|
33
|
+
) -> TurnResult:
|
|
34
|
+
"""Execute autonomous steps for a given user prompt."""
|
|
35
|
+
raise NotImplementedError
|
pari/agent/reflection.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""PARI Layer 4: Reflection & Error Injection Manager.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Aider F5 (Reflection loop token burn: $0.50-$2.00 per stubborn diff).
|
|
5
|
+
When a tool call or edit fails:
|
|
6
|
+
1. Formats the error into a high-signal structured diagnostic.
|
|
7
|
+
2. Limits reflection attempts to max_reflections (default: 3).
|
|
8
|
+
3. If 3 attempts fail, halts execution and prompts the user for intervention,
|
|
9
|
+
preventing infinite token-burning retry loops.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ReflectionAttempt:
|
|
18
|
+
error_type: str
|
|
19
|
+
error_message: str
|
|
20
|
+
attempt_count: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReflectionManager:
|
|
24
|
+
"""Guards reflection retries to prevent unbounded token expenditure."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, max_reflections: int = 3):
|
|
27
|
+
self.max_reflections = max_reflections
|
|
28
|
+
|
|
29
|
+
def should_retry(self, operation_id: str) -> bool:
|
|
30
|
+
"""Check if reflection attempts remain under ceiling."""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def record_failure(self, operation_id: str, error_type: str, message: str) -> str:
|
|
34
|
+
"""Format error feedback prompt for next turn."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
def reset(self, operation_id: Optional[str] = None):
|
|
38
|
+
"""Clear failure counts."""
|
|
39
|
+
raise NotImplementedError
|
pari/agent/runtime.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""PARI Layer 4: Agent Runtime State and Dispatcher.
|
|
2
|
+
|
|
3
|
+
Coordinates session state, tool definitions, undo tracking, and communication with the model.
|
|
4
|
+
Executes the full turn lifecycle:
|
|
5
|
+
User Prompt -> Context Assembly -> Provider Completion -> Tool Execution -> Diff Review -> Response
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from pari.providers.base import LLMProvider, ProviderMessage
|
|
15
|
+
from pari.providers.mock import MockOfflineProvider
|
|
16
|
+
from pari.session.undo import GuardedUndoManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AgentRuntime:
|
|
20
|
+
"""Central runtime controller for a live PARI session."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
workspace: Path,
|
|
25
|
+
session_file: Optional[Path] = None,
|
|
26
|
+
provider: Optional[LLMProvider] = None,
|
|
27
|
+
):
|
|
28
|
+
self.workspace = workspace.resolve()
|
|
29
|
+
self.session_file = session_file or (self.workspace / ".pari" / "session.jsonl")
|
|
30
|
+
self.provider = provider or MockOfflineProvider()
|
|
31
|
+
self.undo_manager = GuardedUndoManager(self.workspace)
|
|
32
|
+
self.pinned_files: List[str] = []
|
|
33
|
+
self.session_id: Optional[str] = None
|
|
34
|
+
self.messages: List[ProviderMessage] = []
|
|
35
|
+
|
|
36
|
+
def start_session(self) -> str:
|
|
37
|
+
"""Initialize session and emit start event to session.jsonl."""
|
|
38
|
+
self.session_id = f"pari_sess_{uuid.uuid4().hex[:12]}"
|
|
39
|
+
self.session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
start_record = {
|
|
41
|
+
"event": "session_start",
|
|
42
|
+
"session_id": self.session_id,
|
|
43
|
+
"workspace": str(self.workspace),
|
|
44
|
+
"timestamp": time.time(),
|
|
45
|
+
}
|
|
46
|
+
with open(self.session_file, "a", encoding="utf-8") as f:
|
|
47
|
+
f.write(json.dumps(start_record) + "\n")
|
|
48
|
+
return self.session_id
|
|
49
|
+
|
|
50
|
+
def add_pinned_file(self, rel_path: str) -> bool:
|
|
51
|
+
"""Pin reference file into active context."""
|
|
52
|
+
file_path = (self.workspace / rel_path).resolve()
|
|
53
|
+
if not file_path.exists():
|
|
54
|
+
return False
|
|
55
|
+
clean_rel = str(file_path.relative_to(self.workspace))
|
|
56
|
+
if clean_rel not in self.pinned_files:
|
|
57
|
+
self.pinned_files.append(clean_rel)
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
async def execute_turn(
|
|
61
|
+
self,
|
|
62
|
+
prompt: str,
|
|
63
|
+
on_tool_call: Optional[Callable[[str, Dict[str, Any]], None]] = None,
|
|
64
|
+
on_diff: Optional[Callable[[str, str], None]] = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Execute conversational turn with autonomous tool execution."""
|
|
67
|
+
if not self.session_id:
|
|
68
|
+
self.start_session()
|
|
69
|
+
|
|
70
|
+
user_msg = ProviderMessage(role="user", content=prompt)
|
|
71
|
+
self.messages.append(user_msg)
|
|
72
|
+
|
|
73
|
+
resp = await self.provider.complete(self.messages)
|
|
74
|
+
assistant_msg = ProviderMessage(role="assistant", content=resp.text)
|
|
75
|
+
self.messages.append(assistant_msg)
|
|
76
|
+
|
|
77
|
+
# Log turn event
|
|
78
|
+
turn_record = {
|
|
79
|
+
"event": "turn_complete",
|
|
80
|
+
"session_id": self.session_id,
|
|
81
|
+
"user_prompt": prompt,
|
|
82
|
+
"response": resp.text,
|
|
83
|
+
"tokens_in": resp.tokens_in,
|
|
84
|
+
"tokens_out": resp.tokens_out,
|
|
85
|
+
"timestamp": time.time(),
|
|
86
|
+
}
|
|
87
|
+
with open(self.session_file, "a", encoding="utf-8") as f:
|
|
88
|
+
f.write(json.dumps(turn_record) + "\n")
|
|
89
|
+
|
|
90
|
+
return resp.text
|
|
91
|
+
|
pari/agent/subagent.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""PARI Layer 4: Fiber-Based Research Subagent Pool.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Pi F2 (Process-per-subagent footprint: 4 subagents = 4 V8 runtimes = 800MB-1.8GB RAM).
|
|
5
|
+
PARI runs subagents as lightweight Python asyncio tasks within the same process.
|
|
6
|
+
Crucial Safety Invariant:
|
|
7
|
+
Subagents are strictly restricted to READ-ONLY tasks (documentation reading, grep searches).
|
|
8
|
+
All code mutations are strictly single-agent to prevent concurrent file conflicts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Coroutine, List
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SubagentTask:
|
|
17
|
+
task_id: str
|
|
18
|
+
description: str
|
|
19
|
+
coro: Coroutine
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FiberSubagentPool:
|
|
23
|
+
"""Manages lightweight concurrent read-only research fibers."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, max_concurrency: int = 4):
|
|
26
|
+
self.max_concurrency = max_concurrency
|
|
27
|
+
|
|
28
|
+
async def run_task(self, task_id: str, coro: Coroutine) -> Any:
|
|
29
|
+
"""Run single research task with concurrency throttling."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
async def run_parallel_research(self, tasks: List[SubagentTask]) -> List[Any]:
|
|
33
|
+
"""Execute multiple read-only tasks concurrently."""
|
|
34
|
+
raise NotImplementedError
|
pari/cli.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""CLI entrypoint for PARI terminal coding agent.
|
|
2
|
+
|
|
3
|
+
Handles top-level command parsing, banner display, subcommands (find, grep, eval),
|
|
4
|
+
and launching the interactive agent REPL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from pari import __version__
|
|
15
|
+
from pari.tui import TerminalApp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_banner():
|
|
19
|
+
"""Print the startup terminal banner."""
|
|
20
|
+
print(f"PARI v{__version__} — Terminal Coding Agent")
|
|
21
|
+
print("Platform-Isolated | Exact & Resilient Edits | Durable Sessions")
|
|
22
|
+
print("-" * 65)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmd_find(args: argparse.Namespace) -> int:
|
|
26
|
+
"""Find files matching pattern in workspace."""
|
|
27
|
+
# TODO: Implement workspace file search via Layer 3 intelligence
|
|
28
|
+
print(f"Finding files matching '{args.pattern}' (placeholder)")
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_grep(args: argparse.Namespace) -> int:
|
|
33
|
+
"""Search for query in workspace files."""
|
|
34
|
+
# TODO: Implement substring/regex grep via Layer 3 intelligence
|
|
35
|
+
print(f"Searching for '{args.query}' (placeholder)")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_eval(args: argparse.Namespace) -> int:
|
|
40
|
+
"""Run local micro-eval benchmark suite."""
|
|
41
|
+
# TODO: Implement micro-eval benchmark via Layer 10 harness
|
|
42
|
+
print(f"Running evaluation suite '{args.suite}' (placeholder)")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
47
|
+
"""Main interactive REPL agent execution."""
|
|
48
|
+
workspace = Path(args.workspace or os.getcwd()).resolve()
|
|
49
|
+
|
|
50
|
+
if args.dry_run:
|
|
51
|
+
print_banner()
|
|
52
|
+
print(f"Workspace: {workspace}")
|
|
53
|
+
print("Self-check passed: environment and CLI options verified.")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
app = TerminalApp(workspace=workspace)
|
|
57
|
+
try:
|
|
58
|
+
asyncio.run(app.run_repl())
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
pass
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main(argv: Optional[list] = None) -> int:
|
|
66
|
+
parser = argparse.ArgumentParser(
|
|
67
|
+
prog="pari",
|
|
68
|
+
description="PARI — Next-generation terminal coding agent.",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-w", "--workspace", type=str, default=None,
|
|
75
|
+
help="Target project workspace directory (default: current working directory)"
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--sandbox", type=str, choices=["auto", "bwrap", "seatbelt", "cloud", "none"],
|
|
79
|
+
default="auto", help="Execution sandbox backend (default: auto)"
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--edit-fallback", type=str, choices=["none", "cascade"], default="none",
|
|
83
|
+
help="Edit engine fallback strategy when exact match fails (default: none)"
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--repomap", type=str, choices=["off", "ppr"], default="off",
|
|
87
|
+
help="Repository AST PageRank symbol map (default: off, uses v1 ripgrep/glob)"
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--add", action="append", default=[], metavar="FILE",
|
|
91
|
+
help="Pin reference documentation or file into context epoch"
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--dry-run", action="store_true",
|
|
95
|
+
help="Validate environment configuration and exit without starting interactive loop"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Subcommands")
|
|
99
|
+
|
|
100
|
+
# pari find
|
|
101
|
+
find_parser = subparsers.add_parser("find", help="Find files matching pattern")
|
|
102
|
+
find_parser.add_argument("pattern", type=str, default="*", nargs="?", help="File glob pattern")
|
|
103
|
+
find_parser.add_argument("-l", "--limit", type=int, default=100, help="Max results")
|
|
104
|
+
|
|
105
|
+
# pari grep
|
|
106
|
+
grep_parser = subparsers.add_parser("grep", help="Search text across workspace files")
|
|
107
|
+
grep_parser.add_argument("query", type=str, help="Search string or regex")
|
|
108
|
+
grep_parser.add_argument("-r", "--regex", action="store_true", help="Treat query as regular expression")
|
|
109
|
+
grep_parser.add_argument("-f", "--filter", type=str, default="*", help="File path pattern filter")
|
|
110
|
+
grep_parser.add_argument("-l", "--limit", type=int, default=50, help="Max matches")
|
|
111
|
+
|
|
112
|
+
# pari eval
|
|
113
|
+
eval_parser = subparsers.add_parser("eval", help="Run benchmark evaluation suites")
|
|
114
|
+
eval_parser.add_argument(
|
|
115
|
+
"--suite", type=str, default="./evals/micro_suite.json",
|
|
116
|
+
help="Path to evaluation benchmark suite JSON"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
args = parser.parse_args(argv)
|
|
120
|
+
|
|
121
|
+
if args.subcommand == "find":
|
|
122
|
+
return cmd_find(args)
|
|
123
|
+
elif args.subcommand == "grep":
|
|
124
|
+
return cmd_grep(args)
|
|
125
|
+
elif args.subcommand == "eval":
|
|
126
|
+
return cmd_eval(args)
|
|
127
|
+
else:
|
|
128
|
+
return cmd_run(args)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
sys.exit(main())
|
pari/config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PARI Global Configuration and Runtime Settings.
|
|
2
|
+
|
|
3
|
+
Encapsulates global agent options, model parameters, timeouts, and sandbox preferences.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PariConfig:
|
|
13
|
+
workspace: Path
|
|
14
|
+
model: str = "claude-3-7-sonnet"
|
|
15
|
+
sandbox_mode: Optional[str] = None
|
|
16
|
+
max_turns: int = 25
|
|
17
|
+
auto_approve: bool = False
|
|
18
|
+
debug: bool = False
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_env(cls, workspace: Optional[Path] = None) -> "PariConfig":
|
|
22
|
+
"""Load configuration options from environment variables and CLI defaults."""
|
|
23
|
+
raise NotImplementedError
|
pari/context/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PARI Layer 2: Context and Cache Architecture.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F4 (KV cache invalidation cascades causing 10x billing spikes)
|
|
5
|
+
and Claude Code F7 / Cursor F3 (Context amnesia & drift on compaction).
|
|
6
|
+
|
|
7
|
+
Subsystems:
|
|
8
|
+
- epoch: Immutable Context Epoch baseline manager
|
|
9
|
+
- chunks: 8-tier context chunk hierarchy assembly
|
|
10
|
+
- cache: Strategic cache breakpoint positioning for prompt caching
|
|
11
|
+
- compaction: Schema-preserving context compaction at 70% threshold
|
|
12
|
+
- warmer: Background economic cache warmer maintaining prompt cache state
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Placeholder imports will be exported once implementations are added
|
pari/context/cache.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PARI Layer 2: Cache Breakpoint Manager.
|
|
2
|
+
|
|
3
|
+
Positions Anthropic / provider prompt cache breakpoints at optimal tier boundaries:
|
|
4
|
+
1. Static Baseline (Tier 3: Repo Map / System Rules boundary)
|
|
5
|
+
2. Active Files boundary (Tier 5: Active files before conversation turns)
|
|
6
|
+
This guarantees high cache hit rates (70-90%) throughout long sessions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import List
|
|
10
|
+
from pari.context.chunks import ContextChunk
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def mark_cache_breakpoints(chunks: List[ContextChunk]) -> List[ContextChunk]:
|
|
14
|
+
"""Inspect chunks and ensure cache_control flags are set only on key tier boundaries."""
|
|
15
|
+
raise NotImplementedError
|
pari/context/chunks.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""PARI Layer 2: 8-Tier Context Chunk Hierarchy.
|
|
2
|
+
|
|
3
|
+
Defines the structured tiers of context injected into the model:
|
|
4
|
+
Tier 1: System prompt & behavioral guardrails
|
|
5
|
+
Tier 2: Tool definition schemas
|
|
6
|
+
Tier 3: Static codebase repository map (AST / symbols)
|
|
7
|
+
Tier 4: Project rules (.pari/rules or README instructions)
|
|
8
|
+
Tier 5: Active loaded files
|
|
9
|
+
Tier 6: Prior conversation turns
|
|
10
|
+
Tier 7: Current user turn
|
|
11
|
+
Tier 8: Ephemeral scratchpad
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import IntEnum
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextTier(IntEnum):
|
|
20
|
+
SYSTEM_PROMPT = 1
|
|
21
|
+
TOOL_DEFINITIONS = 2
|
|
22
|
+
STATIC_REPO_MAP = 3
|
|
23
|
+
PROJECT_RULES = 4
|
|
24
|
+
ACTIVE_FILES = 5
|
|
25
|
+
TURN_HISTORY = 6
|
|
26
|
+
CURRENT_TURN = 7
|
|
27
|
+
SCRATCHPAD = 8
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ContextChunk:
|
|
32
|
+
tier: ContextTier
|
|
33
|
+
content: str
|
|
34
|
+
identifier: str
|
|
35
|
+
cache_control: bool = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def assemble_context(chunks: List[ContextChunk]) -> List[Dict[str, Any]]:
|
|
39
|
+
"""Sort chunks by strict tier hierarchy and format for model payload."""
|
|
40
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""PARI Layer 2: Safe Context Compaction.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Closes Claude Code F7 (Context amnesia on compaction) & Cursor F3 (Context drift).
|
|
5
|
+
When context token usage crosses 70% of model window capacity:
|
|
6
|
+
1. Extracts and preserves API schemas, architectural agreements, and file mutation lists.
|
|
7
|
+
2. Summarizes conversational chitchat and repetitive tool execution outputs.
|
|
8
|
+
3. Replaces Tier 6 (Turn History) with the compressed summary, leaving Tiers 1-5 intact.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
from pari.context.chunks import ContextChunk
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compact_context(
|
|
16
|
+
chunks: List[ContextChunk],
|
|
17
|
+
max_tokens: int = 128000,
|
|
18
|
+
compaction_threshold: float = 0.70,
|
|
19
|
+
) -> List[ContextChunk]:
|
|
20
|
+
"""Inspect total context volume and compact turn history if threshold is exceeded."""
|
|
21
|
+
raise NotImplementedError
|
pari/context/epoch.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""PARI Layer 2: Immutable Context Epoch Baseline Manager.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from OpenCode context-epoch.ts.
|
|
5
|
+
Freezes Tiers 1-4 (System Prompt, Tools, Static Repo Map, Project Rules)
|
|
6
|
+
into an immutable baseline for the duration of the epoch.
|
|
7
|
+
Mid-session file edits or dynamic user turns NEVER invalidate upstream cache
|
|
8
|
+
prefixes, preventing the catastrophic KV-cache invalidation cascades seen in Claude Code.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import List
|
|
13
|
+
from pari.context.chunks import ContextChunk
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ContextEpoch:
|
|
18
|
+
epoch_id: str
|
|
19
|
+
version: int = 1
|
|
20
|
+
system_prompt: str = ""
|
|
21
|
+
tools_schema: str = ""
|
|
22
|
+
static_repo_map: str = ""
|
|
23
|
+
project_rules: str = ""
|
|
24
|
+
is_frozen: bool = False
|
|
25
|
+
|
|
26
|
+
def freeze(self):
|
|
27
|
+
"""Freeze baseline tiers to lock KV cache prefix."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def get_baseline_chunks(self) -> List[ContextChunk]:
|
|
31
|
+
"""Assemble immutable baseline context chunks."""
|
|
32
|
+
raise NotImplementedError
|
pari/context/warmer.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""PARI Layer 2: Autonomous Economic Cache Warmer.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Pi's cache-warmer.ts.
|
|
5
|
+
Cloud LLM prompt caches (Anthropic KV cache) expire after 5 minutes of inactivity.
|
|
6
|
+
If the developer pauses for 6 minutes while reading or typing, the next turn suffers
|
|
7
|
+
a full 100% cache-miss billing spike.
|
|
8
|
+
The CacheWarmer sends a lightweight keep-alive request every 4.5 minutes to maintain
|
|
9
|
+
cache warm status at negligible cost (<$0.001/hr).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Callable, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheWarmer:
|
|
16
|
+
"""Maintains prompt cache warmth via periodic lightweight async pings."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, interval_seconds: float = 270.0, warm_callback: Optional[Callable] = None):
|
|
19
|
+
self.interval_seconds = interval_seconds
|
|
20
|
+
self.warm_callback = warm_callback
|
|
21
|
+
|
|
22
|
+
def touch(self):
|
|
23
|
+
"""Mark recent interaction to reset idle timer."""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def start(self):
|
|
27
|
+
"""Start background keep-alive ping loop."""
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def stop(self):
|
|
31
|
+
"""Stop background keep-alive ping loop."""
|
|
32
|
+
raise NotImplementedError
|
pari/edit/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PARI Layer 5: Edit Engine (Multi-Tier Fuzzy Search/Replace Cascade).
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Aider's search_replace.py.
|
|
5
|
+
Provides robust, resilient code editing across 4 tiers:
|
|
6
|
+
Tier 1: Exact string match with Stop-on-Ambiguity invariant (0 or >1 matches error).
|
|
7
|
+
Tier 2: Git OSR (one-sided merge / 3-way context match).
|
|
8
|
+
Tier 3: Diff-Match-Patch (DMP with strict 0.8 similarity threshold).
|
|
9
|
+
Tier 4: Prefill continuation for large code generation.
|
|
10
|
+
|
|
11
|
+
Subsystems:
|
|
12
|
+
- exact: Exact string search and replace with ambiguity guards
|
|
13
|
+
- cascade: Multi-tier cascade orchestrator
|
|
14
|
+
- git_osr: Git cherry-pick / context alignment fallback
|
|
15
|
+
- dmp: Diff-Match-Patch fuzzy search fallback
|
|
16
|
+
- indenter: Relative indentation normalization
|
|
17
|
+
- diff: Unified diff generation
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Placeholder imports will be exported once implementations are added
|
pari/edit/cascade.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PARI Layer 5: Multi-Tier Edit Cascade Orchestrator.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Sequences the edit attempts across tiers:
|
|
5
|
+
Tier 1: Exact Match (Stop-on-Ambiguity)
|
|
6
|
+
Tier 2: Git OSR (One-Sided-Merge / line alignment)
|
|
7
|
+
Tier 3: Diff-Match-Patch (Fuzzy threshold 0.8)
|
|
8
|
+
Tier 4: Prefill continuation
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EditCascade:
|
|
15
|
+
"""Orchestrates multi-tier edit fallbacks."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, allow_fallbacks: bool = True):
|
|
18
|
+
self.allow_fallbacks = allow_fallbacks
|
|
19
|
+
|
|
20
|
+
def apply(self, original: str, search: str, replace: str) -> Tuple[str, str]:
|
|
21
|
+
"""Apply edit using cascade. Returns (new_content, tier_used)."""
|
|
22
|
+
raise NotImplementedError
|
pari/edit/diff.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""PARI Layer 5: Unified Diff Generation.
|
|
2
|
+
|
|
3
|
+
Formats edits into standard unified diffs for terminal preview and inspection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def generate_unified_diff(original: str, modified: str, filename: str = "target") -> str:
|
|
7
|
+
"""Generate a clean unified diff between original and modified text."""
|
|
8
|
+
raise NotImplementedError
|
pari/edit/dmp.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""PARI Layer 5: Diff-Match-Patch (DMP) Fuzzy Search Fallback.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Uses approximate string matching when exact match and line-based Git OSR fail.
|
|
5
|
+
Strict Invariant:
|
|
6
|
+
Threshold is capped at 0.8 minimum similarity. If best match similarity is < 0.8,
|
|
7
|
+
the edit is rejected to prevent silent code corruption.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def apply_dmp_fuzzy(original: str, search: str, replace: str, threshold: float = 0.8) -> Optional[str]:
|
|
14
|
+
"""Find the best fuzzy match for search block and apply replacement if above threshold."""
|
|
15
|
+
raise NotImplementedError
|
pari/edit/exact.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""PARI Layer 5: Exact String Search & Replace with Ambiguity Guard.
|
|
2
|
+
|
|
3
|
+
Architectural Invariant:
|
|
4
|
+
Stop-on-Ambiguity:
|
|
5
|
+
- If search pattern occurs 0 times: Raise PatternNotFoundError.
|
|
6
|
+
- If search pattern occurs > 1 times: Raise AmbiguousMatchError.
|
|
7
|
+
- An edit is ONLY applied if the search pattern matches EXACTLY once.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PatternNotFoundError(Exception):
|
|
14
|
+
"""Raised when the search string does not exist in target content."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AmbiguousMatchError(Exception):
|
|
19
|
+
"""Raised when the search string occurs multiple times in target content."""
|
|
20
|
+
def __init__(self, count: int, search_str: str, filename: str = "<unknown>"):
|
|
21
|
+
super().__init__(
|
|
22
|
+
f"Ambiguous search target in '{filename}': found {count} identical matches. "
|
|
23
|
+
"Please provide additional surrounding context to identify a unique block."
|
|
24
|
+
)
|
|
25
|
+
self.count = count
|
|
26
|
+
self.search_str = search_str
|
|
27
|
+
self.filename = filename
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def apply_exact_edit(original: str, search: str, replace: str, filename: str = "<unknown>") -> str:
|
|
31
|
+
"""Applies search/replace to text, strictly requiring a single unique match."""
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def preview_edit(original: str, search: str, replace: str, filename: str = "<unknown>") -> Tuple[str, str]:
|
|
36
|
+
"""Applies exact edit and returns both modified content and unified diff."""
|
|
37
|
+
raise NotImplementedError
|
pari/edit/git_osr.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PARI Layer 5: Git One-Sided Merge (OSR) Fallback.
|
|
2
|
+
|
|
3
|
+
Architectural Purpose:
|
|
4
|
+
Harvested from Aider's git cherry-pick / 3-way merge strategy.
|
|
5
|
+
When an exact match fails due to minor surrounding line drift, Git OSR treats
|
|
6
|
+
the search block as a base revision and attempts a 3-way context alignment.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def apply_git_osr(original: str, search: str, replace: str) -> Optional[str]:
|
|
13
|
+
"""Attempt 3-way context alignment using line-based diff matching."""
|
|
14
|
+
raise NotImplementedError
|