# Agents

> AI agent configuration and execution

Agents are the core execution units of the Struere platform, processing up to 10 LLM iterations per request with automatic tool calling, permission checking, and credit billing. Each agent runs within an ActorContext that enforces row-level scope rules and column-level field masks on every operation, supporting 40+ LLM models from OpenAI, Anthropic, Google, and xAI via OpenRouter.

## Architecture

Agent data is split across two tables with different scoping rules:

### agents Table (Shared)

The `agents` table stores identity information that is shared across environments:

| Field | Type | Description |
|-------|------|-------------|
| `organizationId` | ID | Organization that owns this agent |
| `name` | string | Display name |
| `slug` | string | URL-safe identifier for API routing |
| `description` | string | Human-readable description |
| `status` | enum | `"active"`, `"paused"`, or `"deleted"` |

The `slug` is used for API access via `/v1/agents/:slug/chat`.

### agentConfigs Table (Environment-Scoped)

The `agentConfigs` table stores the actual configuration and is scoped per environment using the `by_agent_env` index:

| Field | Type | Description |
|-------|------|-------------|
| `agentId` | ID | Reference to the agents table |
| `environment` | enum | `"development"` or `"production"` |
| `version` | string | Semantic version |
| `name` | string | Config display name |
| `systemPrompt` | string | Compiled system prompt |
| `model` | object | Provider, model name, temperature, maxTokens |
| `tools` | string[] | Array of tool names (built-in and custom) the agent can use |
| `deployedBy` | ID | User who deployed this config |

This split means an agent can have different configurations in development and production. The `struere dev` command syncs to the development config, and `struere deploy` promotes configs to production.

## Execution Flow

When a chat request arrives, the agent executes through this pipeline:

```
POST /v1/chat  or  POST /v1/agents/:slug/chat
         │
         ▼
┌─────────────────────────────┐
│ 1. Authentication           │
│    Extract Bearer token     │
│    Validate API key         │
│    (SHA-256 hash lookup)    │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 2. Load Agent               │
│    Resolve agent by ID/slug │
│    Load config via          │
│    by_agent_env index       │
│    (env from API key)       │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 3. Build ActorContext       │
│    organizationId           │
│    actorType (user/agent)   │
│    environment              │
│    roleIds (resolved)       │
│    isOrgAdmin               │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 4. Prepare Thread           │
│    Get or create thread     │
│    (env-scoped)             │
│    Load message history     │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 5. Process System Prompt    │
│    Resolve {{variables}}    │
│    Execute embedded queries │
│    (permission-aware)       │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 6. LLM Loop (max 10 iter)  │
│    Call LLM API             │
│    ├─ Text response → done  │
│    └─ Tool calls:           │
│       Check permission      │
│       Execute tool          │
│       Add result to context │
│       Continue loop         │
└──────────┬──────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ 7. Persist & Respond        │
│    Append messages to thread│
│    Record execution metrics │
│    Return response          │
└─────────────────────────────┘
```

### LLM Loop

The agent runs an iterative loop with a maximum of 10 iterations. Each iteration:

1. Sends the full message history (system prompt + conversation + tool results) to the LLM
2. If the LLM responds with text only, the loop exits
3. If the LLM makes tool calls, each tool is:
   - Permission-checked via `canUseTool`
   - Executed (built-in tools run as Convex mutations; custom tools run on the tool executor service)
   - Results are appended to the message history
4. The loop continues with the updated history

### Tool Execution

**Built-in tools** run as Convex mutations with full permission checking:

| Tool | Convex Function | Description |
|------|-----------------|-------------|
| `entity.create` | `tools.entities.entityCreate` | Create entity + emit event |
| `entity.get` | `tools.entities.entityGet` | Get entity (field-masked) |
| `entity.query` | `tools.entities.entityQuery` | Query with scope filters |
| `entity.update` | `tools.entities.entityUpdate` | Update + emit event |
| `entity.delete` | `tools.entities.entityDelete` | Soft delete + emit event |
| `event.emit` | `tools.events.eventEmit` | Emit custom event |
| `event.query` | `tools.events.eventQuery` | Query events (visibility filtered) |
| `agent.chat` | `tools.agents.agentChat` | Delegate to another agent |

**Custom tools** are sent to the tool executor service at `tool-executor.struere.dev` for isolated execution with actor context. Custom tool handlers receive a `struere` SDK parameter that provides access to all built-in tools (e.g., `struere.entity.create`, `struere.event.emit`), enabling custom tools to compose platform operations within their handler logic. Tools marked with `templateOnly: true` are executed during system prompt template compilation but are not exposed to the LLM as callable tools at runtime.

## Multi-Agent Communication

The `agent.chat` tool enables agents to delegate work to other agents within the same organization and environment.

```
Caller Agent
    │
    ├─ tool_call: agent.chat({ agent: "analyst", message: "..." })
    │
    ▼
Target Agent Resolution
    │
    ├─ Find agent by slug
    ├─ Create child thread (shared conversationId)
    │
    ▼
Target Agent Execution
    │
    ├─ Full LLM loop with target's own config/tools/permissions
    │
    ▼
Response returned as tool result to Caller Agent
```

