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
14 changes: 13 additions & 1 deletion src/openhuman/agent/harness/subagent_runner/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
4 changes: 4 additions & 0 deletions src/openhuman/agent/harness/subagent_runner/ops_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
129 changes: 126 additions & 3 deletions src/openhuman/agent_orchestration/tools/archetype_delegation.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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",
Expand All @@ -45,19 +74,20 @@ impl Tool for ArchetypeDelegationTool {
}

async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
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")
Expand All @@ -76,6 +106,64 @@ impl Tool for ArchetypeDelegationTool {
}
}

fn render_structured_handoff(prompt: &str, args: &Value) -> String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / parity note: render_structured_handoff reshapes every archetype delegation, not just the new specialists. ArchetypeDelegationTool backs all delegates (crypto, integrations, etc.), so every child prompt is now wrapped in Task:\n… even when no structured fields are passed. It's additive and well-tested, but the "existing prompt delegation remains compatible" parity claim is a bit stronger than reality — the literal child-prompt string changes for all delegates. Worth a one-line note in the parity section, and consider skipping the Task:\n wrapper when no structured fields are present to keep the legacy path byte-identical.

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::*;
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod prompt;
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<String> {
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)
}
Comment on lines +10 to +34

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add required debug/trace instrumentation in prompt assembly flow.

build performs multiple rendering calls and branch decisions, but currently emits no diagnostic logs. Please add stable-prefix debug/trace logs for function entry/exit, per-section render outcome, and included/skipped branches (with correlation fields like agent_id and section names).

Proposed patch
 use anyhow::Result;
+use tracing::{debug, trace};

 const ARCHETYPE: &str = include_str!("prompt.md");

 pub fn build(ctx: &PromptContext<'_>) -> Result<String> {
+    debug!(
+        agent_id = %ctx.agent_id,
+        "[desktop_control_agent::prompt] build start"
+    );
     let mut out = String::with_capacity(4096);
     out.push_str(ARCHETYPE.trim_end());
     out.push_str("\n\n");

+    trace!(agent_id = %ctx.agent_id, section = "user_files", "[desktop_control_agent::prompt] rendering section");
     let user_files = render_user_files(ctx)?;
     if !user_files.trim().is_empty() {
+        debug!(agent_id = %ctx.agent_id, section = "user_files", "[desktop_control_agent::prompt] section included");
         out.push_str(user_files.trim_end());
         out.push_str("\n\n");
+    } else {
+        trace!(agent_id = %ctx.agent_id, section = "user_files", "[desktop_control_agent::prompt] section skipped_empty");
     }

+    trace!(agent_id = %ctx.agent_id, section = "tools", "[desktop_control_agent::prompt] rendering section");
     let tools = render_tools(ctx)?;
     if !tools.trim().is_empty() {
+        debug!(agent_id = %ctx.agent_id, section = "tools", "[desktop_control_agent::prompt] section included");
         out.push_str(tools.trim_end());
         out.push_str("\n\n");
+    } else {
+        trace!(agent_id = %ctx.agent_id, section = "tools", "[desktop_control_agent::prompt] section skipped_empty");
     }

+    trace!(agent_id = %ctx.agent_id, section = "workspace", "[desktop_control_agent::prompt] rendering section");
     let workspace = render_workspace(ctx)?;
     if !workspace.trim().is_empty() {
+        debug!(agent_id = %ctx.agent_id, section = "workspace", "[desktop_control_agent::prompt] section included");
         out.push_str(workspace.trim_end());
         out.push('\n');
+    } else {
+        trace!(agent_id = %ctx.agent_id, section = "workspace", "[desktop_control_agent::prompt] section skipped_empty");
     }

+    debug!(
+        agent_id = %ctx.agent_id,
+        final_chars = out.chars().count(),
+        "[desktop_control_agent::prompt] build done"
+    );
     Ok(out)
 }

As per coding guidelines: “Log entry/exit, branches, external calls, retries/timeouts, state transitions, and errors with stable grep-friendly prefixes” and “Add substantial debug-level logs while implementing features or fixes in Rust using log / tracing crate.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_registry/agents/desktop_control_agent/prompt.rs` around
lines 10 - 34, Add tracing/debug instrumentation to the prompt assembly in
build: emit a stable-prefix entry log when entering build (include
ctx.agent_id), call and instrument each render helper (render_user_files,
render_tools, render_workspace) with trace logs showing start, end,
success/error and the trimmed length or empty/skipped decision, and log branch
decisions (e.g., "INCLUDE_SECTION" vs "SKIP_SECTION") with a section name field
and agent_id; also emit an exit log with final output length or error. Use the
project's tracing/log crate conventions and stable grep-friendly prefixes (e.g.,
"PROMPT_BUILD:ENTRY", "PROMPT_BUILD:SECTION", "PROMPT_BUILD:EXIT") and attach
correlation fields like agent_id and section names to each event.


#[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"));
}
}
Loading
Loading