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.
@@ -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
+