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

state-machine session loaded without conversation

Error message

state-machine session loaded without conversation

What it means

anyhow error returned by StateMachine::step (crates/goose-agent/src/machine.rs) when session.conversation() yields None. The MachineSession trait exposes the conversation as Option<&Conversation>; a loaded session that carries no conversation cannot be stepped, so the machine refuses before running any operations.

Source

Thrown at crates/goose-agent/src/machine.rs:65

pub struct StateMachine<'a, S, E = ConversationEffect> {
    steps: Vec<Step<'a, S, E>>,
    cancel: CancellationToken,
}

impl<'a, S, E> StateMachine<'a, S, E>
where
    S: MachineSession,
    E: MachineEffect + Send + 'static,
{
    pub fn new(steps: Vec<Step<'a, S, E>>, cancel: CancellationToken) -> Self {
        Self { steps, cancel }
    }

    pub async fn step(&self, session: &S, emit: &Emitter) -> Result<Option<StepResult<E>>> {
        let conversation = session
            .conversation()
            .ok_or_else(|| anyhow!("state-machine session loaded without conversation"))?;

        for step in &self.steps {
            let name = step.operation().name();
            let result = if self.cancel.is_cancelled() {
                OperationResult::NotApplicable
            } else {
                let step_fut: OperationFuture<'_, Result<OperationResult<E>>> = match step {
                    Step::Operation(operation) => operation.run(session, conversation, emit),
                    Step::Inference(inference) => {
                        let mut input = InferenceInput::default();
                        for operation in self.steps.iter().map(|step| step.operation()) {
                            input
                                .tools
                                .extend(operation.inference_tools(session).await?);
                            input
                                .prompt_parts
                                .extend(operation.prompt_parts(session, conversation).await?);
                            input

View on GitHub (pinned to 3810898a74)

Solutions

  1. Start from a fresh session (drop --resume / the session id) so a conversation exists
  2. Inspect the session file for a conversation field; if a migration dropped it, re-run migration or discard the session
  3. If you implement MachineSession yourself, make conversation() return Some for any session you intend to step
  4. Report/fix the loader if valid sessions are being persisted without conversation data

Example fix

// before
impl MachineSession for MySession {
    fn conversation(&self) -> Option<&Conversation> { None }
}

// after
impl MachineSession for MySession {
    fn conversation(&self) -> Option<&Conversation> { self.conv.as_ref() } // always Some for steppable sessions
}
Defensive patterns

Strategy: type-guard

Validate before calling

if let Some(conv) = session.conversation() { /* safe to step */ } else { start_new_session().await?; }

Type guard

fn has_conversation<S: MachineSession>(s: &S) -> bool {
    s.conversation().is_some()
}

Try / catch

match machine.step(&session, &emit).await {
    Err(e) if e.to_string().contains("without conversation") => {
        // session record unusable: start fresh rather than retry
        start_new_session().await?
    }
    other => other?,
}

Prevention

When it happens

Trigger: Starting the state-machine agent path (GOOSE_STATE_MACHINE=1) against a session whose persisted data has no conversation section — an empty/newly created session record, a corrupted or truncated session file, or a session type that never stores conversations.

Common situations: Resuming a session file that was created but never received messages; hand-crafted or migrated session JSON missing the conversation key; tests constructing a bare MachineSession without attaching a Conversation.

Related errors


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