> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Steerable Handles

> The universal protocol for mid-flight control of async agent operations

## The problem with flat agent loops

Most agent frameworks work like this: one LLM, one loop, one tool call at a time. The model picks a tool, calls it, reads the result, picks the next. If you want to interrupt — redirect the task, provide new information, pause for something urgent — you cancel and start over.

This is fine for short, predictable tasks. It breaks down when:

* The agent is mid-way through a 30-step research task and you realize you forgot to mention a constraint
* You need the agent to pause its current work, handle something urgent, then resume where it left off
* You want to ask "how's it going?" without disrupting the work in progress
* Multiple operations need to run concurrently with independent steering

In these cases, "cancel and restart" loses all accumulated context and work.

## Steerable handles

Every significant operation in Unity — whether it's searching contacts, updating knowledge, or executing a multi-step task — runs inside its own async LLM tool loop and returns a **steerable handle**. The handle is a live reference to the running operation with a uniform control surface:

```python theme={null}
handle = await actor.act("Research flights to Tokyo and draft an itinerary")

# Twenty seconds later, while it's still working:
await handle.interject("Also check train options from Tokyo to Osaka")

# Ask about progress without disrupting the work:
status_handle = await handle.ask("What have you found so far?")

# Pause for something urgent:
await handle.pause()
# ... deal with the urgent thing ...
await handle.resume()

# Or stop entirely:
await handle.stop("No longer needed")
```

## The protocol

Every handle implements `SteerableToolHandle` — the abstract protocol defined in `unity/common/async_tool_loop.py`:

| Method               | Purpose                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------ |
| `ask(question)`      | Query progress or results without modifying the task. Returns a new handle for the answer. |
| `interject(message)` | Inject new context, corrections, or requirements into the running task.                    |
| `pause()`            | Pause the task. In-flight operations finish, but no new actions start.                     |
| `resume()`           | Resume a paused task. Work that completed during the pause is processed first.             |
| `stop(reason)`       | Cancel the task and all its sub-operations.                                                |
| `done()`             | Check whether the task has completed.                                                      |
| `result()`           | Await the final output.                                                                    |

This is the **minimum universal contract**. Derived handles can extend it — for example, `ActiveTask.stop` adds a `cancel` parameter, and `ConversationManagerHandle.interject` adds `pinned` for priority messages.

## Nested composition

The key insight is that handles **compose through arbitrary depth**. When the Actor calls `primitives.contacts.ask(...)`, the ContactManager starts its own tool loop and returns its own handle — nested inside the Actor's handle, which is nested inside the ConversationManager's handle.

```
ConversationManager (SteerableToolHandle)
    └── Actor.act (SteerableToolHandle)
        ├── ContactManager.ask (SteerableToolHandle)
        ├── KnowledgeManager.update (SteerableToolHandle)
        └── TaskScheduler.execute (SteerableToolHandle)
```

Steering propagates correctly through the full depth:

* **User interjects on the ConversationManager** → the CM decides whether to forward to the Actor → the Actor decides whether to forward to the active manager
* **User pauses the Actor** → all active manager operations continue to their current step, then wait
* **User asks the Actor** → spawns an inspection loop that can query any active sub-handle

Each level makes its own decisions about how to handle steering events. The ConversationManager might translate a user's casual "also check hotels" into a structured interjection on the Actor's handle. The Actor might route that to the specific sub-operation it's relevant to.

## How it works under the hood

### Async tool loops

Each handle is backed by an `asyncio.Task` running an LLM tool loop (`start_async_tool_loop` in `unity/common/async_tool_loop.py`). The loop:

1. Builds a prompt with the current context (including any interjections received since the last turn)
2. Calls the LLM
3. Executes the tool calls from the response
4. Repeats until the LLM produces a final response (no more tool calls)

Interjections are injected between turns via an async queue. Pause/resume uses `asyncio.Event` flags. Stop triggers graceful cancellation of the underlying task.

### Context separation

Nested loops use **role rewriting** to prevent confusion. When an outer loop's context is injected into an inner loop (for `ask` or `interject`), the messages are tagged with distinct roles (`outer_user`, `outer_assistant`) so the inner LLM can distinguish between its own conversation and the parent's context.

### Lineage tracking

Each loop gets a position in a **lineage hierarchy** tracked via `contextvars`. This is used for:

* Structured logging (every log line shows its position in the nesting tree)
* Event attribution (the EventBus tags events with their loop lineage)
* Debugging (you can trace exactly which operation at which depth produced a given action)

The lineage label looks like: `ConversationManager->Actor(a3f2)->ContactManager(7b1c)`.

### Forward dispatching

When a parent loop needs to forward a steering call to a child handle whose concrete type it doesn't know, it uses `forward_handle_call`. This introspects the target method's actual signature, filters out kwargs the target doesn't accept, and applies positional fallbacks — no hand-written `try/except TypeError` cascades needed.

## Why this matters

### vs. OpenClaw

OpenClaw's steering is Gateway-mediated: you can inject a message into the current run's queue or abort the run. There's no nested composition — the Gateway manages one run at a time per session, and subagents are separate sessions, not nested steerable loops.

### vs. HermesAgent

HermesAgent's `interrupt()` is essentially a kill signal with optional message, propagated to child agents. There's no `pause/resume`, no `ask` for status, no mid-task interjection that modifies the operation in progress.

### What Unity enables

The steerable handle pattern means the assistant can hold a real-time voice conversation while executing background tasks, accept corrections mid-flight without losing progress, and manage multiple concurrent operations that the user can independently query, redirect, or cancel. The user doesn't wait in silence — they interact with a working system.

## Where to start reading

| File                                       | What's there                                                                    |
| ------------------------------------------ | ------------------------------------------------------------------------------- |
| `unity/common/async_tool_loop.py`          | `SteerableToolHandle` protocol and `AsyncToolLoopHandle` implementation         |
| `unity/common/_async_tool/loop.py`         | The async tool loop engine — turn execution, interjection handling, compression |
| `unity/common/_async_tool/messages.py`     | `forward_handle_call`, helper tool recognition, context role rewriting          |
| `unity/common/_async_tool/loop_config.py`  | Lineage tracking via contextvars                                                |
| `unity/common/_async_tool/multi_handle.py` | `MultiHandleCoordinator` for concurrent requests in one loop                    |
