Skip to content

Agent loops

The agent loops that drive the tool registry: a deterministic planner for tests and a provider-backed loop that lets a language model call tools, observe results, and iterate toward a design.

agent

Tool-calling agent loops for the Fugacio design copilot.

Two complementary loops share one tool registry:

  • run_agent: the original model-agnostic loop. A planner callable (goal, tool_schemas, transcript) -> decision decides the next tool call or the final answer, where decision is {"tool": name, "arguments": {...}} or {"final_answer": text}. This keeps the control flow fully testable with a scripted planner and lets any decision policy drop in.

  • run_llm_agent: a real multi-turn function-calling loop over an LLMProvider (OpenAI, Anthropic, or the test MockProvider). It maintains the full message history with tool-call ids, executes every requested tool (validating arguments and capturing errors so the model can self-correct), feeds the JSON results back, and returns when the model answers in plain text.

llm_planner bridges the two: it turns a provider into a planner for the simple loop. A deterministic heuristic_planner is provided for tests and offline use.

Classes:

Name Description
AgentResult

Outcome of an agent run.

Functions:

Name Description
run_agent

Run the plan/act loop until the planner returns a final answer.

run_llm_agent

Drive a real function-calling LLM through the tool registry to an answer.

llm_planner

Adapt an LLMProvider into a Planner for run_agent.

heuristic_planner

A deterministic keyword planner: fire the first rule whose keyword is in the goal.

AgentResult dataclass

AgentResult(
    answer: str,
    transcript: list[JsonDict] = list(),
    stop_reason: str = "answer",
    steps: int = 0,
)

Outcome of an agent run.

Attributes:

Name Type Description
answer str

The final natural-language (or structured) answer.

transcript list[JsonDict]

Ordered tool calls with their arguments and results.

stop_reason str

Why the loop ended ("answer" or "budget").

steps int

Number of planner / model turns taken.

run_agent

run_agent(
    goal: str,
    planner: Planner,
    *,
    registry: dict[str, ToolSpec] | None = None,
    max_steps: int = 6,
) -> AgentResult

Run the plan/act loop until the planner returns a final answer.

Parameters:

Name Type Description Default
goal str

The natural-language design goal.

required
planner Planner

Decision function (see module docstring); inject an LLM via llm_planner, or pass a scripted/heuristic planner.

required
registry dict[str, ToolSpec] | None

Tool registry to expose (defaults to default_registry).

None
max_steps int

Maximum number of tool calls before giving up.

6

Returns:

Type Description
AgentResult

An AgentResult with the final answer and the full transcript.

run_llm_agent

run_llm_agent(
    goal: str,
    provider: LLMProvider,
    *,
    registry: dict[str, ToolSpec] | None = None,
    system: str = DEFAULT_SYSTEM_PROMPT,
    max_steps: int = 8,
    temperature: float = 0.0,
    max_tokens: int = 1024,
) -> AgentResult

Drive a real function-calling LLM through the tool registry to an answer.

Maintains the full chat history with tool-call ids, executes each requested tool (validating arguments and turning errors into a JSON error result the model can recover from), and loops until the model replies with plain text or the step budget is exhausted.

Parameters:

Name Type Description Default
goal str

The user's natural-language request.

required
provider LLMProvider

Any LLMProvider.

required
registry dict[str, ToolSpec] | None

Tool registry (defaults to default_registry).

None
system str

System prompt; defaults to DEFAULT_SYSTEM_PROMPT.

DEFAULT_SYSTEM_PROMPT
max_steps int

Maximum model turns.

8
temperature float

Sampling temperature.

0.0
max_tokens int

Per-turn token cap.

1024

Returns:

Type Description
AgentResult

An AgentResult.

llm_planner

llm_planner(
    provider: LLMProvider,
    *,
    system: str = DEFAULT_SYSTEM_PROMPT,
    temperature: float = 0.0,
    max_tokens: int = 1024,
) -> Planner

Adapt an LLMProvider into a Planner for run_agent.

On each call it reconstructs the conversation from the goal and transcript, asks the model for the next step, and maps the first requested tool call to a {"tool", "arguments"} decision (or the text reply to final_answer).

heuristic_planner

heuristic_planner(
    rules: Sequence[tuple[str, JsonDict]],
    *,
    default_answer: str,
) -> Planner

A deterministic keyword planner: fire the first rule whose keyword is in the goal.

Each rule is (keyword, decision); the first keyword found in the goal (case-insensitive) triggers its decision once. After its tool runs (or if no rule matches), the planner returns default_answer. Useful for offline demos and tests without an LLM.