]> git.giacomolli.uk - onechat/commitdiff
Name and order the tool code
authorAnderson A. Giacomolli <agiacomolli@gmail.com>
Sat, 7 Nov 2026 16:59:39 +0000 (13:59 -0300)
committerAnderson A. Giacomolli <agiacomolli@gmail.com>
Sat, 7 Nov 2026 16:59:39 +0000 (13:59 -0300)
Every package-level name of the tools starts with tool: the Toolbox, its
interfaces ToolLog and ToolChat, the argument types, the head and tail
buffer, and the clip helpers. The interfaces chatLog and chatReader read
as parts of the chat, but only the tools use them.

The declarations of tools.go follow the order of the go-code skill. The
behavior does not change.

Issue: 947bddb

tools.go
tools_test.go
turn.go

index 7ca031c7d4566212486e1e42ee103a4aff5465a9..af500baf0af43c5be067dcb05f3adc8b13de6c86 100644 (file)
--- a/tools.go
+++ b/tools.go
@@ -19,83 +19,49 @@ import (
 )
 
 const (
-       // clipCharacters is the most characters of a tool output: its head and
-       // its tail, half each.
-       clipCharacters = 30000
+       // toolClipCharacters is the most characters of a tool output: its head
+       // and its tail, half each.
+       toolClipCharacters = 30000
 
-       // clipBytes is the most bytes that a tool keeps of each end of an
-       // output, enough for clipCharacters/2 characters of 4 bytes.
-       clipBytes = 4 * clipCharacters / 2
+       // toolClipBytes is the most bytes that a tool keeps of each end of an
+       // output, enough for toolClipCharacters/2 characters of 4 bytes.
+       toolClipBytes = 4 * toolClipCharacters / 2
 
-       // shellWaitDelay is how long a command can keep its output open after it
-       // ends or after its kill.
-       shellWaitDelay = 2 * time.Second
+       // toolShellWaitDelay is how long a command can keep its output open
+       // after it ends or after its kill.
+       toolShellWaitDelay = 2 * time.Second
 )
 
