BaseTool._publish_result() helperEvery tool's _handle_request ends with the same 8-line envelope block. BaseTool should own it.
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.
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.
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.
File: src/local/tools/base_tool.py
TOOL_ID: ClassVar[str]
TOOL_NAME: ClassVar[str]
ACTIVITY_SUBJECT: ClassVar[str]
RESULT_SUBJECT: ClassVar[str] # ← add this
CONFIG_NAME: ClassVar[str | None] = None
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 "",
))
Replace step 5 ("Publish the tool.result.* envelope via self._pub") with "Call _publish_result(result, correlation_id)."
Each tool: add RESULT_SUBJECT class var, replace the 8-line envelope block with one line.
| Tool file | RESULT_SUBJECT value |
|---|---|
| web_search_tool.py | TOOL_RESULT_WEB_SEARCH |
| web_fetch_tool.py | TOOL_RESULT_WEB_FETCH |
| search_memory_tool.py | TOOL_RESULT_SEARCH_MEMORY |
| datetime_tool.py | TOOL_RESULT_GET_DATETIME |
| location_tool.py | TOOL_RESULT_GET_LOCATION |
| semantic_scholar_tool.py | TOOL_RESULT_SEARCH_PAPERS |
| search_library_tool.py | TOOL_RESULT_SEARCH_LIBRARY |
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,
))
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)
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 | Change |
|---|---|
| src/local/tools/base_tool.py | Add RESULT_SUBJECT class var; add _publish_result() method; update _handle_request docstring |
| src/local/tools/web_search_tool.py | Add RESULT_SUBJECT; replace envelope block; remove unused subject import |
| src/local/tools/web_fetch_tool.py | Same |
| src/local/tools/search_memory_tool.py | Same |
| src/local/tools/datetime_tool.py | Same |
| src/local/tools/location_tool.py | Same |
| src/local/tools/semantic_scholar_tool.py | Same |
| src/local/tools/search_library_tool.py | Same |
RESULT_SUBJECT declared and no MessageEnvelope.create(message_type="tool_result"...) call remainingbase_tool.py has _publish_result() and RESULT_SUBJECT in the abstract classTOOL_RESULT_* subject imports remaining in individual tool files (moved to be unused once class var is set)