use axum::Json;
use axum::extract::{Query, State};
use axum::response::{IntoResponse, Response};
use crate::cm_api_contract::StatusShellView;
use serde::Deserialize;
use crate::agent::message_pipeline::MESSAGE_PIPELINE_COUNTERS;
use crate::chat_job_queue;
use crate::health;
use crate::tool_registry;
use crate::web::app_state_facets::{WebHealthAppFacet, WebStatusAppFacet};
pub(crate) async fn health_handler(State(facet): State<WebHealthAppFacet>) -> impl IntoResponse {
let eff = facet.effective_workspace_path().await;
let (work_dir, auth_mode, probe, probe_cache_secs, api_base) = {
let g = facet.http.cfg.read().await;
let wd = if eff.trim().is_empty() {
std::path::PathBuf::from(g.command_exec.run_command_working_dir.clone())
} else {
std::path::PathBuf::from(eff)
};
(
wd,
g.llm.llm_http_auth_mode,
g.web_api.health_llm_models_probe,
g.web_api.health_llm_models_probe_cache_secs,
g.llm.api_base.clone(),
)
};
let mut report = health::build_health_report(&work_dir, facet.mount_web_ui).await;
health::append_llm_models_endpoint_probe(
&mut report,
health::LlmModelsEndpointProbeParams {
enabled: probe,
cache_secs: probe_cache_secs,
cache_cell: facet.llm_models_health_cache.as_ref(),
client: &facet.http.client,
api_base: api_base.as_str(),
api_key: facet.http.api_key.as_ref(),
auth_mode,
},
)
.await;
Json(report)
}
#[derive(serde::Serialize)]
struct StatusResponse {
status: &'static str,
model: String,
api_base: String,
max_tokens: u32,
llm_context_tokens: u32,
temperature: f32,
llm_seed: Option<i64>,
tool_count: usize,
tool_names: Vec<String>,
tool_dispatch_registry: &'static [tool_registry::ToolDispatchMeta],
reflection_default_max_rounds: usize,
final_plan_requirement: crate::agent::per_coord::FinalPlanRequirementMode,
plan_rewrite_max_attempts: usize,
final_plan_require_strict_workflow_node_coverage: bool,
final_plan_semantic_check_enabled: bool,
final_plan_semantic_check_accept_legacy_text: bool,
final_plan_semantic_check_max_non_readonly_tools: usize,
final_plan_semantic_check_max_tokens: u32,
planner_executor_mode: &'static str,
orchestration_profile: &'static str,
effective_orchestration_path: String,
sync_default_tool_sandbox_mode: String,
sync_default_tool_sandbox_docker_image: String,
sync_default_tool_sandbox_docker_user_effective: String,
max_message_history: usize,
tool_message_max_chars: usize,
context_char_budget: usize,
effective_context_char_budget: usize,
tiktoken_prompt_counting_model: String,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
tiktoken_new_session_baseline_by_agent_role: std::collections::BTreeMap<String, u32>,
context_summary_trigger_chars: usize,
chat_queue_max_concurrent: usize,
chat_queue_max_pending: usize,
parallel_readonly_tools_max: usize,
read_file_turn_cache_max_entries: usize,
chat_queue_running: usize,
chat_queue_completed_ok: u64,
chat_queue_completed_cancelled: u64,
chat_queue_completed_err: u64,
chat_queue_recent_jobs: Vec<chat_job_queue::ChatJobRecord>,
#[serde(skip_serializing_if = "Vec::is_empty")]
per_active_jobs: Vec<chat_job_queue::PerFlightStatusEntry>,
workspace_allowed_roots_count: usize,
conversation_store_entries: usize,
conversation_store_sqlite_path_configured: bool,
conversation_store_sqlite_active: bool,
long_term_memory_enabled: bool,
long_term_memory_vector_backend: String,
long_term_memory_store_ready: bool,
long_term_memory_index_errors: u64,
project_profile_inject_enabled: bool,
project_profile_inject_max_chars: usize,
project_dependency_brief_inject_enabled: bool,
project_dependency_brief_inject_max_chars: usize,
tool_call_explain_enabled: bool,
tool_call_explain_min_chars: usize,
tool_call_explain_max_chars: usize,
message_pipeline_trim_count_hits: u64,
message_pipeline_trim_char_budget_hits: u64,
message_pipeline_tool_compress_hits: u64,
message_pipeline_orphan_tool_drops: u64,
llm_http_auth_mode: &'static str,
#[serde(skip_serializing_if = "Vec::is_empty")]
agent_role_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default_agent_role_id: Option<String>,
default_session_mode: String,
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
agent_role_default_session_modes: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct StatusQuery {
#[serde(default)]
view: Option<String>,
}
struct StatusShellBuildInput<'a> {
cfg: &'a crate::AgentConfig,
conversation_store_sqlite_path_configured: bool,
conversation_store_sqlite_active: bool,
agent_role_ids: &'a [String],
tiktoken_new_session_baseline_by_agent_role: std::collections::BTreeMap<String, u32>,
}
fn build_status_shell_view(input: StatusShellBuildInput<'_>) -> StatusShellView {
let StatusShellBuildInput {
cfg,
conversation_store_sqlite_path_configured,
conversation_store_sqlite_active,
agent_role_ids,
tiktoken_new_session_baseline_by_agent_role,
} = input;
StatusShellView {
status: StatusShellView::ok_prefix().to_string(),
model: cfg.llm.model.clone(),
api_base: cfg.llm.api_base.clone(),
agent_role_ids: agent_role_ids.to_vec(),
default_agent_role_id: cfg.roles_prompts.default_agent_role_id.clone(),
default_session_mode: cfg.roles_prompts.default_session_mode.as_str().to_string(),
agent_role_default_session_modes: {
let mut m = std::collections::BTreeMap::new();
for (id, spec) in cfg.roles_prompts.agent_roles.iter() {
if let Some(mode) = spec.default_session_mode {
m.insert(id.clone(), mode.as_str().to_string());
}
}
m
},
context_char_budget: cfg.context_pipeline.context_char_budget,
llm_context_tokens: cfg.llm_sampling.llm_context_tokens,
effective_context_char_budget: cfg.effective_context_char_budget_for_pipeline(),
tiktoken_prompt_counting_model:
crate::agent::tiktoken_prompt_tokens::tiktoken_model_id_for_config_model(
cfg.llm.model.as_str(),
),
tiktoken_new_session_baseline_by_agent_role,
executor_model: cfg.llm.executor_model.clone().unwrap_or_default(),
executor_api_base: String::new(),
planner_executor_mode: cfg
.per_plan_policy
.planner_executor_mode
.as_str()
.to_string(),
conversation_store_sqlite_path_configured,
conversation_store_sqlite_active,
}
}
fn tiktoken_new_session_baselines_by_role(
cfg: &crate::AgentConfig,
tool_recorder: &std::sync::Arc<crate::tool_stats::ToolOutcomeRecorder>,
workspace_root: &std::path::Path,
agent_role_ids: &[String],
) -> std::collections::BTreeMap<String, u32> {
let memory_snippet = if cfg.context_bootstrap_inject.agent_memory_file_enabled {
crate::memory::agent_memory::load_memory_snippet(
workspace_root,
cfg.context_bootstrap_inject.agent_memory_file.as_str(),
cfg.context_bootstrap_inject.agent_memory_file_max_chars,
)
} else {
None
};
let mut tiktoken_new_session_baseline_by_agent_role = std::collections::BTreeMap::new();
let push_baseline = |map: &mut std::collections::BTreeMap<String, u32>,
key: String,
role: Option<&str>| {
let system = match crate::context_bootstrap::conversation_turn_bootstrap::augmented_system_for_new_conversation(
cfg,
role,
tool_recorder,
) {
Ok(s) => s,
Err(_) => return,
};
let messages =
crate::context_bootstrap::conversation_turn_bootstrap::new_session_prompt_baseline_messages(
cfg,
&system,
workspace_root,
memory_snippet.clone(),
);
if let Some(snap) =
crate::agent::tiktoken_prompt_tokens::prompt_token_count_vendor_shaped_for_session(
cfg, &messages,
)
{
map.insert(key, snap.prompt_tokens);
}
};
push_baseline(
&mut tiktoken_new_session_baseline_by_agent_role,
String::new(),
None,
);
for id in agent_role_ids {
push_baseline(
&mut tiktoken_new_session_baseline_by_agent_role,
id.clone(),
Some(id.as_str()),
);
}
tiktoken_new_session_baseline_by_agent_role
}
pub(crate) async fn status_handler(
State(state): State<WebStatusAppFacet>,
Query(query): Query<StatusQuery>,
) -> Response {
let cfg = state.http.cfg.read().await;
let mp = MESSAGE_PIPELINE_COUNTERS.snapshot();
let conversation_store_entries = state.conversation_count().await;
let conversation_store_sqlite_path_configured = !cfg
.conversation_persistence
.conversation_store_sqlite_path
.trim()
.is_empty();
let conversation_store_sqlite_active = {
let b = state.conversation.conversation_backing.read().await;
b.is_sqlite()
};
let (ltm_ready, ltm_idx_err) = match state.long_term_memory.as_ref() {
Some(l) => (
true,
l.index_errors.load(std::sync::atomic::Ordering::Relaxed),
),
None => (false, 0u64),
};
let tool_names: Vec<String> = state
.http
.tools
.iter()
.map(|t| t.function.name.clone())
.collect();
let mut agent_role_ids: Vec<String> = cfg.roles_prompts.agent_roles.keys().cloned().collect();
agent_role_ids.sort();
let tool_recorder = &state.process_handles.tool_outcome_recorder;
let workspace_root = std::path::PathBuf::from(state.effective_workspace_path().await);
let tiktoken_new_session_baseline_by_agent_role = tiktoken_new_session_baselines_by_role(
&cfg,
tool_recorder,
workspace_root.as_path(),
&agent_role_ids,
);
let effective_orchestration_path = crate::cm_config::effective_orchestration_path_summary(
cfg.per_plan_policy.planner_executor_mode.as_str(),
cfg.per_plan_policy.orchestration_profile,
);
if query.view.as_deref() == Some("shell") {
return Json(build_status_shell_view(StatusShellBuildInput {
cfg: &cfg,
conversation_store_sqlite_path_configured,
conversation_store_sqlite_active,
agent_role_ids: &agent_role_ids,
tiktoken_new_session_baseline_by_agent_role,
}))
.into_response();
}
Json(StatusResponse {
status: "ok",
model: cfg.llm.model.clone(),
api_base: cfg.llm.api_base.clone(),
max_tokens: cfg.llm_sampling.max_tokens,
llm_context_tokens: cfg.llm_sampling.llm_context_tokens,
temperature: cfg.llm_sampling.temperature,
llm_seed: cfg.llm_sampling.llm_seed,
tool_count: tool_names.len(),
tool_names,
tool_dispatch_registry: tool_registry::all_dispatch_metadata(),
reflection_default_max_rounds: cfg.per_plan_policy.reflection_default_max_rounds,
final_plan_requirement: cfg.per_plan_policy.final_plan_requirement,
plan_rewrite_max_attempts: cfg.per_plan_policy.plan_rewrite_max_attempts,
final_plan_require_strict_workflow_node_coverage: cfg
.per_plan_policy
.final_plan_require_strict_workflow_node_coverage,
final_plan_semantic_check_enabled: cfg.per_plan_policy.final_plan_semantic_check_enabled,
final_plan_semantic_check_accept_legacy_text: cfg
.per_plan_policy
.final_plan_semantic_check_accept_legacy_text,
final_plan_semantic_check_max_non_readonly_tools: cfg
.per_plan_policy
.final_plan_semantic_check_max_non_readonly_tools,
final_plan_semantic_check_max_tokens: cfg
.per_plan_policy
.final_plan_semantic_check_max_tokens,
planner_executor_mode: cfg.per_plan_policy.planner_executor_mode.as_str(),
orchestration_profile: cfg.per_plan_policy.orchestration_profile.as_str(),
effective_orchestration_path,
sync_default_tool_sandbox_mode: cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_mode
.as_str()
.to_string(),
sync_default_tool_sandbox_docker_image: cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_image
.clone(),
sync_default_tool_sandbox_docker_user_effective: match cfg
.sync_tool_sandbox
.sync_default_tool_sandbox_docker_user
.as_docker_user_string()
{
Some(s) => s.to_string(),
None => "image_default".to_string(),
},
max_message_history: cfg.session_ui.max_message_history,
tool_message_max_chars: cfg.tool_transcript.tool_message_max_chars,
context_char_budget: cfg.context_pipeline.context_char_budget,
effective_context_char_budget: cfg.effective_context_char_budget_for_pipeline(),
tiktoken_prompt_counting_model:
crate::agent::tiktoken_prompt_tokens::tiktoken_model_id_for_config_model(
cfg.llm.model.as_str(),
),
tiktoken_new_session_baseline_by_agent_role,
context_summary_trigger_chars: cfg.context_pipeline.context_summary_trigger_chars,
chat_queue_max_concurrent: state.chat_queue.max_concurrent(),
chat_queue_max_pending: state.chat_queue.max_pending(),
parallel_readonly_tools_max: cfg.chat_queues_cache.parallel_readonly_tools_max,
read_file_turn_cache_max_entries: cfg.chat_queues_cache.read_file_turn_cache_max_entries,
chat_queue_running: state.chat_queue.running_count(),
chat_queue_completed_ok: state.chat_queue.completed_ok(),
chat_queue_completed_cancelled: state.chat_queue.completed_cancelled(),
chat_queue_completed_err: state.chat_queue.completed_err(),
chat_queue_recent_jobs: state.chat_queue.recent_jobs(),
per_active_jobs: state.chat_queue.active_per_jobs(),
workspace_allowed_roots_count: cfg.workspace_roots.workspace_allowed_roots.len(),
conversation_store_entries,
conversation_store_sqlite_path_configured,
conversation_store_sqlite_active,
long_term_memory_enabled: cfg.long_term_memory.long_term_memory_enabled,
long_term_memory_vector_backend: cfg
.long_term_memory
.long_term_memory_vector_backend
.as_str()
.to_string(),
long_term_memory_store_ready: ltm_ready,
long_term_memory_index_errors: ltm_idx_err,
project_profile_inject_enabled: cfg.context_bootstrap_inject.project_profile_inject_enabled,
project_profile_inject_max_chars: cfg
.context_bootstrap_inject
.project_profile_inject_max_chars,
project_dependency_brief_inject_enabled: cfg
.context_bootstrap_inject
.project_dependency_brief_inject_enabled,
project_dependency_brief_inject_max_chars: cfg
.context_bootstrap_inject
.project_dependency_brief_inject_max_chars,
tool_call_explain_enabled: cfg.tool_call_explain.tool_call_explain_enabled,
tool_call_explain_min_chars: cfg.tool_call_explain.tool_call_explain_min_chars,
tool_call_explain_max_chars: cfg.tool_call_explain.tool_call_explain_max_chars,
message_pipeline_trim_count_hits: mp.trim_count_hits,
message_pipeline_trim_char_budget_hits: mp.trim_char_budget_hits,
message_pipeline_tool_compress_hits: mp.tool_compress_hits,
message_pipeline_orphan_tool_drops: mp.orphan_tool_drops,
llm_http_auth_mode: cfg.llm.llm_http_auth_mode.as_str(),
agent_role_ids,
default_agent_role_id: cfg.roles_prompts.default_agent_role_id.clone(),
default_session_mode: cfg.roles_prompts.default_session_mode.as_str().to_string(),
agent_role_default_session_modes: {
let mut m = std::collections::BTreeMap::new();
for (id, spec) in cfg.roles_prompts.agent_roles.iter() {
if let Some(mode) = spec.default_session_mode {
m.insert(id.clone(), mode.as_str().to_string());
}
}
m
},
})
.into_response()
}