# Agents

# Agents

Consumer-facing guide to starting, monitoring, and resuming agent sessions on the Amdahl platform. This page covers the `agents.*` tool family end to end. For a tool-by-tool reference see [API reference: workflows](./api-reference/tools/workflows.md). For authentication see [Authentication](./authentication.md). For how sessions differ from substrate and artifacts see [Sessions](./sessions.md).

## What are agents?

An agent session is one execution of a named agent profile on a task. Each session gets its own conversation log, tool surface, turn budget, token ledger, and pause state. You start a session, monitor its progress, answer pauses when the agent needs input, and pick up the final artifact when it completes.

Profiles are named configurations. `content_writer` writes content end to end. `researcher` synthesizes research reports. `copilot` is a general-purpose assistant with a broad tool surface. Each profile decides which tools the agent can call, how many turns it gets, and what deliverable it produces.

Use an agent when the work requires more than one tool call, planning, judgment, or a pause for human input. Call a tool directly when the work is atomic.

## When to use which

| Your need                                                                       | Use                                                                                    |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| A single atomic read or write (list artifacts, run a SQL query, fetch a KB doc) | Tools directly: MCP `data.query`, REST `GET /data/query`, etc.                         |
| Multi-step work that should plan, call several tools, and produce a deliverable | An agent session via `agents.start`                                                    |
| End-to-end content creation with research, outlining, writing, evaluation       | `content_writer` profile                                                               |
| Rigorous research over substrate, KB, and clusters with citations               | `researcher` profile                                                                   |
| General task that might or might not need tools                                 | `copilot` profile                                                                      |
| Custom tool subset or custom system prompt                                      | Custom profiles are not yet self-serve. File a request to extend the built-in catalog. |

## Starting an agent

Use `agents.start` (MCP) or `POST /agents/run` (REST). You must supply a `profile_id` and a `task`. `input_params` is a free-form object the profile interprets (for `content_writer`, common keys are `channel`, `content_type`, `audience`, `voice_profile_id`, `reference_document_ids`).

### MCP

```json
{
  "name": "agents.start",
  "input": {
    "profile_id": "content_writer",
    "task": "Write a LinkedIn post about our Q2 earnings call highlights",
    "input_params": {
      "channel": "linkedin",
      "content_type": "linkedin_post"
    }
  }
}
```

### REST

```bash
curl -X POST https://api.amdahl.com/api/platform/v1/agents/run \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile_id": "content_writer",
    "task": "Write a LinkedIn post about our Q2 earnings call highlights",
    "input_params": { "channel": "linkedin", "content_type": "linkedin_post" }
  }'
```

### Response shape

By default the call is async and returns immediately:

```json
{
  "success": true,
  "session_id": "2f0a8c11-5d1b-4e3e-9b0a-6b9a2d4f8a11",
  "status": "queued",
  "profile_id": "content_writer",
  "created_at": "2026-04-14T18:22:14.509Z",
  "async": true
}
```

Set `async: false` to block for up to 60 seconds and receive the fully-populated session row when the agent finishes. If the agent pauses or the call times out, the response carries `success: false` with a structured `error.code` of `enqueue_failed`, plus the latest session snapshot so the caller can poll or resume.

## Monitoring progress

You have two supported options. Webhook delivery is on the roadmap; see the Webhooks section below.

### 1. Poll `agents.status`

MCP:

```json
{ "name": "agents.status", "input": { "session_id": "<uuid>", "include_events": false } }
```

REST: `GET /api/platform/v1/agents/:session_id/status`

Response shape:

```json
{
  "success": true,
  "session_id": "<uuid>",
  "status": "running",
  "profile_id": "content_writer",
  "task": "Write a LinkedIn post...",
  "created_at": "2026-04-14T18:22:14.509Z",
  "updated_at": "2026-04-14T18:23:02.120Z",
  "started_at": "2026-04-14T18:22:15.102Z",
  "completed_at": null,
  "turns_used": 4,
  "result_artifact_id": null,
  "result_summary": null,
  "progress": {
    "turnsCompleted": 4,
    "toolsUsed": [
      { "toolId": "knowledge_base.search", "count": 3 },
      { "toolId": "data.query", "count": 1 }
    ],
    "currentState": "tool_running",
    "tokensUsed": { "input": 12480, "output": 3210, "cacheRead": 0, "cacheCreate": 0 }
  }
}
```

Set `include_events: true` to add the last 50 persisted `AgentEvent` entries. Poll at a pace that matches your UX: a dashboard can poll every 2 to 5 seconds; a batch integration can poll every 15 to 30 seconds or rely on streaming. See [Rate limits](./rate-limits.md) for the recommended cap of 1 request per second.

### 2. Stream via Server-Sent Events

REST: `GET /api/platform/v1/agents/:session_id/stream`

The stream opens with a `state` event carrying the current session summary, then emits a `progress` event for each new `AgentEvent` as it is persisted. A `:ping` comment fires every 15 seconds to keep proxies from idling the connection. The stream closes automatically when the session reaches a terminal status (`complete`, `error`, `canceled`, `awaiting_input`) with a final `state` frame. If the connection exceeds the 10 minute cap, the server emits `event: timeout` and closes; clients reconnect to keep streaming.

Event shape on the wire:

```
event: progress
data: {"type":"tool_start","toolId":"knowledge_base.search","inputSummary":"{\"query\":\"Q2 earnings\"}","timestamp":"2026-04-14T18:23:01.222Z"}

event: progress
data: {"type":"tool_complete","toolId":"knowledge_base.search","durationMs":1240,"resultSummary":"...","timestamp":"2026-04-14T18:23:02.462Z"}

event: state
data: {"session_id":"...","status":"complete","result_artifact_id":"..."}
```

`AgentEvent` types you will see: `tool_start`, `tool_complete`, `tool_error`, `thinking`, `turn_complete`, `paused`, `completed`, `error`.

### 3. Webhooks (roadmap)

Webhook subscriptions on `agent.session.completed`, `agent.session.paused`, and `agent.session.failed` are planned. Use the existing polling or SSE paths until webhooks land. See [Webhooks](./webhooks.md) for what already works.

## Handling pauses

When an agent needs user input the session transitions to `status: 'awaiting_input'` and the response carries a `pending_input` object with three fields: `pending_input_type`, `pending_input_schema`, and `pending_input_context`. Your reply must match the schema exactly; the resume endpoint validates against it before the worker picks up the resume job.

There are three pause types.

### Approval

The agent wants a sign-off on a draft, an outline, or a destructive step.

```json
{
  "pending_input_type": "approval",
  "pending_input_schema": {
    "type": "object",
    "properties": {
      "approved": { "type": "boolean" },
      "feedback": { "type": "string", "maxLength": 5000 }
    },
    "required": ["approved"],
    "additionalProperties": false
  },
  "pending_input_context": { "tool_id": "pages.create", "tool_use_id": "toolu_01..." }
}
```

Resume with approval or rejection:

```json
{ "approved": true }
```

```json
{ "approved": false, "feedback": "Make the hook sharper; lead with the revenue number" }
```

### Question

The synthetic `ask_user` tool pauses the session to ask a single free-form question. The question is stored in both the schema `description` and the `pending_input_context.question` field.

```json
{
  "pending_input_type": "question",
  "pending_input_schema": {
    "description": "What tone do you want?",
    "type": "object",
    "properties": { "response": { "type": "string", "maxLength": 10000 } },
    "required": ["response"],
    "additionalProperties": false
  },
  "pending_input_context": { "question": "What tone do you want?" }
}
```

Resume:

```json
{ "response": "Professional but approachable; direct, no filler" }
```

### Continue or finish

Fires when the agent exhausts its `maxTurns` budget without finishing. You decide whether to grant more turns or wrap up with whatever work is done.

