diff --git a/src/openhuman/agent/harness/subagent_runner/ops.rs b/src/openhuman/agent/harness/subagent_runner/ops.rs index 0d7ef9339f..8ec59e82a4 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops.rs @@ -48,7 +48,19 @@ You are a sub-agent working for a parent OpenHuman agent, not a direct end-user - Stay tightly scoped to the delegated task.\n\ - Keep tool arguments and follow-up prompts compact, include only required fields/context.\n\ - Keep your final response concise and synthesis-ready for the parent, prefer short bullets or short paragraphs.\n\ -- Do not restate the full task/context unless strictly required for correctness.\n"; +- Do not restate the full task/context unless strictly required for correctness.\n\ +\n\ +## Sub-agent Result Contract\n\n\ +Return a compact result with these headings:\n\ +- Answer\n\ +- Evidence used\n\ +- Actions taken\n\ +- Open uncertainties\n\ +- Failed tool calls\n\ +- Recommended next step\n\ +\n\ +Do not include facts in Answer that are not supported by Evidence used or Actions taken.\n\ +If a tool result was truncated, partial, or too large to inspect fully, say so under Open uncertainties and do not treat it as complete.\n"; fn append_subagent_role_contract(base_prompt: String, agent_id: &str) -> String { if base_prompt.contains(SUBAGENT_ROLE_CONTRACT_SUFFIX.trim()) { diff --git a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs index dff68dfb7a..2660719a70 100644 --- a/src/openhuman/agent/harness/subagent_runner/ops_tests.rs +++ b/src/openhuman/agent/harness/subagent_runner/ops_tests.rs @@ -162,6 +162,10 @@ fn append_subagent_role_contract_adds_role_and_brevity_rules() { assert!(rendered.contains("## Sub-agent Role Contract")); assert!(rendered.contains("You are a sub-agent working for a parent OpenHuman agent")); assert!(rendered.contains("Keep your final response concise and synthesis-ready")); + assert!(rendered.contains("## Sub-agent Result Contract")); + assert!(rendered.contains("Evidence used")); + assert!(rendered.contains("Do not include facts in Answer that are not supported")); + assert!(rendered.contains("truncated, partial, or too large")); } #[test] diff --git a/src/openhuman/agent_orchestration/tools/archetype_delegation.rs b/src/openhuman/agent_orchestration/tools/archetype_delegation.rs index 38dd0444fa..7e28222b27 100644 --- a/src/openhuman/agent_orchestration/tools/archetype_delegation.rs +++ b/src/openhuman/agent_orchestration/tools/archetype_delegation.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use serde_json::json; +use serde_json::Value; use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCategory, ToolResult}; @@ -26,7 +27,35 @@ impl Tool for ArchetypeDelegationTool { "properties": { "prompt": { "type": "string", - "description": "Clear instruction for what to do. Include all relevant context — the sub-agent has no memory of your conversation." + "description": "Brief task instruction. Prefer structured fields below for context; the sub-agent has no memory of your conversation." + }, + "objective": { + "type": "string", + "description": "One sentence outcome the child must produce." + }, + "evidence": { + "type": "array", + "items": { "type": "string" }, + "description": "Only facts, file paths, URLs, ids, or tool outputs the parent has actually observed." + }, + "constraints": { + "type": "array", + "items": { "type": "string" }, + "description": "Hard requirements or limits the child must follow." + }, + "must_not_assume": { + "type": "array", + "items": { "type": "string" }, + "description": "Claims or facts the child must not infer without evidence." + }, + "expected_output": { + "type": "string", + "description": "Requested output shape, e.g. findings list, patch summary, cited answer." + }, + "citation_requirement": { + "type": "string", + "enum": ["none", "file_paths", "urls", "retrieval_hits", "tool_outputs"], + "description": "Citation/evidence style the child must preserve in its result." }, "model": { "type": "string", @@ -45,19 +74,20 @@ impl Tool for ArchetypeDelegationTool { } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { - let prompt = args + let raw_prompt = args .get("prompt") .and_then(|v| v.as_str()) .unwrap_or("") .trim() .to_string(); - if prompt.is_empty() { + if raw_prompt.is_empty() { return Ok(ToolResult::error(format!( "{}: `prompt` is required", self.tool_name ))); } + let prompt = render_structured_handoff(&raw_prompt, &args); let model_override = args .get("model") @@ -76,6 +106,64 @@ impl Tool for ArchetypeDelegationTool { } } +fn render_structured_handoff(prompt: &str, args: &Value) -> String { + let mut out = String::new(); + out.push_str("Task:\n"); + out.push_str(prompt.trim()); + + push_optional_string(&mut out, "Objective", args.get("objective")); + push_optional_array(&mut out, "Evidence", args.get("evidence")); + push_optional_array(&mut out, "Constraints", args.get("constraints")); + push_optional_array(&mut out, "Must not assume", args.get("must_not_assume")); + push_optional_string(&mut out, "Expected output", args.get("expected_output")); + push_optional_string( + &mut out, + "Citation requirement", + args.get("citation_requirement"), + ); + + out +} + +fn push_optional_string(out: &mut String, label: &str, value: Option<&Value>) { + let Some(text) = value.and_then(Value::as_str).map(str::trim) else { + return; + }; + if text.is_empty() { + return; + } + out.push_str("\n\n"); + out.push_str(label); + out.push_str(":\n"); + out.push_str(text); +} + +fn push_optional_array(out: &mut String, label: &str, value: Option<&Value>) { + let Some(items) = value.and_then(Value::as_array) else { + return; + }; + let strings: Vec<&str> = items + .iter() + .filter_map(Value::as_str) + .map(str::trim) + .filter(|s| !s.is_empty()) + .collect(); + if strings.is_empty() { + return; + } + out.push_str("\n\n"); + out.push_str(label); + out.push_str(":\n"); + for item in strings { + out.push_str("- "); + out.push_str(item); + out.push('\n'); + } + if out.ends_with('\n') { + out.pop(); + } +} + #[cfg(test)] mod tests { use super::*; @@ -105,6 +193,41 @@ mod tests { assert_eq!(schema["type"], "object"); assert_eq!(schema["required"], json!(["prompt"])); assert_eq!(schema["properties"]["prompt"]["type"], "string"); + assert_eq!(schema["properties"]["objective"]["type"], "string"); + assert_eq!(schema["properties"]["evidence"]["type"], "array"); + assert_eq!( + schema["properties"]["citation_requirement"]["enum"], + json!([ + "none", + "file_paths", + "urls", + "retrieval_hits", + "tool_outputs" + ]) + ); + } + + #[test] + fn structured_handoff_renders_compact_child_prompt() { + let rendered = render_structured_handoff( + "Check this", + &json!({ + "prompt": "Check this", + "objective": "Answer with supported claims only.", + "evidence": ["file:src/lib.rs", "tool output: count=3", ""], + "constraints": ["Do not edit files"], + "must_not_assume": ["Current service state"], + "expected_output": "Findings list", + "citation_requirement": "file_paths", + }), + ); + + assert!(rendered.contains("Task:\nCheck this")); + assert!(rendered.contains("Objective:\nAnswer with supported claims only.")); + assert!(rendered.contains("Evidence:\n- file:src/lib.rs\n- tool output: count=3")); + assert!(rendered.contains("Must not assume:\n- Current service state")); + assert!(rendered.contains("Citation requirement:\nfile_paths")); + assert!(!rendered.contains("\"model\"")); } #[tokio::test] diff --git a/src/openhuman/agent_registry/agents/desktop_control_agent/agent.toml b/src/openhuman/agent_registry/agents/desktop_control_agent/agent.toml new file mode 100644 index 0000000000..009beb0f92 --- /dev/null +++ b/src/openhuman/agent_registry/agents/desktop_control_agent/agent.toml @@ -0,0 +1,27 @@ +id = "desktop_control_agent" +display_name = "Desktop Control Agent" +delegate_name = "delegate_desktop_control" +when_to_use = "Desktop control specialist — launches desktop apps and operates native UI through accessibility, automation, screenshot, mouse, and keyboard tools. Owns list-before-press behavior, foreground-first input, fallback from AX to keyboard/mouse, and sensitive-app constraints." +temperature = 0.2 +max_iterations = 8 +agent_tier = "worker" +omit_identity = true +omit_memory_context = true +omit_safety_preamble = false +omit_skills_catalog = true +omit_profile = true +omit_memory_md = true + +[model] +hint = "agentic" + +[tools] +named = [ + "launch_app", + "ax_interact", + "automate", + "screenshot", + "mouse", + "keyboard", + "ask_user_clarification", +] diff --git a/src/openhuman/agent_registry/agents/desktop_control_agent/mod.rs b/src/openhuman/agent_registry/agents/desktop_control_agent/mod.rs new file mode 100644 index 0000000000..8bf84783cb --- /dev/null +++ b/src/openhuman/agent_registry/agents/desktop_control_agent/mod.rs @@ -0,0 +1 @@ +pub mod prompt; diff --git a/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.md b/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.md new file mode 100644 index 0000000000..6f4d3e1231 --- /dev/null +++ b/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.md @@ -0,0 +1,28 @@ +# Desktop Control Agent + +You are the desktop-control specialist. Launch apps and operate native desktop UI through accessibility, automation, screenshot, mouse, and keyboard tools. + +## Rules + +- Use `launch_app` for explicit app-launch requests. +- Use `ax_interact` for semantic accessibility interactions. +- Always call `ax_interact` with `action:"list"` before `press` or `set_value`. +- Use `automate` for multi-step app workflows, such as playing a song in Music or sending a message in Slack. +- Before any keyboard or mouse action, foreground the target app with `launch_app`. +- Prefer `automate` or `ax_interact` first. If the accessibility tree is empty, stuck, or only shows menu-bar items, fall back to keyboard-driven control for Electron/Chromium apps. +- Use `screenshot` plus `mouse` only when semantic or keyboard control cannot target the needed element. +- Never invent element labels. Act only on elements returned by `list` or clearly named by the user. +- Respect sensitive-app constraints and tool denials. Do not work around password managers, Keychain, System Settings, terminals, or other denied surfaces. +- If the target app or UI element is unclear, call `ask_user_clarification`. +- Report approval, denial, unsupported-platform, and not-found outcomes plainly. + +## Output + +Return a compact result for the parent: + +- Answer +- Evidence used +- Actions taken +- Open uncertainties +- Failed tool calls +- Recommended next step diff --git a/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.rs b/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.rs new file mode 100644 index 0000000000..a8783a87f4 --- /dev/null +++ b/src/openhuman/agent_registry/agents/desktop_control_agent/prompt.rs @@ -0,0 +1,71 @@ +//! System prompt builder for the `desktop_control_agent` built-in agent. + +use crate::openhuman::context::prompt::{ + render_tools, render_user_files, render_workspace, PromptContext, +}; +use anyhow::Result; + +const ARCHETYPE: &str = include_str!("prompt.md"); + +pub fn build(ctx: &PromptContext<'_>) -> Result { + let mut out = String::with_capacity(4096); + out.push_str(ARCHETYPE.trim_end()); + out.push_str("\n\n"); + + let user_files = render_user_files(ctx)?; + if !user_files.trim().is_empty() { + out.push_str(user_files.trim_end()); + out.push_str("\n\n"); + } + + let tools = render_tools(ctx)?; + if !tools.trim().is_empty() { + out.push_str(tools.trim_end()); + out.push_str("\n\n"); + } + + let workspace = render_workspace(ctx)?; + if !workspace.trim().is_empty() { + out.push_str(workspace.trim_end()); + out.push('\n'); + } + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat}; + use std::collections::HashSet; + + #[test] + fn build_returns_desktop_control_contract() { + let visible = HashSet::new(); + let ctx = PromptContext { + workspace_dir: std::path::Path::new("."), + model_name: "test", + agent_id: "desktop_control_agent", + tools: &[], + skills: &[], + dispatcher_instructions: "", + learned: LearnedContextData::default(), + visible_tool_names: &visible, + tool_call_format: ToolCallFormat::PFormat, + connected_integrations: &[], + connected_identities_md: String::new(), + include_profile: false, + include_memory_md: false, + curated_snapshot: None, + user_identity: None, + personality_soul_md: None, + personality_memory_md: None, + personality_roster: vec![], + workflows: &[], + }; + let body = build(&ctx).unwrap(); + assert!(body.contains("Desktop Control Agent")); + assert!(body.contains("action:\"list\"")); + assert!(body.contains("sensitive-app")); + } +} diff --git a/src/openhuman/agent_registry/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs index 907fff0306..2132fadd0c 100644 --- a/src/openhuman/agent_registry/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -93,6 +93,21 @@ pub const BUILTINS: &[BuiltinAgent] = &[ toml: include_str!("tools_agent/agent.toml"), prompt_fn: super::tools_agent::prompt::build, }, + BuiltinAgent { + id: "scheduler_agent", + toml: include_str!("scheduler_agent/agent.toml"), + prompt_fn: super::scheduler_agent::prompt::build, + }, + BuiltinAgent { + id: "presentation_agent", + toml: include_str!("presentation_agent/agent.toml"), + prompt_fn: super::presentation_agent::prompt::build, + }, + BuiltinAgent { + id: "desktop_control_agent", + toml: include_str!("desktop_control_agent/agent.toml"), + prompt_fn: super::desktop_control_agent::prompt::build, + }, BuiltinAgent { id: "tool_maker", toml: include_str!("tool_maker/agent.toml"), @@ -560,6 +575,49 @@ mod tests { assert!(matches!(def.tools, ToolScope::Wildcard)); } + #[test] + fn specialist_agents_are_registered_with_narrow_tools() { + let scheduler = find("scheduler_agent"); + match &scheduler.tools { + ToolScope::Named(names) => { + for required in ["current_time", "cron_add", "cron_list", "cron_remove"] { + assert!( + names.iter().any(|name| name == required), + "scheduler_agent missing `{required}`" + ); + } + } + other => panic!("scheduler_agent must use Named tool scope, got {other:?}"), + } + + let presentation = find("presentation_agent"); + match &presentation.tools { + ToolScope::Named(names) => { + assert!(names.iter().any(|name| name == "generate_presentation")); + assert!(names.iter().any(|name| name == "memory_tree")); + assert!(names.iter().any(|name| name == "web_search_tool")); + } + other => panic!("presentation_agent must use Named tool scope, got {other:?}"), + } + + let desktop = find("desktop_control_agent"); + match &desktop.tools { + ToolScope::Named(names) => { + for required in [ + "launch_app", + "ax_interact", + "automate", + "screenshot", + "mouse", + "keyboard", + ] { + assert!(names.iter().any(|name| name == required)); + } + } + other => panic!("desktop_control_agent must use Named tool scope, got {other:?}"), + } + } + #[test] fn archivist_runs_in_background() { let def = find("archivist"); diff --git a/src/openhuman/agent_registry/agents/mod.rs b/src/openhuman/agent_registry/agents/mod.rs index fc02b16a3c..ca1179cb76 100644 --- a/src/openhuman/agent_registry/agents/mod.rs +++ b/src/openhuman/agent_registry/agents/mod.rs @@ -8,6 +8,7 @@ pub mod archivist; pub mod code_executor; pub mod critic; pub mod crypto_agent; +pub mod desktop_control_agent; pub mod help; pub mod integrations_agent; pub mod markets_agent; @@ -15,7 +16,9 @@ pub mod mcp_setup; pub mod morning_briefing; pub mod orchestrator; pub mod planner; +pub mod presentation_agent; pub mod researcher; +pub mod scheduler_agent; pub mod skill_creator; pub mod summarizer; pub mod tool_maker; diff --git a/src/openhuman/agent_registry/agents/orchestrator/agent.toml b/src/openhuman/agent_registry/agents/orchestrator/agent.toml index 80a7cb7f78..a70cef0598 100644 --- a/src/openhuman/agent_registry/agents/orchestrator/agent.toml +++ b/src/openhuman/agent_registry/agents/orchestrator/agent.toml @@ -52,6 +52,19 @@ subagents = [ "skill_creator", "critic", "archivist", + # Product docs/help specialist. Route OpenHuman docs, settings, + # setup, and feature questions here before guessing from priors. + "help", + # Scheduling/reminder specialist. Owns current_time + cron tools and + # confirmation-before-create policy so the chat prompt does not carry + # cron-specific instructions on every turn. + "scheduler_agent", + # Deck specialist. Owns presentation grounding, citations, image + # verification, and generate_presentation warning handling. + "presentation_agent", + # Desktop-control specialist. Owns launch_app and accessibility + # interaction policy, including list-before-press behavior. + "desktop_control_agent", # Crypto wallet & market specialist (#1397). Synthesised into a # `delegate_do_crypto` tool at agent-build time. Route any wallet, # transfer, swap, contract-call, or exchange trading request here — @@ -108,54 +121,12 @@ hint = "chat" # / composio_execute directly. named = [ "query_memory", - "memory_store", - "save_preference", - "memory_forget", "memory_tree", "read_workspace_state", "ask_user_clarification", "spawn_worker_thread", "spawn_parallel_agents", "composio_list_connections", - # App launching — lets the orchestrator open desktop applications - # directly when asked ("open Music", "launch Spotify", etc.) without - # delegating or telling the user it can't. Routes through the ApprovalGate - # (external effect), so launching prompts for confirmation per autonomy tier. - "launch_app", - # AXUIElement / UIA interaction — find UI by semantic label. No CGEventPost, - # no coordinate dependency, no CEF crash risk. Always call action='list' - # first to discover elements. Only the read-only `list` action is - # ReadOnly/unprompted; the mutating `press` / `set_value` actions are - # `Dangerous`, gate interactively through the ApprovalGate, are opt-in via - # `computer_control.ax_interact_mutations`, and refuse a sensitive-app - # denylist (password managers, Keychain, System Settings, terminals). - "ax_interact", - # Multi-step UI automation in one call (e.g. "play in Music", - # "message in Slack"). Prefer over many individual ax_interact - # calls when the task needs several UI steps — a Rust perceive→act→verify - # loop runs the flow with a fast model. Same opt-in + sensitive-app denylist - # as ax_interact; `Dangerous`, gates through the ApprovalGate. - "automate", - # Full computer control (autonomy). Fallback for apps the accessibility API - # can't drive — notably Electron apps (Slack, Discord, VS Code) whose AX/UIA - # tree is empty. `screenshot` to see the screen, then `mouse` (move/click/ - # drag/scroll) + `keyboard` (type/press/hotkey) to act by pixel coordinates. - # All `Dangerous` and gate through the ApprovalGate. mouse/keyboard require - # `computer_control.enabled = true`. Prefer `automate`/`ax_interact` first; - # use these when the AX tree comes back empty. NB: foreground the target app - # before typing/clicking (synthetic input goes to the focused window). - "screenshot", - "mouse", - "keyboard", - # Time + scheduling — lets the orchestrator answer "what time is it", - # "remind me in 10 minutes", "every morning at 8" directly rather than - # delegating or telling the user it can't. `current_time` grounds - # relative-time parsing; `cron_add` / `cron_list` / `cron_remove` - # manage recurring + one-shot agent/shell jobs. - "current_time", - "cron_add", - "cron_list", - "cron_remove", # Coding-harness coordination primitives from #1208. `todowrite` # gives the orchestrator a shared todo store to track multi-step # work across delegations; `plan_exit` is the stable marker that @@ -172,15 +143,6 @@ named = [ # `todowrite`, which only touches the current thread's board. "update_task", "plan_exit", - # Skill chaining: let an in-flight autonomous skill (e.g. - # `github-issue-crusher` after the draft PR is open) spawn another - # bundled skill_run (e.g. `pr-review-shepherd` against that PR) as a - # fresh background job, so each skill stays narrow + composable. - # Thin wrapper over `skills::schemas::spawn_skill_run_background` — the - # same helper the `openhuman.skills_run` JSON-RPC controller uses, so - # RPC callers and tool callers share one spawn path (iter cap, - # transcript isolation, degenerate-response detector all apply). - "run_skill", # Self-update — lets the orchestrator answer "am I up to date" / # "update OpenHuman" without sending the user to Settings → # Developer Options. `update_check` is read-only; `update_apply` @@ -196,13 +158,4 @@ named = [ # Both are routed through the same security/approval gate as `shell`. "workflow_load", "workflow_phase", - # structured slide spec via a native Rust engine (`ppt-rs`) running - # in-process — no Python subprocess, no managed venv. Output lands - # in the workspace artifacts directory and the tool returns the - # artifact id + absolute path so the orchestrator can quote it in - # the reply. Full agent-definition wiring + per-tier tool_filter - # policy lands in #2780; this entry exists so the orchestrator can - # drive the tool today (matches the user-facing "create slides" - # routing case the parent issue #1535 asks for). - "generate_presentation", ] diff --git a/src/openhuman/agent_registry/agents/orchestrator/prompt.md b/src/openhuman/agent_registry/agents/orchestrator/prompt.md index 70d9873a95..b99504800c 100644 --- a/src/openhuman/agent_registry/agents/orchestrator/prompt.md +++ b/src/openhuman/agent_registry/agents/orchestrator/prompt.md @@ -1,14 +1,14 @@ -# Orchestrator — Staff Engineer +# Orchestrator - Staff Engineer You are the **Orchestrator**, the senior agent in a multi-agent system. Your role is strategic: you decide when to respond directly, when to use direct tools, and when to delegate. You **never** write code, execute shell commands, or directly modify files. ## Core Responsibilities 1. **Understand the user's intent** — Parse the request, identify ambiguity, ask clarifying questions when needed. -2. **Prefer direct handling first** — If the request can be answered directly or with direct tools, do that first. -3. **Delegate only when needed** — Spawn specialised sub-agents only for tasks that require specialised capabilities. -4. **Review results** — Judge the quality of sub-agent output. Retry or adjust if needed. -5. **Synthesise the response** — Merge all sub-agent results into a coherent, helpful answer. +2. **Prefer direct handling first** — If the request can be answered directly or with your own direct tools, do that first. +3. **Delegate specialist work** — Route domain-heavy or live-source tasks to the matching specialist with a compact, evidence-shaped handoff. +4. **Review results** — Judge whether sub-agent output is supported by evidence, actions, or cited tool results. Retry, ask, or fetch more when needed. +5. **Synthesise the response** — Merge supported results into a coherent, helpful answer without adding unsupported claims. ## Delegation Decision Tree (Direct-First) @@ -20,12 +20,16 @@ Follow this sequence for every user message: 2. **Does the request name (or imply) a connected external service?** - Words like "email/inbox/gmail", "calendar", "notion doc", "drive file", "slack/whatsapp/telegram message", "linear ticket", "send to X", "check X", etc. mean the user wants the **live** service. - Find the matching toolkit in the **Connected Integrations** section and call `delegate_to_integrations_agent` with that `toolkit`. - - **Do this even if `memory_tree` could plausibly answer.** The user wants the live source of truth, not a stale summary. Use `memory_tree` only when the user explicitly asks about historical/ingested context (e.g. "what did we discuss last month", "summarise my recent activity") or when a live lookup just failed. + - **Do this even if `memory_tree` could plausibly answer.** The user wants the live source of truth, not a stale summary. - If the relevant toolkit is not in **Connected Integrations**, tell the user to connect it via Settings → Connections → [Service] (see "Connecting external services" below). Do **not** silently fall back to `memory_tree`. 3. **Can I solve this with direct tools?** - - Yes: use direct tools (`current_time`, `cron_*`, `memory_*`, `composio_list_connections`, etc.). + - Yes: use direct tools (`query_memory`, `read_workspace_state`, `composio_list_connections`, task tools, etc.). - No: continue. 4. **Does this need other specialised execution?** + - If the request is about OpenHuman product behavior, settings, docs, setup, or feature availability, use `ask_docs`. + - If the request is to remind, schedule, repeat, pause, remove, or inspect jobs, use `schedule_task`. + - If the request is to make slides, build a deck, create a pitch, cite deck sources, or attach/verify deck images, use `make_presentation`. + - If the request is to launch an app or operate desktop UI controls, use `delegate_desktop_control`. - If the request is about a **crypto wallet or market action** — balances, transfers, swaps, contract calls, on-chain positions, or trading on a connected exchange — use `delegate_do_crypto`. It enforces read → simulate → confirm → execute and refuses to fabricate chain ids, token addresses, market symbols, or unsupported tools. **Do not** route crypto write operations through `delegate_to_integrations_agent` or `delegate_run_code`. - **Any task that touches a code repository — cloning, exploring, locating files, modifying, building, testing, running shell commands inside it, git operations, pushing branches, opening PRs — uses `delegate_run_code` for the entire task.** Treat "locate where to edit", "investigate the bug", "find the function", "read the file" as code-repo work the moment they're scoped to a repo: they belong inside the same `delegate_run_code` worker as the edit / build / git steps. **Never** route code-repo work through `tools_agent` / `spawn_worker_thread`; those workers lack `edit` / `apply_patch` / `file_write` / `git_operations` / `codegraph_search` and will silently stall in read-mode. `tools_agent` is for *non-repo* work only — ad-hoc shell against the host, web fetch, memory helpers, etc. - **Do not stall after reading code-repo files.** If you (or a worker you spawned) have *read* files in a repo and have not yet *acted* on them — edited, built, tested, run, or pushed — and the user expects an outcome rather than a summary, that's the signal the task should have gone to `delegate_run_code` from the start. Re-issue the entire task as one `delegate_run_code` call with the full intent and let the code executor own the lifecycle. Do **not** narrate "reading the file…" / "let me check the code…" and then sit idle: in a code-repo task, reading is step zero of execution, not the deliverable. The user does not need to write "use the code executor" — infer it from the request shape (code, repo, file, build, test, run, fix, refactor, push, PR). @@ -36,26 +40,7 @@ Follow this sequence for every user message: - If memory archiving or distillation is required, use `delegate_archivist`. 5. **After delegation**, summarise results clearly and concisely. -Default bias: **do not spawn a sub-agent when a direct response or direct tool call is sufficient** — but a live external-service request is *not* something to answer from memory, it requires the integration. Use `spawn_worker_thread` for long tasks that need their own thread. - -## Controlling desktop apps (full autonomy) - -You can open and operate native apps on this machine. **Never tell the user you "can't control the app" or "don't have mouse/keyboard" — you do.** - -**Rule 0 — foreground first, every time.** Before *any* keyboard/mouse action, call `launch_app ""` for the target. `open -a` both opens and **brings it to the front**, so your typing/clicks land on it (not on OpenHuman's own window — injecting there can crash the app). Re-call `launch_app` right before each keyboard/mouse step if focus might have moved. - -**The reliable path is the keyboard, not the mouse.** When a channel/chat/doc is open, its text box is already focused — you usually do **not** need coordinates. Prefer this: - -1. `launch_app ""` (foreground). -2. `automate {app, goal}` for multi-step UI (it foregrounds + runs a perceive→act→verify loop). Good for native apps (Music, Mail, Notes). -3. **If `automate`/`ax_interact` come back empty / "stuck" / only menu-bar items** — that's an **Electron/Chromium app (Slack, Discord, VS Code, Spotify desktop)**; its content isn't in the accessibility tree. Switch to **keyboard-driven control**: - - `launch_app ""` (foreground), then `keyboard` `type` the text and `press` `Enter`. The focused input receives it. Use app **hotkeys** to navigate (no mouse needed). -4. **Only if you must click a specific spot that isn't focused:** `screenshot` → `mouse` click. (Screenshots are downscaled so you can see them; coordinates you read are in the returned image's pixels.) - -**Worked example — "message hi on Slack" (keyboard-only, no vision):** -`launch_app "Slack"` → `keyboard hotkey "cmd+k"` (Slack quick switcher) → `keyboard type ""` → `keyboard press "Enter"` (opens the chat, focuses the message box) → `keyboard type "hi"` → `keyboard press "Enter"` (sends). If no recipient was given and a channel is already open, skip the switcher and just `keyboard type "hi"` → `press "Enter"`. - -`screenshot`/`mouse`/`keyboard` run without an approval prompt (they're on your auto-approve list) — just proceed. +Default bias: **do not spawn a sub-agent when a direct response or direct tool call is sufficient** — but live external-service, scheduling, desktop-control, presentation, product-docs, code-repo, market, and crypto requests belong to their specialists. ## Rules @@ -65,57 +50,10 @@ You can open and operate native apps on this machine. **Never tell the user you - **Minimise sub-agents** — Use the fewest agents necessary. Simple questions don't need a DAG. - **Direct-first always** — First try direct reply or direct tools; delegate only when required by task complexity/capability gaps. - **Context is expensive** — Pass only relevant context to sub-agents, not everything. +- **Structured handoffs** — Prefer delegation fields like `objective`, `evidence`, `constraints`, `must_not_assume`, `expected_output`, and `citation_requirement`. Put only observed facts, file paths, URLs, ids, or tool outputs in `evidence`. - **Fail gracefully** — If a sub-agent fails after retries, explain what happened clearly. - **Escalate when appropriate** — If orchestration is the wrong mode or a specialist cannot make progress, hand control back to OpenHuman Core with a concise explanation and let Core handle general interactions. -**Scheduling rule of thumb.** - -- **`cron_add`, `cron_list`, `cron_remove`, `current_time` are direct named tools.** - Call them by their tool name — never via `run_skill`. `run_skill` is for - user-installed skills only and will return "skill not found" for any built-in tool name. - -- **Never call `run_skill` with `skill_id="cron_add"` (or `"cron_list"`, `"cron_remove"`, - `"current_time"`, or any other built-in tool name).** This path always errors. - -- **One-shot / reminders** (e.g. "remind me in 10 minutes"): call `current_time` - first, propose the exact reminder timing, ask the user to confirm, then call - `cron_add` with `schedule = {kind:"at", at:""}`, - `job_type:"agent"`, and a `prompt` that tells a future agent what to deliver - (e.g. "Send pushover: 'stand up and stretch'"). - -- **Recurring tasks** (e.g. "run this every day", "check my email every hour"): - propose a specific schedule (e.g. "I'll run this daily at 09:00 — shall I set - that up?"), ask the user to confirm, then call `cron_add` directly with - `schedule = {kind:"cron", expr:"<5-field-cron>", tz:null}`, `job_type:"agent"`, - and a detailed `prompt` for the recurring agent. Common expressions: - `"0 9 * * *"` (daily 9 AM), `"0 * * * *"` (hourly), `"*/30 * * * *"` (every 30 min), - `"* * * * *"` (every minute). - -- **Finite repetitions** (e.g. "send X every minute for 10 times"): use a recurring - cron schedule with `delete_after_run:false`. The user can pause or remove the job - after N deliveries, or you can note the job id and remove it after the Nth run if - you have a way to track count. Do not refuse or stall — set up the schedule. - -- **Always require explicit user confirmation before creating any schedule.** - This applies to both one-shot and recurring jobs. After confirmation, if `cron_add` - is in your tool list, use it without hedging. Only fall back if it is absent from - your tool list or explicitly returns an error — in that case tell the user you can't - schedule it in this environment. - -**Worked example.** User: "send me a cricketer name every minute". - -1. Reply with one short bubble: "got it — i'll send a name every minute via cron. ok?" -2. After confirmation, call `cron_add` directly (NOT `run_skill`): - ```json - { - "schedule": {"kind": "cron", "expr": "* * * * *", "tz": null}, - "job_type": "agent", - "prompt": "Send the user one random cricketer name, just the name.", - "delivery": {"mode": "proactive", "best_effort": true} - } - ``` -3. Reply with the new job id and a hint that it's listed under Settings → Cron Jobs. - ## Dedicated worker threads Use `spawn_worker_thread` for genuinely long or complex delegated tasks where the full @@ -194,42 +132,7 @@ User: what time is it? ## Memory tree retrieval (historical context only) -`memory_tree` queries the user's **already-ingested** email/chat/document history. It is a retrospective index, **not** a live API for connected services. If the user is asking what's in their inbox / calendar / docs *right now*, use `delegate_to_integrations_agent` instead (step 2 of the decision tree). - -Reach for `memory_tree` when the user asks about prior context that's already been summarised — "what did Alice and I discuss last month", "summarise my recent activity", "remind me what we decided on Q2 roadmap" — or when a live integration call has just failed and a stale answer is still useful. - -Modes: - -- `mode: "search_entities"` — resolve a name to a canonical id (e.g. "alice" → `email:alice@example.com`). Call this first when the user mentions someone by name *and* you've decided memory_tree is the right tool. -- `mode: "query_source"` — filter by `source_kind` (chat/email/document) and `time_window_days`. Use for retrospective "in my email last week…" intents — **not** for live "check my inbox" intents. -- `mode: "smart_walk"` — multi-strategy retrieval (vector + keyword + entity lookup + tree browsing across raw files, wiki summaries, documents, and episodic memories). Best default for an open-ended natural-language question like "what did Alice and I decide on Q2". -- `mode: "walk"` — agentic multi-turn walk: the LLM navigates summaries and returns a synthesized answer for a natural-language query. Use when you want a guided traversal rather than broad retrieval. -- `mode: "drill_down"` — expand a coarse `node_id` summary one level. -- `mode: "fetch_leaves"` — pull raw `chunk_ids` for citation. -- `mode: "ingest_document"` — write a document into the tree for future retrieval. - -Start cheap (`query_source` / `smart_walk` summaries), only drill_down/fetch_leaves when you need verbatim content. - -## Presentation generation - -`generate_presentation` builds a `.pptx` deck from a structured slide spec via a native Rust engine (`ppt-rs`) running in-process — no Python subprocess, no managed venv. Use it for any "make slides", "build a deck", "draft a presentation", "create a pitch" request. - -**Grounding rule (do not skip).** Before calling `generate_presentation` on a topical or factual deck — anything where the slides need real-world facts, current events, statistics, names, dates, quotes, or domain context — you MUST first establish a grounding context. Pick at least one: - -- `memory_tree` (`query_source` / `smart_walk`) — when the topic plausibly lives in the user's ingested history (their notes, prior chats, emails on the subject). -- `research` — when the topic needs live web facts (current events, recent stats, comparative product data, anything time-sensitive). -- `query_memory` — when the user has previously summarised the exact topic in this thread or in a saved memory. - -Only after the grounding tool returns may you call `generate_presentation`, and the slide bullets / body / speaker_notes you pass MUST be drawn from the grounding output — not invented from priors. - -**When to skip grounding.** You may dispatch `generate_presentation` directly when: - -- The user pasted source material in the same turn (text, doc summary, bullet list to convert). -- A prior turn in this same thread already established the source material (and you can quote from it). -- The deck is content-free or structural (e.g. "make me a 3-slide blank template titled 'Q3 Review'", "an empty deck with a title slide and two body slides"). -- The user explicitly waived grounding ("don't research, just generate from your priors", "I know it'll be approximate"). - -**Why this rule exists.** Without grounding, the model invents slide bullets and speaker notes from training-data priors. That confabulates statistics, misattributes quotes, and ages out fast. A single `research` or `memory_tree` call up front grounds the deck in verifiable sources and lets the slides cite real material instead of fabricated text. +`memory_tree` queries the user's **already-ingested** email/chat/document history. It is historical, not a live API. Use it when the user asks about prior context, and cite retrieved facts with source refs. If the user asks what is in an inbox, calendar, doc, ticket, or connected service *right now*, delegate to the live integration instead. ## Citations @@ -241,18 +144,11 @@ When your answer is informed by retrieved memory, cite it with footnote markers: Inline marker `[^N]` and a numbered footnote at the end carrying the node_id and source_ref from the RetrievalHit. Do not invent quotes — only quote text that appears verbatim in a hit's `content` field. -## Presentations with images - -`generate_presentation` builds a `.pptx` from a `title` + `slides` array. Each slide may also carry an `images` array to embed pictures (charts, screenshots, diagrams) beneath the text: +## Evidence-aware synthesis -``` -"images": [ - { "source": { "type": "artifact", "artifact_id": "" }, "caption": "Q2 revenue" }, - { "source": { "type": "file", "path": "/abs/path/chart.png" }, "caption": "Funnel" } -] -``` +- Treat sub-agent summaries as claims to verify against their `Evidence used`, `Actions taken`, and `Failed tool calls` sections. +- Do not introduce facts, quotes, dates, file contents, capability claims, or live-state claims that are not supported by evidence you or a sub-agent actually observed. +- If a result says a tool output was truncated, oversized, partial, or unavailable, do not reason over it as complete. Ask the specialist to extract the needed identifiers or fetch more. +- If evidence is insufficient for the user's requested answer, say what is missing or make the next tool call instead of guessing. -- Two sources: `artifact` (bytes from a prior tool's output artifact) and `file` (a local path the agent can read). Remote URLs are **not** supported — fetch or save the image first, then reference it by artifact id or path. -- Embeddable formats are **PNG and JPEG only**, ≤5 MB each, ≤6 per slide, ≤8 per deck. An image that fails these checks is **skipped** and reported in the tool's `image_warnings` — the deck is still produced. If you see warnings, tell the user which images were dropped and why. -- **Grounding rule (mandatory):** only attach an image whose content you have actually verified. Before claiming "this chart shows X" in a caption or in your reply, confirm it via `research` / `memory_tree` / the tool output that produced the image. Never assert what an image depicts from its filename or the user's request alone — if you have not seen the contents, describe it neutrally or omit the claim. -- v1 layout is single-column (images stacked beneath the text); a multi-image grid and richer placement are not yet available. +For risky final answers involving current facts, external-service capability, presentations, market/crypto actions, direct quotes, memory retrieval, or truncated outputs, either delegate to the owning specialist/critic or explicitly limit the answer to the evidence you have. diff --git a/src/openhuman/agent_registry/agents/orchestrator/prompt.rs b/src/openhuman/agent_registry/agents/orchestrator/prompt.rs index 3a41bfb2da..f6fa64c17c 100644 --- a/src/openhuman/agent_registry/agents/orchestrator/prompt.rs +++ b/src/openhuman/agent_registry/agents/orchestrator/prompt.rs @@ -339,73 +339,36 @@ mod tests { } #[test] - fn build_requires_grounding_before_generate_presentation() { - // #2780 grounding rule: orchestrator must ground a topical/factual - // deck via `memory_tree` / `research` / `query_memory` before - // dispatching `generate_presentation`. The rule is expressed in - // `prompt.md` and must reach the assembled body verbatim — the - // test pins both the section header and the load-bearing "do - // not skip" instruction so a future prompt refactor can't - // quietly drop the guardrail. The live-web grounding option is - // named `research` (the researcher agent's `delegate_name` - // override, not the default `delegate_researcher` / - // `delegate_research`); it must match the actual tool name the - // orchestrator exposes so the model can dispatch it instead of - // hallucinating a non-tool. + fn build_routes_prompt_heavy_domains_to_specialists() { let body = build(&ctx_with(&[])).unwrap(); + assert!(body.contains("use `ask_docs`")); + assert!(body.contains("use `schedule_task`")); + assert!(body.contains("use `make_presentation`")); + assert!(body.contains("use `delegate_desktop_control`")); assert!( - body.contains("## Presentation generation"), - "presentation section header missing from orchestrator prompt" + !body.contains("## Presentation generation"), + "presentation-specific grounding policy belongs in presentation_agent" ); assert!( - body.contains("Grounding rule (do not skip)"), - "grounding rule missing from presentation section" + !body.contains("Before calling `generate_presentation`"), + "orchestrator prompt should not carry generate_presentation tool policy" ); assert!( - body.contains("Before calling `generate_presentation`"), - "grounding pre-condition phrasing missing from presentation section" - ); - // Each of the three permitted grounding tools must be named so - // the model knows the menu, not just the constraint. - assert!(body.contains("`memory_tree`")); - assert!( - body.contains("`research`"), - "grounding rule must name the researcher tool by its actual \ - delegate_name (`research`), not `delegate_research`" - ); - assert!( - !body.contains("`delegate_research`"), - "stale `delegate_research` reference — researcher's \ - delegate_name override is `research`" - ); - assert!(body.contains("`query_memory`")); - // All four waiver escape hatches must reach the assembled body - // verbatim — without each one the rule blocks a legitimate - // dispatch case. Per graycyrus on #3029: pinning only one waiver - // lets a future prompt edit silently drop or expand the list and - // the safety-critical guardrail decays unnoticed. - // (1) user pasted source material in the same turn. - assert!( - body.contains("user pasted source material"), - "waiver clause for pasted-source decks missing" - ); - // (2) prior turn in same thread already established source material. - assert!( - body.contains("prior turn in this same thread"), - "waiver clause for prior-turn established material missing" - ); - // (3) structural / content-free deck (blank template, layout-only). - assert!( - body.contains("content-free or structural"), - "waiver clause for structural / content-free decks missing" - ); - // (4) explicit user waiver ("don't research, just generate…"). - assert!( - body.contains("explicitly waived grounding"), - "waiver clause for explicit user waiver missing" + !body.contains("## Presentations with images"), + "image policy belongs in presentation_agent" ); } + #[test] + fn build_includes_evidence_aware_synthesis_contract() { + let body = build(&ctx_with(&[])).unwrap(); + assert!(body.contains("## Evidence-aware synthesis")); + assert!(body.contains("Evidence used")); + assert!(body.contains("Failed tool calls")); + assert!(body.contains("Do not introduce facts")); + assert!(body.contains("truncated, oversized, partial, or unavailable")); + } + #[test] fn build_omits_guide_when_no_integrations_connected() { let integrations = vec![ConnectedIntegration { diff --git a/src/openhuman/agent_registry/agents/presentation_agent/agent.toml b/src/openhuman/agent_registry/agents/presentation_agent/agent.toml new file mode 100644 index 0000000000..a37061a271 --- /dev/null +++ b/src/openhuman/agent_registry/agents/presentation_agent/agent.toml @@ -0,0 +1,26 @@ +id = "presentation_agent" +display_name = "Presentation Agent" +delegate_name = "make_presentation" +when_to_use = "Presentation specialist — creates decks from supplied or retrieved evidence. Owns factual grounding, source citation, image verification, and generate_presentation warnings. Use for make slides, build a deck, pitch deck, or presentation requests." +temperature = 0.2 +max_iterations = 10 +agent_tier = "worker" +omit_identity = true +omit_memory_context = false +omit_safety_preamble = false +omit_skills_catalog = true +omit_profile = true +omit_memory_md = true + +[model] +hint = "agentic" + +[tools] +named = [ + "generate_presentation", + "memory_tree", + "query_memory", + "web_search_tool", + "web_fetch", + "http_request", +] diff --git a/src/openhuman/agent_registry/agents/presentation_agent/mod.rs b/src/openhuman/agent_registry/agents/presentation_agent/mod.rs new file mode 100644 index 0000000000..8bf84783cb --- /dev/null +++ b/src/openhuman/agent_registry/agents/presentation_agent/mod.rs @@ -0,0 +1 @@ +pub mod prompt; diff --git a/src/openhuman/agent_registry/agents/presentation_agent/prompt.md b/src/openhuman/agent_registry/agents/presentation_agent/prompt.md new file mode 100644 index 0000000000..473785124e --- /dev/null +++ b/src/openhuman/agent_registry/agents/presentation_agent/prompt.md @@ -0,0 +1,33 @@ +# Presentation Agent + +You are the presentation specialist. Create `.pptx` decks from supplied or retrieved evidence. + +## Grounding + +- For factual or topical decks, establish grounding before `generate_presentation`. +- Use pasted source material, prior-thread source material, retrieved memory, or live web/doc evidence. +- Use memory only as historical context unless the user explicitly asks for historical material. +- Do not invent statistics, quotes, dates, names, or claims from priors. +- If the user explicitly waived grounding or requested a blank/structural deck, say that in `Evidence used`. + +## Images + +- Only attach images whose contents were supplied by the user, produced by a prior tool, or verified through retrieval. +- Do not claim what an image shows from its filename or expected purpose. +- If `generate_presentation` returns `image_warnings`, preserve them in `Failed tool calls` or `Open uncertainties` and tell the parent which images were dropped or skipped. + +## Citations + +- Include source URLs, memory node ids/source refs, file paths, artifact ids, or tool output ids used for slide content. +- Do not include facts in slide body or speaker notes unless they are supported by the cited evidence. + +## Output + +Return a compact result for the parent: + +- Answer +- Evidence used +- Actions taken +- Open uncertainties +- Failed tool calls +- Recommended next step diff --git a/src/openhuman/agent_registry/agents/presentation_agent/prompt.rs b/src/openhuman/agent_registry/agents/presentation_agent/prompt.rs new file mode 100644 index 0000000000..f31ea982b9 --- /dev/null +++ b/src/openhuman/agent_registry/agents/presentation_agent/prompt.rs @@ -0,0 +1,71 @@ +//! System prompt builder for the `presentation_agent` built-in agent. + +use crate::openhuman::context::prompt::{ + render_tools, render_user_files, render_workspace, PromptContext, +}; +use anyhow::Result; + +const ARCHETYPE: &str = include_str!("prompt.md"); + +pub fn build(ctx: &PromptContext<'_>) -> Result { + let mut out = String::with_capacity(4096); + out.push_str(ARCHETYPE.trim_end()); + out.push_str("\n\n"); + + let user_files = render_user_files(ctx)?; + if !user_files.trim().is_empty() { + out.push_str(user_files.trim_end()); + out.push_str("\n\n"); + } + + let tools = render_tools(ctx)?; + if !tools.trim().is_empty() { + out.push_str(tools.trim_end()); + out.push_str("\n\n"); + } + + let workspace = render_workspace(ctx)?; + if !workspace.trim().is_empty() { + out.push_str(workspace.trim_end()); + out.push('\n'); + } + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat}; + use std::collections::HashSet; + + #[test] + fn build_returns_grounding_contract() { + let visible = HashSet::new(); + let ctx = PromptContext { + workspace_dir: std::path::Path::new("."), + model_name: "test", + agent_id: "presentation_agent", + tools: &[], + skills: &[], + dispatcher_instructions: "", + learned: LearnedContextData::default(), + visible_tool_names: &visible, + tool_call_format: ToolCallFormat::PFormat, + connected_integrations: &[], + connected_identities_md: String::new(), + include_profile: false, + include_memory_md: false, + curated_snapshot: None, + user_identity: None, + personality_soul_md: None, + personality_memory_md: None, + personality_roster: vec![], + workflows: &[], + }; + let body = build(&ctx).unwrap(); + assert!(body.contains("Presentation Agent")); + assert!(body.contains("Do not invent statistics")); + assert!(body.contains("image_warnings")); + } +} diff --git a/src/openhuman/agent_registry/agents/scheduler_agent/agent.toml b/src/openhuman/agent_registry/agents/scheduler_agent/agent.toml new file mode 100644 index 0000000000..e997acddab --- /dev/null +++ b/src/openhuman/agent_registry/agents/scheduler_agent/agent.toml @@ -0,0 +1,19 @@ +id = "scheduler_agent" +display_name = "Scheduler Agent" +delegate_name = "schedule_task" +when_to_use = "Scheduling specialist — owns reminders, recurring jobs, cron listing/removal, relative-time grounding, and confirmation-before-create flows. Use for any request to remind, schedule, repeat, pause, remove, or inspect scheduled jobs." +temperature = 0.2 +max_iterations = 8 +agent_tier = "worker" +omit_identity = true +omit_memory_context = true +omit_safety_preamble = false +omit_skills_catalog = true +omit_profile = true +omit_memory_md = true + +[model] +hint = "agentic" + +[tools] +named = ["current_time", "cron_add", "cron_list", "cron_remove", "ask_user_clarification"] diff --git a/src/openhuman/agent_registry/agents/scheduler_agent/mod.rs b/src/openhuman/agent_registry/agents/scheduler_agent/mod.rs new file mode 100644 index 0000000000..8bf84783cb --- /dev/null +++ b/src/openhuman/agent_registry/agents/scheduler_agent/mod.rs @@ -0,0 +1 @@ +pub mod prompt; diff --git a/src/openhuman/agent_registry/agents/scheduler_agent/prompt.md b/src/openhuman/agent_registry/agents/scheduler_agent/prompt.md new file mode 100644 index 0000000000..55458f45e4 --- /dev/null +++ b/src/openhuman/agent_registry/agents/scheduler_agent/prompt.md @@ -0,0 +1,25 @@ +# Scheduler Agent + +You are the scheduling specialist. Own reminders, one-shot jobs, recurring jobs, job listing, job removal, and relative-time grounding. + +## Rules + +- Use `current_time` before interpreting relative times like "in 10 minutes", "tomorrow morning", or "every weekday". +- Never call `run_skill` for built-in tools. `cron_add`, `cron_list`, `cron_remove`, and `current_time` are direct tools. +- Always require explicit user confirmation before creating a schedule. +- For one-shot reminders, confirm the exact local time, then call `cron_add` with `schedule = {kind:"at", at:""}`. +- For recurring jobs, confirm a specific cadence, then call `cron_add` with `schedule = {kind:"cron", expr:"<5-field-cron>", tz:null}`. +- For finite repetitions, use a recurring schedule with clear prompt instructions and explain how the job can be removed. +- If the schedule is ambiguous, call `ask_user_clarification`. +- If a tool fails, report the failed tool and the actionable next step. + +## Output + +Return a compact result for the parent: + +- Answer +- Evidence used +- Actions taken +- Open uncertainties +- Failed tool calls +- Recommended next step diff --git a/src/openhuman/agent_registry/agents/scheduler_agent/prompt.rs b/src/openhuman/agent_registry/agents/scheduler_agent/prompt.rs new file mode 100644 index 0000000000..abdd685f2d --- /dev/null +++ b/src/openhuman/agent_registry/agents/scheduler_agent/prompt.rs @@ -0,0 +1,71 @@ +//! System prompt builder for the `scheduler_agent` built-in agent. + +use crate::openhuman::context::prompt::{ + render_tools, render_user_files, render_workspace, PromptContext, +}; +use anyhow::Result; + +const ARCHETYPE: &str = include_str!("prompt.md"); + +pub fn build(ctx: &PromptContext<'_>) -> Result { + let mut out = String::with_capacity(4096); + out.push_str(ARCHETYPE.trim_end()); + out.push_str("\n\n"); + + let user_files = render_user_files(ctx)?; + if !user_files.trim().is_empty() { + out.push_str(user_files.trim_end()); + out.push_str("\n\n"); + } + + let tools = render_tools(ctx)?; + if !tools.trim().is_empty() { + out.push_str(tools.trim_end()); + out.push_str("\n\n"); + } + + let workspace = render_workspace(ctx)?; + if !workspace.trim().is_empty() { + out.push_str(workspace.trim_end()); + out.push('\n'); + } + + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat}; + use std::collections::HashSet; + + #[test] + fn build_returns_scheduler_contract() { + let visible = HashSet::new(); + let ctx = PromptContext { + workspace_dir: std::path::Path::new("."), + model_name: "test", + agent_id: "scheduler_agent", + tools: &[], + skills: &[], + dispatcher_instructions: "", + learned: LearnedContextData::default(), + visible_tool_names: &visible, + tool_call_format: ToolCallFormat::PFormat, + connected_integrations: &[], + connected_identities_md: String::new(), + include_profile: false, + include_memory_md: false, + curated_snapshot: None, + user_identity: None, + personality_soul_md: None, + personality_memory_md: None, + personality_roster: vec![], + workflows: &[], + }; + let body = build(&ctx).unwrap(); + assert!(body.contains("Scheduler Agent")); + assert!(body.contains("explicit user confirmation")); + assert!(body.contains("Evidence used")); + } +} diff --git a/src/openhuman/mcp_server/resources.rs b/src/openhuman/mcp_server/resources.rs index 89b67d15f3..e3385407dd 100644 --- a/src/openhuman/mcp_server/resources.rs +++ b/src/openhuman/mcp_server/resources.rs @@ -1,7 +1,7 @@ //! Static MCP resource catalog for bundled prompt assets. //! //! Exposes `IDENTITY.md`, `SOUL.md`, `USER.md` and the `prompt.md` template -//! for each of the 18 built-in subagents as MCP resources. The content is +//! for each built-in subagent as MCP resources. The content is //! embedded at compile time via `include_str!`. //! //! ## URI scheme @@ -151,6 +151,24 @@ const RESOURCE_CATALOG: &[PromptResource] = &[ description: "Read-only worker that answers questions from documentation.", content: include_str!("../agent_registry/agents/help/prompt.md"), }, + PromptResource { + uri: "openhuman://prompts/agents/scheduler_agent", + name: "scheduler_agent", + description: "Specialist worker for reminders, recurring jobs, and cron inspection.", + content: include_str!("../agent_registry/agents/scheduler_agent/prompt.md"), + }, + PromptResource { + uri: "openhuman://prompts/agents/presentation_agent", + name: "presentation_agent", + description: "Specialist worker for evidence-grounded presentation generation.", + content: include_str!("../agent_registry/agents/presentation_agent/prompt.md"), + }, + PromptResource { + uri: "openhuman://prompts/agents/desktop_control_agent", + name: "desktop_control_agent", + description: "Specialist worker for desktop app launch and accessibility actions.", + content: include_str!("../agent_registry/agents/desktop_control_agent/prompt.md"), + }, PromptResource { uri: "openhuman://prompts/agents/mcp_setup", name: "mcp_setup", diff --git a/tests/orchestrator_presentation_wiring.rs b/tests/orchestrator_presentation_wiring.rs index 925a453af0..a529538916 100644 --- a/tests/orchestrator_presentation_wiring.rs +++ b/tests/orchestrator_presentation_wiring.rs @@ -1,21 +1,12 @@ -//! Pins the orchestrator's `generate_presentation` wiring contract -//! (#2780 — companion to the tool itself shipped in #2778). +//! Pins presentation delegation wiring. //! //! Two invariants: //! -//! 1. The orchestrator's `agent.toml` MUST list `generate_presentation` -//! in `[tools].named`. Without this entry the orchestrator's -//! function-calling schema does not include the tool and the -//! "create slides" routing case from parent epic #1535 silently -//! falls back to refusing the request. +//! 1. The orchestrator must expose `presentation_agent` as a subagent and +//! must not directly list `generate_presentation`. //! -//! 2. The `code_executor` agent MUST NOT list `generate_presentation`. -//! Presentation rendering is not a code-exec task: it runs in-process -//! via the native Rust `ppt-rs` engine (no Python, no subprocess, -//! distinct from code_executor's `node_exec` / shell surface), and -//! exposing the tool to code_executor would create a second, -//! duplicate dispatch path that bypasses the orchestrator's -//! grounding-rule prompt. +//! 2. The `presentation_agent` must list `generate_presentation` and +//! grounding tools, while `code_executor` still must not list it. //! //! Exact-line matching (not substring) so commented-out entries or //! prefixed names (`generate_presentation_v2`, `generate_presentation_legacy`) @@ -24,6 +15,9 @@ const ORCHESTRATOR_TOML: &str = include_str!("../src/openhuman/agent_registry/agents/orchestrator/agent.toml"); +const PRESENTATION_AGENT_TOML: &str = + include_str!("../src/openhuman/agent_registry/agents/presentation_agent/agent.toml"); + const CODE_EXECUTOR_TOML: &str = include_str!("../src/openhuman/agent_registry/agents/code_executor/agent.toml"); @@ -38,11 +32,32 @@ fn lists_named_tool(toml: &str, name: &str) -> bool { } #[test] -fn orchestrator_lists_generate_presentation() { +fn orchestrator_delegates_presentation_generation() { + assert!( + lists_named_tool(ORCHESTRATOR_TOML, "presentation_agent"), + "orchestrator must expose presentation_agent through subagents" + ); + assert!( + !lists_named_tool(ORCHESTRATOR_TOML, TOOL_NAME), + "orchestrator must not list '{TOOL_NAME}' directly; deck policy belongs to presentation_agent" + ); +} + +#[test] +fn presentation_agent_lists_generate_presentation_and_grounding_tools() { + assert!( + lists_named_tool(PRESENTATION_AGENT_TOML, TOOL_NAME), + "presentation_agent must list '{TOOL_NAME}'" + ); + for grounding_tool in ["memory_tree", "query_memory", "web_search_tool"] { + assert!( + lists_named_tool(PRESENTATION_AGENT_TOML, grounding_tool), + "presentation_agent must list grounding tool '{grounding_tool}'" + ); + } assert!( - lists_named_tool(ORCHESTRATOR_TOML, TOOL_NAME), - "orchestrator agent.toml must list '{TOOL_NAME}' as a named tool entry — \ - removing it silently disables the 'create slides' routing case (#1535)" + PRESENTATION_AGENT_TOML.contains("delegate_name = \"make_presentation\""), + "presentation_agent must expose the make_presentation delegate tool" ); }