From cf89552273e17ca190075a958d726aefa1d34327 Mon Sep 17 00:00:00 2001 From: YellowSnnowmann <167776381+YellowSnnowmann@users.noreply.github.com> Date: Wed, 29 Apr 2026 07:25:49 +0530 Subject: [PATCH] Fix: calendar events (#1004) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../agent/agents/morning_briefing/prompt.rs | 8 +- .../agent/agents/orchestrator/prompt.rs | 15 +- src/openhuman/agent/agents/planner/prompt.rs | 8 +- .../agent/harness/subagent_runner/ops.rs | 13 + .../harness/subagent_runner/ops_tests.rs | 30 +++ tests/calendar_grounding_e2e.rs | 250 ++++++++++++++++++ 6 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 tests/calendar_grounding_e2e.rs diff --git a/src/openhuman/agent/agents/morning_briefing/prompt.rs b/src/openhuman/agent/agents/morning_briefing/prompt.rs index c76525596..f90cd434e 100644 --- a/src/openhuman/agent/agents/morning_briefing/prompt.rs +++ b/src/openhuman/agent/agents/morning_briefing/prompt.rs @@ -6,7 +6,7 @@ //! post-processing in the runner. use crate::openhuman::context::prompt::{ - render_tools, render_user_files, render_workspace, PromptContext, + render_datetime, render_tools, render_user_files, render_workspace, PromptContext, }; use anyhow::Result; @@ -29,6 +29,12 @@ pub fn build(ctx: &PromptContext<'_>) -> Result { out.push_str("\n\n"); } + let datetime = render_datetime(ctx)?; + if !datetime.trim().is_empty() { + out.push_str(datetime.trim_end()); + out.push_str("\n\n"); + } + let workspace = render_workspace(ctx)?; if !workspace.trim().is_empty() { out.push_str(workspace.trim_end()); diff --git a/src/openhuman/agent/agents/orchestrator/prompt.rs b/src/openhuman/agent/agents/orchestrator/prompt.rs index 3f596d304..b77772f14 100644 --- a/src/openhuman/agent/agents/orchestrator/prompt.rs +++ b/src/openhuman/agent/agents/orchestrator/prompt.rs @@ -9,7 +9,8 @@ //! `agent_id` in a shared section impl. use crate::openhuman::context::prompt::{ - render_tools, render_user_files, render_workspace, ConnectedIntegration, PromptContext, + render_datetime, render_tools, render_user_files, render_workspace, ConnectedIntegration, + PromptContext, }; use anyhow::Result; use std::fmt::Write; @@ -45,6 +46,12 @@ pub fn build(ctx: &PromptContext<'_>) -> Result { out.push_str("\n\n"); } + let datetime = render_datetime(ctx)?; + if !datetime.trim().is_empty() { + out.push_str(datetime.trim_end()); + out.push_str("\n\n"); + } + let workspace = render_workspace(ctx)?; if !workspace.trim().is_empty() { out.push_str(workspace.trim_end()); @@ -120,6 +127,12 @@ mod tests { assert!(!body.contains("## Delegation Guide")); } + #[test] + fn build_includes_datetime() { + let body = build(&ctx_with(&[])).unwrap(); + assert!(body.contains("## Current Date & Time")); + } + #[test] fn build_emits_delegation_guide_with_spawn_snippet() { let integrations = vec![ConnectedIntegration { diff --git a/src/openhuman/agent/agents/planner/prompt.rs b/src/openhuman/agent/agents/planner/prompt.rs index ceaec217c..0f233568c 100644 --- a/src/openhuman/agent/agents/planner/prompt.rs +++ b/src/openhuman/agent/agents/planner/prompt.rs @@ -6,7 +6,7 @@ //! post-processing in the runner. use crate::openhuman::context::prompt::{ - render_tools, render_user_files, render_workspace, PromptContext, + render_datetime, render_tools, render_user_files, render_workspace, PromptContext, }; use anyhow::Result; @@ -29,6 +29,12 @@ pub fn build(ctx: &PromptContext<'_>) -> Result { out.push_str("\n\n"); } + let datetime = render_datetime(ctx)?; + if !datetime.trim().is_empty() { + out.push_str(datetime.trim_end()); + out.push_str("\n\n"); + } + let workspace = render_workspace(ctx)?; if !workspace.trim().is_empty() { out.push_str(workspace.trim_end()); diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index e050e0de7..79955766d 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -562,12 +562,25 @@ async fn run_typed_mode( // Merge explicit orchestrator context with the parent's auto-loaded // memory context, but only when the definition opts into memory // inheritance. + let now = chrono::Local::now(); + let now_str = format!( + "Current Date & Time: {} ({})", + now.format("%Y-%m-%d %H:%M:%S"), + now.format("%Z") + ); + let mut context_parts: Vec<&str> = Vec::new(); if !definition.omit_memory_context { if let Some(ref mem_ctx) = parent.memory_context { context_parts.push(mem_ctx); } } + + // Always include temporal context for typed sub-agents. System prompts + // for sub-agents are byte-stable for KV cache reuse, so "now" must + // ride in the user message. + context_parts.push(&now_str); + if let Some(ref ctx) = options.context { context_parts.push(ctx); } diff --git a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs index fcf5588a5..28cc759f9 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs @@ -288,6 +288,36 @@ fn noop_memory() -> Arc { Arc::new(NoopMemory) } +#[tokio::test] +async fn typed_mode_injects_current_date_and_time_into_user_message() { + let provider = ScriptedProvider::new(vec![text_response("ok")]); + let parent = make_parent(provider.clone(), vec![stub("file_read")]); + let def = make_def_named_tools(&[]); + + let _ = with_parent_context(parent, async { + run_subagent( + &def, + "the actual task prompt", + SubagentRunOptions::default(), + ) + .await + }) + .await + .unwrap(); + + let captured = provider.captured.lock(); + let user_msg = captured[0] + .messages + .iter() + .find(|m| m.role == "user") + .expect("user message should be present"); + assert!( + user_msg.content.contains("Current Date & Time:"), + "subagent user message must include current date/time context, got: {}", + user_msg.content + ); +} + #[tokio::test] async fn typed_mode_returns_text_through_runner() { let provider = ScriptedProvider::new(vec![text_response("X is Y")]); diff --git a/tests/calendar_grounding_e2e.rs b/tests/calendar_grounding_e2e.rs new file mode 100644 index 000000000..9c6f46f47 --- /dev/null +++ b/tests/calendar_grounding_e2e.rs @@ -0,0 +1,250 @@ +use anyhow::Result; +use async_trait::async_trait; +use openhuman_core::openhuman::agent::dispatcher::NativeToolDispatcher; +use openhuman_core::openhuman::agent::Agent; +use openhuman_core::openhuman::providers::{ + ChatMessage, ChatRequest, ChatResponse, Provider, ToolCall, +}; +use openhuman_core::openhuman::tools::{PermissionLevel, Tool, ToolResult}; +use parking_lot::Mutex; +use serde_json::json; +use std::sync::Arc; + +struct MockCalendarProvider { + captured_messages: Arc>>, + iter_count: Arc>, +} + +#[async_trait] +impl Provider for MockCalendarProvider { + async fn chat_with_system( + &self, + _system_prompt: Option<&str>, + _message: &str, + _model: &str, + _temperature: f64, + ) -> Result { + Ok("ok".into()) + } + + async fn chat( + &self, + request: ChatRequest<'_>, + _model: &str, + _temperature: f64, + ) -> Result { + let mut count = self.iter_count.lock(); + *count += 1; + + let mut captured = self.captured_messages.lock(); + for msg in request.messages { + captured.push(msg.clone()); + } + + if *count == 1 { + // Return a tool call to GOOGLECALENDAR_EVENTS_LIST + Ok(ChatResponse { + text: Some("Checking your calendar for this week...".into()), + tool_calls: vec![ToolCall { + id: "call_1".into(), + name: "GOOGLECALENDAR_EVENTS_LIST".into(), + arguments: json!({ + "timeMin": "2026-04-27T00:00:00Z", + "timeMax": "2026-05-04T00:00:00Z" + }) + .to_string(), + }], + usage: None, + }) + } else { + // End the loop + Ok(ChatResponse { + text: Some("You have no events this week.".into()), + tool_calls: vec![], + usage: None, + }) + } + } + + fn supports_native_tools(&self) -> bool { + true + } +} + +struct MockCalendarTool; + +#[async_trait] +impl Tool for MockCalendarTool { + fn name(&self) -> &str { + "GOOGLECALENDAR_EVENTS_LIST" + } + fn description(&self) -> &str { + "List calendar events" + } + fn parameters_schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "timeMin": { "type": "string" }, + "timeMax": { "type": "string" } + } + }) + } + async fn execute(&self, _args: serde_json::Value) -> Result { + Ok(ToolResult::success("[]")) + } + fn permission_level(&self) -> PermissionLevel { + PermissionLevel::ReadOnly + } +} + +#[tokio::test] +async fn test_orchestrator_has_current_date_context() -> Result<()> { + let captured_messages = Arc::new(Mutex::new(Vec::new())); + let provider = Arc::new(MockCalendarProvider { + captured_messages: captured_messages.clone(), + iter_count: Arc::new(Mutex::new(0)), + }); + + let mut agent = Agent::builder() + .provider_arc(provider) + .tools(vec![Box::new(MockCalendarTool)]) + .tool_dispatcher(Box::new(NativeToolDispatcher)) + .memory(Arc::new(StubMemory)) + .workspace_dir(std::env::temp_dir()) + .build()?; + + // Trigger a turn + let _ = agent.turn("what is on my calendar this week?").await?; + + let messages = captured_messages.lock(); + let system_prompt = messages + .iter() + .find(|m| m.role == "system" && m.content.contains("## Current Date & Time")) + .expect("System prompt should contain Current Date & Time"); + + assert!(system_prompt.content.contains("202")); + + Ok(()) +} + +#[tokio::test] +async fn test_integrations_agent_has_current_date_context() -> Result<()> { + let captured_messages = Arc::new(Mutex::new(Vec::new())); + let provider = Arc::new(MockCalendarProvider { + captured_messages: captured_messages.clone(), + iter_count: Arc::new(Mutex::new(0)), + }); + + let _ = openhuman_core::openhuman::agent::harness::definition::AgentDefinitionRegistry::init_global_builtins(); + + let parent = openhuman_core::openhuman::agent::harness::ParentExecutionContext { + provider: provider.clone(), + all_tools: Arc::new(vec![Box::new(MockCalendarTool)]), + all_tool_specs: Arc::new(vec![MockCalendarTool.spec()]), + model_name: "test-model".into(), + temperature: 0.4, + workspace_dir: std::env::temp_dir(), + memory: Arc::new(StubMemory), + agent_config: openhuman_core::openhuman::config::AgentConfig::default(), + skills: Arc::new(vec![]), + memory_context: None, + session_id: "test-session".into(), + channel: "test".into(), + connected_integrations: vec![], + composio_client: None, + tool_call_format: openhuman_core::openhuman::context::prompt::ToolCallFormat::PFormat, + session_key: "0_test".into(), + session_parent_prefix: None, + }; + + let def = + openhuman_core::openhuman::agent::harness::definition::AgentDefinitionRegistry::global() + .unwrap() + .get("integrations_agent") + .unwrap() + .clone(); + + let _ = openhuman_core::openhuman::agent::harness::with_parent_context(parent, async { + openhuman_core::openhuman::agent::harness::run_subagent( + &def, + "list my calendar events for today", + openhuman_core::openhuman::agent::harness::SubagentRunOptions::default(), + ) + .await + }) + .await?; + + let messages = captured_messages.lock(); + // Use substring search on all user messages + let mut found = false; + for m in messages.iter() { + if m.role == "user" && m.content.contains("Current Date & Time:") { + found = true; + break; + } + } + + assert!( + found, + "User message should contain Current Date & Time context" + ); + + Ok(()) +} + +struct StubMemory; + +#[async_trait] +impl openhuman_core::openhuman::memory::Memory for StubMemory { + async fn store( + &self, + _: &str, + _: &str, + _: &str, + _: openhuman_core::openhuman::memory::MemoryCategory, + _: Option<&str>, + ) -> Result<()> { + Ok(()) + } + async fn recall( + &self, + _: &str, + _: usize, + _: openhuman_core::openhuman::memory::RecallOpts<'_>, + ) -> Result> { + Ok(vec![]) + } + async fn get( + &self, + _: &str, + _: &str, + ) -> Result> { + Ok(None) + } + async fn list( + &self, + _: Option<&str>, + _: Option<&openhuman_core::openhuman::memory::MemoryCategory>, + _: Option<&str>, + ) -> Result> { + Ok(vec![]) + } + async fn forget(&self, _: &str, _: &str) -> Result { + Ok(true) + } + async fn namespace_summaries( + &self, + ) -> Result> { + Ok(vec![]) + } + async fn count(&self) -> Result { + Ok(0) + } + async fn health_check(&self) -> bool { + true + } + fn name(&self) -> &str { + "stub" + } +}