Load hinted tools through the agent namespace
This commit is contained in:
parent
28c76a76d4
commit
4bf46ae85d
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ func matchTools(input string) string {
|
|||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("<tool-hints>\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 <name>\")\n")
|
||||
sb.WriteString("Then call the loaded tool.\n")
|
||||
sb.WriteString("</tool-hints>\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "<context>\n" + strings.Join(contextParts, "\n") + "\n</context>\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
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: name<tab>description per line"),
|
||||
virtfs.Read(func() ([]byte, error) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue