# Elsai Agent SDK — Cursor Rules

You are an expert in the **Elsai Agent SDK** for building AI agents in Python.

## Installation

```bash
pip install --extra-index-url https://elsai-agents.elsai.ai/root/ elsai-agents==0.1.0
# With provider extras:
pip install --extra-index-url https://elsai-agents.elsai.ai/root/ "elsai-agents[anthropic]==0.1.0"
pip install --extra-index-url https://elsai-agents.elsai.ai/root/ "elsai-agents[openai]==0.1.0"
pip install --extra-index-url https://elsai-agents.elsai.ai/root/ "elsai-agents[gemini]==0.1.0"
```

## Core Imports

```python
from elsai import Agent, tool
from elsai.models import BedrockModel
from elsai.models.anthropic import AnthropicModel
from elsai.models.openai import OpenAIModel
from elsai.models.gemini import GeminiModel
from elsai.models.ollama import OllamaModel
from elsai.models.litellm import LiteLLMModel
from elsai.hooks import (
    BeforeModelCallEvent, AfterModelCallEvent,
    BeforeToolCallEvent, AfterToolCallEvent,
    BeforeInvocationEvent, AfterInvocationEvent,
    AgentErrorEvent, MessageAddedEvent,
)
from elsai.multiagent.graph import GraphBuilder
from elsai.multiagent.swarm import Swarm
from elsai.session.file_session_manager import FileSessionManager
from elsai.session.s3_session_manager import S3SessionManager
from elsai.types.tools import ToolContext
```

## Minimal Agent

```python
from elsai import Agent

agent = Agent()  # defaults to Amazon Bedrock
result = agent("Explain quantum computing")
print(result)

# With a specific model
agent = Agent(model="us.amazon.nova-pro-v1:0")
agent = Agent(model=AnthropicModel(model_id="claude-sonnet-4-6"))
agent = Agent(model=OpenAIModel(model_id="gpt-4o"))
```

## Tool Creation Rules

- **ALWAYS write a docstring** — the model reads it to decide when/how to call the tool
- Type-annotate ALL parameters
- Return `str`, `dict`, `int`, `float`, Pydantic model, or any JSON-serialisable value
- For error: `return {"status": "error", "content": [{"text": "error message"}]}`

```python
from elsai import tool

@tool
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression and return the result.

    Args:
        expression: A valid Python math expression, e.g. '2 + 2 * 3'.
    """
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

@tool(name="web_search", description="Search the internet for current information")
def search(query: str, max_results: int = 5) -> str:
    """Search the web."""
    ...

@tool
async def fetch_url(url: str) -> str:
    """Fetch the content of a URL."""
    ...

# With ToolContext (access agent state and invocation state)
@tool(context=True)
def context_aware(question: str, tool_context: ToolContext) -> str:
    """Tool with access to agent context."""
    agent = tool_context["agent"]
    return f"Agent {agent.name} asked: {question}"
```

## Agent with Tools

```python
agent = Agent(
    model="us.amazon.nova-pro-v1:0",
    system_prompt="You are a helpful assistant with access to tools.",
    tools=[calculate, search, fetch_url],
)

# Direct tool call (bypass LLM)
result = agent.tool.calculate(expression="42 * 7")

# Natural language
result = agent("What is 42 times 7?")
```

## Hooks

```python
agent = Agent()

# Type-hint inference — event type is inferred from parameter annotation
def log_call(event: BeforeModelCallEvent) -> None:
    print(f"Calling model for: {event.agent.name}")

def block_tool(event: BeforeToolCallEvent) -> None:
    if event.tool_use["name"] == "sensitive_tool":
        event.cancel_tool = "Blocked by policy"

def auto_retry(event: AfterToolCallEvent) -> None:
    if event.exception:
        event.retry = True

agent.add_hook(log_call)
agent.add_hook(block_tool)
agent.add_hook(auto_retry)

# Or pass at construction
agent = Agent(hooks=[log_call, block_tool])
```

## Multi-Agent: Graph (deterministic)

```python
from elsai.multiagent.graph import GraphBuilder

planner  = Agent(name="planner",  system_prompt="Plan the task step by step.")
executor = Agent(name="executor", system_prompt="Execute each step.")
reviewer = Agent(name="reviewer", system_prompt="Review and summarise.")

builder = GraphBuilder()
builder.add_node(planner,  "plan")
builder.add_node(executor, "exec")
builder.add_node(reviewer, "review")
builder.add_edge("plan", "exec")
builder.add_edge("exec", "review")
builder.set_entry_point("plan")

graph = builder.build()
result = graph("Build a REST API")
print(result.results["review"].message)
```

## Multi-Agent: Swarm (autonomous handoff)

```python
from elsai.multiagent.swarm import Swarm

researcher = Agent(
    name="researcher",
    system_prompt="Research topics. Hand off calculations to the analyst.",
    tools=[web_search],
)
analyst = Agent(
    name="analyst",
    system_prompt="Analyse data. Hand off research to the researcher.",
    tools=[calculate],
)

swarm = Swarm(agents=[researcher, analyst], entry_agent=researcher)
result = swarm("Analyse the AI market size in 2030")
```

## Agent-as-Tool

```python
sub_agent = Agent(
    name="researcher",
    description="Searches and retrieves information on any topic",
    tools=[web_search],
)

orchestrator = Agent(tools=[sub_agent.as_tool()])
result = orchestrator("Research the history of Python and write a summary")
```

## Sessions (Persistence)

```python
from elsai.session.file_session_manager import FileSessionManager
from elsai.session.s3_session_manager import S3SessionManager

# session_id groups the session; agent_id identifies the agent within it
session = FileSessionManager(session_id="chat-alice", storage_dir="./sessions")
agent = Agent(agent_id="assistant", session_manager=session)

agent("My name is Alice")
# Restart process — conversation restored automatically
agent("What is my name?")  # → "Alice"

# S3 — param is 'bucket' (not 'bucket_name'), 'prefix' (not 's3_prefix')
session = S3SessionManager(session_id="chat-alice", bucket="my-bucket", prefix="sessions/")
agent = Agent(agent_id="assistant", session_manager=session)
```

## Structured Output

```python
from pydantic import BaseModel

class Summary(BaseModel):
    title: str
    key_points: list[str]
    conclusion: str

agent = Agent()
result = agent(
    "Summarise the history of Python",
    structured_output_model=Summary,
)
summary: Summary = result.structured_output
```

## Async

```python
import asyncio

async def main():
    agent = Agent()
    result = await agent.invoke_async("Hello!")
    async for event in agent.stream_async("Tell me a story"):
        if "data" in event:
            print(event["data"], end="")

asyncio.run(main())
```

## Key Rules

1. ALWAYS write docstrings for `@tool` functions — the LLM reads them.
2. Call `agent.cleanup()` when done with MCP tools.
3. Both `agent_id` AND `session_manager` are required for session persistence.
4. `name` + `description` on Agent matter when using `agent.as_tool()`.
5. In Swarm: handoff logic lives in the system prompt ("hand off to X when…").
6. In Graph: each node receives the combined output of all upstream nodes.
7. Hooks mutate event attributes (`event.cancel_tool`, `event.retry`, `event.resume`).
8. All model providers share the same Agent API — just swap `model=`.
