76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
|
|
"""Minimal agentic tool-use loop: plan -> tool call -> observe -> repeat."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from rein_openweights.budget import BudgetExceededError, BudgetTracker
|
||
|
|
from rein_openweights.openrouter_client import OpenRouterClient
|
||
|
|
from rein_openweights.tools import call_tool, openai_tool_schemas
|
||
|
|
|
||
|
|
_SYSTEM_PROMPT = (
|
||
|
|
"You are a bounded coding agent. You may only act through the provided "
|
||
|
|
"tools. When the task is complete, call git_add_commit with a "
|
||
|
|
"descriptive message, then reply with a final message containing the "
|
||
|
|
"word DONE and no further tool calls."
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class LoopResult:
|
||
|
|
turns: int
|
||
|
|
finished: bool
|
||
|
|
transcript: list[dict[str, Any]] = field(default_factory=list)
|
||
|
|
error: str | None = None
|
||
|
|
|
||
|
|
|
||
|
|
def run_loop(
|
||
|
|
client: OpenRouterClient,
|
||
|
|
repo_root: Path,
|
||
|
|
title: str,
|
||
|
|
description: str,
|
||
|
|
*,
|
||
|
|
max_turns: int = 20,
|
||
|
|
budget: BudgetTracker | None = None,
|
||
|
|
) -> LoopResult:
|
||
|
|
messages: list[dict[str, Any]] = [
|
||
|
|
{"role": "system", "content": _SYSTEM_PROMPT},
|
||
|
|
{"role": "user", "content": f"# {title}\n\n{description}"},
|
||
|
|
]
|
||
|
|
tools = openai_tool_schemas()
|
||
|
|
|
||
|
|
for turn in range(1, max_turns + 1):
|
||
|
|
data = client.chat(messages, tools=tools)
|
||
|
|
|
||
|
|
if budget is not None:
|
||
|
|
usage = data.get("usage", {})
|
||
|
|
try:
|
||
|
|
budget.consume(usage.get("total_tokens", 0))
|
||
|
|
except BudgetExceededError as exc:
|
||
|
|
return LoopResult(turns=turn, finished=False, transcript=messages, error=str(exc))
|
||
|
|
|
||
|
|
message = data["choices"][0]["message"]
|
||
|
|
messages.append(message)
|
||
|
|
|
||
|
|
tool_calls = message.get("tool_calls") or []
|
||
|
|
if not tool_calls:
|
||
|
|
finished = "DONE" in (message.get("content") or "")
|
||
|
|
return LoopResult(turns=turn, finished=finished, transcript=messages)
|
||
|
|
|
||
|
|
for call in tool_calls:
|
||
|
|
fn = call["function"]
|
||
|
|
name = fn["name"]
|
||
|
|
try:
|
||
|
|
args = json.loads(fn.get("arguments") or "{}")
|
||
|
|
result = call_tool(repo_root, name, args)
|
||
|
|
except Exception as exc: # noqa: BLE001 - report failure to the model, keep looping
|
||
|
|
result = f"error: {exc}"
|
||
|
|
messages.append(
|
||
|
|
{"role": "tool", "tool_call_id": call.get("id", name), "content": result}
|
||
|
|
)
|
||
|
|
|
||
|
|
return LoopResult(turns=max_turns, finished=False, transcript=messages, error="max_turns exceeded")
|