diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index ededd82..be3e169 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -831,7 +831,14 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl if 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 }, "i": func(args []string) ([]byte, error) { @@ -839,7 +846,14 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl if 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 }, "agent": func(args []string) ([]byte, error) { diff --git a/cmd/olliesrv/internal/prompts/system_prompt.md b/cmd/olliesrv/internal/prompts/system_prompt.md index 439ab5e..12f8e16 100644 --- a/cmd/olliesrv/internal/prompts/system_prompt.md +++ b/cmd/olliesrv/internal/prompts/system_prompt.md @@ -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: -```bash -MEMORY_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/ollie/optmem" \ - "${XDG_CONFIG_HOME:-$HOME/.config}/ollie/optmem/memo" zoom +``` +memory_zoom(range="") ``` ## If you're a subagent: skip everything above diff --git a/data/agents/default.json b/data/agents/default.json index bc68bf5..369fbbd 100644 --- a/data/agents/default.json +++ b/data/agents/default.json @@ -9,6 +9,8 @@ "memory_wake", "memory_recall", "memory_remember", + "memory_nap", + "memory_zoom", "file_read", "file_edit", "file_glob", diff --git a/data/tools/memory_nap.meta b/data/tools/memory_nap.meta new file mode 100644 index 0000000..30decba --- /dev/null +++ b/data/tools/memory_nap.meta @@ -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" +} diff --git a/data/tools/memory_recall.meta b/data/tools/memory_recall.meta index 1b3c745..657d8f5 100644 --- a/data/tools/memory_recall.meta +++ b/data/tools/memory_recall.meta @@ -1,7 +1,7 @@ { "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.", - "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": { "type": "object", "required": ["query"], diff --git a/data/tools/memory_remember.meta b/data/tools/memory_remember.meta index 1ab09be..ce5fa8a 100644 --- a/data/tools/memory_remember.meta +++ b/data/tools/memory_remember.meta @@ -1,7 +1,7 @@ { "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.", - "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": { "type": "object", "required": ["title", "tags", "body"], diff --git a/data/tools/memory_wake.meta b/data/tools/memory_wake.meta index c4f6aec..418b6db 100644 --- a/data/tools/memory_wake.meta +++ b/data/tools/memory_wake.meta @@ -1,10 +1,13 @@ { "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.", - "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": { "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", "scope": "read" diff --git a/data/tools/memory_zoom.meta b/data/tools/memory_zoom.meta new file mode 100644 index 0000000..8e2fb99 --- /dev/null +++ b/data/tools/memory_zoom.meta @@ -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" +} diff --git a/third_party/optmem/memo b/third_party/optmem/memo index 6c8b838..cd233fc 100755 --- a/third_party/optmem/memo +++ b/third_party/optmem/memo @@ -44,6 +44,30 @@ def pretty(p): # PATH, so a bare `memo nap 0-1 "..."` would not: the tool names itself. 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 # 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. @@ -482,12 +506,13 @@ def nap_prompt(d, lo, hi, left): tail = "" if not left else "\n%s after this one." % ( "1 compression remains" if left == 1 else "%d compressions remain" % left) + nap_cmd = tool_cmd("nap", 'range="%d-%d", text=""' % (lo, hi - 1)) return ("Compress memories #%d-%d into one line of at most %d bytes.\n" "Keep what has lasting effect, drop what does not. Invent " "nothing.\n\n" "%s\n%s\n" - "Run: %s nap %d-%d \"\"" - % (lo, hi - 1, ENTRY_CHARS, body, tail, ME, lo, hi - 1)) + "Run: %s" + % (lo, hi - 1, ENTRY_CHARS, body, tail, nap_cmd)) def next_nap(d, T): @@ -595,13 +620,13 @@ def cmd_wake(d, args): if len(args) == 2: T = int(args[1]) if T > now: - die("T=%d, but the log holds %s. Run: %s wake" - % (T, plural(now, "memory"), ME)) + die("T=%d, but the log holds %s. Run: %s" + % (T, plural(now, "memory"), tool_cmd("wake"))) # A part is rendered as of T, so a note landing between two parts cannot # shift a boundary and drop a line. if not T: - print("No memories yet. Record the first with: %s note \"\"" - % ME) + print("No memories yet. Use %s to record your first one." + % tool_cmd("note")) print("You are awake.") return lines = [] @@ -619,9 +644,10 @@ def cmd_wake(d, args): # costing no round trip. print("Cannot wake: the memory context needs #%d-%d, " "which is not compressed yet.\nDo the %s below, " - "then run %s wake again.\n" + "then run %s again.\n" % (lo, hi - 1, - plural(pending_count(d, T), "compression"), ME)) + plural(pending_count(d, T), "compression"), + tool_cmd("wake"))) print(nap) sys.exit(1) 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)) parts = paginate(lines) if not 1 <= k <= len(parts): - die("No part %d: the memory has %s. Run: %s wake" - % (k, plural(len(parts), "part"), ME)) + die("No part %d: the memory has %s. Run: %s" + % (k, plural(len(parts), "part"), tool_cmd("wake"))) if len(parts) > 1: # The count is here so the T in `memo wake 2 296` reads as what it # 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 # 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. - 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: # 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 @@ -683,8 +710,8 @@ def cmd_nap(d, args): print("%d-%d is already settled." % (lo, hi - 1)) else: die("Wrong block: %s. Blocks are built in order; the next is " - "%d-%d. Run: %s nap" - % (args[0], todo[0][0], todo[0][1] - 1, ME)) + "%d-%d. Run: %s" + % (args[0], todo[0][0], todo[0][1] - 1, tool_cmd("nap"))) elif not tree_put(d, lo, hi, check(args[1])): print("%d-%d was settled or forgotten meanwhile." % (lo, hi - 1)) else: @@ -728,8 +755,9 @@ def cmd_forget(d, args): gone = tree_drop(d, *block_id(args[0])) if not gone: die("No summary at %s." % args[0]) - print("Forgot %s, from %d-%d up. Run: %s nap" - % (plural(len(gone), "summary"), gone[0][0], gone[0][1] - 1, ME)) + print("Forgot %s, from %d-%d up. Run: %s" + % (plural(len(gone), "summary"), gone[0][0], gone[0][1] - 1, + tool_cmd("nap"))) def cmd_recall(d, args): @@ -773,8 +801,8 @@ def cmd_zoom(d, args): lo, hi = block_id(args[0]) T = log_len(d) if lo >= T: - die("#%s is beyond the memory: it holds %s. Run: %s wake" - % (args[0], plural(T, "memory"), ME)) + die("#%s is beyond the memory: it holds %s. Run: %s" + % (args[0], plural(T, "memory"), tool_cmd("wake"))) mid = (lo + hi) // 2 for a, b in ((lo, mid), (mid, hi)): if a >= T: @@ -824,7 +852,7 @@ def cmd_import(d, args): % (plural(len(out), "memory"), base, base + len(out) - 1)) n = pending_count(d, log_len(d)) 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,