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

state machine conversation has no kickoff message

Error message

state machine conversation has no kickoff message

What it means

anyhow error from messages_since_kickoff (crates/goose-agent/src/operation.rs) when the conversation contains no message with role User that is user-visible and not a tool response (searched from the end via rposition). The state machine defines the turn as everything since that kickoff user message, so a conversation without one has no definable turn.

Source

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

use rmcp::model::Tool;

pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

pub struct SlashCommand<'a> {
    pub command: &'a str,
    pub params_str: &'a str,
}

pub fn messages_since_kickoff(conversation: &Conversation) -> Result<&[Message]> {
    let messages = conversation.messages();
    let start = messages
        .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() {

View on GitHub (pinned to 3810898a74)

Solutions

  1. Resume from a session that began with a normal user message, or start a new session
  2. If context management trimmed the kickoff, keep at least one visible user message in the retained window
  3. When building conversations programmatically, insert the kickoff user message before stepping the machine
  4. If this reproduces on a normal interactive session, file it — the loader may be dropping user messages

Example fix

// before — stepping a conversation built only from tool responses
machine.step(&session, &emit).await?;

// after — ensure a visible, non-tool-response user message exists first
if messages_since_kickoff(conversation).is_err() {
    conversation.push(Message::user("kickoff"));
}
machine.step(&session, &emit).await?;
Defensive patterns

Strategy: validation

Validate before calling

let ok = conversation.messages().iter().any(|m| m.role == rmcp::model::Role::User && m.is_user_visible() && !m.is_tool_response());
if !ok { /* push kickoff or refuse to step */ }

Type guard

fn has_kickoff(conversation: &Conversation) -> bool {
    conversation.messages().iter().any(|m| {
        m.role == rmcp::model::Role::User && m.is_user_visible() && !m.is_tool_response()
    })
}

Try / catch

match messages_since_kickoff(conversation) {
    Ok(slice) => step_with(slice),
    Err(_) => { conversation.push(Message::user("kickoff")); retry_or_report() }
}

Prevention

When it happens

Trigger: Running a state-machine step on a conversation whose messages are only assistant messages and/or user-role tool responses (all tool responses or all filtered as hidden), or an effectively empty message list.

Common situations: Resuming a session that only recorded tool traffic; replaying a machine-generated conversation with no human kickoff; a truncation/context-management step removed the kickoff user message.

Related errors


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