Skip to main content
This tutorial walks you through every major AFK concept in five hands-on sections. Each section takes about 3 minutes and builds on the previous one. By the end, you’ll have a working understanding of agents, tools, streaming, multi-agent orchestration, and memory.
Prerequisites: pip install afk and an OpenAI API key in your environment (OPENAI_API_KEY).

Minute 1–3: Your first agent

An AFK agent is a configuration object. You describe what it is — the runner handles how it executes.
from afk.agents import Agent
from afk.core import Runner

agent = Agent(
    name="tutor",
    model="gpt-5.2-mini",
    instructions="""
    You are a programming tutor.
    Explain concepts using simple language and short code examples.
    Keep answers under 100 words.
    """,
)

runner = Runner()
result = runner.run_sync(agent, user_message="What is recursion?")

print(result.final_text)   # The model's explanation
print(result.state)        # "completed"
print(result.run_id)       # Unique run ID for tracing
Key takeaway: Agent is pure configuration (no execution logic). Runner is the engine. AgentResult is the output. These three types are the foundation of everything in AFK.

Minute 4–6: Give your agent tools

Tools let agents take actions beyond text generation. Define a tool as a Python function with a Pydantic model for its arguments.
from pydantic import BaseModel
from afk.agents import Agent
from afk.tools import tool
from afk.core import Runner

class LookupArgs(BaseModel):
    topic: str
    max_results: int = 3

@tool(args_model=LookupArgs, name="search_docs", description="Search the documentation by topic.")
def search_docs(args: LookupArgs) -> dict:
    # In production, this would call a real search API
    return {
        "results": [
            {"title": f"Guide: {args.topic}", "relevance": 0.95},
            {"title": f"FAQ: {args.topic}", "relevance": 0.82},
        ][:args.max_results]
    }

agent = Agent(
    name="research-tutor",
    model="gpt-5.2-mini",
    instructions="Use search_docs to find relevant documentation before answering.",
    tools=[search_docs],   # ← Attach tools here
)

runner = Runner()
result = runner.run_sync(agent, user_message="How do I handle errors in Python?")

print(result.final_text)
print(f"Tool calls made: {len(result.tool_executions)}")

# Inspect what tools were called
for rec in result.tool_executions:
    print(f"  {rec.tool_name}: {'[OK]' if rec.success else '[ERR]'} ({rec.latency_ms:.0f}ms)")
Key takeaway: Tools are typed functions. AFK validates arguments via Pydantic, executes the handler, sanitizes the output, and feeds results back to the LLM. The model decides when and how to call tools.

Minute 7–9: Stream responses in real time

For chat UIs and CLI tools, streaming gives you text as it’s generated instead of waiting for the full response.
import asyncio
from afk.agents import Agent
from afk.core import Runner

agent = Agent(
    name="explainer",
    model="gpt-5.2-mini",
    instructions="Give detailed, well-structured explanations.",
    tools=[search_docs],   # Reuse the tool from above
)

async def main():
    runner = Runner()
    handle = await runner.run_stream(
        agent, user_message="Explain the Python GIL and its impact on threading"
    )

    async for event in handle:
        match event.type:
            case "text_delta":
                print(event.text_delta, end="", flush=True)
            case "tool_started":
                print(f"\n[TOOL] Calling {event.tool_name}...")
            case "tool_completed":
                status = "[OK]" if event.success else "[ERR]"
                print(f"   {status} {event.tool_name} done")
            case "completed":
                print(f"\n\n[DONE] Run completed ({event.result.state})")

    # Access the full result after streaming
    result = handle.result
    print(f"Total tokens: {result.usage.total_tokens}")

asyncio.run(main())
Key takeaway: run_stream() returns an async iterator of events. You get text_delta for incremental text, tool lifecycle events, and a final completed event with the full AgentResult.

Minute 10–12: Multi-agent orchestration

When a task requires different expertise, split it across specialist subagents.
import asyncio
from afk.agents import Agent
from afk.core import Runner

# Specialist 1: Research agent
researcher = Agent(
    name="researcher",
    model="gpt-5.2-mini",
    instructions="""
    You are a research specialist.
    Find relevant facts and present them as bullet points.
    Be thorough but concise.
    """,
)

# Specialist 2: Writing agent
writer = Agent(
    name="writer",
    model="gpt-5.2-mini",
    instructions="""
    You are a technical writer.
    Take research findings and write a clear, well-structured summary.
    Use simple language suitable for a general audience.
    """,
)

# Coordinator: delegates and combines
coordinator = Agent(
    name="coordinator",
    model="gpt-5.2-mini",
    instructions="""
    You manage a team:
    - Delegate research tasks to 'researcher'
    - Delegate writing tasks to 'writer'
    Combine their work into a polished final response.
    """,
    subagents=[researcher, writer],
)

async def main():
    runner = Runner()
    result = await runner.run(
        coordinator,
        user_message="Write a brief explainer about quantum computing for beginners",
    )

    print(result.final_text)
    print(f"\nSubagent calls: {len(result.subagent_executions)}")
    for sub in result.subagent_executions:
        print(f"  -> {sub.subagent_name}: {'[OK]' if sub.success else '[ERR]'}")

asyncio.run(main())
Key takeaway: Subagents appear as auto-generated tools to the coordinator (transfer_to_researcher, transfer_to_writer). Each subagent runs a full agent loop with its own model and instructions.

Minute 13–15: Memory and thread continuity

AFK supports multi-turn conversations through thread-based memory. Pass a thread_id to maintain context across runs.
import asyncio
from afk.agents import Agent
from afk.core import Runner

agent = Agent(
    name="tutor",
    model="gpt-5.2-mini",
    instructions="You are a Python tutor. Remember what the student has asked before.",
)

async def main():
    runner = Runner()
    thread = "session-42"   # ← Same thread_id = same conversation

    # Turn 1
    r1 = await runner.run(agent, user_message="What are decorators?", thread_id=thread)
    print(f"Turn 1: {r1.final_text[:80]}...")

    # Turn 2 — the agent remembers Turn 1
    r2 = await runner.run(agent, user_message="Give me an example", thread_id=thread)
    print(f"Turn 2: {r2.final_text[:80]}...")

    # Turn 3 — still has context
    r3 = await runner.run(agent, user_message="How is that different from a wrapper function?", thread_id=thread)
    print(f"Turn 3: {r3.final_text[:80]}...")

asyncio.run(main())
If a run is interrupted (process crash, timeout, pause for human approval), you can resume it from the last checkpoint:
# Start a run
result = await runner.run(agent, user_message="Analyze this data...")

# Later, resume from the checkpoint
resumed = await runner.resume(
    agent,
    run_id=result.run_id,
    thread_id=result.thread_id,
)
print(resumed.state)  # "completed"
AFK persists checkpoints at key boundaries (step start, post-LLM, post-tool batch). On resume, completed tool calls are replayed from cache — no duplicate side effects.
Key takeaway: thread_id connects runs into a conversation. Memory persists automatically. For long threads, use runner.compact_thread() to summarize old messages and control storage growth.

You just learned AFK

Here’s what you covered:
MinuteConceptKey API
1–3Agents & RunnerAgent(...), Runner().run_sync(...)
4–6Tools@tool(...), result.tool_executions
7–9Streamingrunner.run_stream(...), async for event in handle
10–12Multi-agentagent.subagents=[...], result.subagent_executions
13–15Memorythread_id=..., runner.resume(...)

Go deeper