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)
|
h.update(msg, results)
|
||||||
ag.save()
|
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 {
|
if interrupted {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ type Runtime struct {
|
||||||
Preamble *Preamble
|
Preamble *Preamble
|
||||||
Tools []backend.Tool
|
Tools []backend.Tool
|
||||||
ToolMeta map[string]protocol.ToolInfo
|
ToolMeta map[string]protocol.ToolInfo
|
||||||
|
ToolRevision uint64
|
||||||
Exec toolExecutor
|
Exec toolExecutor
|
||||||
GenParams backend.GenerationParams
|
GenParams backend.GenerationParams
|
||||||
CompactionModel string
|
CompactionModel string
|
||||||
|
|
@ -174,6 +175,10 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
|
||||||
}
|
}
|
||||||
allTools = toolInfosToBackend(allToolInfos)
|
allTools = toolInfosToBackend(allToolInfos)
|
||||||
}
|
}
|
||||||
|
var toolRevision uint64
|
||||||
|
if srv != nil {
|
||||||
|
toolRevision = srv.ToolRegistryRevision()
|
||||||
|
}
|
||||||
|
|
||||||
var agentPrompt string
|
var agentPrompt string
|
||||||
var userPrompt string
|
var userPrompt string
|
||||||
|
|
@ -229,6 +234,7 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
|
||||||
ToolServer: srv,
|
ToolServer: srv,
|
||||||
Tools: allTools,
|
Tools: allTools,
|
||||||
ToolMeta: toolMeta,
|
ToolMeta: toolMeta,
|
||||||
|
ToolRevision: toolRevision,
|
||||||
Exec: exec,
|
Exec: exec,
|
||||||
Preamble: preamble,
|
Preamble: preamble,
|
||||||
GenParams: genParams,
|
GenParams: genParams,
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ func matchTools(input string) string {
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("<tool-hints>\n")
|
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 {
|
for _, r := range results {
|
||||||
sb.WriteString("→ `")
|
sb.WriteString("→ `")
|
||||||
sb.WriteString(r.Skill.Name)
|
sb.WriteString(r.Skill.Name)
|
||||||
|
|
@ -113,7 +113,9 @@ func matchTools(input string) string {
|
||||||
sb.WriteString(r.Skill.Description)
|
sb.WriteString(r.Skill.Description)
|
||||||
sb.WriteString("\n")
|
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")
|
sb.WriteString("</tool-hints>\n")
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,21 @@ import (
|
||||||
"ollie/toolsrv/protocol"
|
"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
|
// 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
|
// events to the bus. If a turn is already in progress the prompt is queued as
|
||||||
// an in-stream interruption instead.
|
// an in-stream interruption instead.
|
||||||
|
|
@ -95,16 +110,31 @@ 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
|
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
|
var contextParts []string
|
||||||
if ag.runtime.UserPrompt != "" {
|
if ag.runtime.UserPrompt != "" {
|
||||||
contextParts = append(contextParts, ag.runtime.UserPrompt)
|
contextParts = append(contextParts, ag.runtime.UserPrompt)
|
||||||
}
|
}
|
||||||
|
if skillContent := matchSkills(input); skillContent != "" {
|
||||||
|
contextParts = append(contextParts, skillContent)
|
||||||
|
}
|
||||||
|
if ag.runtime.ToolServer != nil {
|
||||||
if toolHints := matchTools(input); toolHints != "" {
|
if toolHints := matchTools(input); toolHints != "" {
|
||||||
contextParts = append(contextParts, toolHints)
|
contextParts = append(contextParts, toolHints)
|
||||||
}
|
}
|
||||||
if skillContent := matchSkills(input); skillContent != "" {
|
|
||||||
contextParts = append(contextParts, skillContent)
|
|
||||||
}
|
}
|
||||||
if len(contextParts) > 0 {
|
if len(contextParts) > 0 {
|
||||||
input = "<context>\n" + strings.Join(contextParts, "\n") + "\n</context>\n\n" + input
|
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)
|
ag.log.Debug("turn: start input=%s session=%s", auditTruncate(input), ag.sessionID)
|
||||||
|
|
||||||
// Fetch tools from toolsrv (the sole owner of tool state).
|
// Tool schemas and hints were prepared before the user message was built.
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install turn-scoped output handler to intercept events for reply
|
// Install turn-scoped output handler to intercept events for reply
|
||||||
// accumulation, state updates, and usage tracking.
|
// accumulation, state updates, and usage tracking.
|
||||||
var replyBuf strings.Builder
|
var replyBuf strings.Builder
|
||||||
|
|
|
||||||
|
|
@ -349,30 +349,23 @@ func (c *ToolsrvConn) SetOnToolsChanged(fn func()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolRegistryRevision returns the current tool registry revision.
|
// ToolRegistryRevision returns the current tool registry revision.
|
||||||
// Returns 0 if not available (always refresh).
|
|
||||||
func (c *ToolsrvConn) ToolRegistryRevision() uint64 {
|
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 {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
defer fid.Close()
|
defer fid.Close()
|
||||||
|
if _, err := fid.Write([]byte(c.agentID)); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
data, err := io.ReadAll(fid)
|
data, err := io.ReadAll(fid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range splitLines(string(data)) {
|
|
||||||
if idx := indexOf(line, '='); idx >= 0 {
|
|
||||||
if line[:idx] == "tools_rev" {
|
|
||||||
var rev uint64
|
var rev uint64
|
||||||
fmt.Sscanf(line[idx+1:], "%d", &rev)
|
fmt.Sscanf(string(data), "%d", &rev)
|
||||||
return rev
|
return rev
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Process management ---
|
// --- Process management ---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,20 @@ func (st *State) ListTools(agentID string) []protocol.ToolInfo {
|
||||||
return reg.Loaded(agentID)
|
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.
|
// LoadTool loads a tool by name for the given agent.
|
||||||
func (st *State) LoadTool(agentID, name string) error {
|
func (st *State) LoadTool(agentID, name string) error {
|
||||||
if agentID == "" {
|
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.
|
// HandleToolsRequest handles the tools rdwr: write agent ID, read tool list.
|
||||||
func (st *State) HandleToolsRequest(agentID string) string {
|
func (st *State) HandleToolsRequest(agentID string) string {
|
||||||
tools := st.ListTools(agentID)
|
tools := st.ListTools(agentID)
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,12 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
|
||||||
return []byte(srv.Fs.HandleToolsRequest(agentID)), nil
|
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.FileNode("all", 0444,
|
||||||
virtfs.Doc("All available tools on disk: name<tab>description per line"),
|
virtfs.Doc("All available tools on disk: name<tab>description per line"),
|
||||||
virtfs.Read(func() ([]byte, error) {
|
virtfs.Read(func() ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,7 @@
|
||||||
"userPrompts": [
|
"userPrompts": [
|
||||||
"$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md"
|
"$XDG_CONFIG_HOME/ollie/prompts/user-preferences.md"
|
||||||
],
|
],
|
||||||
"autoLoad": [
|
"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"
|
|
||||||
],
|
|
||||||
"temperature": 0.5,
|
"temperature": 0.5,
|
||||||
"maxTokens": 16384
|
"maxTokens": 16384
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,30 @@ op=$(echo "$args" | jq -r '.op // empty')
|
||||||
path=$(echo "$args" | jq -r '.path // empty')
|
path=$(echo "$args" | jq -r '.path // empty')
|
||||||
data=$(echo "$args" | jq -r '.data // 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
|
if [[ -z "$op" ]]; then
|
||||||
echo "error: missing 'op' (read, write, rdwr)" >&2
|
echo "error: missing 'op' (read, write, rdwr)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"description": "9P filesystem client for reading/writing agent state (plans, ctl, peers, etc.)",
|
"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 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```",
|
"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": {
|
"args": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["op", "path"],
|
"required": ["op", "path"],
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
},
|
},
|
||||||
"path": {
|
"path": {
|
||||||
"type": "string",
|
"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": {
|
"data": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue