> ## Documentation Index
> Fetch the complete documentation index at: https://afk.arpan.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Learn AFK in 15 Minutes

> A guided path through agents, tools, streaming, memory, and safety.

This tutorial expands the quickstart into the core workflow used by most AFK applications. Each section introduces one concept and keeps the code small enough to copy into a single file.

## 1. Agent + runner

```python theme={null}
from afk.agents import Agent
from afk.core import Runner

agent = Agent(
    name="tutor",
    model="gpt-5.5",
    instructions="Explain programming concepts in plain language.",
)

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

print(result.final_text)
print(result.run_id)
```

Key point: an agent is declarative. The runner owns execution.

## 2. Typed tools

```python theme={null}
from pydantic import BaseModel

from afk.agents import Agent
from afk.core import Runner
from afk.tools import tool


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


@tool(args_model=LookupArgs, name="search_docs", description="Search docs by topic.")
def search_docs(args: LookupArgs) -> dict:
    return {
        "results": [
            {"title": f"Guide: {args.topic}", "score": 0.95},
            {"title": f"FAQ: {args.topic}", "score": 0.82},
        ][: args.max_results]
    }


agent = Agent(
    name="research-tutor",
    model="gpt-5.5",
    instructions="Search docs before answering user questions.",
    tools=[search_docs],
)

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

print(result.final_text)
print(result.tool_executions)
```

Key point: tool arguments are validated before your function runs.

## 3. Streaming

Use streaming when a UI or CLI should show progress before the final result is ready.

```python theme={null}
import asyncio

from afk.agents import Agent
from afk.core import Runner


agent = Agent(
    name="streamer",
    model="gpt-5.5",
    instructions="Explain topics clearly.",
)


async def main() -> None:
    handle = await Runner().run_stream(
        agent,
        user_message="Explain Python decorators in three bullets.",
    )

    async for event in handle:
        if event.type == "text_delta":
            print(event.text_delta, end="", flush=True)
        elif event.type == "completed":
            print(f"\n\nstate={event.result.state}")


asyncio.run(main())
```

Key point: `run_stream()` returns an `AgentStreamHandle`. Consume it to receive text, tool lifecycle events, errors, and the terminal result.

## 4. Memory continuity

Pass the same `thread_id` to keep conversation context attached to a thread.

```python theme={null}
import asyncio

from afk.agents import Agent
from afk.core import Runner


agent = Agent(
    name="memory-tutor",
    model="gpt-5.5",
    instructions="Remember the user's earlier questions in this thread.",
)


async def main() -> None:
    runner = Runner()
    thread_id = "student-session-42"

    first = await runner.run(
        agent,
        user_message="What are Python decorators?",
        thread_id=thread_id,
    )
    second = await runner.run(
        agent,
        user_message="Give me a short example using that idea.",
        thread_id=thread_id,
    )

    print(first.final_text)
    print(second.final_text)


asyncio.run(main())
```

Key point: thread continuity is explicit. Use the same `thread_id` for related turns.

## 5. Safety limits

Production agents need hard limits even when prompts and tools are well designed.

```python theme={null}
from afk.agents import Agent, FailSafeConfig
from afk.core import Runner, RunnerConfig

agent = Agent(
    name="bounded-agent",
    model="gpt-5.5",
    instructions="Help users, but stop if the task becomes too large.",
    fail_safe=FailSafeConfig(
        max_steps=8,
        max_llm_calls=12,
        max_tool_calls=20,
        max_wall_time_s=45.0,
        max_total_cost_usd=0.25,
    ),
)

runner = Runner(
    config=RunnerConfig(
        sanitize_tool_output=True,
        tool_output_max_chars=8_000,
    ),
)

result = runner.run_sync(agent, user_message="Summarize the tradeoffs of caching.")
print(result.final_text)
```

Key point: limits are part of the agent contract. Set them before shipping.

## What to read next

<CardGroup cols={2}>
  <Card title="Agents" icon="robot" href="/library/agents">
    Agent fields, prompt resolution, subagents, skills, and MCP tools.
  </Card>

  <Card title="Tools" icon="wrench" href="/library/tools">
    Tool schemas, context, hooks, middleware, sandboxing, and output limits.
  </Card>

  <Card title="Streaming" icon="radio" href="/library/streaming">
    Event types, stream handles, cancellation, and UI patterns.
  </Card>

  <Card title="Production Readiness" icon="shield" href="/library/building-with-ai">
    Evals, telemetry, security controls, queues, and deployment.
  </Card>
</CardGroup>
