mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
287 lines
10 KiB
Rust
287 lines
10 KiB
Rust
//! `openhuman subconscious` — CLI for testing and debugging the subconscious loop.
|
|
//!
|
|
//! Usage:
|
|
//! openhuman subconscious tick [--workspace <path>] [--mode simple|aggressive] [--verbose]
|
|
//! openhuman subconscious status [--workspace <path>]
|
|
//! openhuman subconscious scratchpad [--workspace <path>]
|
|
|
|
use anyhow::{anyhow, Result};
|
|
use std::path::PathBuf;
|
|
|
|
pub fn run_subconscious_command(args: &[String]) -> Result<()> {
|
|
if args.is_empty() || is_help(&args[0]) {
|
|
print_help();
|
|
return Ok(());
|
|
}
|
|
|
|
match args[0].as_str() {
|
|
"tick" => run_tick(&args[1..]),
|
|
"status" => run_status(&args[1..]),
|
|
"scratchpad" | "pad" => run_scratchpad(&args[1..]),
|
|
other => Err(anyhow!(
|
|
"unknown subconscious subcommand '{other}'. Run `openhuman subconscious --help`."
|
|
)),
|
|
}
|
|
}
|
|
|
|
// ── tick ────────────────────────────────────────────────────────────────────
|
|
|
|
struct TickFlags {
|
|
workspace: Option<PathBuf>,
|
|
mode: Option<String>,
|
|
verbose: bool,
|
|
}
|
|
|
|
fn parse_tick_flags(args: &[String]) -> Result<TickFlags> {
|
|
let mut workspace: Option<PathBuf> = None;
|
|
let mut mode: Option<String> = None;
|
|
let mut verbose = false;
|
|
let mut i = 0;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--workspace" | "-w" => {
|
|
workspace = Some(PathBuf::from(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow!("missing value for --workspace"))?,
|
|
));
|
|
i += 2;
|
|
}
|
|
"--mode" | "-m" => {
|
|
mode = Some(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow!("missing value for --mode"))?
|
|
.clone(),
|
|
);
|
|
i += 2;
|
|
}
|
|
"--verbose" | "-v" => {
|
|
verbose = true;
|
|
i += 1;
|
|
}
|
|
other => return Err(anyhow!("unknown flag '{other}'")),
|
|
}
|
|
}
|
|
Ok(TickFlags {
|
|
workspace,
|
|
mode,
|
|
verbose,
|
|
})
|
|
}
|
|
|
|
fn run_tick(args: &[String]) -> Result<()> {
|
|
let flags = parse_tick_flags(args)?;
|
|
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
rt.block_on(async {
|
|
let mut config = crate::openhuman::config::Config::load_or_init()
|
|
.await
|
|
.map_err(|e| anyhow!("config load failed: {e}"))?;
|
|
|
|
if let Some(ws) = &flags.workspace {
|
|
config.workspace_dir = ws.clone();
|
|
}
|
|
|
|
if let Some(mode_str) = &flags.mode {
|
|
config.heartbeat.subconscious_mode = match mode_str.as_str() {
|
|
"simple" => crate::openhuman::config::schema::SubconsciousMode::Simple,
|
|
"aggressive" => crate::openhuman::config::schema::SubconsciousMode::Aggressive,
|
|
other => {
|
|
return Err(anyhow!(
|
|
"unknown mode '{other}', expected simple|aggressive"
|
|
))
|
|
}
|
|
};
|
|
config.heartbeat.enabled = true;
|
|
config.heartbeat.inference_enabled = true;
|
|
}
|
|
|
|
// Ensure subconscious is enabled
|
|
if !config.heartbeat.enabled || !config.heartbeat.inference_enabled {
|
|
config.heartbeat.enabled = true;
|
|
config.heartbeat.inference_enabled = true;
|
|
if !config.heartbeat.subconscious_mode.is_enabled() {
|
|
config.heartbeat.subconscious_mode =
|
|
crate::openhuman::config::schema::SubconsciousMode::Simple;
|
|
}
|
|
}
|
|
|
|
let mode = config.heartbeat.effective_subconscious_mode();
|
|
eprintln!(
|
|
"[subconscious] mode={} workspace={}",
|
|
mode.as_str(),
|
|
config.workspace_dir.display()
|
|
);
|
|
|
|
// Init memory client
|
|
let _ = crate::openhuman::memory::global::init(config.workspace_dir.clone());
|
|
|
|
// Init scheduler gate so is_signed_out() works
|
|
crate::openhuman::scheduler_gate::init_global(&config);
|
|
|
|
// Seed signed_out from session token
|
|
match crate::api::jwt::get_session_token(&config) {
|
|
Ok(Some(_)) => {
|
|
crate::openhuman::scheduler_gate::set_signed_out(false);
|
|
eprintln!("[subconscious] session token found — provider available");
|
|
}
|
|
Ok(None) => {
|
|
eprintln!("[subconscious] WARNING: no session token — cloud provider will fail");
|
|
eprintln!(" hint: run `openhuman call auth store_session --token <JWT>` first");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("[subconscious] WARNING: session token read failed: {e}");
|
|
}
|
|
}
|
|
|
|
// Check provider availability
|
|
if let Some(reason) =
|
|
crate::openhuman::subconscious::engine::subconscious_provider_unavailable_reason(
|
|
&config,
|
|
)
|
|
{
|
|
eprintln!("[subconscious] provider unavailable: {reason}");
|
|
return Err(anyhow!("provider unavailable: {reason}"));
|
|
}
|
|
|
|
// Create engine and run tick
|
|
let memory = crate::openhuman::memory::global::client_if_ready();
|
|
let engine = crate::openhuman::subconscious::SubconsciousEngine::new(&config, memory);
|
|
|
|
eprintln!("[subconscious] running tick...");
|
|
let result = engine
|
|
.tick()
|
|
.await
|
|
.map_err(|e| anyhow!("tick failed: {e}"))?;
|
|
|
|
eprintln!(
|
|
"[subconscious] tick complete: duration={}ms response_chars={}",
|
|
result.duration_ms, result.response_chars,
|
|
);
|
|
|
|
if flags.verbose {
|
|
// Print scratchpad state after tick
|
|
let entries = crate::openhuman::subconscious::scratchpad::load(&config.workspace_dir)
|
|
.unwrap_or_default();
|
|
if !entries.is_empty() {
|
|
eprintln!("\n[subconscious] scratchpad after tick:");
|
|
println!("{}", serde_json::to_string_pretty(&entries)?);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
// ── status ─────────────────────────────────────────────────────────────────
|
|
|
|
fn run_status(args: &[String]) -> Result<()> {
|
|
let workspace = parse_workspace_flag(args)?;
|
|
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
rt.block_on(async {
|
|
let mut config = crate::openhuman::config::Config::load_or_init()
|
|
.await
|
|
.map_err(|e| anyhow!("config load failed: {e}"))?;
|
|
if let Some(ws) = workspace {
|
|
config.workspace_dir = ws;
|
|
}
|
|
|
|
let mode = config.heartbeat.effective_subconscious_mode();
|
|
let provider_reason = if mode.is_enabled() {
|
|
crate::openhuman::subconscious::engine::subconscious_provider_unavailable_reason(
|
|
&config,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let last_tick = crate::openhuman::subconscious::store::with_connection(
|
|
&config.workspace_dir,
|
|
crate::openhuman::subconscious::store::get_last_tick_at,
|
|
)
|
|
.ok();
|
|
|
|
let status = serde_json::json!({
|
|
"mode": mode.as_str(),
|
|
"enabled": mode.is_enabled(),
|
|
"provider_available": provider_reason.is_none(),
|
|
"provider_unavailable_reason": provider_reason,
|
|
"last_tick_at": last_tick.filter(|v| *v > 0.0),
|
|
"interval_minutes": mode.default_interval_minutes().max(5),
|
|
});
|
|
|
|
println!("{}", serde_json::to_string_pretty(&status)?);
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
// ── scratchpad ─────────────────────────────────────────────────────────────
|
|
|
|
fn run_scratchpad(args: &[String]) -> Result<()> {
|
|
let workspace = parse_workspace_flag(args)?;
|
|
|
|
let rt = tokio::runtime::Runtime::new()?;
|
|
rt.block_on(async {
|
|
let mut config = crate::openhuman::config::Config::load_or_init()
|
|
.await
|
|
.map_err(|e| anyhow!("config load failed: {e}"))?;
|
|
if let Some(ws) = workspace {
|
|
config.workspace_dir = ws;
|
|
}
|
|
|
|
let entries = crate::openhuman::subconscious::scratchpad::load(&config.workspace_dir)
|
|
.map_err(|e| anyhow!("failed to read scratchpad: {e}"))?;
|
|
|
|
if entries.is_empty() {
|
|
eprintln!("(scratchpad empty)");
|
|
} else {
|
|
println!("{}", serde_json::to_string_pretty(&entries)?);
|
|
}
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
fn parse_workspace_flag(args: &[String]) -> Result<Option<PathBuf>> {
|
|
let mut workspace: Option<PathBuf> = None;
|
|
let mut i = 0;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--workspace" | "-w" => {
|
|
workspace = Some(PathBuf::from(
|
|
args.get(i + 1)
|
|
.ok_or_else(|| anyhow!("missing --workspace"))?,
|
|
));
|
|
i += 2;
|
|
}
|
|
other => return Err(anyhow!("unknown flag '{other}'")),
|
|
}
|
|
}
|
|
Ok(workspace)
|
|
}
|
|
|
|
fn is_help(s: &str) -> bool {
|
|
matches!(s, "--help" | "-h" | "help")
|
|
}
|
|
|
|
fn print_help() {
|
|
eprintln!(
|
|
"Usage: openhuman subconscious <command> [options]
|
|
|
|
Commands:
|
|
tick Run a single subconscious tick (synchronous, waits for completion)
|
|
status Show current subconscious engine status
|
|
scratchpad Dump the persistent scratchpad
|
|
|
|
Tick options:
|
|
--mode <simple|aggressive> Override the subconscious mode
|
|
--workspace <path> Override workspace directory
|
|
--verbose, -v Print scratchpad after tick
|
|
|
|
Common options:
|
|
--workspace <path> Override workspace directory
|
|
"
|
|
);
|
|
}
|