### Safety Mechanisms

| Mechanism | Description |
|-----------|-------------|
| Depth limit | Maximum chain depth of 3 (`MAX_AGENT_DEPTH`) |
| Cycle detection | Target slug checked against caller slug |
| Iteration cap | Each agent limited to 10 LLM iterations independently |
| Action timeout | Convex built-in timeout prevents infinite execution |

> **Gotcha:** `agent.chat` enforces depth 3 with cycle detection — A calling B calling A is blocked. Design shallow agent graphs. See [Platform Gotchas](/platform/gotchas) for details.

### Thread Linking

All threads in a multi-agent conversation share the same `conversationId`. Child threads store a `parentThreadId` linking back to the parent. Thread metadata includes:

```typescript
{
  conversationId: string,
  parentAgentSlug: string,
  depth: number,
  parentContext: object,
}
```

## Thread Context

Every conversation thread carries metadata about the channel it originated from and any context parameters. This data is available in system prompt templates and custom tool handlers.

### Thread Data

| Field | Description |
|-------|-------------|
| `channel` | The originating channel: `"whatsapp"`, `"api"`, `"widget"`, or `"dashboard"` |
| `channelParams` | Channel-specific metadata (see below) |
| `externalId` | External identifier for thread deduplication (e.g., `whatsapp:{connectionId}:{phoneNumber}`) |
| `threadContext` | Custom parameters passed by the caller or auto-populated by the channel |

### WhatsApp Channel Params

When a conversation comes through WhatsApp, the thread's `channelParams` is automatically populated:

```typescript
{
  phoneNumber: "+1234567890",
  contactName: "Maria Garcia",
  lastInboundAt: 1713384000000,
}
```

| Param | Type | Description |
|-------|------|-------------|
| `phoneNumber` | `string` | Sender's phone number with country code |
| `contactName` | `string?` | WhatsApp profile display name |
| `lastInboundAt` | `number?` | Timestamp of the last inbound message |

### Accessing Thread Context in System Prompts

Use template variables to inject thread context into the agent's system prompt:

```
You are a support agent for {{organizationName}}.
Channel: {{threadContext.channel}}
Sender phone: {{threadContext.params.phoneNumber}}
Sender name: {{threadContext.params.contactName}}
```

**Example: Auto-identifying a teacher by phone number**

```
You are a scheduling assistant for {{organizationName}}.

The sender's phone number is {{threadContext.params.phoneNumber}}.
Look up the teacher entity matching this phone number to personalize your responses.
If no teacher is found, ask the sender to identify themselves.
```

### Accessing Thread Context in Custom Tools

Custom tool handlers receive thread context via the `context` parameter. See [Custom Tools](../tools/custom-tools) for the full `ExecutionContext` interface, which includes `organizationId`, `actorId`, and `actorType`.

## Model Pricing

See [Model Configuration](../reference/model-configuration) for the full list of supported models, pricing, and configuration options.

## API Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/v1/chat` | POST | Chat by agent ID (Bearer token with API key) |
| `/v1/agents/:slug/chat` | POST | Chat by agent slug (Bearer token with API key) |

Both endpoints require a valid API key passed as a Bearer token. The environment is determined by the API key's environment field.

### Response Format

```typescript
{
  threadId: string,
  message: string,
  usage: {
    inputTokens: number,
    outputTokens: number,
    totalTokens: number,
    reasoningTokens?: number,
  },
  _executionMeta?: {
    iterationCount: number,
    model: string,
    durationMs: number,
    toolCallSummary: Array<{
      name: string,
      durationMs: number,
      status: string,
      errorType?: string,
      errorMessage?: string,
    }>,
    errorCount: number,
    permissionDenialCount: number,
  },
  _transferred?: {
    targetAgentSlug: string,
    targetAgent: string,
  },
}
```

### Execution Metadata

The `_executionMeta` field provides telemetry about the agent's execution:

| Field | Type | Description |
|-------|------|-------------|
| `iterationCount` | number | Number of LLM iterations in the loop |
| `model` | string | Model ID used (OpenRouter format) |
| `durationMs` | number | Total execution time in milliseconds |
| `toolCallSummary` | array | Per-tool breakdown with name, duration, status, and error details |
| `errorCount` | number | Number of failed tool calls (agent may self-correct) |
| `permissionDenialCount` | number | Number of tool calls denied by the permission engine |

### Router Transfer

When an agent is running inside a routed thread, the `router.transfer` tool is auto-injected into its available tools, regardless of the agent's tool configuration. This allows any agent in a routed conversation to transfer the thread to another agent.

When a transfer occurs, the response includes an empty `message` and a `_transferred` object containing `targetAgentSlug` and `targetAgent` (display name). The caller should use the `_transferred` flag to detect that a transfer happened and handle it accordingly (e.g., re-routing the next message to the new agent).

