use super::memory_scope::{is_low_signal_goal_text, scope_goal_memory_to_project_hints};
use super::types::OrchestrationCtx;
use crate::agent::response_phase::ResponsePhaseOutcome;
use crate::agent::*;
pub(super) async fn build_goal_feed_forward_context(
agent: &Agent,
session_id: &str,
goal_user_text: &str,
recent_messages: &[Value],
project_hints: &[String],
) -> Option<String> {
let low_signal_without_hints =
project_hints.is_empty() && is_low_signal_goal_text(goal_user_text);
let (raw_facts, raw_procedures) = if low_signal_without_hints {
info!(
session_id,
"Skipping goal memory retrieval: low-signal goal text without project hints"
);
(Vec::new(), Vec::new())
} else {
let memory_query = if project_hints.is_empty() {
goal_user_text.to_string()
} else {
format!(
"{goal_user_text}\n\nProject context: {}",
project_hints.join(" ")
)
};
(
agent
.state
.get_relevant_facts(&memory_query, 10)
.await
.unwrap_or_default(),
agent
.state
.get_relevant_procedures(&memory_query, 5)
.await
.unwrap_or_default(),
)
};
let (relevant_facts, relevant_procedures) =
scope_goal_memory_to_project_hints(raw_facts, raw_procedures, project_hints);
if !project_hints.is_empty() && (relevant_facts.is_empty() || relevant_procedures.is_empty()) {
info!(
session_id,
project_hints = ?project_hints,
facts = relevant_facts.len(),
procedures = relevant_procedures.len(),
"Scoped goal memory to project hints"
);
}
if relevant_facts.is_empty()
&& relevant_procedures.is_empty()
&& recent_messages.is_empty()
&& project_hints.is_empty()
{
return None;
}
let ctx = json!({
"relevant_facts": relevant_facts.iter().map(|f| {
json!({"category": f.category, "key": f.key, "value": f.value})
}).collect::<Vec<_>>(),
"relevant_procedures": relevant_procedures.iter().map(|p| {
json!({"name": p.name, "trigger": p.trigger_pattern, "steps": p.steps})
}).collect::<Vec<_>>(),
"recent_messages": recent_messages,
"project_hints": project_hints,
"task_results": [],
});
Some(serde_json::to_string(&ctx).unwrap_or_default())
}
pub(in crate::agent) async fn run_orchestration_phase(
services: &crate::agent::services::AgentServices<'_>,
ctx: &mut OrchestrationCtx<'_>,
) -> anyhow::Result<Option<ResponsePhaseOutcome>> {
let agent = services.agent;
if let Some(outcome) = super::routes::maybe_handle_generic_cancel_request(agent, ctx).await? {
record_orchestration_direct_return(agent, &outcome).await;
return Ok(Some(outcome));
}
let complexity = classify_intent_complexity(ctx.user_text);
let (route, tools_required) = orchestration_route_label(&complexity);
let (intent_summary, intent_metadata) = intent_decision_telemetry(
ctx.intent_gate.needs_tools.unwrap_or(false),
&complexity,
route,
ctx.user_text.chars().count(),
);
agent
.emit_decision_point(
ctx.emitter,
ctx.task_id,
ctx.iteration,
crate::events::DecisionType::IntentGate,
intent_summary,
intent_metadata,
)
.await;
if agent.harness_eval_enabled() {
agent
.with_harness_eval(|eval| eval.record_orchestration_route(route, tools_required))
.await;
}
let outcome = super::routes::route_orchestration_complexity(agent, ctx, complexity).await?;
record_orchestration_direct_return(agent, &outcome).await;
Ok(Some(outcome))
}
fn intent_decision_telemetry(
needs_tools: bool,
complexity: &IntentComplexity,
route: &str,
user_text_len: usize,
) -> (String, Value) {
let complexity_label = match complexity {
IntentComplexity::Simple => "simple",
IntentComplexity::Complex => "complex",
IntentComplexity::Scheduled { .. } => "scheduled",
IntentComplexity::ScheduledMissingTiming => "scheduled_missing_timing",
};
let summary = format!(
"Intent classified: needs_tools={needs_tools} complexity={complexity_label} route={route}"
);
let metadata = json!({
"condition": "intent_classification",
"needs_tools": needs_tools,
"complexity": complexity_label,
"route": route,
"user_text_len": user_text_len,
});
(summary, metadata)
}
fn orchestration_route_label(complexity: &IntentComplexity) -> (&'static str, bool) {
match complexity {
IntentComplexity::ScheduledMissingTiming => ("clarification_required", false),
IntentComplexity::Scheduled { .. } => ("direct_reply", false),
IntentComplexity::Simple => ("default_continue", false),
IntentComplexity::Complex => ("tools_required", true),
}
}
async fn record_orchestration_direct_return(agent: &Agent, outcome: &ResponsePhaseOutcome) {
if !agent.harness_eval_enabled() {
return;
}
if let ResponsePhaseOutcome::Return(result) = outcome {
agent
.with_harness_eval(|eval| eval.record_direct_return(true, result.is_ok()))
.await;
}
}
#[cfg(test)]
mod intent_telemetry_tests {
use super::*;
#[test]
fn intent_telemetry_maps_fields_and_summary() {
let (summary, meta) =
intent_decision_telemetry(true, &IntentComplexity::Complex, "tools_required", 42);
assert!(summary.contains("needs_tools=true"), "summary: {summary}");
assert!(summary.contains("complexity=complex"), "summary: {summary}");
assert!(
summary.contains("route=tools_required"),
"summary: {summary}"
);
assert_eq!(meta["condition"], "intent_classification");
assert_eq!(meta["needs_tools"], true);
assert_eq!(meta["complexity"], "complex");
assert_eq!(meta["route"], "tools_required");
assert_eq!(meta["user_text_len"], 42);
}
#[test]
fn intent_telemetry_labels_each_complexity_variant() {
let cases = [
(IntentComplexity::Simple, "simple"),
(IntentComplexity::Complex, "complex"),
(
IntentComplexity::ScheduledMissingTiming,
"scheduled_missing_timing",
),
(
IntentComplexity::Scheduled {
schedule_raw: "daily".into(),
is_one_shot: false,
},
"scheduled",
),
];
for (complexity, label) in cases {
let (_s, meta) = intent_decision_telemetry(false, &complexity, "default_continue", 0);
assert_eq!(meta["complexity"], label, "variant should map to {label}");
}
}
}