```json
{
  "pending_input_type": "continue_or_finish",
  "pending_input_schema": {
    "type": "object",
    "properties": {
      "action": { "enum": ["continue", "finish"] },
      "additional_turns": { "type": "integer", "minimum": 1, "maximum": 50 }
    },
    "required": ["action"],
    "additionalProperties": false
  },
  "pending_input_context": { "reason": "max_turns", "turns": 25 }
}
```

Resume with a grant or a wrap-up:

```json
{ "action": "continue", "additional_turns": 10 }
```

```json
{ "action": "finish" }
```

### REST resume

```bash
curl -X POST https://api.amdahl.com/api/platform/v1/agents/$SESSION_ID/resume \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "approved": true } }'
```

Response:

```json
{
  "session_id": "<uuid>",
  "status": "queued",
  "message": "Resume job enqueued",
  "job_id": "agent-session-<uuid>-resume"
}
```

On validation failure the handler returns `status: 'invalid_input'` and a `validation_error` string describing the mismatch. On state mismatch (session not currently awaiting input) the handler returns `status: 'invalid_state'` with the current session status.

## Cancellation

Sessions can be canceled at any non-terminal state. `agents.cancel` flips `status` to `canceled` and stamps `completed_at`. The worker observes the new status at the next safe checkpoint and stops making tool calls. Best-effort removal of any queued job runs in parallel; failures here are logged but do not block the cancel.

MCP:

```json
{
  "name": "agents.cancel",
  "input": { "session_id": "<uuid>", "reason": "Scope changed; rerun with new brief" }
}
```

REST: `POST /api/platform/v1/agents/:session_id/cancel`

Already-terminal sessions (`complete`, `error`, `canceled`) return `status: 'cannot_cancel'` with the current status echoed back. The reason field is optional and recorded in operator logs.

## Built-in profiles

| Profile id       | Description                                                                                      | Tool subset                                                                                                                                     | Max turns | Produces                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `content_writer` | End-to-end content creation: research, outline, write, evaluate, finalize.                       | `content.*`, `data.*`, `context.*`, `context_substrate.*`, `context_entries.*`, `artifacts.create`, `artifacts.update`, `knowledge_base.search` | 25        | `platform_artifact` of type `content_piece` (or subtype like `linkedin_post`, `blog_post` when `input_params` imply one)      |
| `researcher`     | Rigorous research over substrate + knowledge base with citations. Read-heavy.                    | `data.*`, `context.*`, `context_substrate.query`, `knowledge_base.search`, `knowledge_base.chat`, `artifacts.create`                            | 20        | `platform_artifact` of type `research_report` with structured `summary`, `sub_questions`, `key_findings`, `gaps`, `citations` |
| `copilot`        | General-purpose assistant. Broad tool surface minus destructive and privileged admin operations. | All registered tools except `artifacts.delete`, `artifacts.archive`, everything under `api_keys.*`, `oauth_clients.*`, `audit_log.*`            | 15        | Optional: the agent decides whether a `platform_artifact` is the right deliverable                                            |

Fetch the live list via `agents.profiles_list` (MCP) or `GET /api/platform/v1/agents/profiles` (REST). The response includes each profile's `id`, `name`, `description`, `version`, `max_turns`, `model`, and `produces_artifact_type`. Internal fields (system prompt, initial message template, tool id list) are deliberately omitted.

## Results

When `status: 'complete'`, the status response carries:

- `result_artifact_id`: UUID of the final `platform_artifact` when the profile produces one. `null` for profiles (like `copilot`) that may return a chat-only response.
- `result_summary`: short narrative the agent wrote describing what was produced.

Fetch the artifact itself.

MCP:

```json
{ "name": "artifacts.get", "input": { "id": "<result_artifact_id>", "include_versions": true } }
```

REST: `GET /api/platform/v1/artifacts/:id`

