feat(prompt): Platform Capabilities preamble at top of system prompt
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.
This commit is contained in:
parent
856ff89973
commit
4299475746
@ -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.
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user