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
8 changes: 8 additions & 0 deletions src/core/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
controllers.extend(crate::openhuman::billing::all_billing_registered_controllers());
// Team and role management
controllers.extend(crate::openhuman::team::all_team_registered_controllers());
// Local assistive surfaces over third-party provider apps
controllers.extend(
crate::openhuman::provider_surfaces::all_provider_surfaces_registered_controllers(),
);
// OS-level text input interactions
controllers.extend(crate::openhuman::text_input::all_text_input_registered_controllers());
// Voice transcription and synthesis
Expand Down Expand Up @@ -189,6 +193,7 @@ fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
schemas.extend(crate::openhuman::referral::all_referral_controller_schemas());
schemas.extend(crate::openhuman::billing::all_billing_controller_schemas());
schemas.extend(crate::openhuman::team::all_team_controller_schemas());
schemas.extend(crate::openhuman::provider_surfaces::all_provider_surfaces_controller_schemas());
schemas.extend(crate::openhuman::text_input::all_text_input_controller_schemas());
schemas.extend(crate::openhuman::voice::all_voice_controller_schemas());
schemas.extend(crate::openhuman::subconscious::all_subconscious_controller_schemas());
Expand Down Expand Up @@ -255,6 +260,9 @@ pub fn namespace_description(namespace: &str) -> Option<&'static str> {
"referral" => Some("Referral codes, stats, and apply flows via the hosted backend API."),
"billing" => Some("Subscription plan, payment links, and credit top-up via the backend."),
"team" => Some("Team member management, invites, and role changes via the backend."),
"provider_surfaces" => Some(
"Local-first assistive surfaces for provider events, respond queues, and drafts.",
),
"voice" => Some("Speech-to-text and text-to-speech using local models."),
"subconscious" => Some("Periodic local-model background awareness loop."),
"text_input" => Some("Read, insert, and preview text in the OS-focused input field."),
Expand Down
3 changes: 3 additions & 0 deletions src/core/socketio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub struct WebChannelEvent {
/// `tool_result` events.
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
/// Optional citations attached to `chat_done` payloads.
#[serde(skip_serializing_if = "Option::is_none")]
pub citations: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize)]
Expand Down
25 changes: 20 additions & 5 deletions src/openhuman/agent/harness/memory_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ pub(crate) async fn build_context(
let mut seen_keys = HashSet::new();

// Pull relevant memories for this message
if let Ok(entries) = mem.recall(user_msg, 5, None).await {
if let Ok(entries) = mem
.recall(user_msg, 5, crate::openhuman::memory::RecallOpts::default())
.await
{
let relevant: Vec<_> = entries
.iter()
.filter(|e| match e.score {
Expand All @@ -40,7 +43,11 @@ pub(crate) async fn build_context(
// facts can influence the turn in a controlled way.
let working_query = format!("working.user {user_msg}");
if let Ok(entries) = mem
.recall(&working_query, WORKING_MEMORY_LIMIT + 2, None)
.recall(
&working_query,
WORKING_MEMORY_LIMIT + 2,
crate::openhuman::memory::RecallOpts::default(),
)
.await
{
let working: Vec<_> = entries
Expand Down Expand Up @@ -86,6 +93,7 @@ mod tests {

async fn store(
&self,
_namespace: &str,
_key: &str,
_content: &str,
_category: MemoryCategory,
Expand All @@ -98,7 +106,7 @@ mod tests {
&self,
query: &str,
_limit: usize,
_session_id: Option<&str>,
_opts: crate::openhuman::memory::RecallOpts<'_>,
) -> anyhow::Result<Vec<MemoryEntry>> {
if query.starts_with("working.user ") {
return Ok(self.working.clone());
Expand All @@ -109,22 +117,29 @@ mod tests {
Ok(self.primary.clone())
}

async fn get(&self, _key: &str) -> anyhow::Result<Option<MemoryEntry>> {
async fn get(&self, _namespace: &str, _key: &str) -> anyhow::Result<Option<MemoryEntry>> {
Ok(None)
}

async fn list(
&self,
_namespace: Option<&str>,
_category: Option<&MemoryCategory>,
_session_id: Option<&str>,
) -> anyhow::Result<Vec<MemoryEntry>> {
Ok(Vec::new())
}

async fn forget(&self, _key: &str) -> anyhow::Result<bool> {
async fn forget(&self, _namespace: &str, _key: &str) -> anyhow::Result<bool> {
Ok(false)
}

async fn namespace_summaries(
&self,
) -> anyhow::Result<Vec<crate::openhuman::memory::NamespaceSummary>> {
Ok(Vec::new())
}

async fn count(&self) -> anyhow::Result<usize> {
Ok(0)
}
Expand Down
1 change: 1 addition & 0 deletions src/openhuman/agent/harness/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ impl AgentBuilder {
skills: self.skills.unwrap_or_default(),
auto_save: self.auto_save.unwrap_or(false),
last_memory_context: None,
last_turn_citations: Vec::new(),
history: Vec::new(),
post_turn_hooks: self.post_turn_hooks,
learning_enabled: self.learning_enabled,
Expand Down
7 changes: 7 additions & 0 deletions src/openhuman/agent/harness/session/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,13 @@ impl Agent {
self.history.clear();
}

/// Drain and return memory citations collected for the latest completed turn.
pub fn take_last_turn_citations(
&mut self,
) -> Vec<crate::openhuman::agent::memory_loader::MemoryCitation> {
std::mem::take(&mut self.last_turn_citations)
}

// ─────────────────────────────────────────────────────────────────
// Static helpers for turn parsing + telemetry
// ─────────────────────────────────────────────────────────────────
Expand Down
41 changes: 38 additions & 3 deletions src/openhuman/agent/harness/session/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::core::event_bus::{publish_global, DomainEvent};
use crate::openhuman::agent::dispatcher::{ParsedToolCall, ToolExecutionResult};
use crate::openhuman::agent::harness;
use crate::openhuman::agent::hooks::{self, ToolCallRecord, TurnContext};
use crate::openhuman::agent::memory_loader::collect_recall_citations;
use crate::openhuman::agent::progress::AgentProgress;
use crate::openhuman::context::prompt::{LearnedContextData, PromptContext, PromptTool};
use crate::openhuman::context::{ReductionOutcome, ARCHIVIST_EXTRACTION_PROMPT};
Expand Down Expand Up @@ -130,11 +131,39 @@ impl Agent {
if self.auto_save {
let _ = self
.memory
.store("user_msg", user_message, MemoryCategory::Conversation, None)
.store(
"",
"user_msg",
user_message,
MemoryCategory::Conversation,
None,
)
.await;
}

log::info!("[agent] loading memory context for user message");
const MEMORY_CITATION_LIMIT: usize = 5;
const MEMORY_CITATION_MIN_RELEVANCE: f64 = 0.4;
match collect_recall_citations(
self.memory.as_ref(),
user_message,
MEMORY_CITATION_LIMIT,
MEMORY_CITATION_MIN_RELEVANCE,
)
.await
{
Ok(citations) => {
log::debug!(
"[agent_loop] memory citations collected count={}",
citations.len()
);
self.last_turn_citations = citations;
}
Err(err) => {
log::warn!("[agent_loop] memory citation collection failed: {err}");
self.last_turn_citations.clear();
}
}
let context = self
.memory_loader
.load_context(self.memory.as_ref(), user_message)
Expand Down Expand Up @@ -505,7 +534,7 @@ impl Agent {
let summary = truncate_with_ellipsis(&final_text, 100);
let _ = self
.memory
.store("assistant_resp", &summary, MemoryCategory::Daily, None)
.store("", "assistant_resp", &summary, MemoryCategory::Daily, None)
.await;
}

Expand Down Expand Up @@ -1053,6 +1082,7 @@ impl Agent {
let obs_entries = self
.memory
.list(
Some("learning_observations"),
Some(&MemoryCategory::Custom("learning_observations".into())),
None,
)
Expand All @@ -1062,6 +1092,7 @@ impl Agent {
let pat_entries = self
.memory
.list(
Some("learning_patterns"),
Some(&MemoryCategory::Custom("learning_patterns".into())),
None,
)
Expand All @@ -1070,7 +1101,11 @@ impl Agent {

let profile_entries = self
.memory
.list(Some(&MemoryCategory::Custom("user_profile".into())), None)
.list(
Some("user_profile"),
Some(&MemoryCategory::Custom("user_profile".into())),
None,
)
.await
.unwrap_or_default();

Expand Down
3 changes: 3 additions & 0 deletions src/openhuman/agent/harness/session/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub struct Agent {
/// Last memory context loaded for the current turn. Stored so it can
/// be forwarded to subagents via `ParentExecutionContext`.
pub(super) last_memory_context: Option<String>,
/// Citation metadata collected from memory recall for the most recent turn.
/// Consumed by web-channel delivery to render source chips in the UI.
pub(super) last_turn_citations: Vec<crate::openhuman::agent::memory_loader::MemoryCitation>,
pub(super) history: Vec<ConversationMessage>,
pub(super) post_turn_hooks: Vec<Arc<dyn PostTurnHook>>,
pub(super) learning_enabled: bool,
Expand Down
12 changes: 10 additions & 2 deletions src/openhuman/agent/harness/subagent_runner/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,7 @@ mod tests {
impl crate::openhuman::memory::Memory for NoopMemory {
async fn store(
&self,
_namespace: &str,
_key: &str,
_content: &str,
_category: crate::openhuman::memory::MemoryCategory,
Expand All @@ -1396,26 +1397,33 @@ mod tests {
&self,
_query: &str,
_limit: usize,
_session_id: Option<&str>,
_opts: crate::openhuman::memory::RecallOpts<'_>,
) -> anyhow::Result<Vec<crate::openhuman::memory::MemoryEntry>> {
Ok(vec![])
}
async fn get(
&self,
_namespace: &str,
_key: &str,
) -> anyhow::Result<Option<crate::openhuman::memory::MemoryEntry>> {
Ok(None)
}
async fn list(
&self,
_namespace: Option<&str>,
_category: Option<&crate::openhuman::memory::MemoryCategory>,
_session_id: Option<&str>,
) -> anyhow::Result<Vec<crate::openhuman::memory::MemoryEntry>> {
Ok(vec![])
}
async fn forget(&self, _key: &str) -> anyhow::Result<bool> {
async fn forget(&self, _namespace: &str, _key: &str) -> anyhow::Result<bool> {
Ok(true)
}
async fn namespace_summaries(
&self,
) -> anyhow::Result<Vec<crate::openhuman::memory::NamespaceSummary>> {
Ok(vec![])
}
async fn count(&self) -> anyhow::Result<usize> {
Ok(0)
}
Expand Down
Loading
Loading