feat: fabric_write tool -- subagents persist key findings to memory before completing#5338
feat: fabric_write tool -- subagents persist key findings to memory before completing#5338ZK-Snarky wants to merge 1 commit into
Conversation
Subagents that find root causes, non-obvious config facts, or key constraints currently have no way to persist those findings. The parent gets a text summary, but that summary dies in the conversation unless the parent explicitly calls the memory tool. This change gives subagents a narrow, append-only write path to MEMORY.md. Changes: - tools/fabric_write_tool.py: new tool with guardrails (3 writes/subagent, 400 chars/entry, same injection scanning as memory_tool) - tools/delegate_tool.py: inject 'fabric' toolset automatically when parent has memory enabled; add write_memory param to delegate_task; update child system prompt to explain when to use fabric_write - toolsets.py: register 'fabric' toolset (not in blocked list) - tests/tools/test_fabric_write.py: 21 tests covering validation, rate limiting, content scanning, entry format, and toolset injection
|
closing request - wrong scope - fabric is small tool used by subset of Hermes users, need to have a subagent memory writing system that works with the default stack in Hermes |
|
Closing this PR -- the approach was wrong. fabric_write targets Fabric specifically. Fabric is an optional plugin: users running mem0, built-in MEMORY.md, Honcho, or any other provider never load it. Wiring subagent persistence to a Fabric-specific tool means the feature silently does nothing for the majority of Hermes deployments. The correct approach threads a restricted write callback through to the subagent using the existing MemoryManager interface, which is the single integration point for all memory providers. The replacement PR implements that: append-only, rate-limited (3 writes / 400 chars each), tagged [subagent], opt-in via write_memory=True on delegate_task, and gracefully a no-op when no memory provider is configured. |
Add write_memory=True param to delegate_task. When enabled, each subagent gets a subagent_memory_write tool that writes findings to the parent agents active memory provider -- builtin MEMORY.md, mem0, Honcho, or any other registered provider. Constraints enforced in make_subagent_memory_writer: - Append-only (no read, replace, or delete) - Max 3 writes per subagent run, 400 chars each - Content tagged [subagent] prefix for parent identification - Thread-safe rate limiting (lock per writer closure) - Graceful no-op when parent has no memory store How it works: - delegate_task creates a write callback closing over parent._memory_store - The callback is stored on the child as _subagent_memory_writer - The subagent_memory_write tool schema is injected into child.tools - run_agent.py dispatches subagent_memory_write to the handler in both the sequential and concurrent tool execution paths - on_memory_write() notifies external providers (mem0, Honcho, etc.) so the write mirrors to the active plugin backend The full memory tool remains blocked for subagents. write_memory=False by default -- subagents cannot write memory without explicit opt-in. Fixes: NousResearch#5338 (previous approach used fabric_write, which is an optional plugin and silently does nothing on non-Fabric deploys)
Problem
Subagents run in isolation. That isolation is the point -- they don't pollute the parent's context with intermediate tool calls. But it creates a blind spot: when a subagent finds something important, that finding lives only in its final summary text. The parent gets the string. If the parent doesn't immediately call
memoryto save it, the finding is gone.This matters for investigation-style tasks. Say the parent delegates a Render outage diagnosis. The subagent digs through logs, traces the deploy failure to a missing
REDIS_URLin the production environment, and writes that up in its summary. The parent reads the summary, fixes the immediate issue, and moves on. Three sessions later, the same symptom appears. The parent has no memory of the root cause. It delegates again.The problem is not that subagents lack intelligence. It's that their output channel is a single text blob, and durable knowledge requires an explicit write to memory.
Solution
A new tool:
fabric_write.fabric_writeis an append-only path for subagents to write key findings directly toMEMORY.mdbefore they complete. It is narrow by design:memorytool.[subagent:TOPIC]so the parent can identify and review them.The tool is injected automatically when the parent agent has memory enabled. No config change required. Set
write_memory=falseindelegate_taskto disable it for a specific delegation.What changes
tools/fabric_write_tool.py(new)The tool implementation. Validates
topicandcontent, applies injection scanning, checks the per-agent write counter, then callsMemoryStore.add(). TheMemoryStorere-reads from disk under a file lock before appending, so concurrent parent and subagent writes are safe.tools/delegate_tool.py_build_child_system_promptgains amemory_write_enabledparam. When true, the prompt tells the subagent when to usefabric_writeand what qualifies as worth writing._inject_fabric_toolset(new function) adds thefabrictoolset to the child's enabled list when the parent has a_memory_storeandwrite_memoryis notFalse._build_child_agentcalls_inject_fabric_toolsetafter_strip_blocked_tools, so the fabric toolset is never accidentally removed.delegate_taskand its schema gain awrite_memoryoptional boolean parameter.DELEGATE_BLOCKED_TOOLScomment updated to clarify thatfabric_writeis the replacement path for subagent writes (notmemory).toolsets.pyAdds the
fabrictoolset. It is absent from_strip_blocked_tools's blocked set, which is what makes the inject-after-strip pattern work.tests/tools/test_fabric_write.py(new)21 tests:
[subagent:TOPIC]tag present)_strip_blocked_toolsdoes not removefabricExample
Parent delegates a production outage investigation:
The subagent investigates and, before returning its summary, calls:
MEMORY.md now contains:
The parent sees this on the next session, before the first turn.
Opt-out
Memory writes are disabled per-call:
Or globally: if the parent has no memory store (
memory_enabled: falsein config), subagents never receivefabric_write.What this does not change
memorytool.DELEGATE_BLOCKED_TOOLSis unchanged.skip_memory=True. They do not read or loadMEMORY.md.MemoryStorestate is not updated mid-session (consistent with how the fullmemorytool works -- updates are visible on next session start).Tests
All 21 pass.