# Logos ADK Advanced Guide

This guide contains detailed explanations and examples based on analysis of the framework's source code.

## Table of Contents

1. [Deep Dive into Providers](#deep-dive-into-providers)
2. [Detailed Memory Operations](#detailed-memory-operations)
3. [RAG: From Ingestion to Search](#rag-from-ingestion-to-search)
4. [Caching: Exact and Semantic](#caching-exact-and-semantic)
5. [Multi-Agent Topologies](#multi-agent-topologies)
6. [Message Handlers (@handler)](#message-handlers-handler)
7. [YAML Configuration](#yaml-configuration)
8. [Streaming: Under the Hood](#streaming-under-the-hood)

---

## Deep Dive into Providers

### How Providers Work

Logos ADK uses a `Provider` abstraction to unify work with various LLM APIs. Each provider converts internal Logos-format messages to the specific API's format.

#### Provider Architecture

```
Provider (abstract class)
├── AnthropicProvider (Claude)
├── OpenAIProvider (GPT-4, GPT-4o)
└── OpenAICompatibleProvider (Ollama, vLLM, LM Studio)
```

### Anthropic Provider (Claude)

#### Internal Workings

AnthropicProvider converts messages to Claude API format:

```python
from logos_adk import Agent
from logos_adk.providers import AnthropicProvider

# Create provider
provider = AnthropicProvider(
    model="claude-sonnet-4-20250514",
    api_key="your-api-key",  # or ANTHROPIC_API_KEY in env
    max_tokens=4096  # Max tokens for response
)

agent = Agent("claude_bot", model=provider)
```

#### Message Conversion

The framework automatically converts messages:

```python
# Logos format → Anthropic format
from logos_adk.types import Message, ToolCall, ToolResult

# Input Logos messages
messages = [
    Message(role="system", content="You are a helpful assistant"),
    Message(role="user", content="Hello!"),
    Message(
        role="assistant",
        content="Hello! How can I help?",
        tool_calls=[ToolCall(id="call1", name="search", arguments={"query": "test"})]
    ),
    Message(
        role="tool",
        content="",
        tool_result=ToolResult(call_id="call1", content="Search results")
    )
]

# Converted to Anthropic format:
anthropic_messages = [
    {"role": "user", "content": "Hello!"},
    {
        "role": "assistant",
        "content": [
            {"type": "text", "text": "Hello! How can I help?"},
            {
                "type": "tool_use",
                "id": "call1",
                "name": "search",
                "input": {"query": "test"}
            }
        ]
    },
    {
        "role": "user",
        "content": [{
            "type": "tool_result",
            "tool_use_id": "call1",
            "content": "Search results",
            "is_error": False
        }]
    }
]
```

#### Streaming with Claude

```python
import asyncio
from logos_adk import Agent
from logos_adk.types import TextChunk

async def stream_with_claude():
    agent = Agent("streamer", model="claude-sonnet-4-20250514")
    
    # Anthropic streaming uses messages.stream()
    async for chunk in agent.stream("Tell me a long story"):
        if isinstance(chunk, TextChunk):
            print(chunk.text, end="")  # Print as generated
    
asyncio.run(stream_with_claude())
```

### OpenAI Provider

#### Creating Provider

```python
from logos_adk.providers import OpenAIProvider, OpenAICompatibleProvider

# OpenAI (GPT-4, GPT-4o)
openai_provider = OpenAIProvider(
    model="gpt-4o",
    api_key="sk-..."  # or OPENAI_API_KEY in env
)

# Ollama (local)
ollama_provider = OpenAICompatibleProvider(
    model="llama3",
    base_url="http://localhost:11434/v1",
    api_key="not-needed"  # Ollama doesn't require key
)

# LM Studio
lm_provider = OpenAICompatibleProvider(
    model="local-model",
    base_url="http://localhost:1234/v1"
)

# Polza AI (Together, etc.)
polza_provider = OpenAICompatibleProvider(
    model="minimax/minimax-m2.5",
    base_url="https://api.polza.ai/api/v1",
    api_key="pza_..."
)
```

#### Tool Conversion to OpenAI Format

```python
# Logos tool
tool_schema = {
    "name": "get_weather",
    "description": "Get weather in a city",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
}

# Converted to OpenAI function-calling format:
openai_format = {
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get weather in a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"}
            },
            "required": ["city"]
        }
    }
}
```

#### Extracting Reasoning

Some models (especially o1, o3) return reasoning:

```python
from logos_adk import Agent

agent = Agent("reasoner", model="o1-preview")
response = await agent.run("Solve a complex math problem")

# Access reasoning (if model supports it)
if response.reasoning:
    print("Model reasoning:")
    print(response.reasoning)
    
print("\nAnswer:")
print(response.content)
```

### Auto-Registration of Providers

The framework automatically registers providers by name patterns:

```python
from logos_adk import Agent

# These strings automatically use the correct provider:
agent1 = Agent("bot1", model="claude-sonnet-4-20250514")  # → AnthropicProvider
agent2 = Agent("bot2", model="gpt-4o")                     # → OpenAIProvider
agent3 = Agent("bot3", model="o1-preview")                 # → OpenAIProvider
agent4 = Agent("bot4", model="o3-mini")                    # → OpenAIProvider
```

---

## Detailed Memory Operations

### Memory Architecture

```
Memory (strategy)
├── SessionMemory (short-term)
│   ├── sliding_window
│   ├── summarize
│   └── token_limit
├── LongTermMemory (long-term)
│   └── backend: InMemory / SQLite
└── SharedMemory (shared)
```

### SessionMemory: Detailed Strategies

#### Sliding Window

```python
from logos_adk import Agent, SessionMemory

# Stores only the last N messages
memory = SessionMemory(
    strategy="sliding_window",
    max_messages=20  # Last 20 messages
)

agent = Agent("chatbot", model="...").memory(memory)

# How it works:
# Messages: [1, 2, 3, ..., 19, 20, 21, 22]
# After 22 messages: [3, 4, ..., 20, 21, 22] (first 2 removed)
```

**When to use:**
- Short conversations
- When only recent context matters
- For token savings

#### Summarize

```python
from logos_adk import Agent, SessionMemory

# Summarizes old messages
memory = SessionMemory(
    strategy="summarize",
    max_messages=10  # Keeps 10 fresh, summarizes the rest
)

agent = Agent("long_chat", model="...").memory(memory)

# How it works:
# After 20 messages, creates a system message:
# "Session summary:
#  - User asked about Python
#  - Assistant explained decorators
#  - User asked about async
#  - Assistant showed async/await example"
# + last 10 messages
```

**When to use:**
- Long conversations
- When overall context needs to be preserved
- For balance between context and tokens

#### Token Limit

```python
from logos_adk import Agent, SessionMemory

# Keeps messages within token limit
memory = SessionMemory(
    strategy="token_limit",
    max_tokens=16000  # Maximum tokens
)

agent = Agent("efficient", model="...").memory(memory)

# Token estimation (approximate):
# 1 token ≈ 4 characters for English
# 1 token ≈ 1-2 characters for Russian
```

**Token estimation algorithm:**
```python
def estimate_tokens(text: str) -> int:
    # Simplified estimation
    return len(text.split())  # Words for English

# For Russian, better to use:
def estimate_tokens_ru(text: str) -> int:
    return len(text) // 3  # Approximately
```

### LongTermMemory: Search and Retrieval

#### Search Strategies

```python
from logos_adk import LongTermMemory, SQLiteBackend

backend = SQLiteBackend("memory.db")

# Keyword search (BM25-like)
keyword_memory = LongTermMemory(
    backend=backend,
    retrieval="keyword",  # Keyword search
    limit=5
)

# Hybrid search (keywords + lexical similarity)
hybrid_memory = LongTermMemory(
    backend=backend,
    retrieval="hybrid",
    limit=5
)

# Semantic search (vector)
semantic_memory = LongTermMemory(
    backend=backend,
    retrieval="semantic",
    limit=5,
    embed_model="sentence-transformers/all-MiniLM-L6-v2"
)
```

#### Memory Scoring Algorithm

The framework uses a combined scoring system:

```python
# Simplified version from retrieval.py
def score_entry(query: str, entry: MemoryEntry, retrieval: str) -> float:
    query_tokens = tokenize(query)  # Tokenization
    entry_tokens = tokenize(entry.content)
    
    # Token overlap
    overlap = len(query_tokens & entry_tokens)
    overlap_ratio = overlap / len(query_tokens) if query_tokens else 0.0
    
    # Exact phrase bonus
    phrase_bonus = 10.0 if query in entry.content else 0.0
    
    # Substring bonus
    substring_bonus = sum(t in entry.content for t in query_tokens)
    
    # Lexical similarity (SequenceMatcher)
    lexical_similarity = SequenceMatcher(None, query, entry.content).ratio()
    
    # Combined score for hybrid
    if retrieval == "hybrid":
        return phrase_bonus * 8 + overlap_ratio * 5 + substring_bonus * 2 + lexical_similarity
    
    return phrase_bonus * 6 + overlap_ratio * 4 + substring_bonus * 2 + lexical_similarity * 3
```

#### LongTermMemory Example

```python
import asyncio
from logos_adk import Agent, LongTermMemory, SQLiteBackend

async def main():
    backend = SQLiteBackend("memory.db")
    memory = LongTermMemory(
        backend=backend,
        retrieval="hybrid",
        limit=5
    )
    
    agent = Agent("persistent_bot", model="...").memory(memory)
    
    # Saving memories
    await agent.run("My name is Alexander, I'm a programmer from Moscow")
    await agent.run("I love Python and work with data")
    await agent.run("My favorite framework is FastAPI")
    
    # Retrieving relevant memories
    response = await agent.run("What do you remember about me?")
    print(response.content)
    # Agent will retrieve relevant memories through scoring
    
asyncio.run(main())
```

### InMemoryBackend vs SQLiteBackend

#### InMemoryBackend

```python
from logos_adk import InMemoryBackend

# Fast, for tests and development
backend = InMemoryBackend()

# Search works through:
# 1. Query normalization (lowercase)
# 2. Tokenization (words >= 4 characters)
# 3. Token inclusion check in content

# Search algorithm:
async def search(self, query: str, limit: int = 10) -> list[MemoryEntry]:
    normalized_query = query.strip().lower()
    keywords = [t for t in re.findall(r"[a-zA-Z0-9_]+", normalized_query) if len(t) >= 4]
    
    matches = []
    for entry in reversed(self._entries.values()):  # Newest first
        content = entry.content.lower()
        if normalized_query in content or any(k in content for k in keywords):
            matches.append(entry)
            if len(matches) >= limit:
                break
    return matches
```

#### SQLiteBackend

```python
from logos_adk import SQLiteBackend

# Persistent storage
backend = SQLiteBackend("memory.db")

# Stores in tables:
# - entries: id, content, metadata, created_at, session_id
# - embeddings: entry_id, vector (for semantic search)

# Advantages:
# - Persistence across restarts
# - Vector search support (via extensions)
# - Scalability
```

---

## RAG: From Ingestion to Search

### Full RAG Pipeline

```
Document → Load → Split → Embed → Vector Store → Search
```

### Document Loaders

#### TextLoader

```python
from logos_adk.rag import TextLoader

# Load all .txt files from directory
loader = TextLoader("docs/texts/")
documents = loader.load()

# Supported extensions: .txt, .text
```

#### MarkdownLoader

```python
from logos_adk.rag import MarkdownLoader

# Load .md files with metadata preservation
loader = MarkdownLoader("docs/markdown/")
documents = loader.load()

# Metadata includes:
# - source_file: file path
# - file_type: "markdown"
```

#### Custom Loader

```python
from logos_adk.rag import DocumentLoader, Document

class DatabaseLoader(DocumentLoader):
    """Load from database."""
    
    def __init__(self, connection_string: str):
        self.conn_string = connection_string
    
    def supports(self, path: str) -> bool:
        # Determines if loader can handle path
        return path.startswith("db://")
    
    async def load(self, path: str) -> list[Document]:
        import sqlite3
        
        conn = sqlite3.connect(self.conn_string)
        cursor = conn.cursor()
        cursor.execute("SELECT id, content FROM articles")
        
        documents = []
        for row in cursor.fetchall():
            documents.append(Document(
                content=row[1],
                metadata={"source": "database", "article_id": row[0]}
            ))
        
        conn.close()
        return documents

# Usage
loader = DatabaseLoader("articles.db")
docs = await loader.load("db://articles")
```

### RecursiveCharacterSplitter: Detailed Splitting

#### Algorithm

```python
from logos_adk.rag import RecursiveCharacterSplitter

splitter = RecursiveCharacterSplitter(
    chunk_size=1000,      # Max characters per chunk
    chunk_overlap=200,    # Overlap between chunks
    separators=["\n\n", "\n", ". ", " ", ""]  # Separators by priority
)

# Algorithm:
# 1. If text <= chunk_size → return as is
# 2. Try split by "\n\n" (paragraphs)
# 3. If parts still too large → recursively by "\n"
# 4. Then by ". " (sentences)
# 5. Then by " " (words)
# 6. If nothing works → force split by chunk_size

# Example:
text = "Paragraph 1...\n\nParagraph 2...\n\nParagraph 3..."
# Split by "\n\n" into paragraphs

long_paragraph = "Very long text without paragraphs. More text. And more."
# Split by ". " into sentences
```

#### Splitting Visualization

```
Source Document (5000 characters)
│
├─ Chunk 1 (0-1000) [overlap: 800-1000]
├─ Chunk 2 (800-1800) [overlap: 1600-1800]
├─ Chunk 3 (1600-2600) [overlap: 2400-2600]
├─ Chunk 4 (2400-3400)
└─ Chunk 5 (3200-4200)
```

### KnowledgeBase: RAG Orchestration

#### Creation and Usage

```python
from logos_adk import Agent, KnowledgeBase
from logos_adk.rag import (
    TextLoader,
    RecursiveCharacterSplitter,
    InMemoryVectorStore
)

# Embedding function (use real model in production)
async def embed_fn(text: str) -> list[float]:
    # Example: sentence-transformers
    # from sentence_transformers import SentenceTransformer
    # model = SentenceTransformer('all-MiniLM-L6-v2')
    # return model.encode(text).tolist()
    return [0.1] * 384  # Placeholder

# Create components
splitter = RecursiveCharacterSplitter(chunk_size=500, chunk_overlap=50)
vector_store = InMemoryVectorStore(embed_fn=embed_fn)

# Create knowledge base
kb = KnowledgeBase(
    store=vector_store,
    embed_fn=embed_fn,
    splitter=splitter
)

# Ingest documents
await kb.ingest("docs/readme.md")
await kb.ingest_text("Important project information", metadata={"type": "note"})

# Search
chunks = await kb.search("How to install the project?", limit=5)
for chunk in chunks:
    print(f"Score: {chunk.metadata.get('score', 'N/A')}")
    print(f"Content: {chunk.content[:200]}...")
```

#### Internal Ingest Process

```python
# Simplified version from knowledge_base.py
async def ingest(self, source: str) -> list[str]:
    # 1. Load file
    for loader in self.loaders:
        if loader.supports(source):
            docs = await loader.load(source)
            
            # 2. Split into chunks
            for doc in docs:
                chunks = self.splitter.split(doc)
                
                # 3. Embed each chunk
                for chunk in chunks:
                    chunk.embedding = await self.embed_fn(chunk.content)
                
                # 4. Save to vector store
                await self.store.add(chunks)
    
    return [doc.id for doc in docs]
```

### Vector Stores

#### InMemoryVectorStore

```python
from logos_adk.rag import InMemoryVectorStore

# Stores vectors in memory
store = InMemoryVectorStore(embed_fn=embed_fn)

# Search via cosine similarity:
async def search(self, query_vector: list[float], limit: int = 5):
    results = []
    for chunk in self._chunks:
        similarity = cosine_similarity(query_vector, chunk.embedding)
        results.append((chunk, similarity))
    
    results.sort(key=lambda x: x[1], reverse=True)
    return [chunk for chunk, score in results[:limit]]
```

#### SQLiteVectorStore

```python
from logos_adk.rag.vector_sqlite import SQLiteVectorStore

# Stores vectors in SQLite
store = SQLiteVectorStore("vectors.db", embed_fn=embed_fn)

# Advantages:
# - Persistent storage
# - Indexing for fast search
# - Scalability
```

#### ChromaVectorStore (optional)

```python
from logos_adk.rag.vector_chroma import ChromaVectorStore

# ChromaDB for production
store = ChromaVectorStore(
    collection="documents",
    embed_fn=embed_fn,
    persist_directory="./chroma_db"
)
```

---

## Caching: Exact and Semantic

### Caching Architecture

```
Request → CacheKeyBuilder → Key → Lookup → (Hit/Miss) → Response
                                    ↓
                            Semantic Search (optional)
```

### CacheKeyBuilder: Key Generation

#### Exact Keys

```python
from logos_adk.cache.key import CacheKeyBuilder

builder = CacheKeyBuilder()

# Create exact match key
key = builder.exact(
    model="gpt-4o",
    system_prompt="You are a helpful assistant",
    messages=[
        {"role": "user", "content": "Hello!"}
    ],
    tools=[
        {"name": "search", "description": "...", "parameters": {...}}
    ]
)

# Key is SHA256 hash of:
# {
#   "model": "gpt-4o",
#   "system_prompt": "You are a helpful assistant",
#   "messages": [...],
#   "tools": [...]
# }
```

#### Scope Hash for Semantic Search

```python
# Scope hash isolates caches by configuration
scope = builder.scope_hash(
    model="gpt-4o",
    system_prompt="You are a helpful assistant",
    tools=None
)

# Used as namespace in vector search
```

### Exact Caching

```python
from logos_adk import Agent, InMemoryCacheBackend

backend = InMemoryCacheBackend()

agent = (
    Agent("cached_bot", model="gpt-4o")
    .cache(backend, ttl=3600)  # 1 hour
)

# Cache operation:
# 1. Request: "What is Python?"
#    → Miss → API call → save to cache
# 2. Request: "What is Python?"
#    → Hit → return from cache
# 3. Request: "What's Python?"
#    → Miss (exact match not found)
```

### Semantic Caching

```python
from logos_adk import Agent, InMemoryCacheBackend

async def embed_fn(text: str) -> list[float]:
    return await model.encode(text)

backend = InMemoryCacheBackend()

agent = (
    Agent("semantic_bot", model="gpt-4o")
    .cache(
        backend,
        ttl=3600,
        semantic=True,           # Enable semantic search
        embed_fn=embed_fn,       # Embedding function
        similarity_threshold=0.95  # Similarity threshold
    )
)

# Cache operation:
# 1. Request: "How to make coffee?"
#    → Miss → API call → save with vector
# 2. Request: "How to brew coffee?"
#    → Semantic search → similarity 0.96 → Hit → return from cache
# 3. Request: "What's the weather?"
#    → Semantic search → similarity 0.3 → Miss → API call
```

#### Semantic Search Algorithm

```python
# Simplified version from cache backend
async def search_similar(
    self,
    vector: list[float],
    threshold: float = 0.95,
    limit: int = 1,
    scope: str = None
) -> list[CacheEntry]:
    results = []
    for entry in self._entries.values():
        if scope and entry.scope != scope:
            continue
        
        similarity = cosine_similarity(vector, entry.vector)
        if similarity >= threshold:
            results.append((entry, similarity))
    
    results.sort(key=lambda x: x[1], reverse=True)
    return [entry for entry, score in results[:limit]]
```

### SQLiteCacheBackend

```python
from logos_adk import SQLiteCacheBackend

# Persistent cache
backend = SQLiteCacheBackend("cache.db")

agent = Agent("bot", model="...").cache(backend, ttl=7200)

# Tables:
# - cache_entries: key, content, tool_calls, usage, created_at
# - cache_vectors: entry_key, vector, scope (for semantic)
```

---

## Multi-Agent Topologies

### Topology Types

#### Mesh (Fully Connected)

```python
from logos_adk import Agent, Team

agent1 = Agent("agent1", model="...")
agent2 = Agent("agent2", model="...")
agent3 = Agent("agent3", model="...")

# Mesh: everyone can communicate with everyone
team = Team(
    "mesh_team",
    agents=[agent1, agent2, agent3],
    topology="mesh"  # Default
)

# Allowed routes:
# agent1 → agent2, agent3
# agent2 → agent1, agent3
# agent3 → agent1, agent2
```

#### Star

```python
# Star: central hub
topology = {
    "hub": ["spoke1", "spoke2", "spoke3"],
    "spoke1": ["hub"],
    "spoke2": ["hub"],
    "spoke3": ["hub"]
}

team = Team(
    "star_team",
    agents=[hub, spoke1, spoke2, spoke3],
    topology=topology
)

# Spokes cannot communicate directly with each other
```

#### Line (Sequential)

```python
# Line: sequential chain
topology = {
    "agent1": ["agent2"],
    "agent2": ["agent3"],
    "agent3": ["agent4"],
    "agent4": []
}

team = Team("line_team", agents=[...], topology=topology)

# agent1 → agent2 → agent3 → agent4
# No feedback loop
```

#### Custom

```python
# Hybrid topology
topology = {
    "manager": ["researcher", "writer", "editor"],
    "researcher": ["writer"],
    "writer": ["editor"],
    "editor": ["manager"]
}

team = Team("custom_team", agents=[...], topology=topology)
```

### Topology Validation

```python
# From topology.py
def resolve_topology(topology, agent_names):
    if topology == "mesh":
        # Everyone can send to everyone except themselves
        name_set = set(agent_names)
        return {name: name_set - {name} for name in agent_names}
    
    if isinstance(topology, dict):
        result = {}
        for source, targets in topology.items():
            # Check: source exists
            if source not in agent_names:
                raise ValueError(f"Agent '{source}' not in team")
            
            # Check: targets exist
            for target in targets:
                if target not in agent_names:
                    raise ValueError(f"Target '{target}' not in team")
            
            result[source] = set(targets)
        
        # Agents not in topology cannot send to anyone
        for name in agent_names:
            if name not in result:
                result[name] = set()
        
        return result
```

### Message Routing

```python
# From topology.py
def validate_send(from_agent, to_agent, topology):
    if from_agent == to_agent:
        raise RoutingError("Agent cannot send to itself")
    
    if to_agent not in topology:
        raise RoutingError(f"Agent '{to_agent}' not found")
    
    allowed = topology.get(from_agent, set())
    if to_agent not in allowed:
        raise RoutingError(
            f"Send from '{from_agent}' to '{to_agent}' not allowed. "
            f"Allowed: {sorted(allowed)}"
        )
```

### Team with Validation Example

```python
import asyncio
from logos_adk import Agent, Team
from logos_adk.errors import RoutingError

async def main():
    manager = Agent("manager", model="...", system="You manage the team")
    researcher = Agent("researcher", model="...", system="You research")
    writer = Agent("writer", model="...", system="You write")
    
    topology = {
        "manager": ["researcher", "writer"],
        "researcher": ["manager"],
        "writer": ["manager"]
    }
    
    team = Team(
        "content_team",
        agents=[manager, researcher, writer],
        orchestrator="manager",
        topology=topology
    )
    
    # This works:
    await manager.send_to("researcher", "Research the topic")
    
    # This raises RoutingError:
    try:
        await researcher.send_to("writer", "Write article")
        # researcher can only send to manager
    except RoutingError as e:
        print(f"Routing error: {e}")

asyncio.run(main())
```

---

## Message Handlers (@handler)

### @handler Decorator

```python
from logos_adk import handler
from dataclasses import dataclass

@dataclass
class ResearchRequest:
    topic: str
    depth: str = "shallow"

@dataclass
class WriteRequest:
    topic: str
    style: str = "formal"

class ResearchAgent:
    def __init__(self):
        self.agent = Agent("researcher", model="...")
    
    @handler(ResearchRequest)
    async def handle_research(self, req: ResearchRequest):
        """Handler for ResearchRequest."""
        response = await self.agent.run(
            f"Research {req.topic} at {req.depth} depth"
        )
        return response.content
    
    @handler(WriteRequest)
    async def handle_write(self, req: WriteRequest):
        """Handler for WriteRequest."""
        response = await self.agent.run(
            f"Write text about {req.topic} in {req.style} style"
        )
        return response.content
```

### Collecting Handlers

```python
# From handler.py
def collect_handlers(instance):
    handlers = {}
    for name in dir(instance):
        if name.startswith("_"):
            continue
        
        attr = getattr(instance, name, None)
        if attr is None or not callable(attr):
            continue
        
        # Check for @handler decorator
        msg_type = getattr(attr, "_logos_handler_type", None)
        if msg_type is not None:
            if msg_type in handlers:
                raise TypeError(
                    f"Duplicate handler for {msg_type.__name__}: "
                    f"{handlers[msg_type].__name__} and {name}"
                )
            handlers[msg_type] = attr
    
    return handlers

# Usage in Team
team = Team("team", agents=[...])

# For each agent, collect handlers
for agent in agents:
    handlers = collect_handlers(agent)
    # handlers = {
    #     ResearchRequest: handle_research,
    #     WriteRequest: handle_write
    # }
```

### Message Handling in Team

```python
# Simplified version from team.py
async def run_rounds(self):
    for _ in range(self._config.max_rounds):
        any_activity = False
        
        for agent_name in self._agents:
            mailbox = self._mailboxes[agent_name]
            messages = mailbox.drain()  # Extract all messages
            
            if not messages:
                continue
            
            any_activity = True
            agent = self._agents[agent_name]
            handlers = self._handlers.get(agent_name, {})
            
            for msg in messages:
                # Find handler by message type
                handler = handlers.get(type(msg))
                
                if handler is not None:
                    # Call handler
                    result = handler(msg)
                    if hasattr(result, '__await__'):
                        await result
                else:
                    # Default handling: run agent
                    content = msg if isinstance(msg, str) else str(msg)
                    await agent.run(content)
        
        if not any_activity:
            break  # All mailboxes empty
```

### Team with Handlers Example

```python
import asyncio
from dataclasses import dataclass
from logos_adk import Agent, Team, handler

@dataclass
class Task:
    description: str
    priority: int = 1

class WorkerAgent:
    def __init__(self, name: str, specialty: str):
        self.name = name
        self.agent = Agent(name, model="...", system=f"You are a {specialty}")
    
    @handler(Task)
    async def handle_task(self, task: Task):
        print(f"{self.name}: Received task '{task.description}' (priority {task.priority})")
        response = await self.agent.run(f"Execute: {task.description}")
        return response.content

async def main():
    # Create worker agents
    coder = WorkerAgent("coder", "programmer")
    writer = WorkerAgent("writer", "writer")
    
    team = Team(
        "worker_team",
        agents=[coder.agent, writer.agent],
        topology="mesh"
    )
    
    # Send task (will be handled by @handler)
    await team.send("coder", "coder", Task("Write a function", priority=2))

asyncio.run(main())
```

---

## YAML Configuration

### YAML Config Parsing

```python
# From config_loader.py
import yaml

def load_agent_config(path: str) -> AgentConfig:
    with open(path) as f:
        data = yaml.safe_load(f)
    
    # data = {
    #     "name": "my_bot",
    #     "model": "gpt-4o",
    #     "system_prompt": "You are a helpful assistant",
    #     "provider": {...},
    #     "tools": [...],
    #     ...
    # }
    
    return parse_agent_config_data(data)
```

### Complete YAML Example

```yaml
# agent.yaml
name: advanced_bot
model: claude-sonnet-4-20250514

# Alternative: provider configuration
# provider:
#   type: openai-compatible
#   base_url: http://localhost:11434/v1
#   model: llama3
#   api_key: not-needed

system_prompt: |
  You are an advanced assistant with tool access.
  Use tools for accurate information.

# Tools from registry
tools:
  - search
  - calculator
  - get_weather

# Memory
memory:
  session:
    strategy: summarize
    max_messages: 10
  long_term:
    backend: sqlite
    path: memory.db
    retrieval: hybrid
    limit: 5

# Guardrails
guardrails:
  input:
    - type: content_filter
      blocked: [spam, fraud]
      action: block
    - type: max_length
      max_chars: 2000
  output:
    - type: keyword_guard
      keywords: [I don't know, Can't say]
      action: override
      override_with: Let me verify that information.

# Observability
observe:
  exporter: console

# Caching
cache:
  backend: sqlite
  path: cache.db
  ttl: 3600
  semantic: true
  similarity_threshold: 0.95

# MCP servers
mcp:
  - url: http://localhost:3000/sse
  - command: npx @server-github

# RAG / Knowledge Base
knowledge:
  sources:
    - docs/readme.md
    - docs/api/
  splitter:
    chunk_size: 500
    chunk_overlap: 50
  vector_store: sqlite
  path: vectors.db
```

### Loading Configuration

```python
from logos_adk import Agent

# Load from YAML
agent = Agent.from_config("agent.yaml")

# Hot reload
agent.reload()  # Reload from original path
agent.reload("new_config.yaml")  # Reload from new path
```

### ConfigWatcher: Hot Reload

```python
from logos_adk import ConfigWatcher

async def on_reload(config):
    print(f"Config reloaded: {config.name}")
    print(f"New system prompt: {config.system_prompt}")

watcher = ConfigWatcher(
    "agent.yaml",
    on_reload=on_reload
)

await watcher.start()

# Now when agent.yaml changes:
# 1. File is modified
# 2. ConfigWatcher detects change
# 3. on_reload(config) is called
# 4. Agent automatically updates configuration
```

---

## Streaming: Under the Hood

### Streaming Architecture

```
Agent.stream() → Provider.stream() → AsyncIterator[Chunk]
                                           ↓
                                    TextChunk / ToolStart / ToolEnd
```

### Anthropic Streaming

```python
# From anthropic.py
async def stream(self, messages, tools=None, **kwargs):
    system, api_messages = _messages_to_anthropic(messages)
    api_tools = _tools_to_anthropic(tools)
    
    # Anthropic messages.stream()
    async with self._client.messages.stream(
        model=self._model,
        max_tokens=self._max_tokens,
        messages=api_messages,
        system=system,
        tools=api_tools
    ) as stream:
        # Text streaming
        async for text in stream.text_stream:
            yield TextChunk(text=text)
        
        # Final response
        response = await stream.get_final_message()
        yield _parse_response(response)
```

### OpenAI Streaming

```python
# From openai.py
async def stream(self, messages, tools=None, **kwargs):
    api_messages = _messages_to_openai(messages)
    api_tools = _tools_to_openai(tools)
    
    # OpenAI chat.completions.create(stream=True)
    stream = await self._client.chat.completions.create(
        model=self._model,
        messages=api_messages,
        tools=api_tools,
        stream=True
    )
    
    collected_text = []
    usage = None
    
    async for chunk in stream:
        if not chunk.choices:
            continue
        
        delta = chunk.choices[0].delta
        if delta.content:
            collected_text.append(delta.content)
            yield TextChunk(text=delta.content)
        
        # Save usage from last chunk
        if chunk.usage:
            usage = chunk.usage
    
    # Final Response
    yield Response(
        content="".join(collected_text),
        usage=Usage(
            input_tokens=usage.prompt_tokens if usage else 0,
            output_tokens=usage.completion_tokens if usage else 0
        )
    )
```

### Chunk Handling in Agent

```python
# Simplified version from agent.py
async def stream(self, prompt, **kwargs):
    # Add user message
    self._get_state(session_id).add_message(
        Message(role="user", content=prompt)
    )
    
    # Build provider prompt
    provider_messages = self._build_provider_messages(...)
    
    # Streaming from provider
    async for chunk in provider.stream(provider_messages, ...):
        if isinstance(chunk, TextChunk):
            # Add to history
            self._get_state(session_id).add_message(
                Message(role="assistant", content=chunk.text)
            )
            yield chunk
        elif isinstance(chunk, Response):
            # Final response
            yield StreamDone(response=chunk)
```

### Custom Chunk Handling Example

```python
import asyncio
from logos_adk import Agent
from logos_adk.types import TextChunk, ToolStart, ToolEnd, StreamDone

async def custom_stream_handler():
    agent = Agent("streamer", model="claude-sonnet-4-20250514")
    
    async for chunk in agent.stream("Tell a story and use a tool"):
        if isinstance(chunk, TextChunk):
            # Text handling
            if chunk.reasoning:
                print(f"🤔 [Reasoning] {chunk.reasoning}")
            if chunk.text:
                print(chunk.text, end="", flush=True)
        
        elif isinstance(chunk, ToolStart):
            # Tool start
            print(f"\n🔧 [Tool Start] {chunk.name}")
            print(f"   Arguments: {chunk.arguments}")
        
        elif isinstance(chunk, ToolEnd):
            # Tool end
            if chunk.result.error:
                print(f"   ❌ Error: {chunk.result.error}")
            else:
                print(f"   ✓ Result: {chunk.result.content[:100]}")
        
        elif isinstance(chunk, StreamDone):
            # Completion
            print(f"\n\n✅ Done!")
            print(f"📊 Tokens: {chunk.response.usage.input_tokens} in, "
                  f"{chunk.response.usage.output_tokens} out")

asyncio.run(custom_stream_handler())
```

---

## Component Summary Table

| Component | Classes | Description |
|-----------|---------|-------------|
| **Providers** | AnthropicProvider, OpenAIProvider, OpenAICompatibleProvider | LLM API abstraction |
| **Memory** | SessionMemory, LongTermMemory, SharedMemory | Context storage strategies |
| **Backends** | InMemoryBackend, SQLiteBackend | Data storage |
| **RAG** | KnowledgeBase, TextLoader, RecursiveCharacterSplitter | Knowledge loading and search |
| **Vectors** | InMemoryVectorStore, SQLiteVectorStore, ChromaVectorStore | Vector stores |
| **Caches** | InMemoryCacheBackend, SQLiteCacheBackend, RedisCacheBackend | Response caching |
| **Guardrails** | ContentFilter, RegexGuard, MaxLengthGuard, KeywordGuard | Content validation |
| **Observability** | ConsoleExporter, InMemoryExporter, OpenTelemetryExporter | Metrics and spans export |
| **Topologies** | mesh, star, line, custom | Team message routing |
