fix(agent): give the orchestrator direct memory tools so recall/store don't over-delegate (#4764)

This commit is contained in:
Mega Mind
2026-07-13 02:56:07 -07:00
committed by GitHub
parent a364693fc7
commit 383581bd6c
3 changed files with 54 additions and 1 deletions
@@ -751,6 +751,28 @@ mod tests {
"orchestrator must have read-only direct tool `{direct}`"
);
}
// Direct memory surface (#4762): recall/store are the product's
// core and must be first-class direct tools, not a sub-agent
// spawn — a trivial recall or a single "remember this" must not
// pay a blocking agentic round-trip (over-delegation, #4744) that
// can hang or return a 0-char result with persistence unconfirmed.
// Deep tree walks / reconciliation still delegate to
// `retrieve_memory` / `manage_profile_memory`.
for direct in ["memory_recall", "memory_store", "save_preference"] {
assert!(
tools.iter().any(|t| t == direct),
"orchestrator must have direct memory tool `{direct}` (#4762)"
);
}
// Memory-protocol close-out (#4116): a direct `memory_store` write
// obliges an `update_memory_md` index reconcile, so the tool that
// performs it must be in scope — otherwise the protocol's guidance
// is unsatisfiable and MEMORY.md (loaded here) drifts from the store.
assert!(
tools.iter().any(|t| t == "update_memory_md"),
"orchestrator must have `update_memory_md` to reconcile MEMORY.md \
after a direct memory_store (#4762)"
);
}
ToolScope::Wildcard => panic!("orchestrator must have named tool allowlist"),
}
@@ -231,6 +231,36 @@ named = [
"web_search_tool",
"web_fetch",
"http_request",
# Direct memory surface (#4762). Recall/store are the product's core and must
# be first-class DIRECT tools, not a sub-agent spawn: a trivial "what is my Q3
# OKR" recall or a "remember my Q3 OKR" write should not pay a blocking
# agentic round-trip (over-delegation, #4744) that can hang or — as observed —
# spawn `manage_profile_memory` which did 0 tool calls and returned a 0-char
# result, leaving the write's persistence unconfirmed. `memory_recall` reads
# the same store `memory_store` writes, so store→recall closes the loop inline
# with a real confirmation. Deep tree walks / reconciliation / people-graph /
# persona edits still delegate to `retrieve_memory` (agent_memory) and
# `manage_profile_memory` (profile_memory_agent) below. These are memory-store
# writes, NOT file/shell mutation — the "no write tools" rule (below) is about
# editing files and running commands, which stays out.
"memory_recall",
"memory_store",
"save_preference",
# Memory-protocol close-out (#4116). A successful `memory_store` is a durable
# write, so the memory-protocol middleware appends a "call `update_memory_md`
# to keep the MEMORY.md index in sync" note to the tool result and warns at run
# end if the index was never reconciled. Since the orchestrator now takes the
# direct-store path itself, it must own that closing step too — otherwise the
# guidance is unsatisfiable and MEMORY.md (which this agent loads,
# `omit_memory_md = false`) drifts from the store. `update_memory_md` is locked
# to the MEMORY.md / SKILL.md workspace index files (enum-gated, symlink-hardened),
# NOT a general file-editor: it is the index-sync counterpart of `memory_store`,
# so it belongs in the same memory bucket as the writes above — the "no write
# tools" rule (below) is about editing arbitrary files (`edit` / `file_write` /
# `apply_patch`) and running commands, which stay out. `save_preference` writes
# to the preference store and is deliberately NOT part of the MEMORY.md wiki, so
# it needs no reconcile.
"update_memory_md",
# Managed file-storage transfer surface for passing artifacts/attachments
# between chat, users, and workers. Storage lifecycle mutators
# (`storage_set_visibility`, `storage_delete_file`) stay out of the broad
@@ -23,7 +23,8 @@ Follow this sequence for every user message:
- **Do this even if remembered context could plausibly answer.** The user wants the live source of truth, not a stale summary.
- If the relevant toolkit is **not** in **Connected Integrations**, call `composio_connect { toolkit: "<slug>" }` **directly** to raise an **inline connect card** so the user can authorize in one click, then continue the task once it returns `connected: true`. Do **not** refuse based on the Connected Integrations list (that is only what is *already* connected, not what is *connectable*), do **not** make "go to Settings → Connections" your first move, and do **not** silently fall back to memory retrieval (see "Connecting external services" below).
3. **Can I solve this with direct tools?**
- Yes: use direct tools (`retrieve_memory`, `read_workspace_state`, `composio_list_connections`, task tools, etc.).
- Yes: use direct tools (`memory_recall`, `read_workspace_state`, `composio_list_connections`, task tools, etc.).
- **Memory is direct work.** Recalling a fact (`memory_recall`), storing a fact (`memory_store`), or saving a preference (`save_preference`) needs **no sub-agent** — call the direct tool and confirm the result. After a `memory_store` write, follow the memory protocol and call `update_memory_md` (targeting `MEMORY.md`) to keep the index in sync with the store; `save_preference` writes the preference store and needs no such reconcile. Reserve the `retrieve_memory` and `manage_profile_memory` **delegates** for genuinely deep work: multi-hop memory-tree walks, ingest/index into the long-term tree, reconciling overlapping notes, people-graph/alias management, or persona edits. Do **not** spawn a sub-agent for a single recall or a single "remember this".
- **Quick lookups are direct work.** Use `web_search_tool` for quick discovery, `web_fetch` for one URL/body read, and `http_request` for basic API/HTTP semantics (methods, headers, JSON endpoints, status/HEAD checks). Reserve `research` for multi-source crawls, comparisons, deep digests, or uncertain evidence gathering.
- **Read-only file lookups are direct work.** Reading a file the user names, grepping for a string, or listing a directory (`file_read` / `grep` / `glob` / `list`) needs no sub-agent. Managed storage transfer is also direct when the user needs uploaded/downloaded/listed/linked artifacts. But you cannot use generic write/edit tools: the moment the task requires *changing* a file — even a one-line edit — delegate it to `run_code` (see below). Never promise an edit you cannot make yourself.
- **Listing conversation threads is direct work.** "List / show my recent threads (or conversations)" is a single `thread_list` call you make yourself — do **not** delegate it to `retrieve_memory` / a memory sub-agent. Memory retrieval walks the *memory tree* (ingested facts), which is the wrong tool for enumerating chat threads. Reserve `retrieve_memory` for questions about remembered content, not the thread index.