feat(subconscious,triage): route background workloads through the subconscious provider (#4082)

This commit is contained in:
sanil-23
2026-06-24 19:29:51 -07:00
committed by GitHub
parent 0e8caf03fd
commit 43914f225d
7 changed files with 433 additions and 69 deletions
@@ -469,13 +469,7 @@ impl Agent {
// chat to the `reasoning` workload regardless of their
// `chat_provider` selection. Subagents still set their own role
// through `ModelSpec::Hint(...)` in the subagent runner.
let provider_role = match config.default_model.as_deref().map(str::trim) {
Some("hint:agentic") => "agentic",
Some("hint:coding") => "coding",
Some("hint:summarization") => "summarization",
Some("hint:reasoning") => "reasoning",
_ => "chat",
};
let provider_role = provider_role_for(agent_id, config.default_model.as_deref());
let (provider, mut model_name): (Box<dyn Provider>, String) =
crate::openhuman::inference::provider::create_chat_provider(provider_role, config)?;
log::info!(
@@ -491,13 +485,29 @@ impl Agent {
let target_is_lead = target_def
.map(|def| !def.subagents.is_empty())
.unwrap_or(true);
if let Some(pinned_model) = config.configured_agent_model(target_agent_id, target_is_lead) {
// The `subconscious` workload's model is governed by `subconscious_provider`
// + the managed-tier registry — NOT by an interactive agent model pin.
// The tick reuses the orchestrator definition (agent_id="orchestrator"),
// so without this guard a configured `[orchestrator].model` pin would
// clobber the resolved subconscious model and send an unrelated tier to
// the Subconscious provider (Codex P2).
if provider_role != "subconscious" {
if let Some(pinned_model) =
config.configured_agent_model(target_agent_id, target_is_lead)
{
log::debug!(
"[session-builder] agent_id={} using config-level model pin model={}",
target_agent_id,
pinned_model
);
model_name = pinned_model.to_string();
}
} else {
log::debug!(
"[session-builder] agent_id={} using config-level model pin model={}",
target_agent_id,
pinned_model
"[session-builder] agent_id={} provider_role=subconscious — skipping agent model \
pin so the subconscious provider/registry model is preserved",
target_agent_id
);
model_name = pinned_model.to_string();
}
// Resolve the user-configured vision flag for the (now-final) model while
@@ -1212,3 +1222,76 @@ fn definition_disallows_tool(disallowed: &[String], name: &str) -> bool {
}
})
}
/// Resolve the provider/workload role for a session build.
///
/// The `subconscious` workload has two entry points and both must route here:
/// - the cloud tick builds via `Agent::from_config` (agent_id `"orchestrator"`)
/// with `default_model = "hint:subconscious"`;
/// - the event-driven long-lived session builds via
/// `Agent::from_config_for_agent(_, "subconscious")` and does NOT set the hint.
///
/// Routing on `agent_id == "subconscious"` covers the second case (Codex P2:
/// otherwise promoted background turns fall through to `chat_provider` and ignore
/// Settings → AI "Subconscious"). Other explicit `hint:<role>` markers route to
/// their workload; everything else (incl. the legacy `default_model` tier the
/// bootstrap pinned) falls through to `chat` so `chat_provider` drives the
/// user-facing turn.
pub(crate) fn provider_role_for(agent_id: &str, default_model: Option<&str>) -> &'static str {
if agent_id.trim() == "subconscious" {
return "subconscious";
}
match default_model.map(str::trim) {
Some("hint:agentic") => "agentic",
Some("hint:coding") => "coding",
Some("hint:summarization") => "summarization",
Some("hint:reasoning") => "reasoning",
Some("hint:subconscious") => "subconscious",
_ => "chat",
}
}
#[cfg(test)]
mod provider_role_tests {
use super::provider_role_for;
#[test]
fn orchestrator_defaults_to_chat() {
assert_eq!(provider_role_for("orchestrator", Some("chat-v1")), "chat");
assert_eq!(provider_role_for("orchestrator", None), "chat");
// A legacy heavy default_model tier still falls through to chat.
assert_eq!(
provider_role_for("orchestrator", Some("reasoning-v1")),
"chat"
);
}
#[test]
fn explicit_hints_route_to_workload() {
assert_eq!(
provider_role_for("orchestrator", Some("hint:agentic")),
"agentic"
);
assert_eq!(
provider_role_for("orchestrator", Some("hint:reasoning")),
"reasoning"
);
// The cloud tick: orchestrator agent_id + the subconscious hint.
assert_eq!(
provider_role_for("orchestrator", Some("hint:subconscious")),
"subconscious"
);
}
#[test]
fn subconscious_agent_id_routes_to_subconscious_without_hint() {
// The event-driven long-lived session builds with agent_id="subconscious"
// and no hint — it must still resolve the subconscious workload (Codex P2).
assert_eq!(provider_role_for("subconscious", None), "subconscious");
assert_eq!(
provider_role_for("subconscious", Some("chat-v1")),
"subconscious"
);
assert_eq!(provider_role_for(" subconscious ", None), "subconscious");
}
}
+101 -39
View File
@@ -14,9 +14,7 @@ use std::sync::Arc;
use anyhow::Context;
use crate::openhuman::config::Config;
use crate::openhuman::inference::provider::{
self, Provider, ProviderRuntimeOptions, INFERENCE_BACKEND_ID,
};
use crate::openhuman::inference::provider::{self, Provider, INFERENCE_BACKEND_ID};
/// The concrete provider + metadata that [`crate::openhuman::agent::triage::evaluator::run_triage`]
/// should use for this particular triage turn.
@@ -135,46 +133,110 @@ pub fn build_local_provider_with_config(config: &Config) -> Option<ResolvedProvi
})
}
/// Whether a resolved provider string targets a local CLI delegate
/// (`claude_agent_sdk` / `claude-code:<model>`). These carry their own auth and
/// spawn a local process, so — like the local HTTP runtimes — they must never be
/// the provider for a triage turn (#1257). Kept separate from
/// `is_local_provider_string`, which only classifies the local HTTP runtimes.
fn is_local_cli_route(provider_string: &str) -> bool {
use crate::openhuman::inference::provider::claude_code;
use crate::openhuman::inference::provider::factory::{
CLAUDE_AGENT_SDK_PREFIX, CLAUDE_AGENT_SDK_PROVIDER,
};
let s = provider_string.trim();
s == CLAUDE_AGENT_SDK_PROVIDER
|| s.starts_with(CLAUDE_AGENT_SDK_PREFIX)
|| s.starts_with(claude_code::PROVIDER_PREFIX)
}
// ── Provider builder ────────────────────────────────────────────────────
/// Build the default remote routed backend provider. Same wiring as
/// `inference::local::ops::agent_chat_simple` uses so we stay consistent with
/// the existing direct-chat path.
/// Build the remote provider for a triage turn, routed through the
/// **`subconscious`** background workload so the Settings → AI → Advanced
/// "Subconscious" provider control governs triage classification.
///
/// The managed model id comes from `make_openhuman_backend` →
/// [`managed_tier_for_role`]`("subconscious")` (i.e. `chat-v1`), the same
/// registry the subconscious tick and the agent harness use — NOT from
/// `default_model`. So triage stays consistent with the tick: one place pins
/// the managed model.
///
/// #1257 invariant — *triage never goes local*: when the resolved
/// `subconscious_provider` is a local runtime (Ollama/LM Studio/MLX/…) or a
/// BYOK-incomplete sentinel, we force the managed backend so a trigger never
/// errors because a local model is down. Only a concrete BYOK **cloud** route is
/// honoured as-is. A build failure also falls back to the managed backend.
fn build_remote_provider(config: &Config) -> anyhow::Result<ResolvedProvider> {
let default_model = config
.default_model
.clone()
.unwrap_or_else(|| crate::openhuman::config::DEFAULT_MODEL.to_string());
let options = ProviderRuntimeOptions {
auth_profile_override: None,
openhuman_dir: config.config_path.parent().map(std::path::PathBuf::from),
secrets_encrypt: config.secrets.encrypt,
reasoning_enabled: config.runtime.reasoning_enabled,
use crate::openhuman::inference::local::profile::is_local_provider_string;
use crate::openhuman::inference::provider::factory::{
create_chat_provider_from_string, PROVIDER_OPENHUMAN,
};
let provider_box = provider::create_routed_provider_with_options(
config.inference_url.as_deref(),
config.api_url.as_deref(),
config.api_key.as_deref(),
&config.reliability,
&config.model_routes,
default_model.as_str(),
&options,
)
.context("building routed remote provider for triage")?;
// `Box<dyn Provider>` → `Arc<dyn Provider>` is a single reallocation
// — the `Provider` trait is `Send + Sync` so this is type-safe.
let provider: Arc<dyn Provider> = Arc::from(provider_box);
tracing::debug!(
provider = %INFERENCE_BACKEND_ID,
model = %default_model,
"[triage::routing] resolved remote provider"
);
Ok(ResolvedProvider {
provider,
provider_name: INFERENCE_BACKEND_ID.to_string(),
model: default_model,
used_local: false,
})
let resolved = provider::provider_for_role("subconscious", config);
let r = resolved.trim();
// #1257: triage must never depend on a local model/CLI being up, and a
// half-configured BYOK route must not error a trigger — force the managed
// backend in all those cases. `is_local_provider_string` only covers the
// local HTTP runtimes (Ollama/LM Studio/MLX/local-openai), so the local CLI
// delegates (`claude_agent_sdk`, `claude-code:<model>`) are excluded
// explicitly here (Codex P2) — otherwise they'd be treated as BYOK cloud and
// triage could hang on an unauthenticated/missing CLI.
let force_managed = is_local_provider_string(r)
|| is_local_cli_route(r)
|| r == provider::BYOK_INCOMPLETE_SENTINEL;
let effective = if force_managed { PROVIDER_OPENHUMAN } else { r };
if force_managed {
tracing::info!(
resolved = %r,
"[triage::routing] subconscious workload not usable for triage (local/incomplete) — \
forcing managed backend (#1257: triage never goes local)"
);
}
// Build through the per-workload factory: managed routes resolve their model
// id via `make_openhuman_backend` → `managed_tier_for_role`, BYOK cloud routes
// via the slug's configured model.
let build = |provider_string: &str| -> anyhow::Result<ResolvedProvider> {
let (provider_box, model) =
create_chat_provider_from_string("subconscious", provider_string, config)?;
let provider: Arc<dyn Provider> = Arc::from(provider_box);
let provider_name = if provider_string == PROVIDER_OPENHUMAN {
INFERENCE_BACKEND_ID.to_string()
} else {
provider_string
.split(':')
.next()
.unwrap_or(provider_string)
.to_string()
};
Ok(ResolvedProvider {
provider,
provider_name,
model,
used_local: false,
})
};
match build(effective) {
Ok(rp) => {
tracing::debug!(
provider = %rp.provider_name,
model = %rp.model,
"[triage::routing] resolved remote provider via subconscious workload"
);
Ok(rp)
}
Err(err) => {
tracing::warn!(
resolved = %effective,
error = %err,
"[triage::routing] subconscious workload provider build failed — \
falling back to managed backend"
);
build(PROVIDER_OPENHUMAN)
}
}
}
// ── Tests ───────────────────────────────────────────────────────────────
+98 -8
View File
@@ -1,27 +1,117 @@
use super::*;
use crate::openhuman::config::schema::cloud_providers::{AuthStyle, CloudProviderCreds};
fn test_config() -> Config {
Config::default()
}
fn openai_entry(id: &str, slug: &str) -> CloudProviderCreds {
CloudProviderCreds {
id: id.to_string(),
slug: slug.to_string(),
label: "OpenAI".to_string(),
endpoint: "https://api.openai.com/v1".to_string(),
auth_style: AuthStyle::Bearer,
default_model: Some("gpt-4o".to_string()),
..Default::default()
}
}
#[test]
fn build_remote_provider_uses_backend_id_and_default_model() {
fn build_remote_provider_managed_uses_backend_id_and_chat_tier() {
// Default config → subconscious workload resolves to the managed backend →
// model pinned to the subconscious registry tier (chat-v1, == DEFAULT_MODEL).
let config = test_config();
let resolved = build_remote_provider(&config).expect("remote provider should build");
assert_eq!(resolved.provider_name, INFERENCE_BACKEND_ID);
assert_eq!(
resolved.model,
crate::openhuman::config::DEFAULT_MODEL.to_string()
);
assert_eq!(resolved.model, crate::openhuman::config::MODEL_CHAT_V1);
assert!(!resolved.used_local, "used_local is always false");
}
#[test]
fn build_remote_provider_uses_configured_default_model() {
fn build_remote_provider_managed_pins_chat_tier_regardless_of_default_model() {
// Triage's managed arm now resolves its model via the subconscious registry
// (make_openhuman_backend -> managed_tier_for_role = chat-v1), NOT
// `default_model` — consistent with the subconscious tick. A heavy
// `default_model` must not drag triage off the lightweight chat tier.
let mut config = test_config();
config.default_model = Some("custom-model-v1".to_string());
config.default_model = Some("reasoning-v1".to_string());
let resolved = build_remote_provider(&config).expect("remote provider should build");
assert_eq!(resolved.model, "custom-model-v1");
assert_eq!(resolved.provider_name, INFERENCE_BACKEND_ID);
assert_eq!(resolved.model, "chat-v1");
assert!(!resolved.used_local);
}
#[test]
fn build_remote_provider_falls_back_to_managed_when_subconscious_local() {
// #1257: triage must never run on a local provider. A local
// `subconscious_provider` falls back to the managed backend so a trigger
// never errors because Ollama is down.
let mut config = test_config();
config.subconscious_provider = Some("ollama:llama3.2:3b".to_string());
config.default_model = Some("chat-v1".to_string());
let resolved = build_remote_provider(&config).expect("remote provider should build");
assert_eq!(resolved.provider_name, INFERENCE_BACKEND_ID);
assert_eq!(resolved.model, "chat-v1");
assert!(!resolved.used_local, "triage never goes local");
}
#[test]
fn build_remote_provider_forces_managed_for_local_cli_routes() {
// #1257 (Codex P2): local CLI delegates (claude_agent_sdk / claude-code:)
// are not caught by `is_local_provider_string`, but triage must not depend on
// a local CLI — force the managed backend (chat-v1) for them too.
for route in [
"claude_agent_sdk",
"claude_agent_sdk:sonnet",
"claude-code:opus",
] {
let mut config = test_config();
config.subconscious_provider = Some(route.to_string());
let resolved = build_remote_provider(&config)
.unwrap_or_else(|e| panic!("route {route} should build, got {e}"));
assert_eq!(
resolved.provider_name, INFERENCE_BACKEND_ID,
"route {route} must force managed backend"
);
assert_eq!(resolved.model, "chat-v1", "route {route} must pin chat-v1");
assert!(!resolved.used_local);
}
}
#[test]
fn is_local_cli_route_classifies_cli_delegates_only() {
assert!(is_local_cli_route("claude_agent_sdk"));
assert!(is_local_cli_route("claude_agent_sdk:sonnet"));
assert!(is_local_cli_route("claude-code:opus"));
assert!(!is_local_cli_route("openai:gpt-4o"));
assert!(!is_local_cli_route("openhuman"));
assert!(!is_local_cli_route("ollama:phi3"));
}
#[test]
fn build_remote_provider_routes_through_byok_subconscious() {
// A concrete BYOK cloud subconscious provider governs triage classification.
let mut config = test_config();
config.cloud_providers.push(openai_entry("p_oai", "openai"));
config.subconscious_provider = Some("openai:gpt-4o-mini".to_string());
let resolved = build_remote_provider(&config).expect("remote provider should build");
assert_eq!(resolved.provider_name, "openai");
assert_eq!(resolved.model, "gpt-4o-mini");
assert!(!resolved.used_local);
}
#[test]
fn build_remote_provider_falls_back_when_byok_build_fails() {
// A non-local, non-managed subconscious provider whose slug has no matching
// `cloud_providers` entry fails to build — triage falls back to the managed
// backend rather than erroring the turn.
let mut config = test_config();
config.subconscious_provider = Some("groq:llama3".to_string());
config.default_model = Some("chat-v1".to_string());
let resolved = build_remote_provider(&config).expect("remote provider should build");
assert_eq!(resolved.provider_name, INFERENCE_BACKEND_ID);
assert_eq!(resolved.model, "chat-v1");
assert!(!resolved.used_local);
}
+32 -9
View File
@@ -113,6 +113,10 @@ pub fn resolve_model_for_hint(hint_or_tier: &str, config: &Config) -> String {
("coding", crate::openhuman::config::MODEL_CODING_V1),
("vision", crate::openhuman::config::MODEL_VISION_V1),
("summarization", "summarization-v1"),
// Background subconscious workload rides the lightweight chat tier on the
// managed backend; its `subconscious` *role* (handled below) still selects
// the provider via `subconscious_provider`.
("subconscious", crate::openhuman::config::MODEL_CHAT_V1),
];
let tier_to_role: &[(&str, &str)] = &[
(crate::openhuman::config::MODEL_REASONING_V1, "reasoning"),
@@ -130,11 +134,17 @@ pub fn resolve_model_for_hint(hint_or_tier: &str, config: &Config) -> String {
.find(|(k, _)| *k == hint_key)
.map(|(_, v)| *v)
.unwrap_or(hint_or_tier);
let role = tier_to_role
.iter()
.find(|(k, _)| *k == tier)
.map(|(_, v)| *v)
.unwrap_or(hint_key);
// Background workloads map to a tier *model* but must keep their own
// role so `provider_for_role` reads their dedicated `*_provider` field
// rather than the chat-tier provider their model happens to share.
let role = match hint_key {
"subconscious" => "subconscious",
_ => tier_to_role
.iter()
.find(|(k, _)| *k == tier)
.map(|(_, v)| *v)
.unwrap_or(hint_key),
};
(tier, role)
} else {
let role = tier_to_role
@@ -828,10 +838,11 @@ pub(crate) fn create_local_chat_provider_from_string(
/// be mapped explicitly.
///
/// Returns `Some(tier)` for the specialised roles that map 1:1 to a managed
/// tier (`reasoning`, `agentic`, `coding`, `vision`). Returns `None` for:
/// tier (`reasoning`, `agentic`, `coding`, `vision`, `subconscious`). Returns
/// `None` for:
///
/// - the generic `chat` role (and any background/unknown role), which keeps
/// inheriting `default_model`: the front-line chat turn and legacy
/// - the generic `chat` role (and any other background/unknown role), which
/// keeps inheriting `default_model`: the front-line chat turn and legacy
/// `default_model = "reasoning-v1"` installs deliberately fall through to the
/// `chat` role (see the session builder) and rely on `default_model` driving
/// the model — pinning `chat` here would regress them.
@@ -841,19 +852,31 @@ pub(crate) fn create_local_chat_provider_from_string(
/// user-configurable `memory_tree.cloud_llm_model`. Pinning `summarization`
/// to a fixed tier would silently ignore that override.
///
/// `subconscious` IS pinned (to the lightweight `chat-v1` tier) even though it
/// is a background workload: the cloud subconscious tick builds via the session
/// builder with `default_model = "hint:subconscious"` (a role-routing marker, not
/// a real tier), so "inherit `default_model`" would forward that marker to the
/// backend. Pinning here resolves the managed model declaratively to `chat-v1` —
/// the cheap monitoring tier the workload wants — independent of `default_model`,
/// while [`provider_for_role`] still lets `subconscious_provider` choose the
/// provider (managed / BYOK / local).
///
/// For `vision` the default-inheritance mismatch is not just suboptimal but
/// fatal: an unset `vision_provider` would resolve to `chat-v1`,
/// `model_supports_vision` would report `false`, and the turn engine would strip
/// every attached image — leaving the managed vision sub-agent blind.
fn managed_tier_for_role(role: &str) -> Option<&'static str> {
use crate::openhuman::config::{
MODEL_AGENTIC_V1, MODEL_CODING_V1, MODEL_REASONING_V1, MODEL_VISION_V1,
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_V1, MODEL_VISION_V1,
};
match role {
"reasoning" => Some(MODEL_REASONING_V1),
"agentic" => Some(MODEL_AGENTIC_V1),
"coding" => Some(MODEL_CODING_V1),
"vision" => Some(MODEL_VISION_V1),
// Background subconscious tick/triage: pinned to the lightweight chat
// tier (see the doc above for why it is pinned despite being background).
"subconscious" => Some(MODEL_CHAT_V1),
_ => None,
}
}
@@ -1068,6 +1068,67 @@ fn make_openhuman_backend_translates_summarization_hint() {
assert_eq!(model, crate::openhuman::config::MODEL_SUMMARIZATION_V1);
}
#[test]
fn managed_backend_pins_subconscious_role_to_chat_tier() {
// Subconscious is pinned to chat-v1 on the managed backend via
// `managed_tier_for_role`, *independent of* `default_model`. The cloud tick
// routes through this role with `default_model` overwritten to the
// "hint:subconscious" marker, so it must NOT inherit `default_model` (which
// would forward the raw marker to the backend → HTTP 400).
let mut config = Config::default();
config.default_model = Some("hint:subconscious".to_string());
let (_, model) =
make_openhuman_backend("subconscious", &config).expect("factory should succeed");
assert_eq!(model, crate::openhuman::config::MODEL_CHAT_V1);
// Even with a heavy `default_model`, the subconscious role stays on chat-v1.
config.default_model = Some("reasoning-v1".to_string());
let (_, model) =
make_openhuman_backend("subconscious", &config).expect("factory should succeed");
assert_eq!(model, crate::openhuman::config::MODEL_CHAT_V1);
}
#[test]
fn create_chat_provider_subconscious_managed_resolves_chat_v1() {
// End-to-end of the managed tick path: provider role `subconscious` with the
// hint default_model, no BYOK subconscious_provider → managed backend, model
// pinned to chat-v1 (no regression vs the pre-change chat-role behaviour).
let mut config = Config::default();
config.default_model = Some("hint:subconscious".to_string());
let (_, model) =
create_chat_provider("subconscious", &config).expect("create_chat_provider must succeed");
assert_eq!(model, crate::openhuman::config::MODEL_CHAT_V1);
}
#[test]
fn create_chat_provider_subconscious_honours_byok_route() {
// When the user pins a concrete cloud provider for the subconscious workload
// in Settings → AI → Advanced, the factory builds that provider and returns
// its exact model id.
let mut config = Config::default();
config.cloud_providers.push(openai_entry("p_oai", "openai"));
config.subconscious_provider = Some("openai:gpt-4o-mini".to_string());
let (_, model) =
create_chat_provider("subconscious", &config).expect("create_chat_provider must succeed");
assert_eq!(model, "gpt-4o-mini");
}
#[test]
fn provider_for_role_subconscious_override_respected() {
let mut config = Config::default();
config.subconscious_provider = Some("ollama:llama3.2:3b".to_string());
assert_eq!(
provider_for_role("subconscious", &config),
"ollama:llama3.2:3b"
);
// Unset → managed backend (background workloads never inherit BYOK).
let default_config = Config::default();
assert_eq!(
provider_for_role("subconscious", &default_config),
"openhuman"
);
}
#[test]
fn make_openhuman_backend_reports_vision_capability() {
let config = Config::default();
@@ -2199,6 +2260,40 @@ fn resolve_model_for_hint_handles_unknown_hint_passthrough() {
assert_eq!(result, "hint:unknown_tier");
}
#[test]
fn resolve_model_for_hint_subconscious_managed_is_chat_v1() {
// Managed (no BYOK subconscious_provider) resolves to the chat tier model so
// the RPC `inference.resolve_model` reports the model the tick actually runs.
let config = Config::default();
assert_eq!(
resolve_model_for_hint("hint:subconscious", &config),
"chat-v1"
);
// An explicit managed sentinel still resolves to the tier, not the raw hint.
let mut config = Config::default();
config.subconscious_provider = Some("openhuman".to_string());
assert_eq!(
resolve_model_for_hint("hint:subconscious", &config),
"chat-v1"
);
}
#[test]
fn resolve_model_for_hint_subconscious_reads_subconscious_provider() {
// The `subconscious` hint must read `subconscious_provider` — NOT the
// chat-tier provider it shares a model with — so a BYOK subconscious route
// surfaces its own model id.
let mut config = Config::default();
config.subconscious_provider = Some("openai:gpt-4o-mini".to_string());
// A different chat_provider must not leak into the subconscious resolution.
config.chat_provider = Some("anthropic:claude-sonnet-4-20250514".to_string());
assert_eq!(
resolve_model_for_hint("hint:subconscious", &config),
"gpt-4o-mini"
);
}
#[test]
fn omlx_provider_builds_with_bearer_key() {
let mut config = crate::openhuman::config::Config::default();
+1 -1
View File
@@ -6,7 +6,7 @@ The subconscious is OpenHuman's background-awareness layer: a SQLite-backed loop
- Maintain a list of `SubconsciousTask`s (system-seeded + user-added) in SQLite; seed three default system tasks on init.
- Run a tick: load due tasks → log them `in_progress` → build a situation report → call the configured LLM → execute `act` tasks, create escalations for `escalate`/`UnapprovedWrite`, mark `noop` otherwise → update log entries in place.
- Route the per-tick evaluation and task execution to a local Ollama/LM Studio model or the OpenHuman cloud, based on config (`workload_local_model("subconscious")` / `subconscious_provider`).
- Route the per-tick evaluation and task execution to a local Ollama/LM Studio model or the OpenHuman cloud, based on config (`workload_local_model("subconscious")` / `subconscious_provider`). The tick builds its agent through the **`subconscious`** workload role — `run_agent` sets `default_model = "hint:subconscious"` so the session builder resolves `subconscious_provider` (not the `chat` role); on the managed backend that pins the lightweight `chat-v1` tier.
- Classify task write-intent via keyword heuristics (`needs_tools` / `needs_agent`); run read-only tasks analysis-only and escalate any recommended write action for approval.
- Emit, cap (`MAX_REFLECTIONS_PER_TICK = 5`), hydrate, and persist proactive reflections; resolve each reflection's `source_refs` into frozen `SourceChunk` snapshots at tick time.
- Manage escalation lifecycle (pending → approved/dismissed); approving executes the task at full permissions.
+11
View File
@@ -352,6 +352,17 @@ impl SubconsciousEngine {
let mut effective = config.clone();
effective.agent.agent_timeout_secs = TOOL_CALL_TIMEOUT_SECS;
// Route the tick build through the `subconscious` background workload so
// Settings → AI → Advanced "Subconscious" actually governs the cloud
// tick provider, instead of riding the `chat` role (the default-model
// fall-through in the session builder). The session builder maps
// `hint:subconscious` → the `subconscious` provider role; on the managed
// backend the model still resolves to `chat-v1` (no regression).
effective.default_model = Some("hint:subconscious".to_string());
debug!(
"[subconscious] tick provider routed via hint:subconscious (subconscious_provider={:?})",
effective.subconscious_provider
);
match self.mode {
SubconsciousMode::Simple => {
effective.autonomy.level = crate::openhuman::security::AutonomyLevel::ReadOnly;