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
53 changes: 53 additions & 0 deletions workspace/executor_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 16 additions & 1 deletion workspace/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
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

Expand Down Expand Up @@ -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:
Expand Down
75 changes: 75 additions & 0 deletions workspace/tests/test_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading