Augments LabsAugments ADK

๐Ÿค– Agents

An Agent is configuration โ€” name, instructions, tools, handoffs, guardrails. The Runner executes; the Agent does not. This separation means a single Agent instance can be reused across many concurrent runs without coordination, and all execution policy (budgets, retries, streaming, checkpoints) lives in one place on the Runner. See Architecture: The Runner for the full rationale.

Anatomy of an Agent

Agent is a @dataclass defined in src/augments/adk/agents/agent.py. All fields have defaults except name and system_prompt.

FieldTypeDefaultDescription
namestrโ€”Unique identifier. Must be non-empty.
descriptionstr | NoneNoneOne-line description used as the tool description when this agent is exposed via as_tool() or listed as a handoff target.
system_promptstr | SystemPrompt | DynamicSystemPromptโ€”Instructions defining the agent's behaviour. Must be non-empty.
toolslist[Tool | Toolset][]All tools and toolsets the agent can use each turn.
handoffsHandoffRoute | list[Agent | Handoff] | NoneNoneDelegation targets โ€” LLM-orchestrated list or code-orchestrated route.
guardrailsAgentGuardrailsAgentGuardrails()Input and output guardrail config.
llmstr | LLM | NoneNonePer-agent model override. None uses the RunConfig default.
llm_configLLMConfig | NoneNoneTemperature, max_output_tokens, num_retries, etc.
middlewareMiddlewareMiddleware()Per-layer plumbing middleware (tools / agents / LLMs).
tool_use_behaviorToolUseBehavior"run_llm_again"What happens after tool execution.
output_schematype | AgentOutputSchemaBase | NoneNoneStructured-output schema; validated against each LLM response.
skillslist[Skill][]Skills that contribute instructions and tools.
skill_activationSkillActivationLAZYWhen skill instructions enter the system prompt.
hooksAgentHooks | NoneNonePer-agent lifecycle callbacks.
verboseVerboseConfig | NoneNonePer-agent verbose-output override.

Minimal working example

import asyncio
import logging
 
from augments.adk.agents import Agent
from augments.adk.run import Runner
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
 
support_agent = Agent(
    name="Customer Support",
    system_prompt=(
        "You are a friendly customer support specialist. "
        "Answer questions clearly and concisely."
    ),
)
 
 
async def main() -> None:
    result = await Runner.arun(support_agent, "How do I reset my password?")
    logger.info("Final output: %s", result.final_output)
 
 
asyncio.run(main())

[!NOTE] Runner.arun() is the only entry point. Agent has no run() or arun() method โ€” invoking execution via the agent class would break concurrent reuse and scatter execution policy across instances.

Instructions: static vs dynamic

Static instructions

Pass a plain str for straightforward system prompts:

agent = Agent(
    name="Analyst",
    system_prompt="Analyse the supplied data and return a structured summary.",
)

For longer prompts with discrete sections, use SystemPrompt:

from augments.adk.prompts import SystemPrompt, SystemPromptTone
 
agent = Agent(
    name="Code Reviewer",
    system_prompt=SystemPrompt(
        role="You are a senior Python code reviewer specialising in security.",
        context="You work at a fintech company. Code must comply with PCI-DSS.",
        guidelines=[
            "Flag security vulnerabilities immediately.",
            "Always suggest type hints.",
        ],
        tone=SystemPromptTone.TECHNICAL,
        constraints=["Never execute code.", "Ask for the full file if the snippet is incomplete."],
        output_format="Use Markdown with a header for each finding.",
    ),
)

Dynamic instructions

DynamicSystemPrompt is a callable that receives DynamicSystemPromptData (containing data.context: RunContext and data.agent: Agent) and returns a str or SystemPrompt. This lets instructions depend on runtime state โ€” per-tenant config, session data, feature flags.

import logging
from augments.adk.agents import Agent
from augments.adk.prompts import DynamicSystemPrompt, DynamicSystemPromptData, SystemPrompt
 
logger = logging.getLogger(__name__)
 
 
def build_prompt(data: DynamicSystemPromptData) -> SystemPrompt:
    tenant_id = data.context.context.get("tenant_id", "default")
    guidelines = fetch_tenant_guidelines(tenant_id)  # your lookup
    logger.debug("Building prompt for tenant %s", tenant_id)
    return SystemPrompt(
        role="You are a compliance assistant.",
        knowledge=guidelines,
    )
 
 
compliance_agent = Agent(
    name="Compliance",
    system_prompt=build_prompt,
)

Async callables are also accepted:

async def build_prompt_async(data: DynamicSystemPromptData) -> str:
    policy = await fetch_policy_async(data.context.context["org"])
    return f"Follow this policy:\n{policy}"

