X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/e737005f89c7c663e1636830001730ad79396bdc..723f6e18adc6482ed913e1618c8a76e981720c2b:/tools.go?ds=inline diff --git a/tools.go b/tools.go index 009e331..a75514f 100644 --- a/tools.go +++ b/tools.go @@ -49,11 +49,13 @@ type ToolChat interface { // the log as it happens: the call as a tool message, the result as an echo // message. The call is durable before its tool runs, so the log never misses an // action outside the agent. Commands and files run as the user in the working -// directory, with no sandbox. Zoom with a name reads the log of a subagent. The -// toolbox of the agent has the runs of its subagents for spawn and tell, the -// WhatsApp link for the WhatsApp tools, and the schedules for the schedule -// tools. The toolbox of a subagent has none of them, so these are unknown tools -// there. +// directory, with no sandbox. Zoom with a name reads a child's view or a +// subagent's log. The toolbox of the agent has the runs of its subagents for +// spawn and tell, the WhatsApp link for the WhatsApp tools, and the schedules +// for the schedule tools. Each permanent agent has the parent-child queue. Only +// the root has child creation and source routes. A child has neither of them +// nor a WhatsApp link. The toolbox of a subagent has none of them, so these are +// unknown tools there. type Toolbox struct { log ToolLog chat ToolChat @@ -62,6 +64,10 @@ type Toolbox struct { whatsapp *WhatsApp schedules *Schedules workdir string + agentName string + messages *DaemonMessages + children *DaemonChildren + routes *Routes } // Call logs the call of the tool name with its JSON arguments, makes the log @@ -71,12 +77,12 @@ type Toolbox struct { func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, error) { _, err := b.log.Append("tool", toolCallText(name, arguments)) if err != nil { - return "", err + return "", fmt.Errorf("append tool call: %w", err) } err = b.log.Flush(ctx) if err != nil && ctx.Err() == nil { - return "", err + return "", fmt.Errorf("flush tool call: %w", err) } output := "not run: the turn was interrupted" @@ -86,7 +92,7 @@ func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, err _, err = b.log.Append("echo", output) if err != nil { - return "", err + return "", fmt.Errorf("append tool result: %w", err) } return output, nil @@ -94,11 +100,15 @@ func (b *Toolbox) Call(ctx context.Context, name, arguments string) (string, err // run runs one tool and returns its output, or its error as text. func (b *Toolbox) run(ctx context.Context, name, arguments string) string { - var output string - var err error + var ( + output string + err error + ) + switch name { case "zoom": var args ToolZoomArguments + err = toolDecodeArguments(arguments, &args) switch { case err != nil: @@ -109,36 +119,42 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "date": var args ToolDateArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.date(args.ID) } case "shell": var args ToolShellArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output = b.shell(ctx, args.Command) } case "read": var args ToolPathArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.read(args.Path) } case "write": var args ToolWriteArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.write(args.Path, args.Text) } case "edit": var args ToolEditArguments + err = toolDecodeArguments(arguments, &args) if err == nil { output, err = b.edit(args.Path, args.Old, args.New) } case "spawn": var args ToolSpawnArguments + err = toolDecodeArguments(arguments, &args) switch { case b.runs == nil: @@ -146,17 +162,62 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { case err == nil: output, err = b.runs.Spawn(ctx, args.Tasks, args.Effort) } + case "agent_create": + var args ToolAgentCreateArguments + + err = toolDecodeArguments(arguments, &args) + switch { + case b.children == nil: + err = fmt.Errorf("unknown tool %q", name) + case err == nil: + output, err = b.children.Create(args.Name, args.Responsibility) + } + case "route", "unroute", "routes": + var args ToolRouteArguments + + switch name { + case "routes": + err = toolDecodeArguments(arguments, &struct{}{}) + case "unroute": + var source ToolRouteSourceArguments + + err = toolDecodeArguments(arguments, &source) + args.Source = source.Source + default: + err = toolDecodeArguments(arguments, &args) + } + + switch { + case b.routes == nil: + err = fmt.Errorf("unknown tool %q", name) + case err != nil: + case name == "routes": + output = b.routes.List() + case name == "route" && (b.messages == nil || b.messages.Child(b.agentName, args.Agent) == nil): + err = errors.New("the agent must be a child of the root") + default: + err = b.routes.Set(args.Source, args.Agent) + output = "Route removed." + if name == "route" { + output = "Route set." + } + } case "tell": var args ToolTellArguments + err = toolDecodeArguments(arguments, &args) switch { case b.runs == nil: err = fmt.Errorf("unknown tool %q", name) + case err == nil && b.messages != nil && b.messages.Child(b.agentName, args.Name) != nil: + err = b.messages.Tell(b.agentName, args.Name, args.Message) + output = "Sent. " + args.Name + "'s reply will reach you as a message." case err == nil: output, err = b.runs.Tell(ctx, args.Name, args.Message) } case "whatsapp_find": var args ToolWhatsAppFindArguments + err = toolDecodeArguments(arguments, &args) switch { case b.whatsapp == nil: @@ -166,6 +227,7 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "whatsapp_send": var args ToolWhatsAppSendArguments + err = toolDecodeArguments(arguments, &args) switch { case b.whatsapp == nil: @@ -175,6 +237,7 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "schedule_set": var args ToolScheduleSetArguments + err = toolDecodeArguments(arguments, &args) switch { case b.schedules == nil: @@ -192,6 +255,7 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { } case "schedule_remove": var args ToolScheduleRemoveArguments + err = toolDecodeArguments(arguments, &args) switch { case b.schedules == nil: @@ -210,8 +274,15 @@ func (b *Toolbox) run(ctx context.Context, name, arguments string) string { return output } -// zoomAgent returns the whole log of the subagent name. +// zoomAgent returns a child's view or the whole log of a subagent. func (b *Toolbox) zoomAgent(ctx context.Context, name string) (string, error) { + if b.messages != nil { + child := b.messages.Child(b.agentName, name) + if child != nil { + return strings.Join(child.memory.ViewLines(nil), ""), nil + } + } + subagent, err := b.subagents.Get(ctx, name) if err != nil { return "", err @@ -234,18 +305,20 @@ func (b *Toolbox) zoom(id, n int) (string, error) { if n == 1 { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read zoom message: %w", err) } return leafSource(message), nil } level := bits.TrailingZeros(uint(n)) - 1 + var lines []string + for _, index := range []int{2 * id / n, 2*id/n + 1} { node, built, err := b.chat.Node(level, index) if err != nil { - return "", err + return "", fmt.Errorf("read zoom node: %w", err) } text := viewPending @@ -267,12 +340,12 @@ func (b *Toolbox) date(id int) (string, error) { message, err := b.chat.Message(id) if err != nil { - return "", err + return "", fmt.Errorf("read dated message: %w", err) } date, err := time.Parse(time.RFC3339Nano, message.Date) if err != nil { - return "", err + return "", fmt.Errorf("parse message date: %w", err) } return date.Local().Format("Monday 2006-01-02 15:04:05 -07:00"), nil @@ -298,7 +371,9 @@ func (b *Toolbox) shell(ctx context.Context, command string) string { err := cmd.Run() text := output.String() + var exit *exec.ExitError + switch { case ctx.Err() != nil: text += "\n[interrupted]" @@ -317,7 +392,7 @@ func (b *Toolbox) shell(ctx context.Context, command string) string { func (b *Toolbox) read(path string) (text string, err error) { file, err := os.Open(b.path(path)) if err != nil { - return "", err + return "", fmt.Errorf("open tool file: %w", err) } defer func() { @@ -327,7 +402,7 @@ func (b *Toolbox) read(path string) (text string, err error) { output := &ToolHeadTail{} _, err = io.Copy(output, file) if err != nil { - return "", err + return "", fmt.Errorf("read tool file: %w", err) } return output.String(), nil @@ -339,12 +414,12 @@ func (b *Toolbox) write(path, text string) (string, error) { path = b.path(path) err := os.MkdirAll(filepath.Dir(path), 0o755) if err != nil { - return "", err + return "", fmt.Errorf("create tool file directory: %w", err) } err = os.WriteFile(path, []byte(text), 0o644) if err != nil { - return "", err + return "", fmt.Errorf("write tool file: %w", err) } return fmt.Sprintf("wrote %d bytes to %s", len(text), path), nil @@ -356,7 +431,7 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { path = b.path(path) data, err := os.ReadFile(path) if err != nil { - return "", err + return "", fmt.Errorf("read file to edit: %w", err) } if old == "" { @@ -370,12 +445,12 @@ func (b *Toolbox) edit(path, old, replacement string) (string, error) { info, err := os.Stat(path) if err != nil { - return "", err + return "", fmt.Errorf("read file mode: %w", err) } err = os.WriteFile(path, []byte(strings.Replace(string(data), old, replacement, 1)), info.Mode().Perm()) if err != nil { - return "", err + return "", fmt.Errorf("write edited file: %w", err) } return "edited " + path, nil @@ -391,8 +466,9 @@ func (b *Toolbox) path(path string) string { return filepath.Join(b.workdir, path) } -// ToolZoomArguments are the arguments of zoom. A Name that is not nil names -// the subagent whose whole log zoom gives, and then ID and N are not used. +// ToolZoomArguments are the arguments of zoom. A Name that is not nil names the +// child whose view or the subagent whose whole log zoom gives. ID and N are +// then not used. type ToolZoomArguments struct { ID int `json:"id"` N int `json:"n"` @@ -434,6 +510,23 @@ type ToolSpawnArguments struct { Effort *string `json:"effort"` } +// ToolAgentCreateArguments are the arguments of agent_create. +type ToolAgentCreateArguments struct { + Name string `json:"name"` + Responsibility string `json:"responsibility"` +} + +// ToolRouteArguments are the arguments of route. +type ToolRouteArguments struct { + Source string `json:"source"` + Agent string `json:"agent"` +} + +// ToolRouteSourceArguments are the arguments of unroute. +type ToolRouteSourceArguments struct { + Source string `json:"source"` +} + // ToolTellArguments are the arguments of tell. type ToolTellArguments struct { Name string `json:"name"` @@ -530,6 +623,7 @@ func toolClip(text string) string { func toolDecodeArguments(arguments string, args any) error { decoder := json.NewDecoder(strings.NewReader(arguments)) decoder.DisallowUnknownFields() + err := decoder.Decode(args) if err != nil { return fmt.Errorf("arguments: %w", err)