-// chatLog is the log that the tools write their calls and results to.
-type chatLog interface {
+// ToolLog is the log that the tools write their calls and results to.
+type ToolLog interface {
        Append(kind, text string) (Message, error)
        Flush(ctx context.Context) error
 }
 
-// chatReader gives the messages and the nodes that zoom and date read.
-type chatReader interface {
+// ToolChat gives the messages and the nodes that zoom and date read.
+type ToolChat interface {
        Len() int
        Message(id int) (Message, error)
        Node(level, index int) (Node, bool, error)
 }
 
-// toolbox runs the tool calls of one agent. Each call and each result goes to
+// Toolbox runs the tool calls of one agent. Each call and each result goes to
 // 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.
-type toolbox struct {
-       log     chatLog
-       chat    chatReader
+// 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.
+type Toolbox struct {
+       log     ToolLog
+       chat    ToolChat
        workdir string
 }
 
-// shellArguments are the arguments of shell.
-type shellArguments struct {
-       Command string `json:"command"`
-}
-
-// pathArguments are the arguments of read.
-type pathArguments struct {
-       Path string `json:"path"`
-}
-
-// writeArguments are the arguments of write.
-type writeArguments struct {
-       Path string `json:"path"`
-       Text string `json:"text"`
-}
-
-// editArguments are the arguments of edit.
-type editArguments struct {
-       Path string `json:"path"`
-       Old  string `json:"old"`
-       New  string `json:"new"`
-}
-
-// zoomArguments are the arguments of zoom.
-type zoomArguments struct {
-       ID int `json:"id"`
-       N  int `json:"n"`
-}
-
-// dateArguments are the arguments of date.
-type dateArguments struct {
-       ID int `json:"id"`
-}
-
 // Call logs the call of the tool name with its JSON arguments, makes the log
 // durable, runs the tool, and logs its clipped output, which it returns. A
 // failed tool gives its error as the output. An error of Call itself is an
 // error of the log. When ctx ends before the flush, the tool does not run.
-func (b *toolbox) Call(ctx context.Context, name, arguments string) (string, error) {
-       _, err := b.log.Append("tool", callText(name, arguments))
+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
        }
@@ -107,7 +73,7 @@ func (b *toolbox) Call(ctx context.Context, name, arguments string) (string, err
 
        output := "not run: the turn was interrupted"
        if err == nil {
-               output = clip(b.run(ctx, name, arguments))
+               output = toolClip(b.run(ctx, name, arguments))
        }
 
        _, err = b.log.Append("echo", output)
@@ -119,43 +85,43 @@ 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 {
+func (b *Toolbox) run(ctx context.Context, name, arguments string) string {
        var output string
        var err error
        switch name {
        case "zoom":
-               var args zoomArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolZoomArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output, err = b.zoom(args.ID, args.N)
                }
        case "date":
-               var args dateArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolDateArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output, err = b.date(args.ID)
                }
        case "shell":
-               var args shellArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolShellArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output = b.shell(ctx, args.Command)
                }
        case "read":
-               var args pathArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolPathArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output, err = b.read(args.Path)
                }
        case "write":
-               var args writeArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolWriteArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output, err = b.write(args.Path, args.Text)
                }
        case "edit":
-               var args editArguments
-               err = decodeArguments(arguments, &args)
+               var args ToolEditArguments
+               err = toolDecodeArguments(arguments, &args)
                if err == nil {
                        output, err = b.edit(args.Path, args.Old, args.New)
                }
@@ -172,7 +138,7 @@ func (b *toolbox) run(ctx context.Context, name, arguments string) string {
 
 // zoom returns message id whole for n = 1, or else the two lines under the line
 // id+n.
-func (b *toolbox) zoom(id, n int) (string, error) {
+func (b *Toolbox) zoom(id, n int) (string, error) {
        if n < 1 || bits.OnesCount(uint(n)) != 1 || id < 0 || id%n != 0 {
                return "", fmt.Errorf("line %d+%d does not exist: n must be a power of 2 and id a multiple of n", id, n)
        }
@@ -210,7 +176,7 @@ func (b *toolbox) zoom(id, n int) (string, error) {
 }
 
 // date returns the date and time of message id, with its weekday.
-func (b *toolbox) date(id int) (string, error) {
+func (b *Toolbox) date(id int) (string, error) {
        if id < 0 || id >= b.chat.Len() {
                return "", fmt.Errorf("message %d does not exist", id)
        }
@@ -231,7 +197,7 @@ func (b *toolbox) date(id int) (string, error) {
 // shell runs command with bash in the working directory and returns its output
 // and error output. A failed command adds its exit status. When ctx ends, the
 // whole process group of the command is killed.
-func (b *toolbox) shell(ctx context.Context, command string) string {
+func (b *Toolbox) shell(ctx context.Context, command string) string {
        cmd := exec.CommandContext(ctx, "bash", "-c", command)
        cmd.Dir = b.workdir
        cmd.SysProcAttr = &syscall.SysProcAttr{
@@ -240,9 +206,9 @@ func (b *toolbox) shell(ctx context.Context, command string) string {
        cmd.Cancel = func() error {
                return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
        }
-       cmd.WaitDelay = shellWaitDelay
+       cmd.WaitDelay = toolShellWaitDelay
 
-       output := &headTail{}
+       output := &ToolHeadTail{}
        cmd.Stdout = output
        cmd.Stderr = output
        err := cmd.Run()
@@ -264,7 +230,7 @@ func (b *toolbox) shell(ctx context.Context, command string) string {
 }
 
 // read returns the text of the file at path.
-func (b *toolbox) read(path string) (text string, err error) {
+func (b *Toolbox) read(path string) (text string, err error) {
        file, err := os.Open(b.path(path))
        if err != nil {
                return "", err
@@ -274,7 +240,7 @@ func (b *toolbox) read(path string) (text string, err error) {
                err = errors.Join(err, file.Close())
        }()
 
-       output := &headTail{}
+       output := &ToolHeadTail{}
        _, err = io.Copy(output, file)
        if err != nil {
                return "", err
@@ -285,7 +251,7 @@ func (b *toolbox) read(path string) (text string, err error) {
 
 // write replaces the file at path with text, and makes its missing
 // directories.
-func (b *toolbox) write(path, text string) (string, error) {
+func (b *Toolbox) write(path, text string) (string, error) {
        path = b.path(path)
        err := os.MkdirAll(filepath.Dir(path), 0o755)
        if err != nil {
@@ -302,7 +268,7 @@ func (b *toolbox) write(path, text string) (string, error) {
 
 // edit replaces the one occurrence of old in the file at path with
 // replacement. It keeps the mode of the file.
-func (b *toolbox) edit(path, old, replacement string) (string, error) {
+func (b *Toolbox) edit(path, old, replacement string) (string, error) {
        path = b.path(path)
        data, err := os.ReadFile(path)
        if err != nil {
@@ -333,7 +299,7 @@ func (b *toolbox) edit(path, old, replacement string) (string, error) {
 
 // path returns path, relative to the working directory when it is not
 // absolute.
-func (b *toolbox) path(path string) string {
+func (b *Toolbox) path(path string) string {
        if filepath.IsAbs(path) {
                return path
        }
@@ -341,24 +307,73 @@ func (b *toolbox) path(path string) string {
        return filepath.Join(b.workdir, path)
 }
 
-// headTail keeps the first and the last clipBytes bytes written to it, and
-// counts the bytes between them that it drops.
-type headTail struct {
+// ToolZoomArguments are the arguments of zoom.
+type ToolZoomArguments struct {
+       ID int `json:"id"`
+       N  int `json:"n"`
+}
+
+// ToolDateArguments are the arguments of date.
+type ToolDateArguments struct {
+       ID int `json:"id"`
+}
+
+// ToolShellArguments are the arguments of shell.
+type ToolShellArguments struct {
+       Command string `json:"command"`
+}
+
+// ToolPathArguments are the arguments of read.
+type ToolPathArguments struct {
+       Path string `json:"path"`
+}
+
+// ToolWriteArguments are the arguments of write.
+type ToolWriteArguments struct {
+       Path string `json:"path"`
+       Text string `json:"text"`
+}
+
+// ToolEditArguments are the arguments of edit.
+type ToolEditArguments struct {
+       Path string `json:"path"`
+       Old  string `json:"old"`
+       New  string `json:"new"`
+}
+
+// ToolHeadTail keeps the first and the last toolClipBytes bytes written to it,
+// and counts the bytes between them that it drops.
+type ToolHeadTail struct {
        head    []byte
        tail    []byte
        dropped int
 }
 
+// String returns the output, clipped when it is too long. Invalid UTF-8 becomes
+// the replacement character.
+func (h *ToolHeadTail) String() string {
+       replacement := string(utf8.RuneError)
+       if h.dropped == 0 {
+               return toolClip(strings.ToValidUTF8(string(h.head)+string(h.tail), replacement))
+       }
+
+       head := toolFirstCharacters(strings.ToValidUTF8(string(h.head), replacement), toolClipCharacters/2)
+       tail := toolLastCharacters(strings.ToValidUTF8(string(h.tail), replacement), toolClipCharacters/2)
+       cut := len(h.head) + h.dropped + len(h.tail) - len(head) - len(tail)
+
+       return toolJoinClipped(head, tail, cut)
+}
+
 // Write keeps the bytes of p at the ends of the output.
-func (h *headTail) Write(p []byte) (int, error) {
+func (h *ToolHeadTail) Write(p []byte) (int, error) {
        n := len(p)
-       room := min(clipBytes-len(h.head), len(p))
+       room := min(toolClipBytes-len(h.head), len(p))
        h.head = append(h.head, p[:room]...)
        p = p[room:]
 
        h.tail = append(h.tail, p...)
-       if len(h.tail) > clipBytes {
-               cut := len(h.tail) - clipBytes
+       if len(h.tail) > toolClipBytes {
+               cut := len(h.tail) - toolClipBytes
                h.dropped += cut
                h.tail = bytes.Clone(h.tail[cut:])
        }
@@ -366,30 +381,29 @@ func (h *headTail) Write(p []byte) (int, error) {
        return n, nil
 }
 
-// String returns the output, clipped when it is too long. Invalid UTF-8 becomes
-// the replacement character.
-func (h *headTail) String() string {
-       replacement := string(utf8.RuneError)
-       if h.dropped == 0 {
-               return clip(strings.ToValidUTF8(string(h.head)+string(h.tail), replacement))
-       }
+// toolCallText returns the log text of a tool call: the name and the JSON
+// arguments.
+func toolCallText(name, arguments string) string {
+       return name + " " + arguments
+}
 
-       head := firstCharacters(strings.ToValidUTF8(string(h.head), replacement), clipCharacters/2)
-       tail := lastCharacters(strings.ToValidUTF8(string(h.tail), replacement), clipCharacters/2)
-       cut := len(h.head) + h.dropped + len(h.tail) - len(head) - len(tail)
+// toolClip returns text when it has at most toolClipCharacters characters.
+// Otherwise it returns the first and the last toolClipCharacters/2 characters,
+// with a mark of the cut between them.
+func toolClip(text string) string {
+       if utf8.RuneCountInString(text) <= toolClipCharacters {
+               return text
+       }
 
-       return joinClipped(head, tail, cut)
-}
+       head := toolFirstCharacters(text, toolClipCharacters/2)
+       tail := toolLastCharacters(text, toolClipCharacters/2)
 
-// callText returns the log text of a tool call: the name and the JSON
-// arguments.
-func callText(name, arguments string) string {
-       return name + " " + arguments
+       return toolJoinClipped(head, tail, len(text)-len(head)-len(tail))
 }
 
-// decodeArguments decodes the JSON arguments of a call into args. An unknown
-// field is an error.
-func decodeArguments(arguments string, args any) error {
+// toolDecodeArguments decodes the JSON arguments of a call into args. An
+// unknown field is an error.
+func toolDecodeArguments(arguments string, args any) error {
        decoder := json.NewDecoder(strings.NewReader(arguments))
        decoder.DisallowUnknownFields()
        err := decoder.Decode(args)
@@ -400,28 +414,8 @@ func decodeArguments(arguments string, args any) error {
        return nil
 }
 
-// clip returns text when it has at most clipCharacters characters. Otherwise it
-// returns the first and the last clipCharacters/2 characters, with a mark of
-// the cut between them.
-func clip(text string) string {
-       if utf8.RuneCountInString(text) <= clipCharacters {
-               return text
-       }
-
-       head := firstCharacters(text, clipCharacters/2)
-       tail := lastCharacters(text, clipCharacters/2)
-
-       return joinClipped(head, tail, len(text)-len(head)-len(tail))
-}
-
-// joinClipped joins the head and the tail of a clipped output with a mark of
-// the cut bytes.
-func joinClipped(head, tail string, cut int) string {
-       return head + "\n[... " + strconv.Itoa(cut) + " bytes cut ...]\n" + tail
-}
-
-// firstCharacters returns the first count characters of text.
-func firstCharacters(text string, count int) string {
+// toolFirstCharacters returns the first count characters of text.
+func toolFirstCharacters(text string, count int) string {
        end := 0
        for range count {
                if end >= len(text) {
@@ -435,8 +429,8 @@ func firstCharacters(text string, count int) string {
        return text[:end]
 }
 
-// lastCharacters returns the last count characters of text.
-func lastCharacters(text string, count int) string {
+// toolLastCharacters returns the last count characters of text.
+func toolLastCharacters(text string, count int) string {
        start := len(text)
        for range count {
                if start <= 0 {
@@ -449,3 +443,9 @@ func lastCharacters(text string, count int) string {
 
        return text[start:]
 }
+
+// toolJoinClipped joins the head and the tail of a clipped output with a mark
+// of the cut bytes.
+func toolJoinClipped(head, tail string, cut int) string {
+       return head + "\n[... " + strconv.Itoa(cut) + " bytes cut ...]\n" + tail
+}
index a5f8f180d4d660a56ebc8b79e950ac7af4455059..8960b8f06ad9b2cfbac27c455f1b14388cbf094e 100644 (file)
@@ -16,7 +16,7 @@ import (
 
 func TestToolRunsAfterItsCallIsDurable(t *testing.T) {
        dir := t.TempDir()
-       log := &recordingLog{
+       log := &ToolRecordingLog{
                marker: filepath.Join(dir, "ran"),
        }
 
@@ -34,7 +34,7 @@ func TestToolRunsAfterItsCallIsDurable(t *testing.T) {
 
 func TestToolDoesNotRunAfterAnInterruptBeforeTheFlush(t *testing.T) {
        dir := t.TempDir()
-       log := &recordingLog{
+       log := &ToolRecordingLog{
                marker: filepath.Join(dir, "ran"),
        }
 
@@ -51,7 +51,7 @@ func TestToolDoesNotRunAfterAnInterruptBeforeTheFlush(t *testing.T) {
 
 func TestShellRunsInTheWorkingDirectory(t *testing.T) {
        dir := t.TempDir()
-       box := newTestToolbox(t, &recordingLog{}, dir)
+       box := newTestToolbox(t, &ToolRecordingLog{}, dir)
        testCases := []struct {
                name    string
                command string
@@ -87,7 +87,7 @@ func TestShellRunsInTheWorkingDirectory(t *testing.T) {
 }
 
 func TestShellInterruptKillsTheCommandAndItsChildren(t *testing.T) {
-       box := newTestToolbox(t, &recordingLog{}, t.TempDir())
+       box := newTestToolbox(t, &ToolRecordingLog{}, t.TempDir())
        ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
        defer cancel()
 
@@ -99,7 +99,7 @@ func TestShellInterruptKillsTheCommandAndItsChildren(t *testing.T) {
 
 func TestFileTools(t *testing.T) {
        dir := t.TempDir()
-       box := newTestToolbox(t, &recordingLog{}, dir)
+       box := newTestToolbox(t, &ToolRecordingLog{}, dir)
        path := filepath.Join(dir, "notes", "a.txt")
 
        require.Equal(t, "wrote 12 bytes to "+path, box.run(t.Context(), "write", `{"path":"notes/a.txt","text":"one two two\n"}`))
@@ -125,7 +125,7 @@ func TestZoomAndDateReadTheChat(t *testing.T) {
                require.NoError(t, err)
        }
 
-       box := &toolbox{
+       box := &Toolbox{
                log:     memory,
                chat:    memory,
                workdir: t.TempDir(),
@@ -170,20 +170,20 @@ func TestZoomAndDateReadTheChat(t *testing.T) {
 }
 
 func TestClipKeepsTheHeadAndTheTail(t *testing.T) {
-       short := strings.Repeat("a", clipCharacters)
-       require.Equal(t, short, clip(short))
+       short := strings.Repeat("a", toolClipCharacters)
+       require.Equal(t, short, toolClip(short))
 
-       text := strings.Repeat("h", clipCharacters/2) + strings.Repeat("é", 5000) + strings.Repeat("t", clipCharacters/2)
-       require.Equal(t, strings.Repeat("h", clipCharacters/2)+"\n[... 10000 bytes cut ...]\n"+strings.Repeat("t", clipCharacters/2), clip(text))
+       text := strings.Repeat("h", toolClipCharacters/2) + strings.Repeat("é", 5000) + strings.Repeat("t", toolClipCharacters/2)
+       require.Equal(t, strings.Repeat("h", toolClipCharacters/2)+"\n[... 10000 bytes cut ...]\n"+strings.Repeat("t", toolClipCharacters/2), toolClip(text))
 
-       half := strings.Repeat("é", clipCharacters/2)
-       clipped := clip(strings.Repeat("é", clipCharacters+2))
+       half := strings.Repeat("é", toolClipCharacters/2)
+       clipped := toolClip(strings.Repeat("é", toolClipCharacters+2))
        require.True(t, utf8.ValidString(clipped))
        require.Equal(t, half+"\n[... 4 bytes cut ...]\n"+half, clipped)
 }
 
 func TestHeadTailKeepsTheEndsOfALongOutput(t *testing.T) {
-       output := &headTail{}
+       output := &ToolHeadTail{}
        for n := range 1000 {
                _, err := fmt.Fprintf(output, "%04d%s\n", n, strings.Repeat("x", 995))
                require.NoError(t, err)
@@ -192,19 +192,19 @@ func TestHeadTailKeepsTheEndsOfALongOutput(t *testing.T) {
        text := output.String()
        require.True(t, strings.HasPrefix(text, "0000xxx"))
        require.True(t, strings.HasSuffix(text, "0999"+strings.Repeat("x", 995)+"\n"))
-       require.Contains(t, text, fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-clipCharacters))
-       require.Equal(t, clipCharacters+len(fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-clipCharacters)), len(text))
+       require.Contains(t, text, fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-toolClipCharacters))
+       require.Equal(t, toolClipCharacters+len(fmt.Sprintf("\n[... %d bytes cut ...]\n", 1000*1000-toolClipCharacters)), len(text))
 }
 
-// recordingLog is a chat log that records each append and flush. The flush
+// ToolRecordingLog is a chat log that records each append and flush. The flush
 // records whether the marker file exists.
-type recordingLog struct {
+type ToolRecordingLog struct {
        mutex  sync.Mutex
        marker string
        events []string
 }
 
-func (r *recordingLog) Append(kind, text string) (Message, error) {
+func (r *ToolRecordingLog) Append(kind, text string) (Message, error) {
        r.mutex.Lock()
        defer r.mutex.Unlock()
 
@@ -213,7 +213,7 @@ func (r *recordingLog) Append(kind, text string) (Message, error) {
        return Message{}, nil
 }
 
-func (r *recordingLog) Flush(ctx context.Context) error {
+func (r *ToolRecordingLog) Flush(ctx context.Context) error {
        r.mutex.Lock()
        defer r.mutex.Unlock()
 
@@ -228,7 +228,7 @@ func (r *recordingLog) Flush(ctx context.Context) error {
        return ctx.Err()
 }
 
-func (r *recordingLog) Events() []string {
+func (r *ToolRecordingLog) Events() []string {
        r.mutex.Lock()
        defer r.mutex.Unlock()
 
@@ -237,7 +237,7 @@ func (r *recordingLog) Events() []string {
 
 // newTestToolbox returns a toolbox with log, an empty chat, and the working
 // directory dir.
-func newTestToolbox(t *testing.T, log chatLog, dir string) *toolbox {
+func newTestToolbox(t *testing.T, log ToolLog, dir string) *Toolbox {
        t.Helper()
 
        memory, err := openMemory(t.Context(), t.TempDir(), newTestClock().Now, newFakeSummarizer())
@@ -247,7 +247,7 @@ func newTestToolbox(t *testing.T, log chatLog, dir string) *toolbox {
                require.NoError(t, memory.Close())
        })
 
-       return &toolbox{
+       return &Toolbox{
                log:     log,
                chat:    memory,
                workdir: dir,
diff --git a/turn.go b/turn.go
index c0a87e8d923a59e104dfe83a024e24331a268411..c6a083bd1418ec11b0b9d7a31b031a062d7631b1 100644 (file)
--- a/turn.go
+++ b/turn.go
@@ -32,7 +32,7 @@ func newTurnLoop(agent *Agent, memory *Memory, client TurnClient, clock func() t
                wake:   make(chan struct{}, 1),
        }
 
-       l.tools = &toolbox{
+       l.tools = &Toolbox{
                log:     l,
                chat:    memory,
                workdir: agent.config.Workdir,
@@ -78,7 +78,7 @@ type TurnLoop struct {
        agent  *Agent
        memory *Memory
        client TurnClient
-       tools  *toolbox
+       tools  *Toolbox
        layout *ViewLayout
        clock  func() time.Time
        notify func(Event)