aaif-goose/goose · error · anyhow::Error

cannot determine the role of an empty conversation

Error message

cannot determine the role of an empty conversation

What it means

anyhow error from last_effective_role (crates/goose-agent/src/operation.rs) when the messages slice it inspects is empty. The function decides the next actor by looking at the last message's effective role; with zero messages there is no basis for a decision.

Source

Thrown at crates/goose-agent/src/operation.rs:41

        .iter()
        .rposition(|message| {
            message.role == rmcp::model::Role::User
                && message.is_user_visible()
                && !message.is_tool_response()
        })
        .ok_or_else(|| anyhow!("state machine conversation has no kickoff message"))?;
    Ok(&messages[start..])
}

pub fn trailing_error(conversation: &Conversation) -> Option<MessageErrorKind> {
    conversation.last().and_then(Message::error_kind)
}

pub fn last_effective_role(messages: &[Message]) -> Result<EffectiveRole> {
    messages
        .last()
        .map(effective_role)
        .ok_or_else(|| anyhow!("cannot determine the role of an empty conversation"))
}

pub fn assistant_turn_count(messages: &[Message]) -> u32 {
    let mut turns = 0;
    let mut in_assistant_block = false;
    for message in messages.iter().rev() {
        if message.role == rmcp::model::Role::Assistant {
            if !in_assistant_block {
                turns += 1;
                in_assistant_block = true;
            }
        } else {
            in_assistant_block = false;
        }
    }
    turns
}

View on GitHub (pinned to 3810898a74)

Solutions

  1. Guard the call site: skip or handle the empty case before computing the effective role
  2. Ensure the conversation has at least one message (kickoff user message) before entering logic that calls last_effective_role
  3. When trimming, never trim to zero — retain the kickoff message
  4. In tests, seed the slice with a message or assert the error deliberately

Example fix

// before
let role = last_effective_role(&messages)?;

// after
let role = match messages.last() {
    Some(_) => last_effective_role(&messages)?,
    None => EffectiveRole::User, // empty conversation: next speaker is the user side
};
Defensive patterns

Strategy: type-guard

Validate before calling

if messages.is_empty() { /* choose default role or skip */ }

Type guard

fn non_empty_messages(messages: &[Message]) -> bool {
    !messages.is_empty()
}

Try / catch

let role = if messages.is_empty() {
    EffectiveRole::User // empty conversation defaults to user's turn
} else {
    last_effective_role(messages)?
};

Prevention

When it happens

Trigger: Calling last_effective_role on an empty slice — typically messages_since_kickoff-style slices that came back empty (e.g. right after cleanup, or a session whose messages were all filtered out), or a freshly built Conversation with nothing pushed yet.

Common situations: First iteration of a machine loop before any message is appended; tests passing empty vecs; context-management trimming everything; deserialization producing an empty message list.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/f2af6349fec2869e. Report an issue: GitHub.