coding-agents 0.0.1.dev0__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.
- coding_agents/__init__.py +37 -0
- coding_agents/cli.py +87 -0
- coding_agents/core/__init__.py +26 -0
- coding_agents/core/agent.py +164 -0
- coding_agents/core/budget.py +75 -0
- coding_agents/core/loop.py +76 -0
- coding_agents/core/models.py +55 -0
- coding_agents/evolution/__init__.py +0 -0
- coding_agents/evolution/parallel_planner.py +67 -0
- coding_agents/evolution/skill_miner.py +106 -0
- coding_agents/py.typed +2 -0
- coding_agents/sandbox/__init__.py +7 -0
- coding_agents/sandbox/policy.py +61 -0
- coding_agents/sandbox/worktree.py +61 -0
- coding_agents/tools/__init__.py +8 -0
- coding_agents/tools/base.py +55 -0
- coding_agents/tools/bash_tool.py +103 -0
- coding_agents/tools/patch_editor.py +111 -0
- coding_agents/verification/__init__.py +15 -0
- coding_agents/verification/fault_localizer.py +51 -0
- coding_agents/verification/oscillation.py +46 -0
- coding_agents/verification/pipeline.py +96 -0
- coding_agents-0.0.1.dev0.dist-info/METADATA +267 -0
- coding_agents-0.0.1.dev0.dist-info/RECORD +27 -0
- coding_agents-0.0.1.dev0.dist-info/WHEEL +5 -0
- coding_agents-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- coding_agents-0.0.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Coding Agents: The reference harness and runtime for autonomous coding systems."""
|
|
2
|
+
|
|
3
|
+
from coding_agents.core.agent import CodingAgent
|
|
4
|
+
from coding_agents.core.budget import Budget
|
|
5
|
+
from coding_agents.core.models import StopCondition, Trajectory, TurnRecord, ToolCall, ToolResult
|
|
6
|
+
from coding_agents.tools.patch_editor import PatchEditor
|
|
7
|
+
from coding_agents.tools.bash_tool import BashTool
|
|
8
|
+
from coding_agents.sandbox.policy import PolicyGate
|
|
9
|
+
from coding_agents.sandbox.worktree import WorktreeSandbox
|
|
10
|
+
from coding_agents.verification.pipeline import VerificationPipeline, VerificationManifest
|
|
11
|
+
from coding_agents.verification.fault_localizer import FaultLocalizer
|
|
12
|
+
from coding_agents.verification.oscillation import OscillationDetector
|
|
13
|
+
from coding_agents.evolution.skill_miner import SkillMiner
|
|
14
|
+
from coding_agents.evolution.parallel_planner import ParallelPlanningEngine
|
|
15
|
+
|
|
16
|
+
__version__ = "0.0.1.dev0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CodingAgent",
|
|
20
|
+
"Budget",
|
|
21
|
+
"StopCondition",
|
|
22
|
+
"Trajectory",
|
|
23
|
+
"TurnRecord",
|
|
24
|
+
"ToolCall",
|
|
25
|
+
"ToolResult",
|
|
26
|
+
"PatchEditor",
|
|
27
|
+
"BashTool",
|
|
28
|
+
"PolicyGate",
|
|
29
|
+
"WorktreeSandbox",
|
|
30
|
+
"VerificationPipeline",
|
|
31
|
+
"VerificationManifest",
|
|
32
|
+
"FaultLocalizer",
|
|
33
|
+
"OscillationDetector",
|
|
34
|
+
"SkillMiner",
|
|
35
|
+
"ParallelPlanningEngine",
|
|
36
|
+
]
|
|
37
|
+
|
coding_agents/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Command-Line Interface for the coding-agents harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from coding_agents import __version__, CodingAgent, Budget, VerificationPipeline, SkillMiner
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="coding-agent",
|
|
17
|
+
description="Coding Agents: The Reference Software Engineering Harness & Runtime",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
20
|
+
|
|
21
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
|
|
22
|
+
|
|
23
|
+
# Command: run
|
|
24
|
+
run_parser = subparsers.add_parser("run", help="Execute an autonomous coding task")
|
|
25
|
+
run_parser.add_argument("--task", "-t", required=True, help="Task description or issue prompt")
|
|
26
|
+
run_parser.add_argument("--model", "-m", default="claude-3-7-sonnet", help="Base model identifier")
|
|
27
|
+
run_parser.add_argument("--max-turns", type=int, default=30, help="Maximum turn budget")
|
|
28
|
+
run_parser.add_argument("--max-cost", type=float, default=5.00, help="Maximum monetary cost budget ($)")
|
|
29
|
+
|
|
30
|
+
# Command: verify
|
|
31
|
+
verify_parser = subparsers.add_parser("verify", help="Run the 5-stage verification pipeline")
|
|
32
|
+
verify_parser.add_argument("--dir", default=".", help="Target workspace directory")
|
|
33
|
+
|
|
34
|
+
# Command: skill-mine
|
|
35
|
+
mine_parser = subparsers.add_parser("skill-mine", help="Synthesize SKILL.md from task description")
|
|
36
|
+
mine_parser.add_argument("--task", "-t", required=True, help="Task description to parameterize")
|
|
37
|
+
mine_parser.add_argument("--out", "-o", default=".agents/skills", help="Output skills directory")
|
|
38
|
+
|
|
39
|
+
return parser
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(args: list[str] | None = None) -> int:
|
|
43
|
+
console = Console()
|
|
44
|
+
parser = create_parser()
|
|
45
|
+
parsed = parser.parse_args(args)
|
|
46
|
+
|
|
47
|
+
if not parsed.subcommand:
|
|
48
|
+
parser.print_help()
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
if parsed.subcommand == "run":
|
|
52
|
+
console.print(Panel(f"[bold cyan]Starting Coding Agent Task[/bold cyan]\nTask: {parsed.task}\nModel: {parsed.model}", title="Coding Agents Harness"))
|
|
53
|
+
budget = Budget(max_turns=parsed.max_turns, max_cost_usd=parsed.max_cost)
|
|
54
|
+
agent = CodingAgent(model=parsed.model, budget=budget)
|
|
55
|
+
trajectory = agent.run(parsed.task)
|
|
56
|
+
console.print(f"[bold green]Task Finished[/bold green]: {trajectory.final_status.value} (Tokens: {trajectory.total_tokens:,}, Cost: ${trajectory.total_cost_usd:.3f})")
|
|
57
|
+
return 0 if trajectory.success else 1
|
|
58
|
+
|
|
59
|
+
elif parsed.subcommand == "verify":
|
|
60
|
+
console.print("[bold yellow]Executing Verification Pipeline...[/bold yellow]")
|
|
61
|
+
pipeline = VerificationPipeline(working_dir=parsed.dir)
|
|
62
|
+
manifest = pipeline.run_all()
|
|
63
|
+
table = Table(title="Verification Manifest")
|
|
64
|
+
table.add_column("Stage", style="cyan")
|
|
65
|
+
table.add_column("Status", style="bold")
|
|
66
|
+
table.add_column("Diagnostics")
|
|
67
|
+
|
|
68
|
+
for stage in manifest.stages:
|
|
69
|
+
status_str = "[green]PASS[/green]" if stage.passed else "[red]FAIL[/red]"
|
|
70
|
+
table.add_row(stage.stage_name, status_str, stage.error_message or "Clean")
|
|
71
|
+
|
|
72
|
+
console.print(table)
|
|
73
|
+
return 0 if manifest.all_passed else 1
|
|
74
|
+
|
|
75
|
+
elif parsed.subcommand == "skill-mine":
|
|
76
|
+
console.print(f"[bold magenta]Mining procedural pattern for task: {parsed.task}[/bold magenta]")
|
|
77
|
+
candidate = SkillMiner.mine_trajectory(parsed.task, [])
|
|
78
|
+
pkg_dir = SkillMiner.export_skill_package(candidate, parsed.out)
|
|
79
|
+
console.print(f"[bold green]Exported Skill Package to:[/bold green] {pkg_dir}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
sys.exit(main())
|
|
87
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Core execution loop, models, budgets, and agent classes."""
|
|
2
|
+
|
|
3
|
+
from coding_agents.core.models import (
|
|
4
|
+
StopCondition,
|
|
5
|
+
ToolCall,
|
|
6
|
+
ToolResult,
|
|
7
|
+
TurnRecord,
|
|
8
|
+
Trajectory,
|
|
9
|
+
)
|
|
10
|
+
from coding_agents.core.budget import Budget, TokenPricing
|
|
11
|
+
from coding_agents.core.loop import BoundedExecutionLoop, FlailDetector
|
|
12
|
+
from coding_agents.core.agent import CodingAgent
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"StopCondition",
|
|
16
|
+
"ToolCall",
|
|
17
|
+
"ToolResult",
|
|
18
|
+
"TurnRecord",
|
|
19
|
+
"Trajectory",
|
|
20
|
+
"Budget",
|
|
21
|
+
"TokenPricing",
|
|
22
|
+
"BoundedExecutionLoop",
|
|
23
|
+
"FlailDetector",
|
|
24
|
+
"CodingAgent",
|
|
25
|
+
]
|
|
26
|
+
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Core CodingAgent reference implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from coding_agents.core.models import StopCondition, ToolCall, ToolResult, TurnRecord, Trajectory
|
|
9
|
+
from coding_agents.core.budget import Budget
|
|
10
|
+
from coding_agents.core.loop import BoundedExecutionLoop, FlailDetector
|
|
11
|
+
from coding_agents.tools.base import BaseTool
|
|
12
|
+
from coding_agents.tools.patch_editor import PatchEditor
|
|
13
|
+
from coding_agents.tools.bash_tool import BashTool
|
|
14
|
+
from coding_agents.sandbox.policy import PolicyGate
|
|
15
|
+
from coding_agents.verification.pipeline import VerificationPipeline
|
|
16
|
+
from coding_agents.verification.oscillation import OscillationDetector
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodingAgent:
|
|
20
|
+
"""The complete 7-component Coding Agent harness."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
model: str = "claude-3-7-sonnet",
|
|
25
|
+
working_dir: str = ".",
|
|
26
|
+
tools: Optional[List[BaseTool]] = None,
|
|
27
|
+
budget: Optional[Budget] = None,
|
|
28
|
+
policy_gate: Optional[PolicyGate] = None,
|
|
29
|
+
verification_pipeline: Optional[VerificationPipeline] = None,
|
|
30
|
+
console: Optional[Console] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self.model = model
|
|
33
|
+
self.working_dir = working_dir
|
|
34
|
+
self.console = console or Console()
|
|
35
|
+
|
|
36
|
+
# 1. Tools
|
|
37
|
+
self.tools: Dict[str, BaseTool] = {}
|
|
38
|
+
default_tools = tools or [
|
|
39
|
+
PatchEditor(working_dir=working_dir),
|
|
40
|
+
BashTool(working_dir=working_dir),
|
|
41
|
+
]
|
|
42
|
+
for tool in default_tools:
|
|
43
|
+
self.tools[tool.name] = tool
|
|
44
|
+
|
|
45
|
+
# 2. Budget & Policy
|
|
46
|
+
self.budget = budget or Budget(max_turns=30, max_cost_usd=5.0)
|
|
47
|
+
self.policy_gate = policy_gate or PolicyGate()
|
|
48
|
+
|
|
49
|
+
# 3. Verification & Oscillation
|
|
50
|
+
self.verification_pipeline = verification_pipeline or VerificationPipeline(working_dir=working_dir)
|
|
51
|
+
self.oscillation_detector = OscillationDetector()
|
|
52
|
+
|
|
53
|
+
# 4. Control Loop
|
|
54
|
+
self.loop = BoundedExecutionLoop(
|
|
55
|
+
budget=self.budget,
|
|
56
|
+
flail_detector=FlailDetector(),
|
|
57
|
+
verification_oracle=self._run_ground_truth_verification,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _run_ground_truth_verification(self) -> bool:
|
|
61
|
+
"""Run verification pipeline to evaluate ground truth correctness."""
|
|
62
|
+
manifest = self.verification_pipeline.run_all(target_dir=self.working_dir)
|
|
63
|
+
return manifest.all_passed
|
|
64
|
+
|
|
65
|
+
def dispatch_tool(self, call: ToolCall) -> ToolResult:
|
|
66
|
+
"""Enforce policy gate and dispatch to the matching tool."""
|
|
67
|
+
# 1. Policy check
|
|
68
|
+
if call.name == "bash":
|
|
69
|
+
cmd = call.arguments.get("command", "")
|
|
70
|
+
allowed, reason = self.policy_gate.check_command(cmd)
|
|
71
|
+
if not allowed:
|
|
72
|
+
return ToolResult(
|
|
73
|
+
tool_call_id=call.id,
|
|
74
|
+
name=call.name,
|
|
75
|
+
output="",
|
|
76
|
+
error=reason,
|
|
77
|
+
exit_code=1,
|
|
78
|
+
)
|
|
79
|
+
elif call.name == "edit_file_exact":
|
|
80
|
+
path = call.arguments.get("path", "")
|
|
81
|
+
allowed, reason = self.policy_gate.check_file_mutation(path)
|
|
82
|
+
if not allowed:
|
|
83
|
+
return ToolResult(
|
|
84
|
+
tool_call_id=call.id,
|
|
85
|
+
name=call.name,
|
|
86
|
+
output="",
|
|
87
|
+
error=reason,
|
|
88
|
+
exit_code=1,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# 2. Tool dispatch
|
|
92
|
+
tool = self.tools.get(call.name)
|
|
93
|
+
if not tool:
|
|
94
|
+
return ToolResult(
|
|
95
|
+
tool_call_id=call.id,
|
|
96
|
+
name=call.name,
|
|
97
|
+
output="",
|
|
98
|
+
error=f"Unknown tool: '{call.name}'",
|
|
99
|
+
exit_code=1,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
res = tool.execute(**call.arguments)
|
|
104
|
+
res.tool_call_id = call.id
|
|
105
|
+
return res
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return ToolResult(
|
|
108
|
+
tool_call_id=call.id,
|
|
109
|
+
name=call.name,
|
|
110
|
+
output="",
|
|
111
|
+
error=f"Exception executing {call.name}: {str(e)}",
|
|
112
|
+
exit_code=1,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def run(
|
|
116
|
+
self,
|
|
117
|
+
task_description: str,
|
|
118
|
+
proposer_callback: Optional[Callable[[List[Dict[str, Any]]], List[ToolCall]]] = None,
|
|
119
|
+
) -> Trajectory:
|
|
120
|
+
"""Execute the agent loop until a deterministic stop condition is reached."""
|
|
121
|
+
trajectory = Trajectory(
|
|
122
|
+
task_id="task-001",
|
|
123
|
+
task_description=task_description,
|
|
124
|
+
final_status=StopCondition.BUDGET,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
turn_idx = 0
|
|
128
|
+
while True:
|
|
129
|
+
# Check budget before turn
|
|
130
|
+
stop_reason = self.loop.evaluate_stop(calls=[])
|
|
131
|
+
if stop_reason:
|
|
132
|
+
trajectory.final_status = stop_reason
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# If no custom proposer is provided, run mock or single turn
|
|
136
|
+
calls = proposer_callback([]) if proposer_callback else []
|
|
137
|
+
|
|
138
|
+
# Check stop condition with proposed calls
|
|
139
|
+
stop_reason = self.loop.evaluate_stop(calls=calls)
|
|
140
|
+
if stop_reason:
|
|
141
|
+
trajectory.final_status = stop_reason
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Execute calls and record turn
|
|
145
|
+
turn_results = [self.dispatch_tool(c) for c in calls]
|
|
146
|
+
turn = TurnRecord(
|
|
147
|
+
turn_index=turn_idx,
|
|
148
|
+
tool_calls=calls,
|
|
149
|
+
tool_results=turn_results,
|
|
150
|
+
)
|
|
151
|
+
trajectory.turns.append(turn)
|
|
152
|
+
|
|
153
|
+
# In simulation, break after 1 turn if no calls
|
|
154
|
+
if not calls:
|
|
155
|
+
trajectory.final_status = StopCondition.DONE if self._run_ground_truth_verification() else StopCondition.ESCALATE
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
turn_idx += 1
|
|
159
|
+
|
|
160
|
+
trajectory.total_cost_usd = self.budget.used_cost_usd
|
|
161
|
+
trajectory.total_tokens = self.budget.total_tokens
|
|
162
|
+
trajectory.success = (trajectory.final_status == StopCondition.DONE)
|
|
163
|
+
return trajectory
|
|
164
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Budget tracking and cost modeling for agent execution loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class TokenPricing:
|
|
11
|
+
"""Pricing per million tokens in USD."""
|
|
12
|
+
prompt_per_million: float = 3.00
|
|
13
|
+
completion_per_million: float = 15.00
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Reference pricing for standard frontier models
|
|
17
|
+
MODEL_PRICING: Dict[str, TokenPricing] = {
|
|
18
|
+
"claude-3-7-sonnet": TokenPricing(prompt_per_million=3.0, completion_per_million=15.0),
|
|
19
|
+
"claude-3-5-sonnet": TokenPricing(prompt_per_million=3.0, completion_per_million=15.0),
|
|
20
|
+
"gpt-4o": TokenPricing(prompt_per_million=2.5, completion_per_million=10.0),
|
|
21
|
+
"deepseek-r1": TokenPricing(prompt_per_million=0.55, completion_per_million=2.19),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Budget:
|
|
27
|
+
"""Resource budget manager enforcing hard resource ceilings."""
|
|
28
|
+
max_turns: int = 30
|
|
29
|
+
max_tokens: int = 200_000
|
|
30
|
+
max_cost_usd: float = 5.00
|
|
31
|
+
max_wall_time_seconds: float = 600.0
|
|
32
|
+
|
|
33
|
+
used_turns: int = 0
|
|
34
|
+
used_prompt_tokens: int = 0
|
|
35
|
+
used_completion_tokens: int = 0
|
|
36
|
+
used_cost_usd: float = 0.0
|
|
37
|
+
elapsed_seconds: float = 0.0
|
|
38
|
+
|
|
39
|
+
pricing: TokenPricing = field(default_factory=TokenPricing)
|
|
40
|
+
|
|
41
|
+
def charge(self, prompt_tokens: int, completion_tokens: int, model: Optional[str] = None) -> float:
|
|
42
|
+
"""Record token consumption and update cumulative monetary spend."""
|
|
43
|
+
self.used_turns += 1
|
|
44
|
+
self.used_prompt_tokens += prompt_tokens
|
|
45
|
+
self.used_completion_tokens += completion_tokens
|
|
46
|
+
|
|
47
|
+
pricing = MODEL_PRICING.get(model or "", self.pricing)
|
|
48
|
+
turn_cost = (
|
|
49
|
+
(prompt_tokens / 1_000_000.0) * pricing.prompt_per_million
|
|
50
|
+
+ (completion_tokens / 1_000_000.0) * pricing.completion_per_million
|
|
51
|
+
)
|
|
52
|
+
self.used_cost_usd += turn_cost
|
|
53
|
+
return turn_cost
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def total_tokens(self) -> int:
|
|
57
|
+
return self.used_prompt_tokens + self.used_completion_tokens
|
|
58
|
+
|
|
59
|
+
def exhausted(self) -> bool:
|
|
60
|
+
"""Check whether any hard budget limit has been reached."""
|
|
61
|
+
if self.used_turns >= self.max_turns:
|
|
62
|
+
return True
|
|
63
|
+
if self.total_tokens >= self.max_tokens:
|
|
64
|
+
return True
|
|
65
|
+
if self.used_cost_usd >= self.max_cost_usd:
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def remaining_summary(self) -> str:
|
|
70
|
+
return (
|
|
71
|
+
f"Turns: {self.used_turns}/{self.max_turns} | "
|
|
72
|
+
f"Tokens: {self.total_tokens:,}/{self.max_tokens:,} | "
|
|
73
|
+
f"Cost: ${self.used_cost_usd:.3f}/${self.max_cost_usd:.2f}"
|
|
74
|
+
)
|
|
75
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Bounded execution loop and oscillation detection for coding agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Callable, List, Optional, Set, Tuple
|
|
8
|
+
from coding_agents.core.models import StopCondition, ToolCall, ToolResult, TurnRecord
|
|
9
|
+
from coding_agents.core.budget import Budget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlailDetector:
|
|
13
|
+
"""Detects cyclic repetition and unproductive action loops."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, history_window: int = 10) -> None:
|
|
16
|
+
self.history_window = history_window
|
|
17
|
+
self.seen_signatures: Set[str] = set()
|
|
18
|
+
self.signature_history: List[str] = []
|
|
19
|
+
|
|
20
|
+
def fingerprint_calls(self, calls: List[ToolCall]) -> str:
|
|
21
|
+
"""Compute deterministic canonical hash of tool proposals."""
|
|
22
|
+
serialized = []
|
|
23
|
+
for call in sorted(calls, key=lambda c: (c.name, c.id)):
|
|
24
|
+
serialized.append({
|
|
25
|
+
"name": call.name,
|
|
26
|
+
"arguments": call.arguments,
|
|
27
|
+
})
|
|
28
|
+
raw = json.dumps(serialized, sort_keys=True)
|
|
29
|
+
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
30
|
+
|
|
31
|
+
def check_and_record(self, calls: List[ToolCall]) -> bool:
|
|
32
|
+
"""Return True if identical tool proposal was seen previously."""
|
|
33
|
+
if not calls:
|
|
34
|
+
return False
|
|
35
|
+
sig = self.fingerprint_calls(calls)
|
|
36
|
+
if sig in self.seen_signatures:
|
|
37
|
+
return True
|
|
38
|
+
self.seen_signatures.add(sig)
|
|
39
|
+
self.signature_history.append(sig)
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BoundedExecutionLoop:
|
|
44
|
+
"""The control loop that coordinates turns, charges budgets, and enforces termination."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
budget: Optional[Budget] = None,
|
|
49
|
+
flail_detector: Optional[FlailDetector] = None,
|
|
50
|
+
verification_oracle: Optional[Callable[[], bool]] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.budget = budget or Budget()
|
|
53
|
+
self.flail_detector = flail_detector or FlailDetector()
|
|
54
|
+
self.verification_oracle = verification_oracle
|
|
55
|
+
|
|
56
|
+
def evaluate_stop(
|
|
57
|
+
self,
|
|
58
|
+
calls: List[ToolCall],
|
|
59
|
+
explicit_done_called: bool = False,
|
|
60
|
+
) -> Optional[StopCondition]:
|
|
61
|
+
"""Check termination invariants at the start/end of each turn."""
|
|
62
|
+
if self.budget.exhausted():
|
|
63
|
+
return StopCondition.BUDGET
|
|
64
|
+
|
|
65
|
+
if self.flail_detector.check_and_record(calls):
|
|
66
|
+
return StopCondition.STUCK
|
|
67
|
+
|
|
68
|
+
if explicit_done_called or not calls:
|
|
69
|
+
# Model proposed it is finished; verify with ground truth oracle
|
|
70
|
+
if self.verification_oracle is not None:
|
|
71
|
+
passed = self.verification_oracle()
|
|
72
|
+
return StopCondition.DONE if passed else StopCondition.ESCALATE
|
|
73
|
+
return StopCondition.DONE
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Data models representing agent states, tool calls, and trajectories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StopCondition(str, Enum):
|
|
11
|
+
"""The four deterministic ways an agent execution loop terminates."""
|
|
12
|
+
DONE = "verified_complete" # External verification oracle passed
|
|
13
|
+
BUDGET = "budget_exhausted" # Token/turn/cost budget exhausted
|
|
14
|
+
STUCK = "oscillation_detected" # No progress / repeated state detected
|
|
15
|
+
ESCALATE = "needs_escalation" # Policy denial or ambiguous invariant
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ToolCall(BaseModel):
|
|
19
|
+
"""A structured tool call proposal from the model."""
|
|
20
|
+
id: str = Field(default="", description="Unique tool invocation ID")
|
|
21
|
+
name: str = Field(description="Name of the tool being called")
|
|
22
|
+
arguments: Dict[str, Any] = Field(default_factory=dict, description="Parsed tool input parameters")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolResult(BaseModel):
|
|
26
|
+
"""The observation returned from tool execution in the environment."""
|
|
27
|
+
tool_call_id: str = Field(default="", description="ID of the matching tool call")
|
|
28
|
+
name: str = Field(description="Name of the tool that executed")
|
|
29
|
+
output: str = Field(description="Stdout or formatted result text")
|
|
30
|
+
error: Optional[str] = Field(default=None, description="Stderr or exception message if failed")
|
|
31
|
+
exit_code: int = Field(default=0, description="Process exit code (0 for success)")
|
|
32
|
+
truncated: bool = Field(default=False, description="Whether output was truncated to fit context budget")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TurnRecord(BaseModel):
|
|
36
|
+
"""A single turn in the agent execution loop."""
|
|
37
|
+
turn_index: int = Field(description="0-indexed turn counter")
|
|
38
|
+
thought: Optional[str] = Field(default=None, description="CoT reasoning trace if available")
|
|
39
|
+
tool_calls: List[ToolCall] = Field(default_factory=list, description="Tool proposals made in this turn")
|
|
40
|
+
tool_results: List[ToolResult] = Field(default_factory=list, description="Observations received from environment")
|
|
41
|
+
prompt_tokens: int = Field(default=0, description="Prompt tokens consumed")
|
|
42
|
+
completion_tokens: int = Field(default=0, description="Completion tokens consumed")
|
|
43
|
+
cost_usd: float = Field(default=0.0, description="Monetary cost of this turn")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Trajectory(BaseModel):
|
|
47
|
+
"""Complete execution history of an agent task."""
|
|
48
|
+
task_id: str = Field(description="Unique task or issue identifier")
|
|
49
|
+
task_description: str = Field(description="User prompt or task specification")
|
|
50
|
+
turns: List[TurnRecord] = Field(default_factory=list, description="Ordered turn records")
|
|
51
|
+
final_status: StopCondition = Field(description="Termination reason")
|
|
52
|
+
success: bool = Field(default=False, description="Whether verified ground truth succeeded")
|
|
53
|
+
total_cost_usd: float = Field(default=0.0, description="Cumulative monetary spend")
|
|
54
|
+
total_tokens: int = Field(default=0, description="Cumulative tokens burned")
|
|
55
|
+
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""SPRINT: Interleaved Parallel Planning and DAG Execution Engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import collections
|
|
6
|
+
from typing import Callable, Dict, List, Optional, Set
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlanNode(BaseModel):
|
|
11
|
+
node_id: str
|
|
12
|
+
action_type: str
|
|
13
|
+
description: str
|
|
14
|
+
dependencies: List[str] = Field(default_factory=list)
|
|
15
|
+
completed: bool = False
|
|
16
|
+
result: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecutionTier(BaseModel):
|
|
20
|
+
tier_index: int
|
|
21
|
+
parallel_nodes: List[PlanNode] = Field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ParallelPlanningEngine:
|
|
25
|
+
"""Computes topological execution tiers to run independent actions concurrently."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self.nodes: Dict[str, PlanNode] = {}
|
|
29
|
+
|
|
30
|
+
def add_node(self, node_id: str, action_type: str, description: str, dependencies: Optional[List[str]] = None) -> None:
|
|
31
|
+
self.nodes[node_id] = PlanNode(
|
|
32
|
+
node_id=node_id,
|
|
33
|
+
action_type=action_type,
|
|
34
|
+
description=description,
|
|
35
|
+
dependencies=dependencies or [],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def compute_tiers(self) -> List[ExecutionTier]:
|
|
39
|
+
"""Group plan nodes into non-conflicting parallel execution tiers."""
|
|
40
|
+
in_degree: Dict[str, int] = {nid: 0 for nid in self.nodes}
|
|
41
|
+
graph: Dict[str, List[str]] = collections.defaultdict(list)
|
|
42
|
+
|
|
43
|
+
for nid, node in self.nodes.items():
|
|
44
|
+
for dep in node.dependencies:
|
|
45
|
+
graph[dep].append(nid)
|
|
46
|
+
in_degree[nid] += 1
|
|
47
|
+
|
|
48
|
+
queue = collections.deque([nid for nid, deg in in_degree.items() if deg == 0])
|
|
49
|
+
tiers: List[ExecutionTier] = []
|
|
50
|
+
current_tier = 0
|
|
51
|
+
|
|
52
|
+
while queue:
|
|
53
|
+
level_size = len(queue)
|
|
54
|
+
tier_nodes: List[PlanNode] = []
|
|
55
|
+
for _ in range(level_size):
|
|
56
|
+
curr = queue.popleft()
|
|
57
|
+
tier_nodes.append(self.nodes[curr])
|
|
58
|
+
for neighbor in graph[curr]:
|
|
59
|
+
in_degree[neighbor] -= 1
|
|
60
|
+
if in_degree[neighbor] == 0:
|
|
61
|
+
queue.append(neighbor)
|
|
62
|
+
|
|
63
|
+
tiers.append(ExecutionTier(tier_index=current_tier, parallel_nodes=tier_nodes))
|
|
64
|
+
current_tier += 1
|
|
65
|
+
|
|
66
|
+
return tiers
|
|
67
|
+
|