Refactor — BaseTool._publish_result() helper

Every tool's _handle_request ends with the same 8-line envelope block. BaseTool should own it.

Problem

Pattern: Despite BaseTool existing, the tool.result.* envelope is constructed identically in all 7 tool implementations. _publish_activity() was already extracted into BaseTool — _publish_result() is the same idea applied to the result envelope.

The repeated block (all 7 tools)

self._pub.publish(MessageEnvelope.create(
    message_type="tool_result",
    subject=TOOL_RESULT_<NAME>,        # only thing that varies
    sender_id=self.TOOL_ID,
    payload={"result": result, "tool": self.TOOL_NAME},
    correlation_id=correlation_id,
    metadata={},
))

The only variation is the subject. Everything else — message_type, sender_id, payload shape, field names — is identical across all tools.

Solution

Approach: Add RESULT_SUBJECT as a class variable (parallel to the existing ACTIVITY_SUBJECT), then add _publish_result() to BaseTool. Each tool declares its result subject and calls the helper instead of constructing the envelope.

BaseTool Changes

File: src/local/tools/base_tool.py

Add class variable

TOOL_ID: ClassVar[str]
TOOL_NAME: ClassVar[str]
ACTIVITY_SUBJECT: ClassVar[str]
RESULT_SUBJECT: ClassVar[str]          # ← add this
CONFIG_NAME: ClassVar[str | None] = None

Add method (alongside _publish_activity)

def _publish_result(
    self,
    result: str,
    correlation_id: str | None,
    extra: dict | None = None,
) -> None:
    """Publish a tool.result.* envelope to the bus.

    Args:
        result: The tool's string output, forwarded to GeneratorAgent.
        correlation_id: Forwarded from the originating request envelope.
        extra: Optional additional payload fields (rarely needed).
    """
    self._pub.publish(MessageEnvelope.create(
        message_type="tool_result",
        subject=self.RESULT_SUBJECT,
        sender_id=self.TOOL_ID,
        payload={"result": result, "tool": self.TOOL_NAME, **(extra or {})},
        correlation_id=correlation_id or "",
    ))

Update _handle_request docstring

Replace step 5 ("Publish the tool.result.* envelope via self._pub") with "Call _publish_result(result, correlation_id)."

Per-Tool Changes

Each tool: add RESULT_SUBJECT class var, replace the 8-line envelope block with one line.

Tool fileRESULT_SUBJECT value
web_search_tool.pyTOOL_RESULT_WEB_SEARCH
web_fetch_tool.pyTOOL_RESULT_WEB_FETCH
search_memory_tool.pyTOOL_RESULT_SEARCH_MEMORY
datetime_tool.pyTOOL_RESULT_GET_DATETIME
location_tool.pyTOOL_RESULT_GET_LOCATION
semantic_scholar_tool.pyTOOL_RESULT_SEARCH_PAPERS
search_library_tool.pyTOOL_RESULT_SEARCH_LIBRARY

Before (e.g. datetime_tool.py)

    def _handle_request(self, envelope: MessageEnvelope) -> None:
        correlation_id = envelope.correlation_id
        self._publish_activity("request", {}, correlation_id)
        result = _get_datetime()
        self._publish_activity("result", {"result": result}, correlation_id)
        self._pub.publish(MessageEnvelope.create(
            message_type="tool_result",
            subject=TOOL_RESULT_GET_DATETIME,
            sender_id=self.TOOL_ID,
            payload={"result": result},
            correlation_id=correlation_id,
        ))

After

    def _handle_request(self, envelope: MessageEnvelope) -> None:
        correlation_id = envelope.correlation_id
        self._publish_activity("request", {}, correlation_id)
        result = _get_datetime()
        self._publish_activity("result", {"result": result}, correlation_id)
        self._publish_result(result, correlation_id)
The subject import (TOOL_RESULT_GET_DATETIME etc.) can be removed from each tool file once the class var is declared and the envelope block is gone — fewer imports per file.

File Map

FileChange
src/local/tools/base_tool.pyAdd RESULT_SUBJECT class var; add _publish_result() method; update _handle_request docstring
src/local/tools/web_search_tool.pyAdd RESULT_SUBJECT; replace envelope block; remove unused subject import
src/local/tools/web_fetch_tool.pySame
src/local/tools/search_memory_tool.pySame
src/local/tools/datetime_tool.pySame
src/local/tools/location_tool.pySame
src/local/tools/semantic_scholar_tool.pySame
src/local/tools/search_library_tool.pySame

Acceptance Criteria