mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: M3gA-Mind <megamind@mahadao.com>
This commit is contained in:
@@ -31,8 +31,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::openhuman::agent::harness::fork_context::with_parent_context;
|
||||
use crate::openhuman::agent_orchestration::parent_context::build_root_parent;
|
||||
use crate::openhuman::agent_orchestration::parent_context::with_root_parent;
|
||||
use crate::openhuman::agent_orchestration::{
|
||||
AgentOrchestrationSession, AgentStatus, SpawnAgentRequest, WaitAgentOptions,
|
||||
};
|
||||
@@ -188,24 +187,21 @@ async fn run_member_loop(
|
||||
run_id: &str,
|
||||
model_override: Option<String>,
|
||||
) {
|
||||
let outcome = match build_root_parent(config, "agent_team_runtime", "team", "teamrun").await {
|
||||
Ok(parent) => {
|
||||
with_parent_context(parent, async {
|
||||
drive_member(
|
||||
config,
|
||||
team_id,
|
||||
member_id,
|
||||
agent_id,
|
||||
&task,
|
||||
run_id,
|
||||
model_override,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
let outcome = with_root_parent(config, "agent_team_runtime", "team", "teamrun", async {
|
||||
drive_member(
|
||||
config,
|
||||
team_id,
|
||||
member_id,
|
||||
agent_id,
|
||||
&task,
|
||||
run_id,
|
||||
model_override,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
// Flatten: outer Err = root-parent build failure, inner = drive_member result.
|
||||
.unwrap_or_else(Err);
|
||||
|
||||
if let Err(err) = outcome {
|
||||
log::error!(
|
||||
|
||||
@@ -291,6 +291,67 @@ async fn drive_member_completes_task_with_worker_output_as_evidence() {
|
||||
assert_eq!(member.current_task_id, None);
|
||||
}
|
||||
|
||||
/// Covers `run_member_loop` — the `with_root_parent` wrapper around
|
||||
/// `drive_member`. With a mock parent already installed, `with_root_parent`
|
||||
/// reuses it (rather than building a real root), so the member drives to
|
||||
/// completion under the canned provider. Same setup as the `drive_member`
|
||||
/// happy-path test, but through the wrapper the live runtime spawns.
|
||||
#[tokio::test]
|
||||
async fn run_member_loop_drives_member_under_ambient_parent() {
|
||||
AgentDefinitionRegistry::init_global_builtins().unwrap();
|
||||
let (_dir, config) = test_config();
|
||||
seed_team(&config, "team-1");
|
||||
seed_member(&config, "team-1", "m1", Some("code_executor"));
|
||||
seed_task(
|
||||
&config,
|
||||
"team-1",
|
||||
"task-a",
|
||||
AgentTeamTaskStatus::Todo,
|
||||
None,
|
||||
vec![],
|
||||
);
|
||||
run_ledger::claim_agent_team_task(&config, "team-1", "task-a", "m1", "teamrun-y").unwrap();
|
||||
run_ledger::mark_agent_team_member_running(
|
||||
&config,
|
||||
"team-1",
|
||||
"m1",
|
||||
"task-a",
|
||||
"teamrun-y",
|
||||
"teamrun-y",
|
||||
)
|
||||
.unwrap();
|
||||
let task = run_ledger::get_agent_team_task(&config, "task-a")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let provider = Arc::new(CannedProvider {
|
||||
output: "did the thing".into(),
|
||||
fail: false,
|
||||
});
|
||||
with_parent_context(mock_parent(provider), async {
|
||||
run_member_loop(
|
||||
&config,
|
||||
"team-1",
|
||||
"m1",
|
||||
"code_executor",
|
||||
task,
|
||||
"teamrun-y",
|
||||
Some("test-model".into()),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
let done = run_ledger::get_agent_team_task(&config, "task-a")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
done.status,
|
||||
AgentTeamTaskStatus::Done,
|
||||
"run_member_loop drove the member to completion through with_root_parent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drive_member_releases_task_when_worker_fails() {
|
||||
AgentDefinitionRegistry::init_global_builtins().unwrap();
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
//! Shared root [`ParentExecutionContext`] builder for controller-spawned
|
||||
//! orchestration tasks (#3374 PR4, extracted from the #3375 workflow engine).
|
||||
//!
|
||||
//! The workflow-run engine ([`workflow_runs::engine`]), the agent-team runtime
|
||||
//! ([`agent_teams::runtime`]), and the subconscious tick
|
||||
//! ([`crate::openhuman::subconscious`]) all need to spawn real sub-agents from a
|
||||
//! background task that has **no** enclosing agent turn on the stack. Those
|
||||
//! spawns read their parent execution context from a task-local
|
||||
//! ([`current_parent`]) that is only set inside an agent turn — so a naive spawn
|
||||
//! fails with `NoParentContext` (the TAURI-RUST-HMW regression, #4337).
|
||||
//!
|
||||
//! The fix (proven in `triage::escalation::dispatch_target_agent`) is to build a
|
||||
//! *root* [`ParentExecutionContext`] from a config-built [`Agent`] and run the
|
||||
//! whole loop inside [`with_parent_context`]. Every nested `spawn_agent` then
|
||||
//! resolves `current_parent()` to this root, inheriting a real provider, tool
|
||||
//! registry, memory, and model — the same construction path `agent_chat` uses.
|
||||
//!
|
||||
//! [`with_root_parent`] is the single blessed entry point that folds the build +
|
||||
//! install into one call, so a surface can neither hand-roll the parent nor
|
||||
//! forget to install it. Every background orchestration surface goes through it.
|
||||
//!
|
||||
//! This was originally inlined in the workflow engine; #3374 PR4 lifted it here
|
||||
//! so each surface reuses the exact same construction (and the single
|
||||
//! registry-initialisation defense) rather than carrying a second copy of the
|
||||
//! ~20-field context literal that could drift.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::openhuman::agent::harness::fork_context::{
|
||||
current_parent, with_parent_context, ParentExecutionContext,
|
||||
};
|
||||
use crate::openhuman::agent::Agent;
|
||||
use crate::openhuman::config::Config;
|
||||
|
||||
const LOG_TARGET: &str = "agent_orchestration::parent_context";
|
||||
|
||||
/// Build a root [`ParentExecutionContext`] from a config-built [`Agent`].
|
||||
///
|
||||
/// Mirrors `triage::escalation::dispatch_target_agent` — the proven path for
|
||||
/// running sub-agents without an enclosing agent turn. The caller supplies the
|
||||
/// identity fields that distinguish one orchestration surface from another:
|
||||
///
|
||||
/// - `agent_definition_id` — labels the root parent (e.g. `"workflow_engine"`,
|
||||
/// `"agent_team_runtime"`); surfaced in spawn metadata / logs.
|
||||
/// - `channel` — the logical channel the spawned work belongs to (e.g.
|
||||
/// `"workflow"`, `"team"`).
|
||||
/// - `session_prefix` — the `session_id` is `"{session_prefix}-{uuid}"`, keeping
|
||||
/// each surface's sessions namespaced and greppable.
|
||||
///
|
||||
/// Every other field is inherited verbatim from the config-built agent, so the
|
||||
/// spawned children behave exactly like a normal sub-agent dispatch.
|
||||
pub(crate) async fn build_root_parent(
|
||||
config: &Config,
|
||||
agent_definition_id: &str,
|
||||
channel: &str,
|
||||
session_prefix: &str,
|
||||
) -> Result<ParentExecutionContext> {
|
||||
// Sub-agent spawns resolve their definition through the global
|
||||
// agent-definition registry, so it MUST be initialised before any spawn.
|
||||
// The full runtime boot (`bootstrap_core_runtime`) does this, but these
|
||||
// engines can also be reached from contexts that only built the HTTP router
|
||||
// (e.g. the JSON-RPC e2e harness) — so init defensively here. `OnceLock`
|
||||
// makes this idempotent: a no-op when the registry is already loaded.
|
||||
if crate::openhuman::agent::harness::AgentDefinitionRegistry::global().is_none() {
|
||||
if let Err(err) = crate::openhuman::agent::harness::AgentDefinitionRegistry::init_global(
|
||||
&config.workspace_dir,
|
||||
) {
|
||||
// A concurrent init may have won the race and populated the registry,
|
||||
// in which case the `AlreadyInitialized`-style error is benign. But if
|
||||
// the registry is *still* `None`, init genuinely failed — fail fast
|
||||
// here rather than letting every downstream `spawn_agent` fail later
|
||||
// with `NoParentContext` after orchestration state has advanced.
|
||||
if crate::openhuman::agent::harness::AgentDefinitionRegistry::global().is_none() {
|
||||
return Err(err)
|
||||
.context("initialize AgentDefinitionRegistry for orchestration root parent");
|
||||
}
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"[parent_context] registry_init_raced err={err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut agent = Agent::from_config(config)
|
||||
.context("build Agent from config for orchestration root parent")?;
|
||||
|
||||
let integrations = crate::openhuman::composio::fetch_connected_integrations(config).await;
|
||||
agent.set_connected_integrations(integrations);
|
||||
|
||||
Ok(ParentExecutionContext {
|
||||
agent_definition_id: agent_definition_id.to_string(),
|
||||
allowed_subagent_ids: HashSet::new(),
|
||||
provider: agent.provider_arc(),
|
||||
all_tools: agent.tools_arc(),
|
||||
all_tool_specs: agent.tool_specs_arc(),
|
||||
// No visibility filter for this spawned/background builder — empty means
|
||||
// "unknown" and callers fall back to the full registry (see field doc).
|
||||
visible_tool_names: HashSet::new(),
|
||||
model_name: agent.model_name().to_string(),
|
||||
temperature: agent.temperature(),
|
||||
workspace_dir: agent.workspace_dir().to_path_buf(),
|
||||
memory: agent.memory_arc(),
|
||||
agent_config: agent.agent_config().clone(),
|
||||
workflows: Arc::new(agent.workflows().to_vec()),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: format!("{session_prefix}-{}", uuid::Uuid::new_v4()),
|
||||
channel: channel.to_string(),
|
||||
connected_integrations: agent.connected_integrations().to_vec(),
|
||||
tool_call_format: crate::openhuman::context::prompt::ToolCallFormat::PFormat,
|
||||
session_key: agent.session_key().to_string(),
|
||||
session_parent_prefix: agent.session_parent_prefix().map(str::to_string),
|
||||
on_progress: None,
|
||||
run_queue: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure a parent execution context is installed for `fut`, then run it — the
|
||||
/// single blessed entry point for **controller-spawned background orchestration
|
||||
/// surfaces** that have no enclosing agent turn (the workflow-run engine, the
|
||||
/// agent-team runtime, the subconscious tick).
|
||||
///
|
||||
/// Folds [`build_root_parent`] + [`with_parent_context`] into one call so a
|
||||
/// surface cannot install a hand-rolled parent, and — the TAURI-RUST-HMW
|
||||
/// failure mode — cannot *forget* to install one at all and have every nested
|
||||
/// `spawn_subagent` die at runtime with
|
||||
/// [`SubagentRunError::NoParentContext`](crate::openhuman::agent::harness::subagent_runner::SubagentRunError::NoParentContext).
|
||||
/// Running a background surface and establishing its root context become the
|
||||
/// same act.
|
||||
///
|
||||
/// When an ambient [`current_parent`] is already installed, `fut` runs under it
|
||||
/// unchanged rather than building a second root. In production these surfaces
|
||||
/// run on freshly-spawned tasks where task-locals never cross the `tokio::spawn`
|
||||
/// boundary, so the ambient parent is always absent and a root is built from
|
||||
/// `config` — but reusing an installed parent keeps the helper correct if it is
|
||||
/// ever nested inside a turn, and lets tests drive a surface under a mock
|
||||
/// parent (and thus a mock provider) hermetically.
|
||||
///
|
||||
/// Returns the future's output on success, or the [`build_root_parent`] error
|
||||
/// when the root context can't be constructed — the caller decides how to
|
||||
/// degrade (fail the run, or fall back to an un-grounded path).
|
||||
///
|
||||
/// Only for surfaces that build their parent *from `Config`* because there is
|
||||
/// no ambient parent. Spawn sites that re-install an **inherited**
|
||||
/// `current_parent()` across a `tokio::spawn` boundary (e.g.
|
||||
/// `spawn_async_subagent`, `spawn_worker_thread`, the orchestration `ops` task)
|
||||
/// are a different pattern and call [`with_parent_context`] directly.
|
||||
pub(crate) async fn with_root_parent<F>(
|
||||
config: &Config,
|
||||
agent_definition_id: &str,
|
||||
channel: &str,
|
||||
session_prefix: &str,
|
||||
fut: F,
|
||||
) -> Result<F::Output>
|
||||
where
|
||||
F: std::future::Future,
|
||||
{
|
||||
// Already inside a turn (nested call, or a test harness installed a mock
|
||||
// parent): reuse it rather than building a second root.
|
||||
if current_parent().is_some() {
|
||||
return Ok(fut.await);
|
||||
}
|
||||
let parent = build_root_parent(config, agent_definition_id, channel, session_prefix).await?;
|
||||
Ok(with_parent_context(parent, fut).await)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::openhuman::agent::harness::fork_context::current_parent;
|
||||
|
||||
fn test_config() -> (tempfile::TempDir, Config) {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = Config {
|
||||
workspace_dir: dir.path().to_path_buf(),
|
||||
..Config::default()
|
||||
};
|
||||
(dir, config)
|
||||
}
|
||||
|
||||
/// Baseline for the bug: with no enclosing agent turn there is no ambient
|
||||
/// parent — exactly the state the subconscious tick spawned `context_scout`
|
||||
/// in (TAURI-RUST-HMW / #4337), which made `run_subagent` return
|
||||
/// `NoParentContext`.
|
||||
#[tokio::test]
|
||||
async fn no_ambient_parent_outside_with_root_parent() {
|
||||
assert!(
|
||||
current_parent().is_none(),
|
||||
"no parent context should be installed by default"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression (TAURI-RUST-HMW / #4337): `with_root_parent` must install a
|
||||
/// real parent for the wrapped future so a background orchestration surface
|
||||
/// (subconscious tick, workflow engine, team runtime) can spawn sub-agents
|
||||
/// without hitting `NoParentContext`. Proven by observing the installed
|
||||
/// parent from inside the future.
|
||||
#[tokio::test]
|
||||
async fn with_root_parent_installs_parent_for_inner_future() {
|
||||
let (_dir, config) = test_config();
|
||||
let observed = with_root_parent(
|
||||
&config,
|
||||
"subconscious",
|
||||
"subconscious",
|
||||
"subconscious",
|
||||
async { current_parent().map(|p| p.agent_definition_id) },
|
||||
)
|
||||
.await
|
||||
.expect("root parent builds from config");
|
||||
assert_eq!(
|
||||
observed.as_deref(),
|
||||
Some("subconscious"),
|
||||
"inner future must observe the installed root parent"
|
||||
);
|
||||
}
|
||||
|
||||
/// When a parent is already installed, `with_root_parent` reuses it instead
|
||||
/// of building a second root — so a surface nested in a turn (or a test
|
||||
/// driving it under a mock parent) runs under the ambient context.
|
||||
#[tokio::test]
|
||||
async fn with_root_parent_reuses_ambient_parent() {
|
||||
let (_dir, config) = test_config();
|
||||
let outer = build_root_parent(&config, "outer", "outer", "outer")
|
||||
.await
|
||||
.expect("build ambient parent");
|
||||
let observed = with_parent_context(outer, async {
|
||||
with_root_parent(&config, "inner", "inner", "inner", async {
|
||||
current_parent().map(|p| p.agent_definition_id)
|
||||
})
|
||||
.await
|
||||
.expect("reuses ambient, no build error")
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
observed.as_deref(),
|
||||
Some("outer"),
|
||||
"with_root_parent must reuse the ambient parent, not build a new 'inner' root"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,10 @@
|
||||
//! Shared root [`ParentExecutionContext`] builder for controller-spawned
|
||||
//! orchestration tasks (#3374 PR4, extracted from the #3375 workflow engine).
|
||||
//! orchestration tasks (#3374 PR4). Export-only; the implementation lives in
|
||||
//! [`builder`] — see it for the rationale and the `with_root_parent` /
|
||||
//! `build_root_parent` construction.
|
||||
//!
|
||||
//! Both the workflow-run engine ([`workflow_runs::engine`]) and the agent-team
|
||||
//! runtime ([`agent_teams::runtime`]) need to spawn real sub-agents from a
|
||||
//! background task that has **no** enclosing agent turn on the stack. Those
|
||||
//! spawns read their parent execution context from a task-local
|
||||
//! ([`current_parent`]) that is only set inside an agent turn — so a naive spawn
|
||||
//! fails with `NoParentContext`.
|
||||
//!
|
||||
//! The fix (proven in `triage::escalation::dispatch_target_agent`) is to build a
|
||||
//! *root* [`ParentExecutionContext`] from a config-built [`Agent`] and run the
|
||||
//! whole loop inside [`with_parent_context`]. Every nested `spawn_agent` then
|
||||
//! resolves `current_parent()` to this root, inheriting a real provider, tool
|
||||
//! registry, memory, and model — the same construction path `agent_chat` uses.
|
||||
//!
|
||||
//! This was originally inlined in the workflow engine; PR4 lifts it here so the
|
||||
//! team runtime reuses the exact same construction (and the single
|
||||
//! registry-initialisation defense) rather than carrying a second copy of the
|
||||
//! ~20-field context literal that could drift.
|
||||
//! [`ParentExecutionContext`]: crate::openhuman::agent::harness::fork_context::ParentExecutionContext
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
mod builder;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::openhuman::agent::harness::fork_context::ParentExecutionContext;
|
||||
use crate::openhuman::agent::Agent;
|
||||
use crate::openhuman::config::Config;
|
||||
|
||||
const LOG_TARGET: &str = "agent_orchestration::parent_context";
|
||||
|
||||
/// Build a root [`ParentExecutionContext`] from a config-built [`Agent`].
|
||||
///
|
||||
/// Mirrors `triage::escalation::dispatch_target_agent` — the proven path for
|
||||
/// running sub-agents without an enclosing agent turn. The caller supplies the
|
||||
/// identity fields that distinguish one orchestration surface from another:
|
||||
///
|
||||
/// - `agent_definition_id` — labels the root parent (e.g. `"workflow_engine"`,
|
||||
/// `"agent_team_runtime"`); surfaced in spawn metadata / logs.
|
||||
/// - `channel` — the logical channel the spawned work belongs to (e.g.
|
||||
/// `"workflow"`, `"team"`).
|
||||
/// - `session_prefix` — the `session_id` is `"{session_prefix}-{uuid}"`, keeping
|
||||
/// each surface's sessions namespaced and greppable.
|
||||
///
|
||||
/// Every other field is inherited verbatim from the config-built agent, so the
|
||||
/// spawned children behave exactly like a normal sub-agent dispatch.
|
||||
pub(crate) async fn build_root_parent(
|
||||
config: &Config,
|
||||
agent_definition_id: &str,
|
||||
channel: &str,
|
||||
session_prefix: &str,
|
||||
) -> Result<ParentExecutionContext> {
|
||||
// Sub-agent spawns resolve their definition through the global
|
||||
// agent-definition registry, so it MUST be initialised before any spawn.
|
||||
// The full runtime boot (`bootstrap_core_runtime`) does this, but these
|
||||
// engines can also be reached from contexts that only built the HTTP router
|
||||
// (e.g. the JSON-RPC e2e harness) — so init defensively here. `OnceLock`
|
||||
// makes this idempotent: a no-op when the registry is already loaded.
|
||||
if crate::openhuman::agent::harness::AgentDefinitionRegistry::global().is_none() {
|
||||
if let Err(err) = crate::openhuman::agent::harness::AgentDefinitionRegistry::init_global(
|
||||
&config.workspace_dir,
|
||||
) {
|
||||
// A concurrent init may have won the race and populated the registry,
|
||||
// in which case the `AlreadyInitialized`-style error is benign. But if
|
||||
// the registry is *still* `None`, init genuinely failed — fail fast
|
||||
// here rather than letting every downstream `spawn_agent` fail later
|
||||
// with `NoParentContext` after orchestration state has advanced.
|
||||
if crate::openhuman::agent::harness::AgentDefinitionRegistry::global().is_none() {
|
||||
return Err(err)
|
||||
.context("initialize AgentDefinitionRegistry for orchestration root parent");
|
||||
}
|
||||
log::debug!(
|
||||
target: LOG_TARGET,
|
||||
"[parent_context] registry_init_raced err={err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut agent = Agent::from_config(config)
|
||||
.context("build Agent from config for orchestration root parent")?;
|
||||
|
||||
let integrations = crate::openhuman::composio::fetch_connected_integrations(config).await;
|
||||
agent.set_connected_integrations(integrations);
|
||||
|
||||
Ok(ParentExecutionContext {
|
||||
agent_definition_id: agent_definition_id.to_string(),
|
||||
allowed_subagent_ids: HashSet::new(),
|
||||
provider: agent.provider_arc(),
|
||||
all_tools: agent.tools_arc(),
|
||||
all_tool_specs: agent.tool_specs_arc(),
|
||||
// No visibility filter for this spawned/background builder — empty means
|
||||
// "unknown" and callers fall back to the full registry (see field doc).
|
||||
visible_tool_names: HashSet::new(),
|
||||
model_name: agent.model_name().to_string(),
|
||||
temperature: agent.temperature(),
|
||||
workspace_dir: agent.workspace_dir().to_path_buf(),
|
||||
memory: agent.memory_arc(),
|
||||
agent_config: agent.agent_config().clone(),
|
||||
workflows: Arc::new(agent.workflows().to_vec()),
|
||||
memory_context: Arc::new(None),
|
||||
session_id: format!("{session_prefix}-{}", uuid::Uuid::new_v4()),
|
||||
channel: channel.to_string(),
|
||||
connected_integrations: agent.connected_integrations().to_vec(),
|
||||
tool_call_format: crate::openhuman::context::prompt::ToolCallFormat::PFormat,
|
||||
session_key: agent.session_key().to_string(),
|
||||
session_parent_prefix: agent.session_parent_prefix().map(str::to_string),
|
||||
on_progress: None,
|
||||
run_queue: None,
|
||||
})
|
||||
}
|
||||
pub(crate) use builder::{build_root_parent, with_root_parent};
|
||||
|
||||
@@ -125,16 +125,25 @@ pub async fn run_context_scout(question: &str, focus: Option<&str>) -> anyhow::R
|
||||
}
|
||||
|
||||
/// Same as [`run_context_scout`] but with an **explicitly-supplied** tool
|
||||
/// catalogue, so it can run *outside* an agent turn — e.g. from the
|
||||
/// subconscious engine's structured tick, where `current_parent()` is unset
|
||||
/// and the parent's visible tool set can't be auto-derived.
|
||||
/// catalogue — for callers *outside* an agent turn that can't auto-derive the
|
||||
/// parent's visible tool set from `current_parent()` (e.g. the subconscious
|
||||
/// engine's structured tick).
|
||||
///
|
||||
/// The caller passes the catalogue of tools the eventual decision agent can
|
||||
/// actually call (one `- name: description` per line), so the bundle's
|
||||
/// `recommended_tool_calls` stay grounded in callable tools. Progress /
|
||||
/// subagent-lifecycle events stay best-effort: with no parent context the
|
||||
/// `parent_session` falls back to `standalone` and the progress sink is absent,
|
||||
/// so those sends simply no-op.
|
||||
/// `recommended_tool_calls` stay grounded in callable tools.
|
||||
///
|
||||
/// **A parent execution context is still required.** Like [`run_context_scout`]
|
||||
/// this spawns `context_scout` via `run_subagent`, which resolves its provider /
|
||||
/// tools / model from the `PARENT_CONTEXT` task-local and returns
|
||||
/// `NoParentContext` when it is unset. A background surface with no enclosing
|
||||
/// turn MUST establish a root parent first — call this *inside*
|
||||
/// [`with_root_parent`](crate::openhuman::agent_orchestration::parent_context::with_root_parent).
|
||||
/// Skipping that is exactly the TAURI-RUST-HMW failure (#4337): every spawn
|
||||
/// died with `NoParentContext` and the tick ran un-grounded. Only the
|
||||
/// progress / subagent-lifecycle telemetry degrades gracefully without a
|
||||
/// parent — `parent_session` falls back to `standalone` and the absent progress
|
||||
/// sink no-ops — but the spawn itself does not.
|
||||
pub async fn run_context_scout_with_catalog(
|
||||
question: &str,
|
||||
focus: Option<&str>,
|
||||
|
||||
@@ -34,8 +34,7 @@ use std::sync::{Arc, Mutex, OnceLock};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::openhuman::agent::harness::fork_context::with_parent_context;
|
||||
use crate::openhuman::agent_orchestration::parent_context::build_root_parent;
|
||||
use crate::openhuman::agent_orchestration::parent_context::with_root_parent;
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::session_db::run_ledger::{
|
||||
get_workflow_run, upsert_workflow_run, WorkflowRun, WorkflowRunStatus, WorkflowRunUpsert,
|
||||
@@ -300,15 +299,12 @@ pub async fn resume_workflow_run(config: &Config, id: &str) -> Result<WorkflowRu
|
||||
async fn run_engine_loop(config: &Config, run_id: &str, definition: WorkflowDefinition) {
|
||||
let cancel = lookup_cancel_flag(run_id).unwrap_or_else(|| register_cancel_flag(run_id));
|
||||
|
||||
let outcome = match build_root_parent(config, "workflow_engine", "workflow", "workflow").await {
|
||||
Ok(parent) => {
|
||||
with_parent_context(parent, async {
|
||||
super::graph::drive_phases(config, run_id, &definition, &cancel).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
let outcome = with_root_parent(config, "workflow_engine", "workflow", "workflow", async {
|
||||
super::graph::drive_phases(config, run_id, &definition, &cancel).await
|
||||
})
|
||||
.await
|
||||
// Flatten: outer Err = root-parent build failure, inner = drive_phases result.
|
||||
.unwrap_or_else(Err);
|
||||
|
||||
if let Err(err) = outcome {
|
||||
log::error!(
|
||||
|
||||
@@ -329,6 +329,35 @@ async fn unit_phases_execute_in_dependency_order() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Covers `run_engine_loop` — the `with_root_parent` wrapper around
|
||||
/// `drive_phases`. With a mock parent installed, `with_root_parent` reuses it
|
||||
/// (rather than building a real root), so the engine loop drives the run to
|
||||
/// completion under the mock provider. Mirrors the `drive_phases` happy path,
|
||||
/// but through the wrapper the live engine spawns on its background task.
|
||||
#[tokio::test]
|
||||
async fn run_engine_loop_completes_run_under_ambient_parent() {
|
||||
AgentDefinitionRegistry::init_global_builtins().unwrap();
|
||||
let (_dir, config) = test_config();
|
||||
let provider = PeakProvider::default();
|
||||
let def = linear_def(2, 8, 2);
|
||||
let id = seed_run(
|
||||
&config,
|
||||
&def,
|
||||
json!({ "question": "what is X?", "modelOverride": "test-model" }),
|
||||
);
|
||||
|
||||
with_parent_context(mock_parent(Arc::new(provider.clone())), async {
|
||||
run_engine_loop(&config, &id, def).await
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
status_of(&config, &id),
|
||||
WorkflowRunStatus::Completed,
|
||||
"run_engine_loop drove the run to completion through with_root_parent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unit_concurrency_cap_is_respected() {
|
||||
AgentDefinitionRegistry::init_global_builtins().unwrap();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
use super::store;
|
||||
use super::types::{SubconsciousStatus, TickResult};
|
||||
use crate::openhuman::agent_orchestration::parent_context::with_root_parent;
|
||||
use crate::openhuman::config::schema::SubconsciousMode;
|
||||
use crate::openhuman::config::Config;
|
||||
use crate::openhuman::credentials::{AuthService, APP_SESSION_PROVIDER};
|
||||
@@ -309,7 +310,7 @@ impl SubconsciousEngine {
|
||||
let has_external_content = true;
|
||||
|
||||
// ── Stage 2: prepare_context — ground the diff before deciding ───────
|
||||
let prepared_context = self.prepare_context(&world_diff).await;
|
||||
let prepared_context = self.prepare_context(&config, &world_diff).await;
|
||||
|
||||
// ── Stage 3: decide — slim agent acts on diff + prepared context ─────
|
||||
let mut agent_prompt =
|
||||
@@ -379,20 +380,35 @@ impl SubconsciousEngine {
|
||||
/// Stage 2: run the read-only `context_scout` over the world diff to gather
|
||||
/// grounding context. Best-effort — on any error the decision agent simply
|
||||
/// runs without a prepared-context section.
|
||||
async fn prepare_context(&self, world_diff: &str) -> String {
|
||||
///
|
||||
/// The tick is a controller-spawned background surface with **no enclosing
|
||||
/// agent turn**, so the `context_scout` spawn has no ambient
|
||||
/// `current_parent()`. We establish a root parent for the spawn via the
|
||||
/// shared [`with_root_parent`] helper — the same construction the
|
||||
/// workflow-run engine and agent-team runtime use. Without it every tick's
|
||||
/// scout died with `NoParentContext` and the decision ran un-grounded
|
||||
/// (Sentry TAURI-RUST-HMW; #4337). The build runs only on ticks that have
|
||||
/// world changes (the sole caller is past the `has_changes` gate), so quiet
|
||||
/// ticks pay nothing.
|
||||
async fn prepare_context(&self, config: &Config, world_diff: &str) -> String {
|
||||
let question = format!(
|
||||
"Background awareness check. Here is what changed in the user's connected sources \
|
||||
since the last check:\n\n{world_diff}\n\nSurface what the user should be aware of or \
|
||||
act on, and the context that grounds a good decision.",
|
||||
);
|
||||
|
||||
match crate::openhuman::agent_orchestration::tools::run_context_scout_with_catalog(
|
||||
&question,
|
||||
None,
|
||||
SUBCONSCIOUS_TOOL_CATALOG,
|
||||
)
|
||||
// Flatten: outer Err = root-parent build failure, inner = scout result.
|
||||
let scout = with_root_parent(config, "subconscious", "subconscious", "subconscious", {
|
||||
crate::openhuman::agent_orchestration::tools::run_context_scout_with_catalog(
|
||||
&question,
|
||||
None,
|
||||
SUBCONSCIOUS_TOOL_CATALOG,
|
||||
)
|
||||
})
|
||||
.await
|
||||
{
|
||||
.and_then(|inner| inner);
|
||||
|
||||
match scout {
|
||||
Ok(result) if !result.is_error => {
|
||||
debug!(
|
||||
"[subconscious] prepared context bundle ({} chars)",
|
||||
|
||||
Reference in New Issue
Block a user