Artifacts carry the rendered output in `content_markdown` and/or `content_json`, plus structured metadata like `sources`, `citations`, and `tags`. Every profile that produces an artifact writes citations into `content_json` so downstream viewers can render a source list.

## Error handling

Terminal error states land in `status: 'error'` with a message on the top-level `session_error` field (distinct from the envelope `error` field used for invalid inputs).

Common error conditions and how to recover:

- **Anthropic rate limits.** The runner retries with jittered exponential backoff (up to 12 attempts, max 8s delay). A persistent failure lands in `status: 'error'`. Retry the task after the rate window passes by calling `agents.start` again.
- **Tool execution failures.** A single failing tool call does not end the session; the runner injects an error tool_result and lets the agent decide what to do. Chronic tool failure shows up in the event log as repeated `tool_error` events. Inspect via `agents.status` with `include_events: true`.
- **Invalid resume payload.** `agents.resume` returns `status: 'invalid_input'` with a `validation_error`. Fix the payload to match `pending_input_schema` and retry.
- **Max turns hit.** The session pauses with `continue_or_finish`. See the pause section.
- **Session not found / forbidden.** Returns `status: 'not_found'` on both missing rows and cross-business access, so one tenant cannot probe another's session ids.

See [API reference: errors](./api-reference/errors.md) for the full error code catalogue and recovery matrix.

## Security

Every agent runs under the caller's identity and scopes. The worker builds a tool-auth context that mirrors the HTTP translator: the agent sees exactly the tool surface the caller can execute. Scope checks happen at tool-dispatch time, so even if a profile references a tool beyond the caller's scope, that tool is silently dropped from the runtime tool list.

- Agents cannot escalate privileges. If your API key or user cannot call a tool directly, the agent cannot call it for you.
- Every tool call the agent makes is written to the platform audit log with the caller's identity and correlation id. The audit trail is the same one you get for direct REST or MCP calls.
- Response redaction runs on every tool result. Secrets are masked with `[MASKED]` and paired with a `<field>_masked` sentinel so clients know a value existed without ever seeing it.
- The agent's system prompt receives the caller's `businessId` for grounding but never renders raw credentials or tokens.
- The SSE stream authenticates before flushing headers so auth failures return clean HTTP status codes, not broken streams.

## Quick recipes

- **Draft a blog post.** `agents.start` with `profile_id: 'content_writer'`, `task: 'Draft a 1200-word blog post about X'`, `input_params: { channel: 'blog', content_type: 'blog_post' }`. Poll until `awaiting_input` on outline approval, resume with `{ approved: true }`, then fetch the artifact when `complete`. See [recipes/draft-blog-post](./recipes/draft-blog-post.md).
- **Run comprehensive research on topic X.** `agents.start` with `profile_id: 'researcher'`, `task: 'Research how mid-market SaaS companies are adopting usage-based pricing'`, optional `input_params: { must_cover: ['billing models', 'churn impact', 'expansion revenue'] }`. The result is a single `research_report` artifact with citations. See [recipes/run-research](./recipes/run-research.md).
- **Ask a general question that might need tools.** `agents.start` with `profile_id: 'copilot'`, `task: '<question or instruction>'`. Poll status; the agent decides whether to answer in chat (check `result_summary`) or write an artifact (check `result_artifact_id`).

## Rate limits

Per-tenant rate limits on `agents.start`, `agents.resume`, and the SSE stream endpoint are on the roadmap. Until they ship, the platform relies on the global per-IP limiter (see [Rate limits](./rate-limits.md)) and worker queue concurrency to protect capacity. Expect bursts of 10+ concurrent sessions to enqueue cleanly and process at the worker pool's rate.

## See also

- [API reference: workflows tools](./api-reference/tools/workflows.md) for the `agents.*` tool reference
- [Sessions](./sessions.md) for how agent sessions differ from substrate sessions and artifact sessions
- [Authentication](./authentication.md) for API key scopes required to call `agents.*`
- [Webhooks](./webhooks.md) for event subscriptions once they land
