From 429947574607777336efb168bb3fcc399c705f7f Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 29 Apr 2026 21:31:13 -0700 Subject: [PATCH] feat(prompt): Platform Capabilities preamble at top of system prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2332 item 1 (workspace awareness — agents don't surface platform-native tools up front). The dogfooding session surfaced that agents weren't using A2A delegation, persistent memory, or send_message_to_user. The tools were registered AND documented in the system prompt — but only in sections #8 (Inter-Agent Communication) and #9 (Hierarchical Memory), which agents read AFTER they've already started reasoning about a plan from earlier sections. This adds a tight inventory at section #1.5 (immediately after Platform Instructions, before role-specific prompt files) — every tool name + its short description in a bulleted block. Detailed when_to_use docs in sections #8/#9 stay; this preamble is the elevator pitch ("you have these"), the later sections are the manual ("here's when and how"). Generated from `platform_tools.registry` ToolSpecs — every tool's `name` + `short` flow through automatically, no manual sync. A new `get_capabilities_preamble(mcp: bool)` helper in executor_helpers mirrors the existing get_a2a_instructions / get_hma_instructions pattern. CLI-runtime agents (mcp=False) get an empty preamble — they see _A2A_INSTRUCTIONS_CLI's hand-written subcommand vocabulary further down, and the registry's MCP tool names would conflict. Tests: - test_capabilities_preamble_appears_in_mcp_prompt: header present - test_capabilities_preamble_lists_every_registry_tool: every a2a + memory tool from registry shows up (drift catches at test time — adding a new tool to registry surfaces here automatically) - test_capabilities_preamble_precedes_prompt_files: ordering invariant (toolkit before role docs) - test_capabilities_preamble_skipped_for_cli_runtime: empty when mcp=False All 40 prompt + platform_tools tests pass. --- workspace/executor_helpers.py | 53 ++++++++++++++++++++++++ workspace/prompt.py | 17 +++++++- workspace/tests/test_prompt.py | 75 ++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/workspace/executor_helpers.py b/workspace/executor_helpers.py index 8779ea8e..a71ce2ee 100644 --- a/workspace/executor_helpers.py +++ b/workspace/executor_helpers.py @@ -370,6 +370,59 @@ def _render_section(heading: str, specs, footer: str = "") -> str: return "\n".join(parts).rstrip() + "\n" +def get_capabilities_preamble(mcp: bool = True) -> str: + """Return a top-of-prompt one-glance summary of platform-native tools. + + Shipped 2026-04-30 (#2332): the dogfooding session surfaced that + agents weren't using A2A delegation, persistent memory, or + send_message_to_user — these capabilities WERE documented further + down in the system prompt (## Inter-Agent Communication, ## HMA), + but agents tend to read top-down and commit to a plan before + reaching that section. + + The preamble is the elevator pitch: every tool name + its short + description in a tight bulleted block, immediately after Platform + Instructions. The detailed when_to_use docs further down still + apply — this is "you have these tools; consult the dedicated + section for usage details." + + Generated from the same `platform_tools.registry` ToolSpecs as the + detailed sections, so renames/additions in registry.py flow through + automatically. Returns "" for CLI-runtime agents (mcp=False) — they + get a different overall prompt shape and the registry's MCP-named + tools wouldn't match the CLI command vocabulary. + """ + if not mcp: + # CLI-runtime agents see _A2A_INSTRUCTIONS_CLI's hand-written + # command list instead. Skip the preamble to avoid confusing + # agents with two name vocabularies (MCP tool names vs CLI + # subcommand keywords). + return "" + + from platform_tools.registry import a2a_tools, memory_tools + + parts = [ + "## Platform Capabilities", + "", + ( + "You have native access to these platform tools. Use them " + "proactively — they're how multi-agent collaboration, " + "persistent memory, and user communication actually work. " + "Detailed usage guidance for each lives in the dedicated " + "sections below; this preamble is just the inventory." + ), + "", + "**Inter-agent collaboration (A2A):**", + ] + for spec in a2a_tools(): + parts.append(f"- `{spec.name}` — {spec.short}") + parts.append("") + parts.append("**Persistent memory (HMA):**") + for spec in memory_tools(): + parts.append(f"- `{spec.name}` — {spec.short}") + return "\n".join(parts).rstrip() + "\n" + + def get_a2a_instructions(mcp: bool = True) -> str: """Return inter-agent communication instructions for system-prompt injection. diff --git a/workspace/prompt.py b/workspace/prompt.py index 7d93152e..6a80ab05 100644 --- a/workspace/prompt.py +++ b/workspace/prompt.py @@ -4,7 +4,11 @@ import logging import os from pathlib import Path -from executor_helpers import get_a2a_instructions, get_hma_instructions +from executor_helpers import ( + get_a2a_instructions, + get_capabilities_preamble, + get_hma_instructions, +) from skill_loader.loader import LoadedSkill from shared_runtime import build_peer_section @@ -92,6 +96,17 @@ def build_system_prompt( parts.append("# Platform Instructions\n") parts.append(platform_instructions) + # Platform Capabilities preamble (#2332): tight inventory of every + # native tool agents have access to, generated from the registry. + # Goes BEFORE prompt files so the role-specific docs read against + # a known toolkit, not a discovery problem. Detailed when_to_use + # docs still appear later in the A2A and HMA sections — this + # preamble is the elevator pitch ("you have these"); the later + # sections are the manual ("here's when and how"). + capabilities = get_capabilities_preamble(mcp=a2a_mcp) + if capabilities: + parts.append(capabilities) + # Load prompt files in order files_to_load = list(prompt_files or []) if not files_to_load: diff --git a/workspace/tests/test_prompt.py b/workspace/tests/test_prompt.py index 5969de2b..054db163 100644 --- a/workspace/tests/test_prompt.py +++ b/workspace/tests/test_prompt.py @@ -469,3 +469,78 @@ def test_tool_instructions_precede_peer_section(tmp_path): a2a_idx = result.index("## Inter-Agent Communication") peers_idx = result.index("## Your Peers") assert a2a_idx < peers_idx, "A2A instructions must come before the peer list" + + +# --- Capabilities preamble (#2332) --- + + +def test_capabilities_preamble_appears_in_mcp_prompt(tmp_path): + """MCP-runtime agents see the Platform Capabilities preamble at top.""" + (tmp_path / "system-prompt.md").write_text("Role-specific content.") + + result = build_system_prompt( + config_path=str(tmp_path), + workspace_id="ws-1", + loaded_skills=[], + peers=[], + ) + + assert "## Platform Capabilities" in result + + +def test_capabilities_preamble_lists_every_registry_tool(tmp_path): + """Every tool in the registry appears in the preamble — drift catches at test time.""" + (tmp_path / "system-prompt.md").write_text("Base.") + + result = build_system_prompt( + config_path=str(tmp_path), + workspace_id="ws-1", + loaded_skills=[], + peers=[], + ) + + from platform_tools.registry import a2a_tools, memory_tools + + preamble_start = result.index("## Platform Capabilities") + # Detailed sections come later — only check the slice between the + # preamble heading and the next ## heading after it. + next_section = result.index("\n## ", preamble_start + 1) + preamble_block = result[preamble_start:next_section] + + for spec in a2a_tools() + memory_tools(): + assert f"`{spec.name}`" in preamble_block, ( + f"tool {spec.name!r} from registry missing from capabilities preamble" + ) + + +def test_capabilities_preamble_precedes_prompt_files(tmp_path): + """Preamble lands before role-specific prompt files so agents see the + toolkit before reading their role docs.""" + (tmp_path / "system-prompt.md").write_text("ROLE_MARKER_SENTINEL") + + result = build_system_prompt( + config_path=str(tmp_path), + workspace_id="ws-1", + loaded_skills=[], + peers=[], + ) + + cap_idx = result.index("## Platform Capabilities") + role_idx = result.index("ROLE_MARKER_SENTINEL") + assert cap_idx < role_idx, "Capabilities preamble must precede role prompt files" + + +def test_capabilities_preamble_skipped_for_cli_runtime(tmp_path): + """CLI-runtime agents see _A2A_INSTRUCTIONS_CLI's hand-written commands + instead — the preamble's MCP tool names would conflict.""" + (tmp_path / "system-prompt.md").write_text("Base.") + + result = build_system_prompt( + config_path=str(tmp_path), + workspace_id="ws-1", + loaded_skills=[], + peers=[], + a2a_mcp=False, + ) + + assert "## Platform Capabilities" not in result