[!TIP] Dynamic instructions are resolved once per agent turn, just before the LLM call. The callable is invoked with the current RunContext, so it sees any context values you passed to Runner.arun(..., context=...).

Tools

The tools field accepts any mix of Tool and Toolset instances:

  • FunctionTool โ€” wraps a Python callable decorated with @function_tool.
  • Toolset variants โ€” FunctionToolset, PrefixedToolset, FilteredToolset, MCPToolset โ€” live collections that materialise per turn. Useful for MCP servers and dynamic tool lists without name collisions.
from augments.adk.tools import function_tool
 
@function_tool(name="lookup", description="Look up a support article by keyword.")
def lookup(keyword: str) -> str:
    return search_knowledge_base(keyword)  # your implementation
 
 
agent = Agent(
    name="Support",
    system_prompt="Help users by looking up articles.",
    tools=[lookup],
)

For full coverage of tool types, hosted tools (web search, code execution), toolsets, tool governance (requires_approval, rate_limit, schema_enforcement), and tool-level guardrails, see Tools guide.

Handoffs

handoffs specifies how this agent can permanently transfer control to another agent:

  • LLM-orchestrated โ€” pass a list[Agent | Handoff]. The framework adds transfer_to_<name> tools to the LLM's tool list. The LLM decides when and to whom to hand off.
  • Code-orchestrated โ€” pass a HandoffRoute. The agent outputs a structured Intent type; application code routes deterministically with zero routing tokens.
from augments.adk.agents import Agent
from augments.adk.handoffs import Handoff
 
# LLM-orchestrated
triage = Agent(
    name="Triage",
    system_prompt="Route customer requests to the right specialist.",
    handoffs=[
        Handoff(target=refunds_agent, description="Handle refund requests."),
        Handoff(target=billing_agent, description="Handle billing questions."),
    ],
)

For history transfer cost control (HandoffConfig), code-orchestrated routing (HandoffRoute), typed handoff input, and input filters, see Handoffs guide.

Guardrails

AgentGuardrails holds two phase-typed lists:

  • guardrails.input โ€” validates input before (or in parallel with) the agent. A fired tripwire raises AgentInputGuardrailTripwireTriggered.
  • guardrails.output โ€” validates the agent's output. A fired tripwire raises AgentOutputGuardrailTripwireTriggered, or triggers re-prompting when remediation is set.
from augments.adk.agents import Agent, AgentGuardrails
 
agent = Agent(
    name="Support",
    system_prompt="Help customers.",
    guardrails=AgentGuardrails(
        input=[pii_guardrail],
        output=[content_policy_guardrail],
    ),
)

For decorator syntax (@agent_input_guardrail, @agent_output_guardrail), severity levels, timeout policies, remediation loops, and the difference between agent-level and run-level guardrails, see Guardrails guide.

Agent.as_tool() โ€” agent-as-tool composition

as_tool() wraps a sub-agent as a FunctionTool that the parent LLM calls like any other tool. Control stays with the parent throughout โ€” unlike handoffs, where control transfers permanently.

The sub-agent starts fresh: it sees only its own system prompt and the task string provided in the tool input. The parent sees only the final output string, not the sub-agent's internal turns.

import logging
from augments.adk.agents import Agent
from augments.adk.run import Runner
 
logger = logging.getLogger(__name__)
 
 
researcher = Agent(
    name="Researcher",
    system_prompt="Summarise key findings on the given topic.",
)
 
writer = Agent(
    name="Writer",
    system_prompt="Produce polished prose from the supplied content.",
)
 
supervisor = Agent(
    name="Supervisor",
    system_prompt=(
        "Coordinate a research-and-writing pipeline.\n"
        "1. Use the researcher tool to gather information.\n"
        "2. Use the writer tool to produce a polished article."
    ),
    tools=[
        researcher.as_tool(tool_description="Research a topic and return key findings."),
        writer.as_tool(tool_description="Write polished prose from provided content."),
    ],
)

Key governance parameters on as_tool():

ParameterDescription
tool_nameName the parent LLM sees. Defaults to snake_case(agent.name).
tool_descriptionDescription for the parent LLM. Defaults to "Delegate a task to the {name} agent."
max_turnsMaximum agent-loop turns for the sub-agent (default 10).
timeoutSeconds before aborting via asyncio.wait_for. On timeout, an error string is returned to the parent โ€” execution does not raise.
budgetLLMUsageLimits for the sub-agent run, merged into its RunConfig.
max_result_tokensTruncates the result before the parent sees it.
extractorCallback receiving the full RunResult; its return value becomes the tool output string.
input_schemaCustom Pydantic model for structured tool input (default: single input: str field).
on_streamCallback receiving each StreamEvent from the sub-agent in real time.

[!TIP] Use agent.get_agent_graph() to introspect the full agent topology โ€” it returns a nested dict of delegate agents and handoff targets, handling cycles. agent.get_delegate_tools() returns just the FunctionTool wrappers.

Non-mutating agent copies with dataclasses.replace

Agent is a standard @dataclass, so non-mutating copies with field overrides use dataclasses.replace(). This is the correct pattern when you need to vary a field per run without affecting other callers sharing the same agent definition:

import dataclasses
from augments.adk.agents import Agent
from augments.adk.llms import LLMConfig
 
base_agent = Agent(
    name="Analyst",
    system_prompt="Analyse the supplied data.",
    llm_config=LLMConfig(temperature=0.3),
)
 
# Creative variant โ€” higher temperature, same everything else
creative = dataclasses.replace(base_agent, llm_config=LLMConfig(temperature=0.9))
 
# Per-tenant variant โ€” different instructions, same tools
tenant_agent = dataclasses.replace(
    base_agent,
    system_prompt=f"Analyse data under {tenant_policy} constraints.",
)

Because Agent carries no mutable runtime state, the original base_agent is unaffected by either replace call.

Structured output with output_schema

When output_schema is set, the Runner instructs the LLM to return JSON matching the schema and validates the response automatically.

import asyncio
import logging
from pydantic import BaseModel, Field
from augments.adk.agents import Agent
from augments.adk.run import Runner
 
logger = logging.getLogger(__name__)
 
 
class SupportResponse(BaseModel):
    category: str = Field(description="Request category: billing, refund, or technical.")
    priority: int = Field(ge=1, le=5, description="Priority 1 (low) to 5 (critical).")
    response: str = Field(description="Agent response text.")
 
 
classifier = Agent(
    name="Support Classifier",
    system_prompt="Classify and respond to customer support requests.",
    output_schema=SupportResponse,
)
 
 
async def main() -> None:
    result = await Runner.arun(classifier, "My invoice is wrong.")
    parsed = result.final_output_as(SupportResponse)
    logger.info("Category: %s, Priority: %d", parsed.category, parsed.priority)
 
 
asyncio.run(main())

Pass a plain type (Pydantic BaseModel, TypedDict, @dataclass) directly and the framework wraps it automatically. For advanced enforcement control, pass an explicit AgentOutputSchema instance:

from augments.adk.schemas import AgentOutputSchema, SchemaEnforcement
 
agent = Agent(
    name="Support Classifier",
    system_prompt="...",
    output_schema=AgentOutputSchema(
        SupportResponse,
        schema_enforcement=SchemaEnforcement.NORMALIZED,
    ),
)

Common patterns

Triage with LLM-orchestrated handoffs

A front-line triage agent routes to specialists. Each specialist is a separate Agent; the triage agent holds both as handoff targets:

refunds_agent = Agent(
    name="Refunds",
    system_prompt="Process refund requests. Ask for order ID if not provided.",
)
 
billing_agent = Agent(
    name="Billing",
    system_prompt="Handle billing questions: invoices, charges, payment methods.",
)
 
triage_agent = Agent(
    name="Triage",
    system_prompt="Route customer requests to the right specialist.",
    handoffs=[refunds_agent, billing_agent],
)

For token-efficient routing (zero routing tokens), use HandoffRoute with structured intent types. See Handoffs guide for the full pattern.

Agent + tools combination

Combine tools and handoffs on the same agent. The LLM uses tools for direct actions and handoff targets for specialist delegation:

agent = Agent(
    name="Operations",
    system_prompt=(
        "Help users with account operations. "
        "Use tools for lookups; escalate complex billing issues."
    ),
    tools=[lookup_account, reset_password],
    handoffs=[billing_agent],
)

Reusing an agent across runs

Because Agent carries no execution state, the same instance can be passed to Runner.arun() concurrently:

import asyncio
from augments.adk.run import Runner
 
# Run the same agent with three different inputs in parallel
results = await asyncio.gather(
    Runner.arun(support_agent, "How do I reset my password?"),
    Runner.arun(support_agent, "Where is my invoice?"),
    Runner.arun(support_agent, "Cancel my subscription."),
)

Each call gets its own isolated message list and RunContext. No locking or copying required.

See also

  • Architecture: The Runner โ€” the execution loop, budgets, streaming, RunConfig, and the rationale for separating config from execution.
  • Tools guide โ€” function tools, hosted tools, toolsets, and tool governance.
  • Handoffs guide โ€” LLM-orchestrated and code-orchestrated routing, HandoffConfig, and context transfer cost control.
  • Guardrails guide โ€” input/output guardrails, severity levels, timeout policies, and remediation loops.