use anyhow::{Context, Result};
use clap::Parser;
use openhuman_core::openhuman::agent::Agent;
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::inference::provider::create_chat_provider;
use openhuman_core::openhuman::inference::provider::traits::{ChatMessage, ChatRequest};
use openhuman_core::openhuman::tools::traits::ToolSpec;
use serde_json::json;
#[derive(Parser, Debug)]
#[command(name = "inference-probe")]
struct Args {
#[arg(long, default_value = "harness")]
mode: String,
#[arg(long, default_value = "reasoning")]
role: String,
#[arg(long, default_value = "pformat")]
raw_mode: String,
#[arg(long, default_value = "hey list my top 5 emails")]
prompt: String,
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let args = Args::parse();
let config = Config::load_or_init()
.await
.context("loading user config (Config::load_or_init)")?;
eprintln!("[probe] config.default_model = {:?}", config.default_model);
eprintln!(
"[probe] config.agent.tool_dispatcher = {:?}",
config.agent.tool_dispatcher
);
match args.mode.as_str() {
"harness" => run_harness(&config, &args.prompt).await,
"raw" => run_raw(&config, &args.role, &args.raw_mode, &args.prompt).await,
other => anyhow::bail!("unknown --mode {other:?}; want 'harness' or 'raw'"),
}
}
async fn run_harness(config: &Config, prompt: &str) -> Result<()> {
eprintln!("[probe] mode = harness (orchestrator)");
eprintln!("[probe] prompt = {prompt:?}");
let mut agent = Agent::from_config(config).context("Agent::from_config failed")?;
eprintln!("[probe] fetching connected integrations...");
agent.fetch_connected_integrations().await;
let refreshed = agent.refresh_delegation_tools();
let conn_count = agent.connected_integrations().len();
eprintln!(
"[probe] connected_integrations = {} (delegation tools refreshed = {})",
conn_count, refreshed
);
eprintln!("[probe] visible tool count = {}", agent.tools().len());
eprintln!("[probe] model = {}", agent.model_name());
eprintln!("[probe] >>> agent.run_single() ...");
let started = std::time::Instant::now();
let response = agent
.run_single(prompt)
.await
.context("agent.run_single failed")?;
let elapsed = started.elapsed();
eprintln!("[probe] <<< returned in {elapsed:?}");
eprintln!();
println!("=== FINAL ASSISTANT REPLY ===");
println!("{response}");
println!("=== /FINAL ASSISTANT REPLY ===");
Ok(())
}
async fn run_raw(config: &Config, role: &str, raw_mode: &str, prompt: &str) -> Result<()> {
let (provider, model_name) =
create_chat_provider(role, config).context("create_chat_provider failed")?;
eprintln!("[probe] mode = raw");
eprintln!("[probe] role = {role}");
eprintln!("[probe] resolved model = {model_name}");
eprintln!(
"[probe] provider.supports_native_tools() = {}",
provider.supports_native_tools()
);
eprintln!("[probe] raw_mode = {raw_mode}");
let pformat_system = r#"You are a helpful assistant with access to tools.
## Tools
### get_weather
Get the current weather for a city.
Call as: get_weather[city|unit]
### list_files
List files in a directory.
Call as: list_files[path]
## Tool Use Protocol
Emit tool calls as `<tool_call>name[arg1|arg2]</tool_call>` blocks.
"#;
let native_system =
"You are a helpful assistant. Use the provided tools when the user asks something \
a tool can answer.";
let (system, tools_for_request): (&str, Option<Vec<ToolSpec>>) = match raw_mode {
"native" => (
native_system,
Some(vec![
ToolSpec {
name: "get_weather".into(),
description: "Get the current weather for a city.".into(),
parameters: json!({
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["metric", "imperial"]}
},
"required": ["city"]
}),
},
ToolSpec {
name: "list_files".into(),
description: "List files in a directory.".into(),
parameters: json!({
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}),
},
]),
),
"pformat" => (pformat_system, None),
other => anyhow::bail!("unknown --raw-mode {other:?}"),
};
let messages = vec![
ChatMessage::system(system),
ChatMessage::user(prompt.to_string()),
];
let request = ChatRequest {
messages: &messages,
tools: tools_for_request.as_deref(),
stream: None,
max_tokens: None,
};
eprintln!("[probe] >>> raw provider.chat()...");
let started = std::time::Instant::now();
let response = provider
.chat(request, &model_name, 0.4)
.await
.context("provider.chat() failed")?;
let elapsed = started.elapsed();
eprintln!("[probe] <<< {elapsed:?}");
println!();
println!("=== RAW RESPONSE TEXT ===");
println!("{}", response.text_or_empty());
println!("=== /RAW RESPONSE TEXT ===");
let text = response.text_or_empty();
let saw_pformat = text.contains("<tool_call>") || text.contains("<toolcall>");
let saw_native = !response.tool_calls.is_empty();
eprintln!(
"[probe] response.tool_calls.len() = {}",
response.tool_calls.len()
);
eprintln!(
"[probe] response contains <tool_call> tag = {}",
saw_pformat
);
if saw_native {
for tc in &response.tool_calls {
eprintln!(
"[probe] native tool_call name={} arg_bytes={}",
tc.name,
tc.arguments.len()
);
}
}
if !saw_pformat && !saw_native {
eprintln!("[probe] VERDICT: ZERO tool calls.");
}
Ok(())
}