magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

impl AgentSession {
    /// Adds one user-role hint after the typed prompt. Disabled or skipped runs leave the
    /// request unchanged; Jev failures only drop the hint.
    pub(super) fn apply_skill_suggestion(
        &self,
        state: &mut PrintTurnState<'_>,
        run: &mut AgentRunRequest<'_, '_>,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<()> {
        let Some(context) = self.skill_suggestion.as_ref() else {
            return Ok(());
        };
        if run.prompt_origin != crate::output::UserPromptOrigin::User
            || run.invocation_mode == InvocationMode::Subagent
            || context.prompt_names_skill(run.prompt)
        {
            return Ok(());
        }
        cancellation.check()?;
        let result = context.suggest(run.prompt);
        cancellation.check()?;
        let suggestion = match result {
            Ok(Some(suggestion)) => suggestion,
            Ok(None) => return Ok(()),
            Err(error) => {
                if self.show_jev_debug_messages
                    && let Some(sink) = run.output_sink.as_deref_mut()
                {
                    sink.output_event(OutputEvent::Diagnostic {
                        level: "warning".into(),
                        message: format!("Jev skill suggestion skipped: {error}"),
                    })?;
                }
                return Ok(());
            }
        };
        let hint = suggestion.hint();
        state.session_persistence.try_record(
            SessionEventKind::SkillSuggestion,
            json!({
                "schema_version": 1,
                "skill": suggestion.skill,
                "gate": suggestion.gate,
                "fit": suggestion.fit,
                "hint": hint,
            }),
            &mut run.output_sink,
        )?;
        state.turn_state.append_skill_suggestion_hint(hint);
        if self.show_jev_debug_messages
            && let Some(sink) = run.output_sink.as_deref_mut()
        {
            sink.output_event(OutputEvent::Diagnostic {
                level: "info".into(),
                message: format!("Jev suggested skill: {}", suggestion.skill),
            })?;
        }
        Ok(())
    }

    pub(super) fn verify_or_continue_terminal_completion(
        &self,
        state: &mut PrintTurnState<'_>,
        run: &mut AgentRunRequest<'_, '_>,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<bool> {
        if !self.completion_verification.enabled {
            return Ok(false);
        }
        cancellation.check()?;
        let proposed_response = state.turn_state.assistant_segment().to_string();
        let attempt = state.turn_state.completion_verification_attempts();
        let verification = verify_completion(
            &self.completion_verification,
            run.prompt,
            &proposed_response,
            &state.base_conversation,
            state.turn_state.request_items_slice(),
            attempt,
        );
        if let Err(error) = cancellation.check() {
            self.record_cancelled_terminal_status(state, run)?;
            return Err(error);
        }
        let decision = verification.decision;
        let action = match &decision {
            CompletionVerificationDecision::Accept => "accept",
            CompletionVerificationDecision::Continue { .. } => "continue",
            CompletionVerificationDecision::FinishWithLimitation { .. } => "finish_with_limitation",
        };
        state.session_persistence.try_record(
            SessionEventKind::Diagnostic,
            json!({"kind": "completion_verification", "attempt": attempt, "action": action, "assessment": verification.diagnostic}),
            &mut run.output_sink,
        )?;
        if let Some(sink) = run.output_sink.as_deref_mut() {
            let diagnostic = match &decision {
                CompletionVerificationDecision::Accept if verification.status != "passed" => {
                    Some((
                        "warning",
                        format!(
                            "Completion check {}. Response accepted without verified completion.",
                            verification.status
                        ),
                    ))
                }
                CompletionVerificationDecision::Accept if self.show_jev_debug_messages => {
                    Some(("info", "Completion check passed.".to_string()))
                }
                CompletionVerificationDecision::Accept => None,
                CompletionVerificationDecision::Continue { .. } => Some((
                    "warning",
                    "Some work still needs attention. Continuing…".to_string(),
                )),
                CompletionVerificationDecision::FinishWithLimitation { message, .. } => {
                    Some(("warning", message.clone()))
                }
            };
            if let Some((level, message)) = diagnostic {
                sink.output_event(OutputEvent::Diagnostic {
                    level: level.into(),
                    message,
                })?;
            }
        }
        match decision {
            CompletionVerificationDecision::Accept => Ok(false),
            CompletionVerificationDecision::Continue { prompt } => {
                state.turn_state.finish_text_action_for_continuation();
                state.turn_state.append_auto_continue(&prompt);
                state
                    .turn_state
                    .increment_completion_verification_attempts();
                Ok(true)
            }
            CompletionVerificationDecision::FinishWithLimitation { message, .. } => {
                let suffix = format!("\n\n{message}");
                state.turn_state.push_assistant_delta(&suffix);
                state.output.text.push_str(&suffix);
                Ok(false)
            }
        }
    }

    pub(super) fn complete_assistant_turn_without_tools(
        &self,
        state: &mut PrintTurnState<'_>,
        run: &mut AgentRunRequest<'_, '_>,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<()> {
        let assistant_text = state.turn_state.assistant_segment().to_string();
        if let Err(error) = cancellation.check() {
            self.record_cancelled_terminal_status(state, run)?;
            return Err(error);
        }
        let assistant_complete_error = if let Some(sink) = run.output_sink.as_deref_mut() {
            sink.output_event(OutputEvent::AssistantComplete {
                text: assistant_text.clone(),
            })
            .err()
        } else {
            None
        };
        if let Err(error) = state.assistant_chunk_batch.flush() {
            state
                .session_persistence
                .warn_once(&mut run.output_sink, &error)?;
        }
        if !assistant_text.is_empty() && assistant_text.trim().is_empty() {
            state.output.text.clear();
        } else {
            state.session_persistence.try_record(
                SessionEventKind::AssistantOutput,
                json!({"text": assistant_text, "usage": state.output.usage}),
                &mut run.output_sink,
            )?;
        }
        if let Some(error) = assistant_complete_error {
            return Err(error);
        }
        if !assistant_text.is_empty() {
            let assistant_message_id = format!("assistant-turn-{}", state.turn_state.iteration());
            let hook_outcome = run_message_phase_hooks(
                run.hooks,
                HookPhase::AfterAssistant,
                &assistant_message_id,
                &assistant_text,
                &state.hook_context.for_tool_call(
                    format!("turn-{}", state.turn_state.iteration()),
                    &assistant_message_id,
                ),
                cancellation,
                &mut state.session_persistence,
                &mut run.output_sink,
            )?;
            for item in &hook_outcome.context_items {
                record_provider_context_item(
                    &mut state.session_persistence,
                    &mut run.output_sink,
                    item,
                )?;
            }
            state
                .turn_state
                .append_provider_context_items(hook_outcome.context_items);
            if let Some(diagnostic) = hook_outcome.failure {
                return Err(HookPolicyError::new(diagnostic).into());
            }
            if let Err(error) = cancellation.check() {
                self.record_cancelled_terminal_status(state, run)?;
                return Err(error);
            }
        }
        Ok(())
    }

    pub(super) fn prepare_tool_turn_assistant_complete(
        &self,
        state: &mut PrintTurnState<'_>,
    ) -> Option<String> {
        if state.turn_state.assistant_segment().trim().is_empty() {
            return None;
        }
        state.turn_state.prepare_tool_turn();
        Some(state.turn_state.assistant_segment().to_string())
    }

    pub(super) fn run_tool_turn_and_maybe_emit_assistant_complete<'run, 'sink>(
        &self,
        tool_calls: Vec<ToolCall>,
        context_window_usage: crate::output::ContextWindowUsage,
        state: &mut PrintTurnState<'run>,
        run: &mut AgentRunRequest<'run, 'sink>,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<()> {
        let manual_compaction_request = (tool_calls.len() == 1
            && tool_calls[0].name == crate::tools::contract::tool_name::MAGI_CONTROL
            && tool_calls[0]
                .arguments
                .get("action")
                .and_then(serde_json::Value::as_str)
                == Some("compact"))
        .then(|| ManualCompactionRequest {
            context: tool_calls[0]
                .arguments
                .get("context")
                .and_then(serde_json::Value::as_str)
                .map(str::trim)
                .filter(|context| !context.is_empty())
                .map(str::to_string),
        });
        let tool_turn_assistant_complete = self.prepare_tool_turn_assistant_complete(state);
        let assistant_complete_error = if let Some(text) = tool_turn_assistant_complete
            && let Some(sink) = run.output_sink.as_deref_mut()
        {
            sink.output_event(OutputEvent::AssistantComplete { text })
                .err()
        } else {
            None
        };
        if let Err(error) = state.assistant_chunk_batch.flush() {
            state
                .session_persistence
                .warn_once(&mut run.output_sink, &error)?;
        }
        if let Some(error) = assistant_complete_error {
            return Err(error);
        }
        // Steering can persist a new user turn through several continuation paths.
        // Refresh only between tool batches: the previous batch has finished, and
        // queued but unpersisted steering must not claim this batch's edits.
        self.update_tool_checkpoint_context(run)?;
        let request_context = crate::typesafe::evidence::conversation_evidence(
            state.base_conversation.iter().chain(state.turn_state.request_items_slice()).filter(|item| matches!(item,
                ProviderConversationItem::Message(message) if message.role == crate::providers::MessageRole::User
            )),
            4096,
        );
        run_tool_lifecycle(ToolLifecycleRun {
            tool_calls,
            tools: run.tools,
            hooks: run.hooks,
            herdr_reporter: state.herdr_reporter.as_ref(),
            output_sink: &mut run.output_sink,
            cancellation,
            turn_state: &mut state.turn_state,
            output: &mut state.output,
            session_persistence: &mut state.session_persistence,
            subdir_instruction_state: Some(&mut state.subdir_instruction_state),
            hook_context: state.hook_context.clone(),
            context_window_usage: Some(context_window_usage),
            request_context,
        })?;
        if manual_compaction_request.is_some()
            && state
                .output
                .tool_results
                .last()
                .is_some_and(|result| result.success)
        {
            state.output.manual_compaction_request = manual_compaction_request;
        }
        state.turn_state.finish_tool_iteration();
        Ok(())
    }
}