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}, optionally with a "stop_reason" that explains an early stop. 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 and provider blocks, executes every requested tool (validating arguments and capturing errors so the model can self-correct), feeds the JSON results back with failures flagged, and returns when the model answers in plain text. A refused or truncated reply ends the loop with an explicit outcome instead of an answer.

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", "budget", "refusal" (the model declined, so there's no answer), or "max_tokens" (a reply hit the token cap before it finished).

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. A final decision's optional "stop_reason" becomes the result's.

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,
    max_tokens: int = 16000,
) -> AgentResult

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

Maintains the full chat history with tool-call ids and provider blocks, executes each requested tool (validating arguments and turning errors into a JSON error result, flagged as an error, that the model can recover from), and loops until the model replies with plain text or the step budget is exhausted. A "refusal" or "max_tokens" reply ends the loop with that stop reason: its content never becomes the answer, and its tool calls never run. A "pause_turn" reply is resent so the model can resume it. No temperature is sent, so the provider's sampling defaults apply.

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
max_tokens int

Per-turn token cap (thinking included, where it applies).

16000

Returns:

Type Description
AgentResult

An AgentResult.

llm_planner

llm_planner(
    provider: LLMProvider,
    *,
    system: str = DEFAULT_SYSTEM_PROMPT,
    max_tokens: int = 16000,
) -> 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). A paused reply is resent so the model can resume it. A refused or truncated reply, or one still paused after the resends, becomes a final_answer that explains the stop, with its stop_reason. No temperature is sent, so the provider's sampling defaults apply.

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.

Accountable process design

run_design_agent completes only through a recorded, accepted current-case run. Its final report is deterministic; unsupported model text is retained in the transcript and can't become the engineering answer. See the saved-case workflow.

design_agent

A design-agent loop whose completion is grounded in computed case artifacts.

Classes:

Name Description
DesignAgentResult

Deterministic report and structured evidence from a submitted design.

Functions:

Name Description
run_design_agent

Plan and compute a process design; ignore unsupported final-text claims.

DesignAgentResult dataclass

DesignAgentResult(
    answer: str,
    design: dict[str, Any] | None = None,
    transcript: list[dict[str, Any]] = list(),
    stop_reason: str = "budget",
    steps: int = 0,
)

Deterministic report and structured evidence from a submitted design.

Attributes:

Name Type Description
answer str

The deterministic design report, or why no design was submitted.

design dict[str, Any] | None

The submitted design and its evidence, if one was submitted.

transcript list[dict[str, Any]]

Tool calls with their arguments and results, plus events such as unverified_text that record model text never accepted as an answer.

stop_reason str

"submitted", "budget", "refusal" (the model declined), or "max_tokens" (a reply hit the token cap).

steps int

Number of model turns taken.

run_design_agent

run_design_agent(
    goal: str,
    provider: LLMProvider,
    *,
    workspace: CaseWorkspace,
    max_steps: int = 12,
    max_calls: int = 40,
    max_tokens: int = 16000,
) -> DesignAgentResult

Plan and compute a process design; ignore unsupported final-text claims.

The general run_llm_agent remains suitable for explanatory questions. This stricter loop requires an accepted artifact and generates its answer from recorded values. Both model-turn and tool-call budgets are bounded. A refused or truncated reply ends the loop with that stop reason and no design; its content is recorded as an unaccepted event, and its tool calls never run. A paused reply is resent so the model can resume it. No temperature is sent, so the provider's sampling defaults apply.

case_tools

Case-building tools and accountable design submission over trusted run artifacts.

Classes:

Name Description
DesignSession

Case revision and trusted run state for one accountable design-agent session.

Functions:

Name Description
case_format

Return units, registry capabilities, and a complete editable example.

case_tool_specs

Stateless schema, validation, and audited solve tools for the general copilot.

DesignSession

DesignSession(workspace: CaseWorkspace)

Case revision and trusted run state for one accountable design-agent session.

Disk artifacts are inspectable, but only runs computed in this session are eligible for submission. A hash verifies integrity, not authorship. The explicit workspace is the only storage surface exposed to model tools.

Methods:

Name Description
create_case

Validate and select a new immutable case revision.

load_case

Select a verified saved revision; prior submission eligibility is cleared.

update_case

Apply a complete reviewed replacement only to the current base revision.

set_parameters

Create a new revision with dimension-checked parameter defaults.

run_case

Compute, audit, and save a run, preserving failed calculation evidence.

study_case

Run a bounded sweep, optimization, or independently checked sensitivity study.

diagnose_case

Inspect topology and declared dependencies without granting run eligibility.

inspect

Inspect a verified artifact without granting it trusted execution status.

submit_design

Submit selected recorded metrics from an accepted current-case run.

registry

Create tools bound to this session's explicit workspace and revision state.

create_case

create_case(case: dict[str, Any]) -> dict[str, Any]

Validate and select a new immutable case revision.

load_case

load_case(case_id: str) -> dict[str, Any]

Select a verified saved revision; prior submission eligibility is cleared.

update_case

update_case(
    base_case_id: str, case: dict[str, Any]
) -> dict[str, Any]

Apply a complete reviewed replacement only to the current base revision.

set_parameters

set_parameters(
    case_id: str, overrides: dict[str, Any]
) -> dict[str, Any]

Create a new revision with dimension-checked parameter defaults.

run_case

run_case(
    case_id: str,
    overrides: dict[str, Any] | None = None,
    backend: str = "sequential",
    column_solver: str = "block",
    plant_solver: str = "sparse",
) -> dict[str, Any]

Compute, audit, and save a run, preserving failed calculation evidence.

study_case

study_case(
    case_id: str,
    kind: str,
    request: dict[str, Any],
    backend: str = "sequential",
    column_solver: str = "block",
    plant_solver: str = "sparse",
) -> dict[str, Any]

Run a bounded sweep, optimization, or independently checked sensitivity study.

diagnose_case

diagnose_case(case_id: str) -> dict[str, Any]

Inspect topology and declared dependencies without granting run eligibility.

inspect

inspect(artifact_id: str) -> dict[str, Any]

Inspect a verified artifact without granting it trusted execution status.

submit_design

submit_design(
    run_id: str,
    metrics: list[str],
    baseline_id: str | None = None,
) -> dict[str, Any]

Submit selected recorded metrics from an accepted current-case run.

Model-authored numerical claims and prose aren't accepted as arguments. The report is generated deterministically from the stored artifact.

registry

registry() -> dict[str, Any]

Create tools bound to this session's explicit workspace and revision state.

case_format

case_format() -> dict[str, Any]

Return units, registry capabilities, and a complete editable example.

case_tool_specs

case_tool_specs() -> list[Any]

Stateless schema, validation, and audited solve tools for the general copilot.