> ## Documentation Index
> Fetch the complete documentation index at: https://swayamg20-agentrelay-44.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Handoffs: structured cross-agent task transfer explained

> A handoff packages context — diffs, contracts, questions — and routes it from one developer's coding agent to a teammate's agent on another laptop.

A handoff is the core unit of work in AgentRelay: a structured task transfer from one developer's agent to another's. When Bob's agent finishes refactoring the `/users` API, it packages the relevant context — a file diff, the new API shape, an open question — into a handoff and routes it to Frank's agent over the relay. Frank's agent receives the full context, already provenance-wrapped, and can draft a plan without any human copy-paste in between.

## Handoff lifecycle

Every handoff moves through a deterministic state machine. States are stored in the relay's database and enforced server-side; the client cannot skip a transition.

```
pending  →  accepted  →  completed
   ↓
cancelled   (sender only, before acceptance)
```

| State       | Meaning                                                | Who can act                                                   |
| ----------- | ------------------------------------------------------ | ------------------------------------------------------------- |
| `pending`   | Handoff created; recipient has not yet pulled context  | Sender can cancel; either side can send messages              |
| `accepted`  | Recipient pulled the full thread into their session    | Recipient can complete or send messages; sender cannot cancel |
| `completed` | Recipient marked the thread done with a result summary | Read-only                                                     |
| `cancelled` | Sender withdrew before acceptance                      | Read-only                                                     |

**Invariants enforced by the relay:**

* Only the recipient can `accept` or `complete` a thread.
* Only the sender can `cancel`, and only while the thread is still `pending`.
* Both participants can append messages while `pending` or `accepted`.
* Both participants can read at any state.

If two sessions race to accept the same handoff, the relay uses a `UPDATE … WHERE status = 'pending'` row-level check: one wins, the other receives a `409 state_changed` error.

## Intent types

Every handoff carries an `intent` field that declares what the sender expects from the receiver.

<CardGroup cols={2}>
  <Card title="inform" icon="circle-info">
    Bob is sharing a result and no action is expected. Use this when you've finished a task and want to give your teammate full context — the diff, the test results, the API shape — so their agent starts its next session already up to speed.
  </Card>

  <Card title="ask_question" icon="circle-question">
    Bob wants information from Frank's codebase. This opens a back-and-forth Q\&A thread. Frank's agent reads the question, looks up the answer in Frank's local context, and replies — all via `send_message` on the same thread.
  </Card>

  <Card title="propose_action" icon="code-pull-request" color="#f59e0b">
    **(v0.1.5)** Bob is requesting a specific code change in Frank's codebase, with an optional suggested diff. Frank's agent drafts the change; Frank's human approves before any file is modified. The `proposed_action` field must be non-null when this intent is set.
  </Card>
</CardGroup>

<Note>
  `inform` and `ask_question` ship in v0.1. `propose_action` is a v0.1.5 addition — the relay validates that a `proposed_action` payload is present exactly when `intent` is `propose_action`, returning error `-32012 invalid_intent_payload` otherwise.
</Note>

## Artifact types

A handoff can carry one or more artifacts: typed, structured payloads that give the receiving agent concrete material to work with rather than a wall of prose.

```json theme={null}
[
  {"type": "file_diff", "path": "src/api/users.py", "diff": "..."},
  {"type": "file_ref", "path": "openapi.yaml", "git_sha": "abc123"},
  {"type": "test_command", "command": "pytest tests/users/"},
  {"type": "api_contract", "schema_url": "https://..."},
  {"type": "link", "url": "https://...", "title": "PR #42"}
]
```

| Type           | Fields                       | Purpose                                                                      |
| -------------- | ---------------------------- | ---------------------------------------------------------------------------- |
| `file_diff`    | `path`, `diff`               | A unified diff the receiver can inspect and optionally apply                 |
| `file_ref`     | `path`, `git_sha?`, `lines?` | A pointer to a specific file (and optionally a line range) at a known commit |
| `test_command` | `command`, `cwd?`            | A test or lint command the receiver's agent can run to verify behaviour      |
| `api_contract` | `schema_url?`, `inline?`     | An OpenAPI/JSON Schema contract, by URL or inline                            |
| `link`         | `url`, `title?`              | An external reference (PR, issue, runbook)                                   |

The full TypeScript type for `Artifact` is defined in the MCP server's `handoff_to_teammate` tool schema.

## Thread model

A handoff is not just a single message — it's a thread. The thread has three conceptual sections:

1. **Initial summary** — the text and artifacts the sender included when creating the handoff. The summary is also stored as `messages` row with `sequence_no = 1` for consistent ordering.
2. **Subsequent messages** — back-and-forth clarifications appended by either participant via `send_message`. Each message is immutable and ordered by a monotonic `sequence_no`.
3. **Final result** — when the recipient calls `complete_handoff`, they attach a `result_summary`. This closes the thread as read-only and fires a Slack notification to the sender.

Every message body is stored with a `payload` field for structured attachments (such as file refs) alongside freeform text.

## Idempotency

The MCP server generates a UUIDv4 client-side idempotency key for every state-changing call before it sends the request to the relay. This prevents duplicate handoffs if the agent retries a tool call or the network drops mid-send.

The relay's behaviour when it receives an idempotency key:

* **Key absent:** proceed normally.
* **Key present, payload matches:** return the original result with `200` — safe replay.
* **Key present, payload differs:** return `-32011 duplicate_idempotency_key`.

Keys expire after 24 hours. The agent never sees them; the MCP server manages them transparently.

## Concrete example

Bob refactors the `/users` API and wants Frank to update the client. In Claude Code, Bob says:

> *"Send a handoff to frank\@acme telling him I refactored /users. Include the diff and let him know the response shape changed to `{ items, next_cursor }`."*

The `handoff_to_teammate` tool sends this to the relay:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "message/send",
  "params": {
    "recipient": "frank@acme",
    "intent": "inform",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "Refactored /users API. Response is now paginated: { items: User[], next_cursor: string | null }."
        }
      ]
    },
    "artifacts": [
      {
        "type": "file_diff",
        "path": "src/api/users.py",
        "diff": "@@ -12,7 +12,9 @@ ...\n- return users\n+ return { 'items': users, 'next_cursor': cursor }"
      },
      {
        "type": "test_command",
        "command": "pytest tests/users/"
      }
    ],
    "metadata": {
      "client_idempotency_key": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
    }
  }
}
```

The relay persists the handoff (status `pending`), fires a Slack DM to Frank, and returns a `thread_id`. Frank's agent picks up the handoff on its next `check_inbox` call, accepts it, and receives the full context provenance-wrapped and ready to act on.

## State transitions at a glance

| From                       | Transition     | Allowed actor      | Result                |
| -------------------------- | -------------- | ------------------ | --------------------- |
| `pending`                  | `accept`       | Recipient only     | → `accepted`          |
| `pending`                  | `cancel`       | Sender only        | → `cancelled`         |
| `accepted`                 | `complete`     | Recipient only     | → `completed`         |
| Any active state           | `send_message` | Either participant | Message appended      |
| `completed` or `cancelled` | any            | —                  | `409 thread_terminal` |

<Tip>
  If a recipient decides they cannot fulfil a handoff after accepting it, they call `complete_handoff` with a `result_summary` explaining why. There is no `reject` transition post-acceptance — completion with a negative result is the correct pattern, and it keeps the state machine simple.
</Tip>
