molecule-core/workspace/platform_tools
Hongming Wang 09e99a09c6 feat(a2a-mcp): add chat_history tool for prior turns with a peer
When a peer_agent push lands and the agent needs context from prior
turns with that workspace ("what task did this peer assign me last
hour?", "what did I tell them?"), the only options today are
re-deriving from memory (lossy) or scrolling activity_logs in the
canvas (no agent-facing tool). Surface the platform's existing
audit log directly via a new MCP tool so agents can read both sides
of an A2A conversation in chronological order.

Implementation:
- a2a_tools.py: new tool_chat_history(peer_id, limit=20, before_ts="")
  hits /workspaces/<self>/activity?peer_id=X&limit=N (the new server
  filter from molecule-core#2472). Reverses the DESC response into
  chronological order so the agent reads top-down. Graceful error
  envelope on validation/network/non-200 — never crashes the MCP
  server, agent can branch on Error: prefix.
- platform_tools/registry.py: ToolSpec wired into the A2A section so
  the rendered system-prompt block automatically includes it. Same
  pattern as the existing inbox_peek/inbox_pop/wait_for_message.
- a2a_mcp_server.py: dispatch in handle_tool_call.
- executor_helpers.py: _CLI_A2A_COMMAND_KEYWORDS gets a None entry
  (CLI runtimes don't expose chat history today; flip to a keyword
  when a2a_cli grows a `history` subcommand).
- snapshots/a2a_instructions_mcp.txt regenerated.

Tests: 10 new branches in TestChatHistory (validation / param
forwarding / limit cap / before_ts pass-through / DESC→chronological
reorder / 400 verbatim / 500 generic / network exc / non-list resp).
Mutation-verified: reverting a2a_tools.py fails 10/10. Full test
suite remains green at 1516 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:54:23 -07:00
..
__init__.py feat(platform): single-source-of-truth tool registry — adapters consume, no drift 2026-04-28 17:11:36 -07:00
README.md docs: registry pattern + harness scripts READMEs 2026-04-28 22:19:40 -07:00
registry.py feat(a2a-mcp): add chat_history tool for prior turns with a peer 2026-05-01 17:54:23 -07:00

Platform tool registry

Single source of truth for every tool the platform exposes to agents (A2A delegation, hierarchical memory, broadcast, introspection).

Why this exists

Pre-#2240, three places independently declared each tool:

  1. MCP server (workspace/a2a_mcp_server.py) — the TOOLS JSON list
  2. LangChain @tool wrappers (workspace/builtin_tools/{delegation,memory}.py)
  3. Agent-facing system-prompt docs (workspace/executor_helpers.py)

Adding a tool to one and forgetting the others happened repeatedly. The canonical case: send_message_to_user was registered in MCP TOOLS but the executor_helpers doc string never mentioned it, so agents saw the tool as available but had no usage guidance — a silent capability regression.

What the registry does

registry.py defines each tool ONCE as a frozen ToolSpec:

ToolSpec(
    name="delegate_task",
    short="Delegate a task to a peer workspace via A2A and WAIT for the response.",
    when_to_use="Use for QUICK questions and small sub-tasks where you can afford to wait inline...",
    input_schema={...},          # JSON Schema, consumed by MCP server
    impl=tool_delegate_task,     # the actual coroutine
    section="a2a",               # which prompt section it belongs to
)

Adapters consume specs; no hardcoded names anywhere else:

  • MCP server builds its TOOLS list from _PLATFORM_TOOL_SPECS at import time
  • LangChain @tool wrappers read name=spec.name from the registry
  • Doc generator (executor_helpers._render_section()) produces the system-prompt block from spec.short (bullet) + spec.when_to_use (heading + paragraph)

CLI subprocess block — special case

Non-MCP runtimes (ollama, custom subprocess adapters) use a separate hand-maintained block in executor_helpers._A2A_INSTRUCTIONS_CLI because the CLI subcommand vocabulary (peers, delegate, status, info) differs from the MCP tool names (list_peers, delegate_task, etc.). Auto-generation would lose the readable invocation syntax.

Alignment is enforced via _CLI_A2A_COMMAND_KEYWORDS (in executor_helpers.py): every a2a-section spec must be keyed there with either a CLI subcommand keyword OR an explicit None if the tool is intentionally not exposed via subprocess (e.g. send_message_to_user because its structured attachments field doesn't survive positional-arg shell invocation).

Tests that catch drift

workspace/tests/test_platform_tools.py:

Test What it catches
test_mcp_server_registers_every_registry_tool MCP TOOLS list out of sync with registry
test_mcp_tool_descriptions_match_registry_short hand-edited MCP description that drifted
test_mcp_tool_input_schemas_match_registry schema duplicated in server file
test_a2a_instructions_text_includes_every_a2a_tool doc generator missed a tool
test_old_pre_rename_names_not_present_in_docs stale name leaked back in
test_a2a_mcp_instructions_match_snapshot rendered shape (bullet ordering, headings, footers) drifted
test_a2a_cli_instructions_match_snapshot CLI block edited in a way that changes shape
test_hma_instructions_match_snapshot HMA section drifted
test_cli_keyword_mapping_covers_every_a2a_tool tool added to registry without a CLI mapping decision
test_cli_keyword_substrings_appear_in_cli_block CLI keyword in the mapping but missing from the doc block

The snapshot files at workspace/tests/snapshots/*.txt are LF-pinned in .gitattributes so a Windows contributor with core.autocrlf=true doesn't get mysterious test failures.

Adding a new tool

  1. Append a ToolSpec(...) to TOOLS in registry.py.
  2. Add the LangChain @tool wrapper in workspace/builtin_tools/ (the wrapper body just calls spec.impl).
  3. Update _CLI_A2A_COMMAND_KEYWORDS in executor_helpers.py — set the value to the CLI subcommand keyword, or to None if the tool isn't exposed via the subprocess interface.
  4. Regenerate snapshots — see the comment block at the top of workspace/tests/test_platform_tools.py for the one-liner.
  5. Run pytest workspace/tests/test_platform_tools.py --no-cov.

Renaming a tool

Edit name in registry.py only. Then:

  1. The MCP TOOLS list rebuilds automatically.
  2. The doc generator regenerates automatically (snapshots will fail the diff — regenerate them).
  3. Search workspace/ for the old literal in case a non-adapter consumer (tests, plugin code) hardcoded the old name; update those.
  4. Update any _CLI_A2A_COMMAND_KEYWORDS key + the literal substring in _A2A_INSTRUCTIONS_CLI if applicable.

Removing a tool

Delete the ToolSpec and the _CLI_A2A_COMMAND_KEYWORDS key. Adapters and doc generators stop registering it automatically; the structural tests prevent stale references from surviving.