diff --git a/cmd/olliesrv/internal/agent/loop.go b/cmd/olliesrv/internal/agent/loop.go
index 127d80c..a7e12a0 100644
--- a/cmd/olliesrv/internal/agent/loop.go
+++ b/cmd/olliesrv/internal/agent/loop.go
@@ -221,6 +221,20 @@ func (ag *Agent) run(ctx context.Context) error {
h.update(msg, results)
ag.save()
+ // A tool call may autoload tools in toolsrv. Refresh this agent's
+ // schemas only when its per-agent registry revision changed.
+ if len(toolCalls) > 0 && rt.ToolServer != nil {
+ revision := rt.ToolServer.ToolRegistryRevision()
+ if revision == 0 || revision != rt.ToolRevision {
+ ti, listErr := rt.ToolServer.ListTools()
+ if listErr != nil {
+ return fmt.Errorf("refresh tools: %w", listErr)
+ }
+ ag.applyTools(ti, revision)
+ h.injectPreamble(rt.PreambleString())
+ }
+ }
+
if interrupted {
return ctx.Err()
}
diff --git a/cmd/olliesrv/internal/agent/runtime.go b/cmd/olliesrv/internal/agent/runtime.go
index 4273238..02744ed 100644
--- a/cmd/olliesrv/internal/agent/runtime.go
+++ b/cmd/olliesrv/internal/agent/runtime.go
@@ -143,6 +143,7 @@ type Runtime struct {
Preamble *Preamble
Tools []backend.Tool
ToolMeta map[string]protocol.ToolInfo
+ ToolRevision uint64
Exec toolExecutor
GenParams backend.GenerationParams
CompactionModel string
@@ -174,6 +175,10 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
}
allTools = toolInfosToBackend(allToolInfos)
}
+ var toolRevision uint64
+ if srv != nil {
+ toolRevision = srv.ToolRegistryRevision()
+ }
var agentPrompt string
var userPrompt string
@@ -229,6 +234,7 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
ToolServer: srv,
Tools: allTools,
ToolMeta: toolMeta,
+ ToolRevision: toolRevision,
Exec: exec,
Preamble: preamble,
GenParams: genParams,
diff --git a/cmd/olliesrv/internal/agent/skill_match.go b/cmd/olliesrv/internal/agent/skill_match.go
index bc53fe9..821b93b 100644
--- a/cmd/olliesrv/internal/agent/skill_match.go
+++ b/cmd/olliesrv/internal/agent/skill_match.go
@@ -105,7 +105,7 @@ func matchTools(input string) string {
var sb strings.Builder
sb.WriteString("\n")
- sb.WriteString("Relevant tools for this request (auto-load on first call):\n\n")
+ sb.WriteString("Relevant tools for this request:\n\n")
for _, r := range results {
sb.WriteString("→ `")
sb.WriteString(r.Skill.Name)
@@ -113,7 +113,9 @@ func matchTools(input string) string {
sb.WriteString(r.Skill.Description)
sb.WriteString("\n")
}
- sb.WriteString("\nDo NOT say \"I don't have this tool\" — just CALL IT.\n")
+ sb.WriteString("\nTo make one callable, use client_9p to load it through the agent ctl file:\n")
+ sb.WriteString("client_9p(op=\"rdwr\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl\", data=\"tool_load \")\n")
+ sb.WriteString("Then call the loaded tool.\n")
sb.WriteString("\n")
return sb.String()
}
diff --git a/cmd/olliesrv/internal/agent/turn.go b/cmd/olliesrv/internal/agent/turn.go
index f163be7..d6994a4 100644
--- a/cmd/olliesrv/internal/agent/turn.go
+++ b/cmd/olliesrv/internal/agent/turn.go
@@ -13,6 +13,21 @@ import (
"ollie/toolsrv/protocol"
)
+// applyTools updates the model-facing tool schemas and textual tool section.
+func (ag *Agent) applyTools(ti []protocol.ToolInfo, revision uint64) {
+ ag.runtime.Tools = toolInfosToBackend(ti)
+ meta := make(map[string]protocol.ToolInfo, len(ti))
+ for _, info := range ti {
+ meta[info.Name] = info
+ }
+ ag.runtime.ToolMeta = meta
+ ag.runtime.ToolRevision = revision
+ toolPreamble := RenderTools(ti)
+ if ag.runtime.Preamble.Get(SectionTools) != toolPreamble {
+ ag.runtime.Preamble.Set(SectionTools, toolPreamble)
+ }
+}
+
// Submit processes one line of user input: it starts an agent turn that streams
// events to the bus. If a turn is already in progress the prompt is queued as
// an in-stream interruption instead.
@@ -95,17 +110,32 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
input = "Run the memory_wake tool now. Follow its output completely before addressing my request.\n\n" + input
}
- // Build context block: user prompts + matched skills + tool hints
+ // Fetch tools from toolsrv before constructing the user message. Tool hints
+ // must be part of the message stored in history and sent to the backend.
+ if ag.runtime.ToolServer != nil {
+ ti, listErr := ag.runtime.ToolServer.ListTools()
+ if listErr != nil {
+ ag.log.Error("tool server unavailable: %v", listErr)
+ ag.emit(Event{Role: "error", Content: fmt.Sprintf("tool server unavailable: %v", listErr)})
+ ag.SetState("idle")
+ return ""
+ }
+ ag.applyTools(ti, ag.runtime.ToolServer.ToolRegistryRevision())
+ }
+
+ // Build context block: user prompts, matched skills, and tool hints.
var contextParts []string
if ag.runtime.UserPrompt != "" {
contextParts = append(contextParts, ag.runtime.UserPrompt)
}
- if toolHints := matchTools(input); toolHints != "" {
- contextParts = append(contextParts, toolHints)
- }
if skillContent := matchSkills(input); skillContent != "" {
contextParts = append(contextParts, skillContent)
}
+ if ag.runtime.ToolServer != nil {
+ if toolHints := matchTools(input); toolHints != "" {
+ contextParts = append(contextParts, toolHints)
+ }
+ }
if len(contextParts) > 0 {
input = "\n" + strings.Join(contextParts, "\n") + "\n\n\n" + input
}
@@ -155,27 +185,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
ag.log.Debug("turn: start input=%s session=%s", auditTruncate(input), ag.sessionID)
- // Fetch tools from toolsrv (the sole owner of tool state).
- if ag.runtime.ToolServer != nil {
- ti, listErr := ag.runtime.ToolServer.ListTools()
- if listErr != nil {
- ag.log.Error("tool server unavailable: %v", listErr)
- ag.emit(Event{Role: "error", Content: fmt.Sprintf("tool server unavailable: %v", listErr)})
- ag.SetState("idle")
- return ""
- }
- ag.runtime.Tools = toolInfosToBackend(ti)
- meta := make(map[string]protocol.ToolInfo, len(ti))
- for _, info := range ti {
- meta[info.Name] = info
- }
- ag.runtime.ToolMeta = meta
- toolPreamble := RenderTools(ti)
- if ag.runtime.Preamble.Get(SectionTools) != toolPreamble {
- ag.runtime.Preamble.Set(SectionTools, toolPreamble)
- }
- }
-
+ // Tool schemas and hints were prepared before the user message was built.
// Install turn-scoped output handler to intercept events for reply
// accumulation, state updates, and usage tracking.
var replyBuf strings.Builder
diff --git a/cmd/olliesrv/internal/toolclient/toolsrv.go b/cmd/olliesrv/internal/toolclient/toolsrv.go
index c36b9b9..04af629 100644
--- a/cmd/olliesrv/internal/toolclient/toolsrv.go
+++ b/cmd/olliesrv/internal/toolclient/toolsrv.go
@@ -349,29 +349,22 @@ func (c *ToolsrvConn) SetOnToolsChanged(fn func()) {
}
// ToolRegistryRevision returns the current tool registry revision.
-// Returns 0 if not available (always refresh).
func (c *ToolsrvConn) ToolRegistryRevision() uint64 {
- fid, err := c.fsys.Open("info", plan9.OREAD)
+ fid, err := c.fsys.Open("tools_rev", plan9.ORDWR)
if err != nil {
return 0
}
defer fid.Close()
-
+ if _, err := fid.Write([]byte(c.agentID)); err != nil {
+ return 0
+ }
data, err := io.ReadAll(fid)
if err != nil {
return 0
}
-
- for _, line := range splitLines(string(data)) {
- if idx := indexOf(line, '='); idx >= 0 {
- if line[:idx] == "tools_rev" {
- var rev uint64
- fmt.Sscanf(line[idx+1:], "%d", &rev)
- return rev
- }
- }
- }
- return 0
+ var rev uint64
+ fmt.Sscanf(string(data), "%d", &rev)
+ return rev
}
// --- Process management ---
diff --git a/cmd/toolsrv/internal/server/proc.go b/cmd/toolsrv/internal/server/proc.go
index 8e8c0cc..eda1ac8 100644
--- a/cmd/toolsrv/internal/server/proc.go
+++ b/cmd/toolsrv/internal/server/proc.go
@@ -160,6 +160,20 @@ func (st *State) ListTools(agentID string) []protocol.ToolInfo {
return reg.Loaded(agentID)
}
+// Revision returns the current tool registry revision for an agent.
+func (st *State) Revision(agentID string) uint64 {
+ if agentID == "" {
+ return 0
+ }
+ st.mu.RLock()
+ reg := st.registry
+ st.mu.RUnlock()
+ if reg == nil {
+ return 0
+ }
+ return reg.Revision(agentID)
+}
+
// LoadTool loads a tool by name for the given agent.
func (st *State) LoadTool(agentID, name string) error {
if agentID == "" {
@@ -666,6 +680,11 @@ func (st *State) HandleCtl(input string) error {
}
}
+// HandleToolsRevision returns the registry revision for an agent.
+func (st *State) HandleToolsRevision(agentID string) string {
+ return strconv.FormatUint(st.Revision(strings.TrimSpace(agentID)), 10)
+}
+
// HandleToolsRequest handles the tools rdwr: write agent ID, read tool list.
func (st *State) HandleToolsRequest(agentID string) string {
tools := st.ListTools(agentID)
diff --git a/cmd/toolsrv/internal/server/server.go b/cmd/toolsrv/internal/server/server.go
index b8e3d02..6e329d2 100644
--- a/cmd/toolsrv/internal/server/server.go
+++ b/cmd/toolsrv/internal/server/server.go
@@ -132,6 +132,12 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
return []byte(srv.Fs.HandleToolsRequest(agentID)), nil
}),
),
+ virtfs.FileNode("tools_rev", 0666,
+ virtfs.Doc("Tool registry revision: write agent ID, read revision"),
+ virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) {
+ return []byte(srv.Fs.HandleToolsRevision(string(data))), nil
+ }),
+ ),
virtfs.FileNode("all", 0444,
virtfs.Doc("All available tools on disk: namedescription per line"),
virtfs.Read(func() ([]byte, error) {
diff --git a/data/agents/default.json b/data/agents/default.json
index e86cb7e..d8e3de7 100644
--- a/data/agents/default.json
+++ b/data/agents/default.json
@@ -5,33 +5,7 @@
"userPrompts": [
"$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md"
],
- "autoLoad": [
- "shell",
- "reasoning_think",
- "memory_wake",
- "memory_recall",
- "memory_remember",
- "memory_nap",
- "memory_zoom",
- "file_read",
- "file_edit",
- "file_glob",
- "file_grep",
- "subagent_spawn",
- "codebase_overview",
- "code_symbols",
- "code_outline",
- "code_query",
- "code_rewrite",
- "code_dependencies",
- "lsp_definition",
- "lsp_diagnostics",
- "lsp_hover",
- "lsp_references",
- "lsp_symbols",
- "lsp_rename",
- "lsp_completion"
- ],
+ "autoLoad": [],
"temperature": 0.5,
"maxTokens": 16384
}
diff --git a/data/tools/client_9p b/data/tools/client_9p
index a1c7f27..24d050b 100755
--- a/data/tools/client_9p
+++ b/data/tools/client_9p
@@ -8,6 +8,30 @@ op=$(echo "$args" | jq -r '.op // empty')
path=$(echo "$args" | jq -r '.path // empty')
data=$(echo "$args" | jq -r '.data // empty')
+# Tool calls commonly use the session placeholders from the client_9p API.
+# Expand them here because jq returns the path literally and ollie-9p does
+# not perform shell expansion on arguments.
+expand_path() {
+ local value=$1
+ if [[ "$value" == *'$OLLIE_SESSION_ID'* ]]; then
+ if [[ -z "${OLLIE_SESSION_ID:-}" ]]; then
+ echo "error: OLLIE_SESSION_ID is not set" >&2
+ exit 1
+ fi
+ value=${value//\$OLLIE_SESSION_ID/$OLLIE_SESSION_ID}
+ fi
+ if [[ "$value" == *'$OLLIE_UNAME'* ]]; then
+ if [[ -z "${OLLIE_UNAME:-}" ]]; then
+ echo "error: OLLIE_UNAME is not set" >&2
+ exit 1
+ fi
+ value=${value//\$OLLIE_UNAME/$OLLIE_UNAME}
+ fi
+ printf '%s' "$value"
+}
+
+path=$(expand_path "$path")
+
if [[ -z "$op" ]]; then
echo "error: missing 'op' (read, write, rdwr)" >&2
exit 1
diff --git a/data/tools/client_9p.meta b/data/tools/client_9p.meta
index 5ce7e32..d6d6771 100644
--- a/data/tools/client_9p.meta
+++ b/data/tools/client_9p.meta
@@ -1,6 +1,6 @@
{
- "description": "9P filesystem client for reading/writing agent state (plans, ctl, peers, etc.)",
- "prompt": "## client_9p\n\nRead and write 9P namespace paths. Use instead of shelling out to ollie-9p.\n\n**Operations:**\n- `read`: read file contents\n- `write`: write data to file\n- `rdwr`: write then read (for request-response files like ctl, generate)\n\n**Examples:**\n```\nclient_9p(op=\"read\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan\")\nclient_9p(op=\"write\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan\", data=\"## Plan\\n- [ ] step one\")\nclient_9p(op=\"rdwr\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl\", data=\"compact\")\n```",
+ "description": "9P filesystem client for reading/writing virtual agent state paths. Paths are 9P namespace paths, not host filesystem paths.",
+ "prompt": "## client_9p\n\nRead and write the virtual 9P namespace. The `path` argument is always a 9P namespace path, never a host filesystem path. Use paths such as `session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan`; do not use `/home/...` or other physical paths.\n\n**Operations:**\n- `read`: read file contents\n- `write`: write data to file\n- `rdwr`: write then read (for request-response files like ctl, generate)\n\n**Examples:**\n```\nclient_9p(op=\"read\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan\")\nclient_9p(op=\"write\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan\", data=\"## Plan\\n- [ ] step one\")\nclient_9p(op=\"rdwr\", path=\"session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl\", data=\"compact\")\n```",
"args": {
"type": "object",
"required": ["op", "path"],
@@ -12,7 +12,7 @@
},
"path": {
"type": "string",
- "description": "9P path (e.g., session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan)"
+ "description": "Virtual 9P namespace path only, such as session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan. Never a host filesystem path."
},
"data": {
"type": "string",