mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-26 11:12:03 +00:00
feat(memory): batch operations for single-turn memory updates (#48507)
The memory tool was strictly one-op-per-call. With the store running near its char limit by design, a new add that would overflow gets rejected with 'consolidate now, then retry' -- but the model could not consolidate and add in one call. It had to remove/replace across several turns, then retry the add, each turn re-sending the whole conversation context. Expensive thrash. Add an 'operations' array: a list of add/replace/remove ops applied atomically against the FINAL char budget. The model frees space and adds new entries in ONE call, even when an add alone would overflow. All-or-nothing: any bad op aborts the whole batch, nothing written. Root-cause note: the two agent-level memory interception sites (agent_runtime_helpers.py, tool_executor.py) silently dropped any param not in their explicit kwarg list, so 'operations' never reached the handler and batch calls failed with 'Unknown action None'. Both now pass it through and bridge each add/replace op to external memory providers. Also: success response is now terminal (done=true + 'do not repeat' note, no full-entries echo that invited re-edits); schema rewritten to lead with the batch mechanism and an explicit one-shot stop rule (2138 -> 1476 chars). Live-verified: near-full consolidate-and-add went 7 calls -> 1 call, stable across 3 reps. 103 memory/approval tests + 398 background-review/ run_agent tests green; 6 new batch tests added.
This commit is contained in:
parent
2fa16ec2d2
commit
38c8a9c10f
6 changed files with 417 additions and 60 deletions
|
|
@ -1012,28 +1012,42 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
elif function_name == "memory":
|
||||
def _execute(next_args: dict) -> Any:
|
||||
target = next_args.get("target", "memory")
|
||||
operations = next_args.get("operations")
|
||||
from tools.memory_tool import memory_tool as _memory_tool
|
||||
result = _memory_tool(
|
||||
action=next_args.get("action"),
|
||||
target=target,
|
||||
content=next_args.get("content"),
|
||||
old_text=next_args.get("old_text"),
|
||||
operations=operations,
|
||||
store=agent._memory_store,
|
||||
)
|
||||
# Bridge: notify external memory provider of built-in memory writes
|
||||
if agent._memory_manager and next_args.get("action") in {"add", "replace"}:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
next_args.get("action", ""),
|
||||
target,
|
||||
next_args.get("content", ""),
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", None),
|
||||
),
|
||||
# Bridge: notify external memory provider of built-in memory writes.
|
||||
# Covers both the single-op shape and each add/replace inside a batch.
|
||||
if agent._memory_manager:
|
||||
if operations:
|
||||
_mem_ops = [
|
||||
op for op in operations
|
||||
if isinstance(op, dict) and op.get("action") in {"add", "replace"}
|
||||
]
|
||||
else:
|
||||
_mem_ops = (
|
||||
[{"action": next_args.get("action"), "content": next_args.get("content")}]
|
||||
if next_args.get("action") in {"add", "replace"} else []
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
for _op in _mem_ops:
|
||||
try:
|
||||
agent._memory_manager.on_memory_write(
|
||||
_op.get("action", ""),
|
||||
target,
|
||||
_op.get("content", "") or "",
|
||||
metadata=agent._build_memory_write_metadata(
|
||||
task_id=effective_task_id,
|
||||
tool_call_id=getattr(tool_call, "id", None),
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
function_result, function_args = _run_agent_tool_execution_middleware(
|
||||
agent,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue