// 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,
-// and the WhatsApp link for the WhatsApp tools. The toolbox of a subagent has
-// neither, so these are unknown tools there.
+// 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.
type Toolbox struct {
log ToolLog
chat ToolChat
subagents *Subagents
runs *SubagentRuns
whatsapp *WhatsApp
+ schedules *Schedules
workdir string
}
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"
_, err = b.log.Append("echo", output)
if err != nil {
- return "", err
+ return "", fmt.Errorf("append tool result: %w", err)
}
return output, nil
case err == nil:
output, err = b.whatsapp.Send(ctx, args.ID, args.Chat, args.Text)
}
+ case "schedule_set":
+ var args ToolScheduleSetArguments
+ err = toolDecodeArguments(arguments, &args)
+ switch {
+ case b.schedules == nil:
+ err = fmt.Errorf("unknown tool %q", name)
+ case err == nil:
+ output, err = b.schedules.Set(args.Text, args.At, args.Cron)
+ }
+ case "schedule_list":
+ err = toolDecodeArguments(arguments, &struct{}{})
+ switch {
+ case b.schedules == nil:
+ err = fmt.Errorf("unknown tool %q", name)
+ case err == nil:
+ output = b.schedules.List()
+ }
+ case "schedule_remove":
+ var args ToolScheduleRemoveArguments
+ err = toolDecodeArguments(arguments, &args)
+ switch {
+ case b.schedules == nil:
+ err = fmt.Errorf("unknown tool %q", name)
+ case err == nil:
+ output, err = b.schedules.Remove(args.ID)
+ }
default:
err = fmt.Errorf("unknown tool %q", name)
}
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
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
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
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() {
output := &ToolHeadTail{}
_, err = io.Copy(output, file)
if err != nil {
- return "", err
+ return "", fmt.Errorf("read tool file: %w", err)
}
return output.String(), nil
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
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 == "" {
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
Text string `json:"text"`
}
+// ToolScheduleSetArguments are the arguments of schedule_set. Exactly one of At
+// and Cron is not nil.
+type ToolScheduleSetArguments struct {
+ Text string `json:"text"`
+ At *string `json:"at"`
+ Cron *string `json:"cron"`
+}
+
+// ToolScheduleRemoveArguments are the arguments of schedule_remove.
+type ToolScheduleRemoveArguments struct {
+ ID int `json:"id"`
+}
+
// ToolHeadTail keeps the first and the last toolClipBytes bytes written to it,
// and counts the bytes between them that it drops.
type ToolHeadTail struct {