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.
Files changed (81) hide show
  1. pari/__init__.py +3 -0
  2. pari/agent/__init__.py +14 -0
  3. pari/agent/loop.py +35 -0
  4. pari/agent/reflection.py +39 -0
  5. pari/agent/runtime.py +91 -0
  6. pari/agent/subagent.py +34 -0
  7. pari/cli.py +132 -0
  8. pari/config.py +23 -0
  9. pari/context/__init__.py +15 -0
  10. pari/context/cache.py +15 -0
  11. pari/context/chunks.py +40 -0
  12. pari/context/compaction.py +21 -0
  13. pari/context/epoch.py +32 -0
  14. pari/context/warmer.py +32 -0
  15. pari/edit/__init__.py +20 -0
  16. pari/edit/cascade.py +22 -0
  17. pari/edit/diff.py +8 -0
  18. pari/edit/dmp.py +15 -0
  19. pari/edit/exact.py +37 -0
  20. pari/edit/git_osr.py +14 -0
  21. pari/edit/indenter.py +12 -0
  22. pari/eval/__init__.py +14 -0
  23. pari/eval/judge.py +21 -0
  24. pari/eval/logging.py +19 -0
  25. pari/eval/metrics.py +22 -0
  26. pari/eval/runner.py +34 -0
  27. pari/fs/__init__.py +16 -0
  28. pari/fs/atomic.py +53 -0
  29. pari/fs/path.py +28 -0
  30. pari/fs/queue.py +22 -0
  31. pari/intelligence/__init__.py +15 -0
  32. pari/intelligence/ast.py +25 -0
  33. pari/intelligence/cache.py +21 -0
  34. pari/intelligence/ppr.py +21 -0
  35. pari/intelligence/repomap.py +21 -0
  36. pari/intelligence/search.py +33 -0
  37. pari/output/__init__.py +14 -0
  38. pari/output/parser.py +14 -0
  39. pari/output/spooler.py +33 -0
  40. pari/output/truncation.py +20 -0
  41. pari/providers/__init__.py +24 -0
  42. pari/providers/anthropic.py +23 -0
  43. pari/providers/base.py +56 -0
  44. pari/providers/deepseek.py +24 -0
  45. pari/providers/mock.py +46 -0
  46. pari/providers/ollama.py +25 -0
  47. pari/providers/openai.py +23 -0
  48. pari/routing/__init__.py +16 -0
  49. pari/routing/anti_verbosity.py +13 -0
  50. pari/routing/classifier.py +24 -0
  51. pari/routing/router.py +23 -0
  52. pari/sandbox/__init__.py +16 -0
  53. pari/sandbox/base.py +74 -0
  54. pari/sandbox/bwrap.py +39 -0
  55. pari/sandbox/dispatcher.py +31 -0
  56. pari/sandbox/e2b.py +32 -0
  57. pari/sandbox/process.py +38 -0
  58. pari/sandbox/seatbelt.py +32 -0
  59. pari/session/__init__.py +15 -0
  60. pari/session/checkpoint.py +24 -0
  61. pari/session/dag.py +34 -0
  62. pari/session/events.py +50 -0
  63. pari/session/logger.py +41 -0
  64. pari/session/undo.py +99 -0
  65. pari/tools/__init__.py +14 -0
  66. pari/tools/base.py +35 -0
  67. pari/tools/bash.py +24 -0
  68. pari/tools/fs.py +53 -0
  69. pari/tools/registry.py +36 -0
  70. pari/tools/search.py +38 -0
  71. pari/tui/__init__.py +28 -0
  72. pari/tui/app.py +355 -0
  73. pari/tui/commands.py +45 -0
  74. pari/tui/diff_view.py +17 -0
  75. pari/tui/status.py +65 -0
  76. pari_cli-0.1.0.dist-info/METADATA +167 -0
  77. pari_cli-0.1.0.dist-info/RECORD +81 -0
  78. pari_cli-0.1.0.dist-info/WHEEL +5 -0
  79. pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
  80. pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  81. pari_cli-0.1.0.dist-info/top_level.txt +1 -0
pari/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """PARI — High-performance terminal coding agent."""
2
+
3
+ __version__ = "0.1.0"
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
@@ -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
@@ -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