# Elsai Agent SDK — GitHub Copilot Instructions

This repository uses the **Elsai Agent SDK** for building AI agents in Python. Use these conventions and patterns when suggesting code.

## 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

Always write a docstring — the model reads it to decide when and how to call the tool. Type-annotate all parameters.

```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],
)

result = agent.tool.calculate(expression="42 * 7")  # direct tool call
result = agent("What is 42 times 7?")               # natural language
```

## Hooks

```python
agent = Agent()

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 pipeline)

```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

# Both agent_id AND session_manager are required for persistence
session = FileSessionManager(session_id="chat-alice", storage_dir="./sessions")
agent = Agent(agent_id="assistant", session_manager=session)

agent("My name is Alice")
agent("What is my name?")  # → "Alice" (restored from session)

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

## 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 Conventions

1. Always write docstrings for `@tool` functions — the LLM reads them to decide when to call.
2. Call `agent.cleanup()` when done with MCP tools.
3. Both `agent_id` AND `session_manager` are required for session persistence.
4. Set `name` + `description` on Agent when using `agent.as_tool()`.
5. In Swarm: handoff logic belongs 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=`.
9. Return errors from tools as `{"status": "error", "content": [{"text": "message"}]}`.
