Files
openhuman/docs/agent/prompt-audit.md
T

10 KiB

Agent prompt audit: grounding, tools, skills, MCPs

Audit + remediation of the orchestrator and sub-agent system prompts, focused on anti-hallucination discipline and de-duplication. Inspired by NousResearch/hermes-agent's named, reusable guidance blocks.

Why this exists

The system prompts for the orchestrator and the ~27 sub-agents had accreted "slop": the same anti-fabrication paragraph copy-pasted (and quietly drifting) across many agents, specialist walkthroughs living in the wrong agent, a ## Safety block that said nothing about grounding, and an em-dash rule restated inline despite a global suffix already banning it. There was no single source of truth for how an agent should treat its tool list, skills, or missing context, so each agent re-invented it.

How the prompt system assembles (reference)

  • SystemPromptBuilder (src/openhuman/agent/prompts/builder.rs) renders an ordered Vec<Box<dyn PromptSection>>, joins with \n\n, and appends GLOBAL_STYLE_SUFFIX.
  • Concrete sections live in src/openhuman/agent/prompts/sections.rs.
  • Three render paths produce a final system prompt:
    1. Static chain (SystemPromptBuilder::with_defaults / for_subagent).
    2. Dynamic builders (from_dynamic) — the orchestrator and ~25 other agents/<id>/prompt.rs files each hand-assemble their body by calling the free render_* helpers in render_helpers.rs. They funnel through SystemPromptBuilder::build() for the final wrap.
    3. Narrow index-based renderer (render_subagent_system_prompt_with_format) used by the sub-agent runner for spawned sub-agents (does NOT go through build()).
  • Skills: the orchestrator lists installed skills by name+desc; full SKILL.md bodies are injected at turn time by src/openhuman/workflows/inject.rs, not in the system prompt.
  • MCPs: not injected into any agent prompt. OpenHuman's MCP server only exposes tools to external clients; agents act through their own tool surface. There is no agent-facing MCP catalogue section, by design.
  • KV-cache contract: the system prompt is built once per session and frozen. Anything in the cache-friendly prefix must be byte-stable (no time / RNG / host).

Findings

# Finding Resolution
1 Anti-fabrication paragraph ("never invent ids; a tool not in your list does not exist") copy-pasted across crypto / markets / integrations / account-admin / mcp-setup / morning-briefing / researcher with no shared source. Centralized into GROUNDING_BODY (one source of truth).
2 ## Safety covered exfiltration / destructive commands but said nothing about grounding, not inventing tools/ids/files, or not ending a turn with a promise instead of an action. Added the orthogonal grounding contract; kept ## Safety for security.
3 Orchestrator prompt.md (235 lines) carried specialist content: a full Apple-Music click-sequence + keyboard walkthrough, and a ~47-line cron JSON walkthrough. Moved to desktop_control_agent / scheduler_agent; orchestrator is now a lean routing table (175 lines).
4 Desktop-control instructions triple-duplicated across SOUL.md, orchestrator prompt.md, and desktop_control_agent. SOUL.md + orchestrator trimmed to pointers; the worked example lives only in desktop_control_agent.
5 Em-dash rule restated inline in orchestrator/prompt.md although GLOBAL_STYLE_SUFFIX already bans em-dashes in every output. Removed the inline restatement.
6 omit_skills_catalog rendering effect is inert (skills are agent-owned; the narrow renderer no longer emits a skills catalog). Retained. See "Decisions" — the field is still live API surface (serde, CLI, 30+ TOMLs); removing it is a 60-site churn with no functional benefit. Documented as inert-but-retained.

The grounding contract

GROUNDING_BODY (src/openhuman/agent/prompts/sections.rs) is the canonical block. It is deliberately generic: every clause must be true for every agent, including the integrations executor. Agent-specific routing ("delegate external services", "pull slugs from composio_list_tools") stays in that agent's own prompt.md.

## Grounding and tool use

- Your tools are exactly the ones listed in this prompt. You can only act through them.
  If a capability is not one of your tools, say so plainly rather than pretending it exists.
- Never invent tool names, arguments, ids, slugs, file paths, URLs, chain ids, addresses,
  quotes, metrics, or any other value. If you do not have it from a tool result or the user,
  ask for it or look it up with a tool.
- Use your tools to act. Do not just describe what you would do and stop, and never end a
  turn with a promise of future action: do it now, or hand back a concrete result.
- Never substitute plausible looking but fabricated output (made up data, invented file
  contents, synthesised tool or API responses) for results you could not actually produce.
  If a step failed, say it failed.
- Ground every factual claim in evidence you actually observed: a tool result, the user's
  message, or cited memory. If the evidence is missing, partial, or truncated, say so or
  fetch more instead of guessing.
