Skip to content

feat: fabric_write tool -- subagents persist key findings to memory before completing#5338

Closed
ZK-Snarky wants to merge 1 commit into
NousResearch:mainfrom
ZK-Snarky:feat/subagent-fabric-write
Closed

feat: fabric_write tool -- subagents persist key findings to memory before completing#5338
ZK-Snarky wants to merge 1 commit into
NousResearch:mainfrom
ZK-Snarky:feat/subagent-fabric-write

Conversation

@ZK-Snarky

Copy link
Copy Markdown

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 memory to 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_URL in 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_write is an append-only path for subagents to write key findings directly to MEMORY.md before they complete. It is narrow by design:

  • Append-only. No read, replace, or remove.
  • 3 writes per subagent execution (tracked on the agent instance).
  • 400 chars per entry.
  • Same injection and exfiltration scanning as the full memory tool.
  • Entries are tagged [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=false in delegate_task to disable it for a specific delegation.

What changes

tools/fabric_write_tool.py (new)

The tool implementation. Validates topic and content, applies injection scanning, checks the per-agent write counter, then calls MemoryStore.add(). The MemoryStore re-reads from disk under a file lock before appending, so concurrent parent and subagent writes are safe.

tools/delegate_tool.py

  • _build_child_system_prompt gains a memory_write_enabled param. When true, the prompt tells the subagent when to use fabric_write and what qualifies as worth writing.
  • _inject_fabric_toolset (new function) adds the fabric toolset to the child's enabled list when the parent has a _memory_store and write_memory is not False.
  • _build_child_agent calls _inject_fabric_toolset after _strip_blocked_tools, so the fabric toolset is never accidentally removed.
  • delegate_task and its schema gain a write_memory optional boolean parameter.
  • DELEGATE_BLOCKED_TOOLS comment updated to clarify that fabric_write is the replacement path for subagent writes (not memory).

toolsets.py

Adds the fabric toolset. 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:

  • Input validation (empty, too long)
  • Rate limiting (3 writes succeed, 4th fails; failed writes don't consume a slot)
  • Content scanning (injection patterns blocked, normal findings pass)
  • Entry format ([subagent:TOPIC] tag present)
  • Toolset injection (memory present vs absent, write_memory=False, no duplicates)
  • System prompt content (fabric_write instructions appear/absent based on flag)
  • _strip_blocked_tools does not remove fabric

Example

Parent delegates a production outage investigation:

delegate_task(
    goal="Diagnose why the Render deploy is failing on the production worker",
    context="Deploy logs: worker exits with code 1 during health check. Repo: /srv/app",
    toolsets=["terminal", "file", "web"],
)

The subagent investigates and, before returning its summary, calls:

fabric_write(
    topic="render_root_cause",
    content="Worker health check fails because REDIS_URL is not set in Render prod env vars. App reads it at startup -- missing key causes immediate exit. Add REDIS_URL to Render dashboard under Environment."
)

MEMORY.md now contains:

[subagent:render_root_cause] Worker health check fails because REDIS_URL is not set in Render prod env vars. App reads it at startup -- missing key causes immediate exit. Add REDIS_URL to Render dashboard under Environment.

The parent sees this on the next session, before the first turn.

Opt-out

Memory writes are disabled per-call:

delegate_task(goal="Summarize the README", write_memory=False)

Or globally: if the parent has no memory store (memory_enabled: false in config), subagents never receive fabric_write.

What this does not change

  • Subagents still cannot use the full memory tool. DELEGATE_BLOCKED_TOOLS is unchanged.
  • Subagents still run with skip_memory=True. They do not read or load MEMORY.md.
  • The parent's in-memory MemoryStore state is not updated mid-session (consistent with how the full memory tool works -- updates are visible on next session start).
  • Batch delegations behave identically -- each child gets the same fabric injection.

Tests

python3 -m pytest tests/tools/test_fabric_write.py -v

All 21 pass.

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
@ZK-Snarky

Copy link
Copy Markdown
Author

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

@ZK-Snarky ZK-Snarky closed this Apr 5, 2026
@ZK-Snarky

Copy link
Copy Markdown
Author

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.

ZK-Snarky added a commit to ZK-Snarky/hermes-agent that referenced this pull request Apr 5, 2026
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)
@ZK-Snarky ZK-Snarky deleted the feat/subagent-fabric-write branch May 9, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant