Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HITL and Mailbox

This page explains how Awaken handles human-in-the-loop (HITL) interactions through tool call suspension and the mailbox queue that manages run requests.

SuspendTicket

When a tool call needs external approval or input, it produces a SuspendTicket:

pub struct SuspendTicket {
    pub suspension: Suspension,
    pub pending: PendingToolCall,
    pub resume_mode: ToolCallResumeMode,
}

suspension – the external-facing payload describing what input is needed:

pub struct Suspension {
    pub id: String,             // Unique suspension ID
    pub action: String,         // Action identifier (e.g., "confirm", "approve")
    pub message: String,        // Human-readable prompt
    pub parameters: Value,      // Action-specific parameters
    pub response_schema: Option<Value>,  // JSON Schema for expected response
}

pending – the tool call projection emitted to the event stream so the frontend knows which call is waiting:

pub struct PendingToolCall {
    pub id: String,        // Tool call ID
    pub name: String,      // Tool name
    pub arguments: Value,  // Original arguments
}

resume_mode – how the agent loop should handle the decision when it arrives.

ToolCallResumeMode

pub enum ToolCallResumeMode {
    ReplayToolCall,          // Re-execute the original tool call
    UseDecisionAsToolResult, // Use the decision payload as the tool result directly
    PassDecisionToTool,      // Pass the decision payload into the tool as new arguments
}

ReplayToolCall is the default. The original tool call is re-executed after the decision arrives. Use this when the decision unlocks execution (e.g., permission granted, now run the tool).

UseDecisionAsToolResult skips re-execution entirely. The external decision payload becomes the tool result. Use this when a human provides the answer directly (e.g., “the correct value is X”).

PassDecisionToTool re-executes the tool but injects the decision payload into the arguments. Use this when the decision modifies how the tool should run (e.g., “use this alternative path instead”).

ResumeDecisionAction

pub enum ResumeDecisionAction {
    Resume,  // Proceed with the tool call
    Cancel,  // Cancel the tool call
}

Each decision carries an action. Resume continues execution according to the ToolCallResumeMode. Cancel transitions the tool call to ToolCallStatus::Cancelled.

ToolCallResume

The full resume payload:

pub struct ToolCallResume {
    pub decision_id: String,         // Idempotency key
    pub action: ResumeDecisionAction,
    pub result: Value,               // Decision payload
    pub reason: Option<String>,      // Human-readable reason
    pub updated_at: u64,             // Unix millis timestamp
}

Permission Plugin and Ask-Mode

The awaken-ext-permission plugin uses suspension to implement ask-mode approvals:

  1. A tool call matches a permission rule with behavior: ask.
  2. The permission checker creates a SuspendTicket with ToolCallResumeMode::ReplayToolCall.
  3. The suspension payload describes the tool and its arguments.
  4. The tool call transitions to ToolCallStatus::Suspended.
  5. The run transitions to RunStatus::Waiting.
  6. A frontend presents the approval prompt to the user.
  7. The user submits a ToolCallResume with ResumeDecisionAction::Resume or Cancel.
  8. On Resume, the tool call replays and executes normally.
  9. On Cancel, the tool call is cancelled and the run may continue with remaining calls.

Mailbox Architecture

The mailbox provides a persistent queue for all run requests. Every run – streaming, background, A2A, internal – enters the system as a MailboxJob.

MailboxJob

pub struct MailboxJob {
    // Identity
    pub job_id: String,          // UUID v7
    pub mailbox_id: String,      // Thread ID (routing anchor)

    // Request payload
    pub agent_id: String,
    pub messages: Vec<Message>,
    pub origin: MailboxJobOrigin,
    pub sender_id: Option<String>,
    pub parent_run_id: Option<String>,
    pub request_extras: Option<Value>,

    // Queue semantics
    pub priority: u8,            // 0 = highest, 255 = lowest, default 128
    pub dedupe_key: Option<String>,
    pub generation: u64,

    // Lifecycle
    pub status: MailboxJobStatus,
    pub available_at: u64,
    pub attempt_count: u32,
    pub max_attempts: u32,
    pub last_error: Option<String>,

    // Lease
    pub claim_token: Option<String>,
    pub claimed_by: Option<String>,
    pub lease_until: Option<u64>,

    // Timestamps
    pub created_at: u64,
    pub updated_at: u64,
}

MailboxJobStatus

Queued --claim--> Claimed --ack--> Accepted (terminal)
  |                  |
  |               nack(retry) --> Queued (attempt_count++, available_at = retry_at)
  |                  |
  |               nack(permanent) --> DeadLetter (terminal)
  |
  |-- cancel --> Cancelled (terminal)
  +-- interrupt(generation bump) --> Superseded (terminal)
pub enum MailboxJobStatus {
    Queued,      // Waiting to be claimed
    Claimed,     // Claimed by a consumer, executing
    Accepted,    // Successfully processed (terminal)
    Cancelled,   // Cancelled by caller (terminal)
    Superseded,  // Replaced by a newer generation (terminal)
    DeadLetter,  // Permanently failed (terminal)
}

MailboxJobOrigin

pub enum MailboxJobOrigin {
    User,      // HTTP API, SDK
    A2A,       // Agent-to-Agent protocol
    Internal,  // Child run notification, handoff
}

MailboxStore Trait

MailboxStore defines the persistent queue interface:

  • enqueue – persist a job, auto-assign generation, reject duplicate dedupe_key
  • claim – atomically claim up to N Queued jobs for a mailbox (lease-based)
  • claim_job – claim a specific job by ID (for inline streaming)
  • ack – mark a job as Accepted (validates claim token)
  • nack – return a job to Queued for retry, or DeadLetter if max attempts exceeded
  • cancel – cancel a Queued job
  • extend_lease – heartbeat to extend an active claim
  • interrupt – atomically bump generation, supersede stale Queued jobs, return the active Claimed job for cancellation

Implementations must guarantee: durable enqueue, atomic claim (exactly one winner), claim token validation on ack/nack, and atomic interrupt with generation bump.

MailboxInterrupt

When a new high-priority request arrives for a thread that already has queued or running work:

pub struct MailboxInterrupt {
    pub new_generation: u64,       // Generation after bump
    pub active_job: Option<MailboxJob>,  // Currently running job to cancel
    pub superseded_count: usize,   // Queued jobs superseded
}

The caller cancels the active_job’s runtime run if present, ensuring the new request takes priority.

See Also