- Skills run only via `run_workflow`, and only the skills listed as installed exist. Do not
  invent skill ids.

The first, third, and fourth bullets mirror Hermes's TOOL_USE_ENFORCEMENT_GUIDANCE ("use tools to act, never end a turn with a promise") and TASK_COMPLETION_GUIDANCE ("never substitute fabricated output for results you couldn't produce").

How it reaches every agent

Rather than splice it into all ~26 dynamic prompt.rs files (drift-prone), the contract is appended at the two chokepoints that every prompt funnels through, just like GLOBAL_STYLE_SUFFIX:

  • SystemPromptBuilder::build() — covers the static chain and all dynamic agents.
  • render_subagent_system_prompt_with_format() — covers spawned sub-agents.

Both reference the same GROUNDING_BODY const, so they can never drift. A render_grounding() helper and a GroundingSection PromptSection are also exposed for explicit use. Placement is near the tail (just before the output-style rules) so it reads as a closing contract; the text is byte-stable, so it stays in the cache-friendly prefix.

Coverage is asserted by grounding_contract_appended_to_every_build_path (mod_tests.rs), which checks the marker appears exactly once across the static, sub-agent, and dynamic build paths, plus an assertion in the narrow-renderer test.

Decisions / deviations from the original plan

  • No omit_grounding flag. The contract is always on. It is universal anti-hallucination guidance and the text is generic enough to be true for every agent, so threading an opt-out through serde + every caller would be churn for zero benefit.
  • omit_skills_catalog retained, not removed. Its rendering effect is inert, but the field is wired into serde defaults, the agent CLI dump, and 30+ agent.toml files. Removing it is a 60-site change with real regression surface and no functional payoff, which is out of proportion for a prompt audit. Flagged here instead.
  • Cron delegate-vs-direct contradiction left in place. The orchestrator decision tree routes scheduling to schedule_task, while the (now compressed) scheduling rules still let it call cron_add directly when that tool is in its list. Resolving which is authoritative is a behavioral change beyond this audit; the load-bearing rules (never via run_workflow; always confirm) were preserved.

Touched files

  • src/openhuman/agent/prompts/sections.rsGROUNDING_BODY + GroundingSection.
  • src/openhuman/agent/prompts/builder.rs — central append in build().
  • src/openhuman/agent/prompts/render_helpers.rsrender_grounding() + narrow-renderer append.
  • src/openhuman/agent/prompts/mod.rs / mod_tests.rs — re-export + coverage tests.
  • src/openhuman/agent_registry/agents/{crypto_agent,markets_agent}/prompt.md — drop generic clause.
  • src/openhuman/agent_registry/agents/orchestrator/prompt.md — slimmed routing table.
  • src/openhuman/agent_registry/agents/{desktop_control_agent,scheduler_agent}/prompt.md — received the moved walkthroughs.
  • src/openhuman/agent/prompts/SOUL.md — trimmed duplicated desktop sequence.

Verifying

GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent::prompts::
GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib agent_registry::agents

Provider prompt-cache behaviour (#3939)

The byte-stable prompt prefix above is what makes KV-cache reuse possible. How much of that reuse a user actually gets depends on the backend they route to. Provider::prompt_cache_capabilities() (src/openhuman/inference/provider/traits.rs, PromptCacheCapabilities) makes that contract explicit per provider:

Provider automatic prefix cache reports cached input tokens explicit cache-control thread/session grouping
OpenHuman backend ✓ (thread_id extension)
OpenAI / OpenRouter / GMI (OpenAI-compatible) — (prefix identity)
Other / custom / LM Studio compatible conservative default — all false

Notes:

  • Conservative by default. Unknown or custom OpenAI-compatible slugs report no caching, so we never assume cache hits or send cache-only request fields a provider may not honour. Opting a provider in is a one-line edit to prompt_cache_for_compatible_slug (compatible.rs) once verified.
  • Usage normalization is provider-agnostic. extract_usage already folds the OpenAI usage.prompt_tokens_details.cached_tokens shape and the openhuman.usage.cached_input_tokens extension into UsageInfo.cached_input_tokens, so cached-prefix cost accounting (src/openhuman/agent/cost.rs) is exact wherever the provider reports it.
  • No OpenHuman-only leakage. explicit_cache_control stays false for every OpenAI-compatible provider (the chat-completions API has no such field), and thread_id grouping is declared only on OpenHumanBackendProvider.

Follow-ups (not in this slice): explicit cache-control request shaping for providers that support it (e.g. Anthropic cache_control), a ChatRequest cache-boundary marker, and extending cached-token parsing to non-OpenAI usage shapes (e.g. DeepSeek prompt_cache_hit_tokens).