use super::*;
impl AgentSession {
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);
}
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(())
}
}