Skip to content

Agents

Neurosurfer ships three agent types, all built on the same engine and re-exported from neurosurfer.agents:

Agent Use it when
AgenticLoop You want multi-step tool use via the provider's native function-calling API.
ReactAgent Your model has no native tool API — it drives tools by parsing text (ReAct).
Agent You want a single bounded call, optionally with tools or structured output.

Constructing an agent

Every agent takes the same core keyword arguments:

from pathlib import Path
from neurosurfer.agents import AgenticLoop, Guardrails
from neurosurfer.tools import default_pool

agent = AgenticLoop(
    provider=provider,                       # any Provider (see the Providers guide)
    tools=default_pool(),                     # the tools the agent may call
    system_prompt="You are a helpful assistant. Use tools, then finish.",
    guardrails=Guardrails(),                  # enforced limits (see below)
    io=AutoIO(),                              # how approvals/questions are answered
    cwd=Path.cwd(),                           # working directory for file tools
)

The io handler

io is an IOHandler — the object an agent calls when a tool needs a decision (answer a question, approve a shell command, approve a file write). Interactive apps supply a UI-backed handler (the CLI uses a Rich terminal one); scripts and notebooks use an auto-approving handler. The full approval model — the handler methods, AutoApproveIOHandler, and the safety caveats — lives in Permissions & Safety. Examples below use a headless AutoIO() for brevity.

AgenticLoop

AgenticLoop.run(prompt) is an async generator that streams events as the agent thinks, calls tools, and answers:

import asyncio

async def main():
    agent = AgenticLoop(
        provider=provider, tools=default_pool(),
        system_prompt="Complete the task, then call the finish tool.",
        guardrails=Guardrails(), io=AutoIO(), cwd=Path.cwd(),
    )
    async for event in agent.run("List the .md files here and summarise the README."):
        if hasattr(event, "text"):
            print(event.text, end="", flush=True)

asyncio.run(main())

ReactAgent

Same constructor and streaming interface as AgenticLoop, but tool calls are parsed from the model's text output — use it with local models that lack a native tool-calling API:

from neurosurfer.agents import ReactAgent

agent = ReactAgent(
    provider=local_provider, tools=default_pool(),
    system_prompt="Solve the task using the available tools.",
    guardrails=Guardrails(), io=AutoIO(), cwd=Path.cwd(),
)

One-shot Agent

Agent runs a single bounded turn. In text mode it returns the final string; with an output_schema it returns a validated Pydantic model. Call complete() to run to completion and get the result directly (or iterate run() for events):

from pydantic import BaseModel
from neurosurfer.agents import Agent

class Summary(BaseModel):
    title: str
    points: list[str]

agent = Agent(
    provider=provider, tools=default_pool(),
    system_prompt="Answer concisely.",
    guardrails=Guardrails(), io=AutoIO(), cwd=Path.cwd(),
    output_schema=Summary,
)
result = await agent.complete("Summarise Neurosurfer in three bullet points.")
print(result.title, result.points)   # validated Summary instance

Pass max_tool_rounds=N to allow a bounded number of tool calls before the final answer.

Events

Both run() generators yield typed events from neurosurfer.agents (also under neurosurfer.agents.events):

Event Meaning
TextDelta A chunk of the answer (.text).
ThinkingDelta A chunk of reasoning, when the model exposes it (.text).
ToolStarted / ToolFinished A tool call began / returned.
TurnCompleted One model turn finished (carries usage + stop reason).
ModeChanged The permission mode changed mid-run.
Compacted The context was summarised to stay within the window.
RunFinished The run ended (status + final text).
AgentError An error surfaced during the run.
from neurosurfer.agents import TextDelta, ToolStarted, RunFinished

async for ev in agent.run(prompt):
    if isinstance(ev, ToolStarted):
        print(f"\n[tool] {ev}")
    elif isinstance(ev, TextDelta):
        print(ev.text, end="", flush=True)
    elif isinstance(ev, RunFinished):
        print(f"\n[done] {ev}")

Permissions and guardrails

Every agent takes a guardrails= (what it's allowed to do) and a mode= (the runtime posture, e.g. "plan" / "default" / "accept_edits" / "bypass"):

agent = AgenticLoop(
    provider=provider, tools=default_pool(),
    system_prompt="…", io=AutoIO(), cwd=Path.cwd(),
    guardrails=Guardrails(write_scope=["out/**"], shell_policy="denied"),
    mode="default",
)

The full field reference, the permission modes, and the approval flow are covered in Permissions & Safety.

Sub-agents

Agents can spawn scoped sub-agents (via the spawn_agent tool) to parallelise work, bounded by the guardrails above. See the Sub-agents guide.

Context management

Long runs stay within the context window via automatic compaction (ContextManager) while DurableState pins the plan/todos/decisions that must survive it. Pass context_manager= and/or durable=; see Context & Memory.