Add list tools; normalize tool names and paths in transcript
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -132,6 +133,13 @@ func agyToolCall(name string, params json.RawMessage) (string, any, error) {
|
||||
}{}
|
||||
err := json.Unmarshal(params, &p)
|
||||
return "Bash", map[string]any{"command": p.CommandLine}, err
|
||||
case "read_file", "view_file":
|
||||
p := struct {
|
||||
Path string
|
||||
AbsolutePath string
|
||||
}{}
|
||||
err := json.Unmarshal(params, &p)
|
||||
return "Read", map[string]any{"file_path": cmp.Or(p.Path, p.AbsolutePath)}, err
|
||||
case "call_mcp_tool":
|
||||
p := struct {
|
||||
ServerName string
|
||||
|
||||
+57
-14
@@ -181,6 +181,21 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
|
||||
return m.clientGrep(ctx, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_list",
|
||||
Description: listDescription + " It lists a directory on a client Mac, with the byre in its installed Byre.app.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
|
||||
"path": {"type": "string", "description": "The directory, absolute or starting with ~/."}
|
||||
},
|
||||
"required": ["client", "path"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.clientList(ctx, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_hands",
|
||||
Description: "Call one of your hands tools (screen_list, capture, ax_tree, act, browser, chrome) on a client Mac instead of the Mac byre runs on: " +
|
||||
@@ -552,39 +567,67 @@ func (m *Manager) clientGrep(ctx context.Context, raw json.RawMessage) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_grep call: %w", err)
|
||||
}
|
||||
err = clientPath("path", args.Path)
|
||||
path, err := clientShellPath(args.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := shellQuote(args.Path)
|
||||
rest, ok := strings.CutPrefix(args.Path, "~/")
|
||||
if ok {
|
||||
path = "~/" + shellQuote(rest)
|
||||
}
|
||||
command := clientByre + " grep"
|
||||
flags := ""
|
||||
if args.Glob != "" {
|
||||
command += " --glob " + shellQuote(args.Glob)
|
||||
flags += " --glob " + shellQuote(args.Glob)
|
||||
}
|
||||
if args.IgnoreCase {
|
||||
command += " --ignore-case"
|
||||
flags += " --ignore-case"
|
||||
}
|
||||
if args.FilesOnly {
|
||||
command += " --files-only"
|
||||
flags += " --files-only"
|
||||
}
|
||||
command += " -- " + shellQuote(args.Pattern) + " " + path
|
||||
return m.runClientByre(ctx, args.Client, "grep", flags+" -- "+shellQuote(args.Pattern)+" "+path)
|
||||
}
|
||||
|
||||
type clientListArgs struct {
|
||||
Client string `json:"client"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (m *Manager) clientList(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
args := clientListArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_list call: %w", err)
|
||||
}
|
||||
path, err := clientShellPath(args.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return m.runClientByre(ctx, args.Client, "list", " "+path)
|
||||
}
|
||||
|
||||
func clientShellPath(p string) (string, error) {
|
||||
err := clientPath("path", p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rest, ok := strings.CutPrefix(p, "~/")
|
||||
if ok {
|
||||
return "~/" + shellQuote(rest), nil
|
||||
}
|
||||
return shellQuote(p), nil
|
||||
}
|
||||
|
||||
func (m *Manager) runClientByre(ctx context.Context, client, sub, args string) (string, error) {
|
||||
command := clientByre + " " + sub + args
|
||||
ms := int(fileTimeout.Milliseconds())
|
||||
cctx, cancel := context.WithTimeout(ctx, fileTimeout+replyGrace)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Bash, Command: &command, TimeoutMs: &ms})
|
||||
reply, err := m.clients.Call(cctx, client, api.ClientRequest{Op: api.Bash, Command: &command, TimeoutMs: &ms})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reply.ExitCode == nil || reply.Output == nil {
|
||||
return "", fmt.Errorf("%s's reply has no exit_code or output", args.Client)
|
||||
return "", fmt.Errorf("%s's reply has no exit_code or output", client)
|
||||
}
|
||||
if *reply.ExitCode != 0 {
|
||||
return "", fmt.Errorf("byre grep on %s exited %d: %s", args.Client, *reply.ExitCode, *reply.Output)
|
||||
return "", fmt.Errorf("byre %s on %s exited %d: %s", sub, client, *reply.ExitCode, *reply.Output)
|
||||
}
|
||||
return *reply.Output, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"g.fc.run/ai/byre/internal/dirlist"
|
||||
"g.fc.run/ai/byre/internal/grep"
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
)
|
||||
@@ -19,6 +20,9 @@ import (
|
||||
const grepDescription = "Search files for lines matching a regular expression (Go RE2 syntax), as ripgrep does: a directory is searched recursively, skipping .git, what .gitignore files exclude, and binary files. " +
|
||||
"Each match comes back as path:line:text, at most 64 KiB in all."
|
||||
|
||||
const listDescription = "List one directory's entries, not recursively, one name per line sorted by name, with / after each directory (symlinks to directories included). " +
|
||||
"Nothing is left out: dotfiles, binary and gitignored files all show. At most 64 KiB in all."
|
||||
|
||||
func (m *Manager) fileTools(l *live) []mcp.Tool {
|
||||
return []mcp.Tool{
|
||||
{
|
||||
@@ -88,6 +92,19 @@ func (m *Manager) fileTools(l *live) []mcp.Tool {
|
||||
return m.grep(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Description: listDescription + " It lists a directory on the Mac byre runs on, outside any sandbox your own tools have.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "The directory, relative to your worktree, absolute, or starting with ~/ (default your worktree)."}
|
||||
}
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.list(l, raw)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +267,26 @@ func (m *Manager) grep(ctx context.Context, l *live, raw json.RawMessage) (strin
|
||||
return grep.Search(cctx, args.Pattern, l.dir, path, args.options())
|
||||
}
|
||||
|
||||
type listArgs struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (m *Manager) list(l *live, raw json.RawMessage) (string, error) {
|
||||
args := listArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed list call: %w", err)
|
||||
}
|
||||
if args.Path == "" {
|
||||
args.Path = "."
|
||||
}
|
||||
full, err := l.resolve(args.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dirlist.List(full)
|
||||
}
|
||||
|
||||
var imageTypes = map[string]string{".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp"}
|
||||
|
||||
func readResult(path string, data []byte, offset, limit *int) mcp.Result {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package dirlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const keep = 64<<10 - 100
|
||||
|
||||
func List(path string) (string, error) {
|
||||
entries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return fmt.Sprintf("%s is empty.\n", path), nil
|
||||
}
|
||||
b := &strings.Builder{}
|
||||
for i, e := range entries {
|
||||
name := e.Name()
|
||||
info, err := os.Stat(filepath.Join(path, name))
|
||||
if err == nil && info.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
if b.Len()+len(name)+1 > keep {
|
||||
fmt.Fprintf(b, "(%d more entries not shown; output stops at 64 KiB)\n", len(entries)-i)
|
||||
break
|
||||
}
|
||||
b.WriteString(name + "\n")
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
UPDATE events SET payload = json_set(payload, '$.name', 'Read', '$.input', json_object('file_path', coalesce(json_extract(payload, '$.input.Path'), json_extract(payload, '$.input.AbsolutePath'))))
|
||||
WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') IN ('read_file', 'view_file');
|
||||
|
||||
UPDATE events SET payload = json_set(payload, '$.name', 'mcp__byre__' || json_extract(payload, '$.name'))
|
||||
WHERE kind = 'agent.tool_call' AND json_extract(payload, '$.name') IN ('submit_review', 'set_task_summary', 'finish_stage', 'take_snapshot');
|
||||
Reference in New Issue
Block a user