Skip to content
Back to skills

Continuum Handoffs

ASecurity

Build agent-to-agent transitions with Continuum's `Handoff` system — triage routing, history summarization modes (FULL/SUMMARY/RECENT_N/HYBRID), cycle detection, depth tracking, return-to-parent. Invoke when the user asks "route customer requests to specialists", "agent that can transfer to another", "summarize history before handing off", or anything multi-agent that's a *transition* (not a *workflow*).

  • 85 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
ai-agentspython

Works with

  • cli

Security analysis

A100/100

Scanned June 5, 2026

npx -y skills add shyftlabs/continuum --skill continuum-handoffs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Continuum Handoffs?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Continuum Handoffs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shyftlabs-continuum-handoffs/badge)](https://www.skillsdirectory.com/skills/shyftlabs-continuum-handoffs)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: continuum-handoffs
description: Build agent-to-agent transitions with Continuum's `Handoff` system — triage routing, history summarization modes (FULL/SUMMARY/RECENT_N/HYBRID), cycle detection, depth tracking, return-to-parent. Invoke when the user asks "route customer requests to specialists", "agent that can transfer to another", "summarize history before handing off", or anything multi-agent that's a *transition* (not a *workflow*).
---

# Continuum Handoffs Skill

Authoritative source: [`docs/agent.md`](../../../docs/agent.md), §7.

> Handoffs are agent-to-agent **transitions** ("triage → billing"),
> distinct from workflow agents which compose multiple agents into one
> (Sequential, Parallel, etc.). Use a `RouterAgent` for stateless
> routing; use handoffs when the source agent should be able to decide
> at any turn that the target should take over.

---

## Imports

```python
from orchestrator.agent import BaseAgent, AgentRunner, Handoff
from orchestrator.agent.types import HistorySummarizationMode, HandoffData, HandoffResult
from orchestrator.agent.exceptions import (
    HandoffNotAllowedError, HandoffDepthExceededError,
    HandoffTargetNotFoundError,
)
# HandoffCycleDetectedError is NOT re-exported from `orchestrator.agent`:
from orchestrator.agent.exceptions import HandoffCycleDetectedError
```

---

## Define a triage agent

```python
billing   = BaseAgent(name="billing",   instructions="Help with invoices and refunds.")
technical = BaseAgent(name="technical", instructions="Help with bugs and outages.")

triage = BaseAgent(
    name="triage",
    instructions="Route the customer to the right specialist.",
    handoffs=[
        Handoff(target_agent="billing",
                description="Billing, payments, refunds, invoices"),
        Handoff(target_agent="technical",
                description="Bugs, errors, outages, integration issues"),
    ],
)

runner = AgentRunner(agent_registry={
    "triage": triage, "billing": billing, "technical": technical,
})
resp = await runner.run(triage, "I want a refund for invoice 1234",
                        user_id="u1", session_id="s1")
print(resp.handoff_chain)            # ["triage", "billing"]
print(resp.content)                  # the billing agent's reply
```

---

## `Handoff` fields

| Field | Type | Default | Purpose |
|---|---|---|---|
| `target_agent` | `str` | required | Name of the target agent |
| `description` | `str` | required | When this handoff should fire — read by the LLM |
| `condition` | `str \| Callable \| None` | `None` | Extra guard (rarely used) |
| `transfer_history` | `bool` | `True` | Pass conversation history to the target |
| `summarize_history` | `bool` | `True` | Run an LLM summarization before transferring |
| `summarization_mode` | `HistorySummarizationMode` | `HYBRID` | See table below |
| `recent_messages` | `int` | `5` | Used by `RECENT_N` and `HYBRID` |
| `return_to_parent` | `bool` | `True` | After the target finishes, control returns to the source |

---

## History summarization modes

| Mode | Behaviour |
|---|---|
| `FULL` | Pass the entire conversation verbatim — no summarization |
| `SUMMARY` | Replace history with a single LLM-generated summary |
| `RECENT_N` | Pass only the last `recent_messages` messages |
| `HYBRID` *(default)* | LLM summary + last `recent_messages` raw messages |

```python
from orchestrator.agent.types import HistorySummarizationMode

Handoff(
    target_agent="billing",
    description="...",
    summarization_mode=HistorySummarizationMode.RECENT_N,
    recent_messages=8,
)
```

---

## Tool prefix (internal detail)

The framework injects a hidden tool per handoff named
**`handoff_to_<target>`** (NOT `transfer_to_…`). The LLM calls this
tool to trigger the transition; you never write the tool yourself.

`agent.is_handoff_tool_call(tool_name) -> tuple[bool, str | None]`
detects whether a tool call is actually a handoff invocation.

---

## Multi-level handoffs (depth + cycles)

`HandoffConfig.max_handoff_depth` defaults to `10`. Beyond that:
`HandoffDepthExceededError(current_depth, max_depth, ...)`.

Cycles (e.g. `A → B → A`) are detected before the call — they raise
`HandoffCycleDetectedError(from_agent, to_agent, agent_stack, ...)`.
Import path: `orchestrator.agent.exceptions` (not re-exported at the
package root).

---

## Inspecting the result

```python
resp = await runner.run(triage, "...")
resp.agents_used      # ["triage", "billing"]
resp.handoff_chain    # ["triage", "billing"]
resp.handoff          # HandoffData of the most recent handoff (or None)
resp.handoff_result   # HandoffResult with success / error / returned_to_parent
```

Streaming exposes `EventType.HANDOFF_START` / `HANDOFF_END` /
`HANDOFF_RETURN` events with `event.data = {"from_agent": ..., "to_agent": ..., "reason": ...}`.

---

## Driving handoffs manually with `HandoffManager`

```python
from orchestrator.agent import HandoffManager

mgr = HandoffManager(llm_client=runner.llm_client, tracing_manager=None, max_depth=10)

mgr.validate_handoff(from_agent=triage, to_agent_name="billing")
data = await mgr.prepare_handoff(
    from_agent=triage, to_agent=billing,
    reason="Refund request",
    messages=current_messages,
    handoff_config=triage.get_handoff("billing"),
)
```

Most apps don't need this — let the runner drive it.

---

## RouterAgent vs Handoff

| Use case | Pick |
|---|---|
| LLM picks ONE specialist for the whole turn | `RouterAgent` (`create_router_agent`) |
| Source agent might decide *during the conversation* to transfer | `Handoff` |
| Source agent should regain control after the specialist finishes | `Handoff` with `return_to_parent=True` |
| Stateless mapping of intent → agent | `RouterAgent` |

A `RouterAgent` never looks back; a handoff chain can. Use whichever
matches the conversation shape.

---

## Don't

- Don't forget to register the target via `runner.register_agent(...)`
  or `agent_registry={...}` — `HandoffTargetNotFoundError`.
- Don't reference the handoff tool by `transfer_to_<target>` — actual
  prefix is `handoff_to_<target>`.
- Don't try to import `HandoffCycleDetectedError` from
  `orchestrator.agent` — only from `orchestrator.agent.exceptions`.
- Don't crank `max_handoff_depth` to skip cycle errors — fix the routing
  logic instead. Cycles indicate either bad descriptions or a missing
  fallback agent.
- Don't pass async hooks for `on_handoff` — hooks are sync.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…