Memory tools: never expose raw memo commands to agents
- Add MEMO_TOOLS=1 env var to memo script; when set, all printed instructions reference native tool names instead of memo paths - Set MEMO_TOOLS=1 in all memory tool .meta wrappers - Add memory_nap tool for compressions - Add memory_zoom tool for tree navigation - Add part/T pagination args to memory_wake - Update system prompt to use memory_zoom tool call - Fix inject ctl: submit as user message when agent is idle
This commit is contained in:
parent
9bb2e41e33
commit
b4baddd826
|
|
@ -831,7 +831,14 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return nil, fmt.Errorf("inject requires text")
|
return nil, fmt.Errorf("inject requires text")
|
||||||
}
|
}
|
||||||
a.InjectRewrite(text)
|
if a.IsRunning() {
|
||||||
|
a.InjectRewrite(text)
|
||||||
|
} else {
|
||||||
|
startAsync(s.Ctx, func() {
|
||||||
|
a.Submit(s.Ctx, text)
|
||||||
|
a.EnsureTrailingNewline()
|
||||||
|
})
|
||||||
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
"i": func(args []string) ([]byte, error) {
|
"i": func(args []string) ([]byte, error) {
|
||||||
|
|
@ -839,7 +846,14 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
if text == "" {
|
if text == "" {
|
||||||
return nil, fmt.Errorf("inject requires text")
|
return nil, fmt.Errorf("inject requires text")
|
||||||
}
|
}
|
||||||
a.InjectRewrite(text)
|
if a.IsRunning() {
|
||||||
|
a.InjectRewrite(text)
|
||||||
|
} else {
|
||||||
|
startAsync(s.Ctx, func() {
|
||||||
|
a.Submit(s.Ctx, text)
|
||||||
|
a.EnsureTrailingNewline()
|
||||||
|
})
|
||||||
|
}
|
||||||
return []byte("ok\n"), nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
"agent": func(args []string) ([]byte, error) {
|
"agent": func(args []string) ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,8 @@ If `memory_remember` asks a compression: do it before your next action.
|
||||||
|
|
||||||
Your memories also form a binary tree: #0-1, #2-3 ... exist as one-line summaries, pairs of those as #0-3, and so on — every `#a-b` line wake prints is one node of it. To open a node into its two halves, down to the raw memories:
|
Your memories also form a binary tree: #0-1, #2-3 ... exist as one-line summaries, pairs of those as #0-3, and so on — every `#a-b` line wake prints is one node of it. To open a node into its two halves, down to the raw memories:
|
||||||
|
|
||||||
```bash
|
```
|
||||||
MEMORY_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem" \
|
memory_zoom(range="<a-b>")
|
||||||
"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo" zoom <a-b>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## If you're a subagent: skip everything above
|
## If you're a subagent: skip everything above
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
"memory_wake",
|
"memory_wake",
|
||||||
"memory_recall",
|
"memory_recall",
|
||||||
"memory_remember",
|
"memory_remember",
|
||||||
|
"memory_nap",
|
||||||
|
"memory_zoom",
|
||||||
"file_read",
|
"file_read",
|
||||||
"file_edit",
|
"file_edit",
|
||||||
"file_glob",
|
"file_glob",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"description": "Submit a memory compression (nap) to OptMem.",
|
||||||
|
"prompt": "## memory_nap\n\nSubmit a compression for a memory block range. Called when memory_wake or memory_remember prints a compression prompt.\n\n**Calling convention:**\n```\nmemory_nap(range=\"0-1\", text=\"compressed one-line summary\")\n```\n- `range`: block range as printed by the compression prompt (e.g. \"0-1\")\n- `text`: the compressed one-line summary (max 280 bytes)\n\nIf more compressions remain, the tool prints the next one.",
|
||||||
|
"cmd": "input=$(cat); range=$(printf '%s' \"$input\" | jq -er '.range'); text=$(printf '%s' \"$input\" | jq -er '.text'); MEMO_TOOLS=1 MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" nap \"$range\" \"$text\"",
|
||||||
|
"args": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["range", "text"],
|
||||||
|
"properties": {
|
||||||
|
"range": {"type": "string", "description": "Block range (e.g. \"0-1\")"},
|
||||||
|
"text": {"type": "string", "description": "Compressed one-line summary (max 280 bytes)"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope": "global"
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"description": "Search stored memories for relevant context.",
|
"description": "Search stored memories for relevant context.",
|
||||||
"prompt": "## memory_recall\n\nSearch stored memories for relevant context using OptMem's bounded memory index.\n\n**Calling convention:**\n```\nmemory_recall(query=\"keyword\")\n```\n- `query`: regular expression or short search term.\n\n**Returns:** matching memory records from the persistent OptMem store.",
|
"prompt": "## memory_recall\n\nSearch stored memories for relevant context using OptMem's bounded memory index.\n\n**Calling convention:**\n```\nmemory_recall(query=\"keyword\")\n```\n- `query`: regular expression or short search term.\n\n**Returns:** matching memory records from the persistent OptMem store.",
|
||||||
"cmd": "input=$(cat); query=$(printf '%s' \"$input\" | jq -er '.query'); MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" recall \"$query\"",
|
"cmd": "input=$(cat); query=$(printf '%s' \"$input\" | jq -er '.query'); MEMO_TOOLS=1 MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" recall \"$query\"",
|
||||||
"args": {
|
"args": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["query"],
|
"required": ["query"],
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"description": "Persist a fact that would otherwise be lost when the session ends.",
|
"description": "Persist a fact that would otherwise be lost when the session ends.",
|
||||||
"prompt": "## memory_remember\n\nPersist a durable fact in OptMem's append-only memory store.\n\n**Calling convention:**\n```\nmemory_remember(title=\"...\", tags=\"...\", body=\"...\")\n```\n- `title`: short noun phrase\n- `tags`: comma-separated tags\n- `body`: one standalone fact, no more than 280 bytes\n\nThe title, tags, and body are stored as one searchable memory record.",
|
"prompt": "## memory_remember\n\nPersist a durable fact in OptMem's append-only memory store.\n\n**Calling convention:**\n```\nmemory_remember(title=\"...\", tags=\"...\", body=\"...\")\n```\n- `title`: short noun phrase\n- `tags`: comma-separated tags\n- `body`: one standalone fact, no more than 280 bytes\n\nThe title, tags, and body are stored as one searchable memory record.",
|
||||||
"cmd": "input=$(cat); title=$(printf '%s' \"$input\" | jq -er '.title'); tags=$(printf '%s' \"$input\" | jq -er '.tags'); body=$(printf '%s' \"$input\" | jq -er '.body'); record=\"[$tags] $title: $body\"; bytes=$(printf '%s' \"$record\" | wc -c); [ \"$bytes\" -le 280 ] || { printf 'memory exceeds OptMem limit: %s bytes (maximum 280)\\n' \"$bytes\" >&2; exit 1; }; MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" note \"$record\"",
|
"cmd": "input=$(cat); title=$(printf '%s' \"$input\" | jq -er '.title'); tags=$(printf '%s' \"$input\" | jq -er '.tags'); body=$(printf '%s' \"$input\" | jq -er '.body'); record=\"[$tags] $title: $body\"; bytes=$(printf '%s' \"$record\" | wc -c); [ \"$bytes\" -le 280 ] || { printf 'memory exceeds OptMem limit: %s bytes (maximum 280)\\n' \"$bytes\" >&2; exit 1; }; MEMO_TOOLS=1 MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" note \"$record\"",
|
||||||
"args": {
|
"args": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["title", "tags", "body"],
|
"required": ["title", "tags", "body"],
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"description": "Load the bounded OptMem context at session startup.",
|
"description": "Load the bounded OptMem context at session startup.",
|
||||||
"prompt": "## memory_wake\n\nLoad the bounded persistent memory context. Run this once before other tools at the start of every top-level session. Follow any printed OptMem compression instruction before continuing. Do not run from a sub-agent.",
|
"prompt": "## memory_wake\n\nLoad the bounded persistent memory context. Run this once before other tools at the start of every top-level session. Follow any printed OptMem compression instruction before continuing. Do not run from a sub-agent.",
|
||||||
"cmd": "MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" wake",
|
"cmd": "input=$(cat); part=$(printf '%s' \"$input\" | jq -r '.part // empty'); T=$(printf '%s' \"$input\" | jq -r '.T // empty'); args=''; [ -n \"$part\" ] && args=\"$part\"; [ -n \"$T\" ] && args=\"$args $T\"; MEMO_TOOLS=1 MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" wake $args",
|
||||||
"args": {
|
"args": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {}
|
"properties": {
|
||||||
|
"part": {"type": "integer", "description": "Page number for paginated memory (default: 1)"},
|
||||||
|
"T": {"type": "integer", "description": "Memory snapshot count (used with part for pagination)"}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tier": "cold",
|
"tier": "cold",
|
||||||
"scope": "read"
|
"scope": "read"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"description": "Expand a memory tree node into its two halves.",
|
||||||
|
"prompt": "## memory_zoom\n\nOpen a memory tree node into its two halves, down to raw memories.\n\n**Calling convention:**\n```\nmemory_zoom(range=\"0-3\")\n```\n- `range`: block range as printed by memory_wake (e.g. \"0-3\")\n\nReturns the two child nodes (summaries or raw memories).",
|
||||||
|
"cmd": "input=$(cat); range=$(printf '%s' \"$input\" | jq -er '.range'); MEMO_TOOLS=1 MEMORY_DIR=\"${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem\" exec \"${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo\" zoom \"$range\"",
|
||||||
|
"args": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["range"],
|
||||||
|
"properties": {
|
||||||
|
"range": {"type": "string", "description": "Block range to expand (e.g. \"0-3\")"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tier": "cold",
|
||||||
|
"scope": "read"
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,30 @@ def pretty(p):
|
||||||
# PATH, so a bare `memo nap 0-1 "..."` would not: the tool names itself.
|
# PATH, so a bare `memo nap 0-1 "..."` would not: the tool names itself.
|
||||||
ME = pretty(__file__)
|
ME = pretty(__file__)
|
||||||
|
|
||||||
|
# When called through native tool wrappers, MEMO_TOOLS=1 switches all printed
|
||||||
|
# instructions to reference tool names instead of raw memo commands.
|
||||||
|
TOOL_MODE = os.environ.get("MEMO_TOOLS") == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def tool_cmd(verb, args=""):
|
||||||
|
"""Return the command string the agent should run. In tool mode this is a
|
||||||
|
tool call; otherwise it's the raw memo invocation."""
|
||||||
|
if TOOL_MODE:
|
||||||
|
MAP = {
|
||||||
|
"wake": "memory_wake",
|
||||||
|
"note": "memory_remember",
|
||||||
|
"nap": "memory_nap",
|
||||||
|
"recall": "memory_recall",
|
||||||
|
"zoom": "memory_zoom",
|
||||||
|
}
|
||||||
|
name = MAP.get(verb, "%s %s" % (ME, verb))
|
||||||
|
if args:
|
||||||
|
return "%s(%s)" % (name, args)
|
||||||
|
return name
|
||||||
|
if args:
|
||||||
|
return "%s %s %s" % (ME, verb, args)
|
||||||
|
return "%s %s" % (ME, verb)
|
||||||
|
|
||||||
# The sizes a memory may override in its own `config` file: the default, and
|
# The sizes a memory may override in its own `config` file: the default, and
|
||||||
# what it means. `memo config` shows and edits them. The globals below start
|
# what it means. `memo config` shows and edits them. The globals below start
|
||||||
# at these defaults, so a memory that overrides nothing follows the tool.
|
# at these defaults, so a memory that overrides nothing follows the tool.
|
||||||
|
|
@ -482,12 +506,13 @@ def nap_prompt(d, lo, hi, left):
|
||||||
tail = "" if not left else "\n%s after this one." % (
|
tail = "" if not left else "\n%s after this one." % (
|
||||||
"1 compression remains" if left == 1 else
|
"1 compression remains" if left == 1 else
|
||||||
"%d compressions remain" % left)
|
"%d compressions remain" % left)
|
||||||
|
nap_cmd = tool_cmd("nap", 'range="%d-%d", text="<your line>"' % (lo, hi - 1))
|
||||||
return ("Compress memories #%d-%d into one line of at most %d bytes.\n"
|
return ("Compress memories #%d-%d into one line of at most %d bytes.\n"
|
||||||
"Keep what has lasting effect, drop what does not. Invent "
|
"Keep what has lasting effect, drop what does not. Invent "
|
||||||
"nothing.\n\n"
|
"nothing.\n\n"
|
||||||
"%s\n%s\n"
|
"%s\n%s\n"
|
||||||
"Run: %s nap %d-%d \"<your line>\""
|
"Run: %s"
|
||||||
% (lo, hi - 1, ENTRY_CHARS, body, tail, ME, lo, hi - 1))
|
% (lo, hi - 1, ENTRY_CHARS, body, tail, nap_cmd))
|
||||||
|
|
||||||
|
|
||||||
def next_nap(d, T):
|
def next_nap(d, T):
|
||||||
|
|
@ -595,13 +620,13 @@ def cmd_wake(d, args):
|
||||||
if len(args) == 2:
|
if len(args) == 2:
|
||||||
T = int(args[1])
|
T = int(args[1])
|
||||||
if T > now:
|
if T > now:
|
||||||
die("T=%d, but the log holds %s. Run: %s wake"
|
die("T=%d, but the log holds %s. Run: %s"
|
||||||
% (T, plural(now, "memory"), ME))
|
% (T, plural(now, "memory"), tool_cmd("wake")))
|
||||||
# A part is rendered as of T, so a note landing between two parts cannot
|
# A part is rendered as of T, so a note landing between two parts cannot
|
||||||
# shift a boundary and drop a line.
|
# shift a boundary and drop a line.
|
||||||
if not T:
|
if not T:
|
||||||
print("No memories yet. Record the first with: %s note \"<one line>\""
|
print("No memories yet. Use %s to record your first one."
|
||||||
% ME)
|
% tool_cmd("note"))
|
||||||
print("You are awake.")
|
print("You are awake.")
|
||||||
return
|
return
|
||||||
lines = []
|
lines = []
|
||||||
|
|
@ -619,9 +644,10 @@ def cmd_wake(d, args):
|
||||||
# costing no round trip.
|
# costing no round trip.
|
||||||
print("Cannot wake: the memory context needs #%d-%d, "
|
print("Cannot wake: the memory context needs #%d-%d, "
|
||||||
"which is not compressed yet.\nDo the %s below, "
|
"which is not compressed yet.\nDo the %s below, "
|
||||||
"then run %s wake again.\n"
|
"then run %s again.\n"
|
||||||
% (lo, hi - 1,
|
% (lo, hi - 1,
|
||||||
plural(pending_count(d, T), "compression"), ME))
|
plural(pending_count(d, T), "compression"),
|
||||||
|
tool_cmd("wake")))
|
||||||
print(nap)
|
print(nap)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
s = tree_get(d, lo, hi) # a parallel session may have paid it
|
s = tree_get(d, lo, hi) # a parallel session may have paid it
|
||||||
|
|
@ -633,8 +659,8 @@ def cmd_wake(d, args):
|
||||||
lines.append("#%d-%d %s" % (lo, hi - 1, s))
|
lines.append("#%d-%d %s" % (lo, hi - 1, s))
|
||||||
parts = paginate(lines)
|
parts = paginate(lines)
|
||||||
if not 1 <= k <= len(parts):
|
if not 1 <= k <= len(parts):
|
||||||
die("No part %d: the memory has %s. Run: %s wake"
|
die("No part %d: the memory has %s. Run: %s"
|
||||||
% (k, plural(len(parts), "part"), ME))
|
% (k, plural(len(parts), "part"), tool_cmd("wake")))
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
# The count is here so the T in `memo wake 2 296` reads as what it
|
# The count is here so the T in `memo wake 2 296` reads as what it
|
||||||
# is: the snapshot this document was written from.
|
# is: the snapshot this document was written from.
|
||||||
|
|
@ -645,7 +671,8 @@ def cmd_wake(d, args):
|
||||||
# This footer is the only instruction that survives every harness's
|
# This footer is the only instruction that survives every harness's
|
||||||
# truncation (pi drops the HEAD of a long output), so it has to say
|
# truncation (pi drops the HEAD of a long output), so it has to say
|
||||||
# both that the read is unfinished and how to continue it.
|
# both that the read is unfinished and how to continue it.
|
||||||
print("Not awake yet. Run: %s wake %d %d" % (ME, k + 1, T))
|
print("Not awake yet. Run: %s"
|
||||||
|
% tool_cmd("wake", 'part=%d, T=%d' % (k + 1, T)))
|
||||||
else:
|
else:
|
||||||
# always, even for a one-part memory: the contract an agent is given
|
# always, even for a one-part memory: the contract an agent is given
|
||||||
# is "run parts until one says awake", so it must always arrive
|
# is "run parts until one says awake", so it must always arrive
|
||||||
|
|
@ -683,8 +710,8 @@ def cmd_nap(d, args):
|
||||||
print("%d-%d is already settled." % (lo, hi - 1))
|
print("%d-%d is already settled." % (lo, hi - 1))
|
||||||
else:
|
else:
|
||||||
die("Wrong block: %s. Blocks are built in order; the next is "
|
die("Wrong block: %s. Blocks are built in order; the next is "
|
||||||
"%d-%d. Run: %s nap"
|
"%d-%d. Run: %s"
|
||||||
% (args[0], todo[0][0], todo[0][1] - 1, ME))
|
% (args[0], todo[0][0], todo[0][1] - 1, tool_cmd("nap")))
|
||||||
elif not tree_put(d, lo, hi, check(args[1])):
|
elif not tree_put(d, lo, hi, check(args[1])):
|
||||||
print("%d-%d was settled or forgotten meanwhile." % (lo, hi - 1))
|
print("%d-%d was settled or forgotten meanwhile." % (lo, hi - 1))
|
||||||
else:
|
else:
|
||||||
|
|
@ -728,8 +755,9 @@ def cmd_forget(d, args):
|
||||||
gone = tree_drop(d, *block_id(args[0]))
|
gone = tree_drop(d, *block_id(args[0]))
|
||||||
if not gone:
|
if not gone:
|
||||||
die("No summary at %s." % args[0])
|
die("No summary at %s." % args[0])
|
||||||
print("Forgot %s, from %d-%d up. Run: %s nap"
|
print("Forgot %s, from %d-%d up. Run: %s"
|
||||||
% (plural(len(gone), "summary"), gone[0][0], gone[0][1] - 1, ME))
|
% (plural(len(gone), "summary"), gone[0][0], gone[0][1] - 1,
|
||||||
|
tool_cmd("nap")))
|
||||||
|
|
||||||
|
|
||||||
def cmd_recall(d, args):
|
def cmd_recall(d, args):
|
||||||
|
|
@ -773,8 +801,8 @@ def cmd_zoom(d, args):
|
||||||
lo, hi = block_id(args[0])
|
lo, hi = block_id(args[0])
|
||||||
T = log_len(d)
|
T = log_len(d)
|
||||||
if lo >= T:
|
if lo >= T:
|
||||||
die("#%s is beyond the memory: it holds %s. Run: %s wake"
|
die("#%s is beyond the memory: it holds %s. Run: %s"
|
||||||
% (args[0], plural(T, "memory"), ME))
|
% (args[0], plural(T, "memory"), tool_cmd("wake")))
|
||||||
mid = (lo + hi) // 2
|
mid = (lo + hi) // 2
|
||||||
for a, b in ((lo, mid), (mid, hi)):
|
for a, b in ((lo, mid), (mid, hi)):
|
||||||
if a >= T:
|
if a >= T:
|
||||||
|
|
@ -824,7 +852,7 @@ def cmd_import(d, args):
|
||||||
% (plural(len(out), "memory"), base, base + len(out) - 1))
|
% (plural(len(out), "memory"), base, base + len(out) - 1))
|
||||||
n = pending_count(d, log_len(d))
|
n = pending_count(d, log_len(d))
|
||||||
if n:
|
if n:
|
||||||
print("%s pending. Run: %s nap" % (plural(n, "compression"), ME))
|
print("%s pending. Run: %s" % (plural(n, "compression"), tool_cmd("nap")))
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note,
|
COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue