Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
18e0edb
chore: files changed src/openhuman/agent/harness/session/builder/sett…
senamakel Sep 11, 2026
2138250
chore: files changed src/openhuman/agent/harness/session/builder/sett…
senamakel Sep 11, 2026
2d99e72
chore: files changed src/openhuman/agent/harness/session/types.rs
senamakel Sep 11, 2026
ce9ad1c
chore: files changed src/openhuman/agent/harness/session/builder/sett…
senamakel Sep 11, 2026
6c92603
chore: files changed src/openhuman/agent/harness/session/builder/mod.rs
senamakel Sep 11, 2026
a2043fd
chore: files changed src/openhuman/agent/harness/session/builder/mod.rs
senamakel Sep 11, 2026
7a4b906
chore: files changed src/openhuman/agent/harness/fork_context.rs,src/…
senamakel Sep 11, 2026
7bee71f
chore: files changed src/openhuman/agent/harness/session/builder/mod.rs
senamakel Sep 11, 2026
d28b417
chore: files changed src/openhuman/agent/harness/session/builder/mod.rs
senamakel Sep 11, 2026
5ad8c17
chore: Let me analyze this diff carefully.
senamakel Sep 11, 2026
035345e
chore: files changed src/openhuman/agent/harness/session/turn/context…
senamakel Sep 11, 2026
029b28a
chore: files changed src/openhuman/agent/orchestration/agent_teams/ru…
senamakel Sep 11, 2026
79c2770
chore: files changed src/openhuman/agent/harness/session/session_test…
senamakel Sep 11, 2026
5ad56d4
chore: files changed src/openhuman/agent/orchestration/tools/agent_pr…
senamakel Sep 11, 2026
9a72c6a
chore: files changed src/openhuman/agent/orchestration/tools/agent_pr…
senamakel Sep 11, 2026
b18d9d3
chore: files changed tests/raw_coverage/agent_session_turn_raw_covera…
senamakel Sep 11, 2026
9bc94f2
chore: files changed src/openhuman/agent/harness/session/builder/buil…
senamakel Sep 11, 2026
95f250e
chore: files changed src/openhuman/agent/harness/session/builder/buil…
senamakel Sep 11, 2026
35b4136
chore: files changed src/openhuman/agent/harness/session/builder/buil…
senamakel Sep 11, 2026
507a4cd
chore: files changed src/openhuman/agent/harness/session/builder/buil…
senamakel Sep 11, 2026
5618663
chore: files changed src/openhuman/agent/harness/session/builder/fact…
senamakel Sep 11, 2026
526fcb4
chore: files changed src/openhuman/agent/harness/archivist/lifecycle.…
senamakel Sep 11, 2026
37da10c
chore: files changed src/openhuman/agent/harness/archivist/lifecycle.rs
senamakel Sep 11, 2026
8ecb27f
chore: files changed src/openhuman/agent/experience/ops.rs,src/openhu…
senamakel Sep 11, 2026
10405ee
chore: files changed src/openhuman/agent/experience/ops.rs,src/openhu…
senamakel Sep 11, 2026
d284ccb
chore: files changed src/openhuman/agent/harness/session/builder/mod.rs
senamakel Sep 11, 2026
929b880
chore: files changed src/openhuman/agent/harness/session/turn/context.rs
senamakel Sep 11, 2026
fb67e6e
chore: files changed src/openhuman/agent/orchestration/agent_teams/ru…
senamakel Sep 11, 2026
0866c61
docs(agent): document shared tool spec schemas
senamakel Sep 11, 2026
f3fbb10
Merge remote-tracking branch 'origin/main' into library-agent-handle
senamakel Sep 11, 2026
9c48902
test(session): wrap tool specs in Arc in builder tests
senamakel Sep 11, 2026
6e85cd9
test(session): assert load_skill is scoped rather than shared
senamakel Sep 11, 2026
e68e1c9
test(agent): move spec-sharing tests to a new part file
senamakel Sep 11, 2026
b6a6c77
test(agent): cover session builder edge cases
senamakel Sep 11, 2026
fe0c231
refactor(agent): move agent build logic out of setters
senamakel Sep 11, 2026
52d3022
feat(agent): add session builder module
senamakel Sep 11, 2026
4146f1f
refactor(agent): extract session builder build logic
senamakel Sep 11, 2026
bc5c838
refactor(agent): extract session builder setters into a module
senamakel Sep 11, 2026
221ac0e
chore: I don't see an actual diff in your message — the "Diff:" secti…
senamakel Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/openhuman/agent/harness/archivist/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ impl ArchivistHook {
/// `new`, `disabled` or `new_with_stubs*` — and the tests that do need a
/// deterministic LLM install the task-local around the call itself, which
/// is where it has to be anyway for `summarise` to see it.
pub fn with_config(mut self, config: Config) -> Self {
pub fn with_config(mut self, config: std::sync::Arc<Config>) -> Self {
// Probe the summariser: can this host build a chat model for the recap
// role right now? The model is dropped immediately — see above.
let probe = crate::openhuman::inference::provider::create_chat_model_with_model_id(
Expand Down Expand Up @@ -559,7 +559,10 @@ impl ArchivistHook {
segment.segment_id, summary
);
crate::openhuman::memory::goals::spawn_enrich_goals(
cfg.clone(),
// The hook now shares the factory's `Arc<Config>`; this
// detached task still owns a `Config`, so materialise one
// here. Once per closed segment, not once per live agent.
cfg.as_ref().clone(),
cfg.workspace_dir.clone(),
context,
);
Expand Down
8 changes: 7 additions & 1 deletion src/openhuman/agent/harness/archivist/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ pub struct ArchivistHook {
///
/// When `None`, the tree-ingest path is skipped. Set via
/// [`ArchivistHook::with_config`] on the production path.
pub(super) config: Option<Config>,
///
Comment thread
senamakel marked this conversation as resolved.
/// Held behind an `Arc` so the hook shares the session factory's single
/// `Config` snapshot rather than deep-cloning a 95-field struct with
/// nested `Vec`s into every live agent (openhuman#6218). `Config` is
/// immutable after construction, so sharing and copying are behaviourally
/// identical here.
pub(super) config: Option<std::sync::Arc<Config>>,
Comment thread
senamakel marked this conversation as resolved.
/// Whether an LLM summariser can be built for this workspace. `false`
/// means the heuristic bookend summary is used instead.
///
Expand Down
4 changes: 2 additions & 2 deletions src/openhuman/agent/harness/fork_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub struct ParentExecutionContext {
/// the provider for prefix-cache reuse. The parent's synthesised
/// delegation specs are deliberately absent: a sub-agent is never handed a
/// `delegate_*` tool (#4452), so there is no instance here for one.
pub all_tool_specs: Arc<Vec<ToolSpec>>,
pub all_tool_specs: Arc<Vec<Arc<ToolSpec>>>,

/// Names of the tools the parent actually advertises and will execute this
/// turn. Consumers that recommend or directly invoke parent tools consult
Expand All @@ -68,7 +68,7 @@ pub struct ParentExecutionContext {
/// building a child's tool set — that is [`Self::all_tools`] +
/// [`Self::all_tool_specs`], which carry no delegate. Empty when the
/// builder does not know the parent's surface.
pub visible_tool_specs: Arc<Vec<ToolSpec>>,
pub visible_tool_specs: Arc<Vec<Arc<ToolSpec>>>,

/// Explicit profile/channel ceiling inherited by child agents. This is not
/// the parent's role-specific visible surface: an orchestrator may delegate
Expand Down
305 changes: 305 additions & 0 deletions src/openhuman/agent/harness/session/builder/builder_build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
//! `AgentBuilder::build` — validates and assembles the final [`Agent`].

use super::{dedup_visible_tool_specs, visible_tool_specs_for_policy};
use crate::openhuman::agent::context::ContextManager;
use crate::openhuman::agent::harness::session::types::{Agent, AgentBuilder};
use crate::openhuman::tools::agent_policy::ToolPolicyEngine;
use crate::openhuman::tools::{Tool, ToolSpec};
use anyhow::Result;
use std::sync::Arc;

impl AgentBuilder {
/// Validates the configuration and constructs a new `Agent` instance.
///
/// This method is responsible for wiring together the provided components,
/// setting up the context manager, and initializing the conversation history.
/// It ensures that all required fields (provider, tools, memory, etc.) are present.
pub fn build(self) -> Result<Agent> {
let tools = self
.tools
.ok_or_else(|| anyhow::anyhow!("tools are required"))?;
// The synthesised set lives beside the durable registry, never inside
// it (`Agent::synthesized_tools`); a durable name wins a collision.
let synthesized_tools = super::drop_synthesized_name_collisions(
&tools,
self.synthesized_tools.unwrap_or_default(),
);
let synthesized_tool_names: std::collections::HashSet<String> = synthesized_tools
.iter()
.map(|tool| tool.name().to_string())
.collect();
// Durable specs first, synthesised after — every reader's order.
//
// Each schema is built once and handed out behind an `Arc`. The three
// spec views an agent keeps (`durable_tool_specs`, `tool_specs`,
// `visible_tool_specs`) overlap heavily — the durable set is a prefix
// of the full set, and the visible set is a filtered subset of it — so
// materialising them as independent `Vec<ToolSpec>` kept every
// JSON-Schema `parameters` value resident up to three times per agent.
// Sharing the leaves makes the extra views cost one pointer per entry
// (openhuman#6218).
let durable_tool_specs: Vec<Arc<ToolSpec>> =
tools.iter().map(|tool| Arc::new(tool.spec())).collect();
let tool_specs: Vec<Arc<ToolSpec>> = durable_tool_specs
.iter()
.cloned()
.chain(synthesized_tools.iter().map(|tool| Arc::new(tool.spec())))
.collect();

let mut visible_names = self.visible_tool_names.unwrap_or_default();
// Resolved here rather than at its historical position below: the pack
// withholding is per-agent (a pack is skipped for the specialist that
// owns its family), so the id has to exist before the strip.
let agent_definition_name = self
.agent_definition_name
.clone()
.unwrap_or_else(|| "main".to_string());
// On-demand tool disclosure: withhold packed tools' schemas from the
// provider and advertise `load_skill` / `use_skill` in their place. The
// tools stay in the registry below and stay executable — only the
// advertised surface shrinks. Applied here, before the policy filter,
// so the visible set and the policy session cannot disagree.
if visible_names.is_empty() {
visible_names = tools
.iter()
.chain(synthesized_tools.iter())
.map(|tool| tool.name().to_string())
.collect();
}
crate::openhuman::tools::toolpacks::strip_packed_from_visible(
&mut visible_names,
&agent_definition_name,
);
let config = self.config.clone().unwrap_or_default();
let event_session_id = self
.event_session_id
.clone()
.unwrap_or_else(|| "standalone".to_string());
let event_channel = self
.event_channel
.clone()
.unwrap_or_else(|| "internal".to_string());
// Classify both sets: a synthesised delegate needs a decision too.
let all_tools: Vec<&dyn Tool> = tools
.iter()
.chain(synthesized_tools.iter())
.map(|tool| tool.as_ref())
.collect();
let tool_policy_session = ToolPolicyEngine::build_session_from_refs(
&agent_definition_name,
&event_channel,
"session",
&config.channel_permissions,
&all_tools,
&visible_names,
);

// A child agent inherits explicit profile and channel restrictions, but
// not the primary agent's own role-specific tool scope. The Master Agent
// can write directly, while specialists may still need tools outside its
// intentionally compact default surface. Conflating those two surfaces
// silently strips specialist capabilities (#5118 merge).
//
// Build a second policy snapshot without the role visibility filter.
// `tool_policy_session` marks both channel-blocked and role-hidden tools
// as restricted, so deriving the child ceiling from it would reintroduce
// exactly that conflation.
let channel_policy_session = ToolPolicyEngine::build_session_from_refs(
&agent_definition_name,
&event_channel,
"session",
&config.channel_permissions,
&all_tools,
&std::collections::HashSet::new(),
);
let mut subagent_tool_ceiling_names = self.subagent_tool_ceiling_names.unwrap_or_default();
if channel_policy_session.has_restrictions() {
let policy_allowed: std::collections::HashSet<String> = tool_specs
.iter()
.filter(|spec| channel_policy_session.is_allowed(&spec.name))
.map(|spec| spec.name.clone())
.collect();
if subagent_tool_ceiling_names.is_empty() {
subagent_tool_ceiling_names = policy_allowed;
} else {
subagent_tool_ceiling_names.retain(|name| policy_allowed.contains(name));
if subagent_tool_ceiling_names.is_empty() {
subagent_tool_ceiling_names.insert("__subagent_no_tools__".to_string());
}
}
}

// Build the filtered spec list that the main agent sends to the
// provider. The explicit visible-tool allowlist and the resolved
// channel permission policy must stay aligned so prompt-visible
// tools cannot exceed the runtime execution boundary.
let visible_tool_specs_unfiltered =
visible_tool_specs_for_policy(&tool_specs, &visible_names, &tool_policy_session);

// Dedupe by tool name. Anthropic (and other strict providers)
// rejects a chat/completions request that lists two tools with
// the same name — OpenHuman's own backend and OpenAI silently
// accept duplicates, which hid this bug until #1710's per-role
// routing started sending the same tool list to Anthropic.
let visible_tool_specs: Vec<Arc<ToolSpec>> =
dedup_visible_tool_specs(visible_tool_specs_unfiltered);

let visible_names_list: Vec<&str> =
visible_tool_specs.iter().map(|s| s.name.as_str()).collect();
log::info!(
"[agent] tool spec filter: total={} visible={} (filter_active={} policy_restricted={}) names=[{}]",
tool_specs.len(),
visible_tool_specs.len(),
!visible_names.is_empty(),
tool_policy_session.has_restrictions(),
visible_names_list.join(", ")
);

// Pull the model source out of the builder once; the Agent holds it and
// builds a fresh tiered crate `ChatModel` set from it per turn.
let turn_model_source = self
.turn_model_source
.ok_or_else(|| anyhow::anyhow!("provider is required"))?;

let prompt_builder = self.prompt_builder.unwrap_or_else(
crate::openhuman::agent::context::prompt::SystemPromptBuilder::with_defaults,
);

let model_name = self
.model_name
.unwrap_or_else(|| crate::openhuman::config::DEFAULT_MODEL.into());

// Assemble the per-session ContextManager. The manager owns
// the prompt builder, the reduction pipeline, and the
// summarizer — every concern that touches "what's in the
// model's context window" routes through this single handle.
let context_config = self.context_config.unwrap_or_default();

// Live history reduction moved to the tinyagents graph
// (`ContextCompressionMiddleware` + `MessageTrimMiddleware`, issue
// #4249), so the session no longer constructs an in-turn summarizer
// here. The archivist hook still drives durable segment recaps on its
// own post-turn path; it is no longer coupled to context compaction.
let context = ContextManager::new(&context_config, prompt_builder);

let workspace_dir = self
.workspace_dir
.unwrap_or_else(|| std::path::PathBuf::from("."));
let action_dir = self.action_dir.unwrap_or_else(|| workspace_dir.clone());
let memory_subdir = self.memory_subdir.unwrap_or_else(|| "memory".to_string());
let session_raw_subdir = self
.session_raw_subdir
.unwrap_or_else(|| "session_raw".to_string());

let tools = Arc::new(tools);
// The pack tools live inside this registry, so they can only be pointed
// at it once it exists. Re-bind after any later rebuild of this `Arc`.
crate::openhuman::tools::toolpacks::bind_pack_registry(&tools);

Ok(Agent {
turn_model_source,
tools,
synthesized_tools: Arc::new(synthesized_tools),
tool_specs: Arc::new(tool_specs),
durable_tool_specs: Arc::new(durable_tool_specs),
visible_tool_specs: Arc::new(visible_tool_specs),
visible_tool_names: visible_names,
subagent_tool_ceiling_names,
tool_policy_session,
memory: self
.memory
.ok_or_else(|| anyhow::anyhow!("memory is required"))?,
shared_experience_memory: self.shared_experience_memory,
auto_recall: self.auto_recall,
tool_dispatcher: std::sync::Arc::from(
self.tool_dispatcher
.ok_or_else(|| anyhow::anyhow!("tool_dispatcher is required"))?,
),
config,
model_name,
model_vision: self.model_vision.unwrap_or(false),
temperature: self.temperature.unwrap_or(0.7),
workspace_dir,
action_dir,
workspace_descriptor: self.workspace_descriptor,
workflows: self.workflows.unwrap_or_default(),
auto_save: self.auto_save.unwrap_or(false),
last_memory_context: None,
last_turn_citations: Vec::new(),
pending_citations: None,
last_turn_usage_totals: None,
last_turn_hit_cap: false,
history: Vec::new(),
post_turn_hooks: self.post_turn_hooks,
learning_enabled: self.learning_enabled,
explicit_preferences_enabled: self.explicit_preferences_enabled,
event_session_id,
event_channel,
agent_definition_name: agent_definition_name.clone(),
// Canonical registry id — captured here at build time
// before any caller can call `set_agent_definition_name`
// and clobber the transcript-facing name. Used by
// `refresh_delegation_tools` to re-resolve the agent's
// `subagents` declaration against the global registry.
agent_definition_id: agent_definition_name.clone(),
active_profile_id: self.active_profile_id,
personality_soul_md: self.personality_soul_md,
personality_memory_md: self.personality_memory_md,
memory_subdir,
session_raw_subdir,
session_transcript_path: None,
session_history: None,
session_history_locator: self.session_history_locator,
persisted_transcript_messages: Vec::new(),
session_key: {
let unix_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let sanitized: String = agent_definition_name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect();
format!("{unix_ts}_{sanitized}")
},
session_parent_prefix: self.session_parent_prefix,
cached_transcript_messages: None,
context,
on_progress: None,
run_queue: None,
connected_integrations: Vec::new(),
connected_integrations_initialized: false,
runtime_config: None,
// Default to `true` (omit) so legacy / custom agents built
// without a definition stay lean. Opt-in agents thread their
// `omit_profile = false` through the builder.
omit_profile: self.omit_profile.unwrap_or(true),
omit_memory_md: self.omit_memory_md.unwrap_or(true),
payload_summarizer: self.payload_summarizer,
trigger_memory_agent: self.trigger_memory_agent.unwrap_or_default(),
tokenjuice_compression: self.tokenjuice_compression,
tool_policy: self.tool_policy.unwrap_or_else(|| {
Arc::new(crate::openhuman::agent::tool_policy::AllowAllToolPolicy)
}),
last_seen_integrations_hash: 0,
composio_integrations_rx: None,
skill_events_rx: None,
announced_integrations: std::collections::HashSet::new(),
pending_integration_announcement: Vec::new(),
announced_mcp_servers: std::collections::HashSet::new(),
pending_mcp_announcement: Vec::new(),
announced_skills: std::collections::HashSet::new(),
pending_skill_announcement: Vec::new(),
pending_skill_retraction: Vec::new(),
archivist_hook: self.archivist_hook,
synthesized_tool_names,
pending_turn_overrides: super::super::types::TurnOverrides::default(),
})
}
}
Loading
Loading