Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 36 additions & 19 deletions src/openhuman/agent/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ pub trait ToolDispatcher: Send + Sync {
/// Provide instructions for the system prompt on how the model should call tools.
fn prompt_instructions(&self, tools: &[Box<dyn Tool>]) -> String;

/// Provide instructions from already-filtered tool specs when a dispatcher
/// embeds the tool catalogue in its prompt protocol.
fn prompt_instructions_for_specs(&self, _specs: &[ToolSpec]) -> Option<String> {
None
}

/// Convert internal conversation history into provider-specific messages.
fn to_provider_messages(&self, history: &[ConversationMessage]) -> Vec<ChatMessage>;

Expand Down Expand Up @@ -116,26 +122,12 @@ impl ToolDispatcher for XmlToolDispatcher {
}

fn prompt_instructions(&self, tools: &[Box<dyn Tool>]) -> String {
let mut instructions = String::new();
instructions.push_str("## Tool Use Protocol\n\n");
instructions
.push_str("To use a tool, wrap a JSON object in <tool_call></tool_call> tags:\n\n");
instructions.push_str(
"```\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n```\n\n",
);
instructions.push_str("### Available Tools\n\n");

for tool in tools {
let _ = writeln!(
instructions,
"- **{}**: {}\n Parameters: `{}`",
tool.name(),
tool.description(),
tool.parameters_schema()
);
}
let specs = Self::tool_specs(tools);
Self::prompt_instructions_from_specs(&specs)
}

instructions
fn prompt_instructions_for_specs(&self, specs: &[ToolSpec]) -> Option<String> {
Some(Self::prompt_instructions_from_specs(specs))
}

fn to_provider_messages(&self, history: &[ConversationMessage]) -> Vec<ChatMessage> {
Expand All @@ -162,10 +154,35 @@ impl ToolDispatcher for XmlToolDispatcher {
}

fn should_send_tool_specs(&self) -> bool {
// XML dispatcher embeds tool schemas in prompt text instead of
// sending native tool specs through the provider API.
false
}
}

impl XmlToolDispatcher {
pub fn prompt_instructions_from_specs(specs: &[ToolSpec]) -> String {
let mut instructions = String::new();
instructions.push_str("## Tool Use Protocol\n\n");
instructions
.push_str("To use a tool, wrap a JSON object in <tool_call></tool_call> tags:\n\n");
instructions.push_str(
"```\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n```\n\n",
);
instructions.push_str("### Available Tools\n\n");

for spec in specs {
let _ = writeln!(
instructions,
"- **{}**: {}\n Parameters: `{}`",
spec.name, spec.description, spec.parameters
);
}

instructions
}
}

/// Text-based dispatcher that emits and parses **P-Format** ("Parameter
/// Format") tool calls — the compact `tool_name[arg1|arg2|...]` syntax.
///
Expand Down
82 changes: 52 additions & 30 deletions src/openhuman/agent/harness/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::openhuman::agent::harness::definition::{
};
use crate::openhuman::agent::host_runtime;
use crate::openhuman::agent::memory_loader::{DefaultMemoryLoader, MemoryLoader};
use crate::openhuman::agent_tool_policy::{ToolPolicyEngine, ToolPolicySession};
use crate::openhuman::config::{Config, ContextConfig};
use crate::openhuman::context::prompt::SystemPromptBuilder;
use crate::openhuman::context::{ContextManager, ProviderSummarizer, SegmentRecapSummarizer};
Expand Down Expand Up @@ -59,6 +60,21 @@ pub(super) fn dedup_visible_tool_specs(specs: Vec<ToolSpec>) -> Vec<ToolSpec> {
deduped
}

pub(super) fn visible_tool_specs_for_policy(
tool_specs: &[ToolSpec],
visible_names: &std::collections::HashSet<String>,
tool_policy: &ToolPolicySession,
) -> Vec<ToolSpec> {
tool_specs
.iter()
.filter(|spec| {
(visible_names.is_empty() || visible_names.contains(&spec.name))
&& tool_policy.is_allowed(&spec.name)
})
.cloned()
.collect()
}

impl AgentBuilder {
/// Creates a new `AgentBuilder` with default values.
pub fn new() -> Self {
Expand Down Expand Up @@ -384,21 +400,34 @@ impl AgentBuilder {
let tool_specs: Vec<ToolSpec> = tools.iter().map(|tool| tool.spec()).collect();

let visible_names = self.visible_tool_names.unwrap_or_default();
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());
let agent_definition_name = self
.agent_definition_name
.clone()
.unwrap_or_else(|| "main".to_string());
let tool_policy_session = ToolPolicyEngine::build_session(
&agent_definition_name,
&event_channel,
"session",
&config.channel_permissions,
&tools,
&visible_names,
);

// Build the filtered spec list that the main agent sends to the
// provider. When the filter is empty every tool is visible
// (backward compat). When populated, only allowlisted tools
// appear in the function-calling schema so the LLM literally
// cannot call skill tools directly — it must use spawn_subagent.
let visible_tool_specs_unfiltered: Vec<ToolSpec> = if visible_names.is_empty() {
tool_specs.clone()
} else {
tool_specs
.iter()
.filter(|spec| visible_names.contains(&spec.name))
.cloned()
.collect()
};
// 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
Expand All @@ -409,10 +438,11 @@ impl AgentBuilder {
dedup_visible_tool_specs(visible_tool_specs_unfiltered);

log::info!(
"[agent] tool spec filter: total={} visible={} (filter_active={})",
"[agent] tool spec filter: total={} visible={} (filter_active={} policy_restricted={})",
tool_specs.len(),
visible_tool_specs.len(),
!visible_names.is_empty()
!visible_names.is_empty(),
tool_policy_session.has_restrictions()
);

// Pull the provider out of the builder once. We store it on
Expand Down Expand Up @@ -495,6 +525,7 @@ impl AgentBuilder {
tool_specs: Arc::new(tool_specs),
visible_tool_specs: Arc::new(visible_tool_specs),
visible_tool_names: visible_names,
tool_policy_session,
memory: self
.memory
.ok_or_else(|| anyhow::anyhow!("memory is required"))?,
Expand All @@ -504,7 +535,7 @@ impl AgentBuilder {
memory_loader: self
.memory_loader
.unwrap_or_else(|| Box::new(DefaultMemoryLoader::default())),
config: self.config.unwrap_or_default(),
config,
model_name,
temperature: self.temperature.unwrap_or(0.7),
workspace_dir: self
Expand All @@ -519,31 +550,22 @@ impl AgentBuilder {
post_turn_hooks: self.post_turn_hooks,
learning_enabled: self.learning_enabled,
explicit_preferences_enabled: self.explicit_preferences_enabled,
event_session_id: self
.event_session_id
.unwrap_or_else(|| "standalone".to_string()),
event_channel: self.event_channel.unwrap_or_else(|| "internal".to_string()),
agent_definition_name: self
.agent_definition_name
.clone()
.unwrap_or_else(|| "main".to_string()),
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: self
.agent_definition_name
.clone()
.unwrap_or_else(|| "main".to_string()),
agent_definition_id: agent_definition_name.clone(),
session_transcript_path: None,
session_key: {
let unix_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let agent_id = self.agent_definition_name.as_deref().unwrap_or("main");
let sanitized: String = agent_id
let sanitized: String = agent_definition_name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
Expand Down
33 changes: 22 additions & 11 deletions src/openhuman/agent/harness/session/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::types::{Agent, AgentBuilder};
use crate::core::event_bus::{publish_global, DomainEvent};
use crate::openhuman::agent::dispatcher::ParsedToolCall;
use crate::openhuman::agent::error::AgentError;
use crate::openhuman::agent_tool_policy::ToolPolicyEngine;
use crate::openhuman::inference::provider::{self, ConversationMessage, Provider, ToolCall};
use crate::openhuman::memory::Memory;
use crate::openhuman::prompt_injection::{
Expand Down Expand Up @@ -157,6 +158,7 @@ impl Agent {
pub fn set_event_context(&mut self, session_id: impl Into<String>, channel: impl Into<String>) {
self.event_session_id = session_id.into();
self.event_channel = channel.into();
self.rebuild_tool_policy_session();
}

/// Override the agent definition name used for session transcript
Expand Down Expand Up @@ -195,6 +197,7 @@ impl Agent {
.unwrap_or("0");
self.session_key = format!("{prefix}_{sanitized}");
self.agent_definition_name = name;
self.rebuild_tool_policy_session();
}

/// Attach a progress event sender for real-time turn updates.
Expand All @@ -210,19 +213,27 @@ impl Agent {
}

/// Restrict which tools the main agent can see and call for this
/// session. An empty set restores the default "all visible"
/// behavior.
/// session. An empty set restores the default "all visible" behavior,
/// still subject to the configured channel permission policy.
pub fn set_visible_tool_names(&mut self, names: HashSet<String>) {
self.visible_tool_names = names;
let visible_specs = if self.visible_tool_names.is_empty() {
(*self.tool_specs).clone()
} else {
self.tool_specs
.iter()
.filter(|spec| self.visible_tool_names.contains(&spec.name))
.cloned()
.collect()
};
self.rebuild_tool_policy_session();
}

pub(super) fn rebuild_tool_policy_session(&mut self) {
self.tool_policy_session = ToolPolicyEngine::build_session(
&self.agent_definition_name,
&self.event_channel,
"session",
&self.config.channel_permissions,
self.tools.as_slice(),
&self.visible_tool_names,
);
let visible_specs = super::builder::visible_tool_specs_for_policy(
self.tool_specs.as_slice(),
&self.visible_tool_names,
&self.tool_policy_session,
);
self.visible_tool_specs = Arc::new(super::builder::dedup_visible_tool_specs(visible_specs));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Expand Down
Loading
Loading