Packages

fnord

0.9.13

AI code archaeology

Current section

Files

Jump to
fnord lib ai agent memory consolidator.ex
Raw

lib/ai/agent/memory/consolidator.ex

defmodule AI.Agent.Memory.Consolidator do
@moduledoc """
Agent that examines a single long-term memory ("focus") alongside its most
similar candidates and decides whether to merge, delete, or keep them.
Returns a structured JSON response (no prose) with shape:
{
"actions": [ action objects... ],
"keep": true | false
}
Action objects:
- {"action": "merge", "target": {"scope": "...", "title": "..."}, "content": "...", "reason": "..."}
Merge the candidate INTO the focus memory. The "content" field is the
rewritten focus content incorporating the candidate's information. The
candidate will be deleted after the merge.
- {"action": "delete", "target": {"scope": "...", "title": "..."}, "reason": "..."}
Delete a candidate that is fully subsumed by the focus memory.
When "keep" is false, the focus memory itself is redundant and should be
deleted (e.g. it duplicates something already processed earlier in the run).
"""
@behaviour AI.Agent
@model AI.Model.large_context()
@prompt """
You are the Memory Consolidator. You will receive a JSON payload containing:
- "focus": a single long-term memory (title, content, scope, topics)
- "candidates": a list of similar memories with similarity scores and tier labels
Your job is to decide how to consolidate these memories. You MUST return a
single JSON object (no prose) with these keys:
- "actions": an array of action objects (may be empty)
- "keep": boolean -- whether the focus memory should be kept
Action objects must be one of:
{"action":"merge","target":{"scope":"...","title":"..."},"content":"...","reason":"..."}
{"action":"delete","target":{"scope":"...","title":"..."},"reason":"..."}
Every action MUST include a "reason" field -- a brief explanation of why this
action was chosen (e.g. "near-duplicate of focus", "fully subsumed by focus",
"contains outdated info superseded by focus"). This is used for diagnostic
logging.
When "keep" is false, include a top-level "reason" field explaining why the
focus itself is redundant.
### Merge action
When a candidate is highly similar to the focus memory, merge the candidate's
unique information INTO the focus memory. The "content" field must contain the
complete rewritten content for the focus memory, incorporating any valuable
information from the candidate. The candidate will be deleted after the merge.
When merging, preserve the focus memory's title and scope. The merged content
should be a clean synthesis -- not a concatenation -- of both memories.
### Delete action
When a candidate is fully subsumed by the focus memory (all its information
is already captured), emit a delete action for the candidate.
### Keep = false
Set "keep" to false ONLY when the focus memory itself is completely redundant
-- all of its information is already captured in other memories that will NOT
be deleted by this response's actions. This is rare; default to true.
### Similarity tiers
Each candidate includes a "tier" label based on cosine similarity:
- "high" (score > 0.5): Strong match -- likely duplicate or near-duplicate. Merge or delete.
- "moderate" (score 0.3-0.5): Possible overlap -- merge only if content confirms real redundancy.
- "low" (score < 0.3): Weak match -- included for context only. Rarely worth merging.
### Rules
- Do NOT merge memories that merely share a topic but contain distinct information.
- Do NOT delete memories that contain information not captured elsewhere.
- Preserve all unique, valuable information. When in doubt, keep both.
- If no candidates warrant action, return {"actions": [], "keep": true}.
- The "Me" identity memory (global, titled "Me") should absorb personality/style
observations from other memories, but never be deleted.
IMPORTANT: Return *only* valid JSON. No explanatory text or commentary.
"""
@impl AI.Agent
def get_response(opts) do
with {:ok, agent} <- Map.fetch(opts, :agent),
{:ok, payload} <- Map.fetch(opts, :payload) do
messages = [AI.Util.system_msg(@prompt), AI.Util.system_msg(payload)]
# Empty toolbox: the agent returns structured JSON decisions and the
# consolidator module applies them directly.
agent
|> AI.Agent.get_completion(
model: @model,
log_msgs: false,
log_tool_calls: false,
messages: messages,
toolbox: %{}
)
|> case do
{:ok, %{response: response}} -> {:ok, response}
{:error, reason} -> {:error, reason}
end
end
end
end