Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cagent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@
"type": "string",
"description": "Additional instruction on how to use this toolset"
},
"toon": {
"type": "string",
"description": "A comma-delimited list of regular expressions of tools to toonify"
},
"ref": {
"type": "string",
"description": "Reference to external tool (e.g., docker:context7)",
Expand Down
12 changes: 12 additions & 0 deletions examples/github-toon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env cagent run
version: "2"

agents:
root:
model: anthropic/claude-sonnet-4-0
description: GitHub Agent - Help with GitHub using MCP tools
instruction: You are a helpful assistant that can help with all things GitHub
toolsets:
- type: mcp
ref: docker:github-official
toon: ".*" # toonify all the tools
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any specific reason for giving the choice of tools to toonify here? i was initially thinking it'd be simpler to do all or nothing, so i'm curious to know if you encountered any weird behavior etc along the way

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all tools will return a json

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ require (
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/alpkeskin/gotoon v0.1.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NT
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alpkeskin/gotoon v0.1.0 h1:qtKx9kqpTYycxEolgHctyOmu2L5CUNwEeXfMizscO/k=
github.com/alpkeskin/gotoon v0.1.0/go.mod h1:eCkjhBz/wmCoXAWKERuhPSb3+jW7ajluruYIgsfbriU=
github.com/anthropics/anthropic-sdk-go v1.14.0 h1:EzNQvnZlaDHe2UPkoUySDz3ixRgNbwKdH8KtFpv7pi4=
github.com/anthropics/anthropic-sdk-go v1.14.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
Expand Down
1 change: 1 addition & 0 deletions pkg/config/v2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type Toolset struct {
Type string `json:"type,omitempty"`
Tools []string `json:"tools,omitempty"`
Instruction string `json:"instruction,omitempty"`
Toon string `json:"toon,omitempty"`

// For the `mcp` tool
Command string `json:"command,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions pkg/teamloader/teamloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ func getToolsForAgent(ctx context.Context, a *latest.AgentConfig, parentDir stri

wrapped := WithToolsFilter(tool, toolset.Tools...)
wrapped = WithInstructions(wrapped, toolset.Instruction)
wrapped = WithToon(wrapped, toolset.Toon)

toolSets = append(toolSets, wrapped)
}
Expand Down
73 changes: 73 additions & 0 deletions pkg/teamloader/toon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package teamloader

import (
"context"
"encoding/json"
"regexp"
"strings"

"github.com/alpkeskin/gotoon"

"github.com/docker/cagent/pkg/tools"
)

type toonTools struct {
tools.ToolSet
toolRegexps []*regexp.Regexp
}

func (f *toonTools) Tools(ctx context.Context) ([]tools.Tool, error) {
allTools, err := f.ToolSet.Tools(ctx)
if err != nil {
return nil, err
}

for i, tool := range allTools {
for _, regex := range f.toolRegexps {
if !regex.MatchString(tool.Name) {
continue
}

handler := tool.Handler
tool.Handler = func(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
res, err := handler(ctx, toolCall)
if err != nil {
return res, err
}

var o map[string]any
err = json.Unmarshal([]byte(res.Output), &o)
if err != nil {
return res, nil
}

tooned, err := gotoon.Encode(o)
if err != nil {
return res, err
}

res.Output = tooned
return res, nil
}
allTools[i] = tool
}
}

return allTools, nil
}

func WithToon(inner tools.ToolSet, toon string) tools.ToolSet {
if toon == "" {
return inner
}

var toolRegexps []*regexp.Regexp

for toolName := range strings.SplitSeq(toon, ",") {
toolRegexps = append(toolRegexps, regexp.MustCompile(strings.TrimSpace(toolName)))
}
return &toonTools{
ToolSet: inner,
toolRegexps: toolRegexps,
}
}
74 changes: 74 additions & 0 deletions pkg/teamloader/toon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package teamloader

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/cagent/pkg/tools"
)

func mockHandler(output string) tools.ToolHandler {
return func(ctx context.Context, toolCall tools.ToolCall) (*tools.ToolCallResult, error) {
return &tools.ToolCallResult{
Output: output,
}, nil
}
}

func TestToon(t *testing.T) {
testcases := []struct {
name string
toolResult string
expected string
filter string
}{
{
name: "should return a toon representation of a json response",
toolResult: `{"key": "value", "number": 42}`,
expected: "key: value\nnumber: 42",
},
{
name: "should return originial if not a json",
toolResult: "plain text output",
expected: "plain text output",
},
{
name: "should return original if not toon-ed",
toolResult: `{"key": "value", "number": 42}`,
expected: `{"key": "value", "number": 42}`,
filter: "other_tool",
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
inner := &mockToolSet{
toolsFunc: func(ctx context.Context) ([]tools.Tool, error) {
return []tools.Tool{
{
Name: "test_tool",
Handler: mockHandler(tc.toolResult),
},
}, nil
},
}
toolFilter := "test_tool"
if tc.filter != "" {
toolFilter = tc.filter
}
wrapped := WithToon(inner, toolFilter)

resultTools, err := wrapped.Tools(t.Context())
require.NoError(t, err)
require.Len(t, resultTools, 1)

result, err := resultTools[0].Handler(t.Context(), tools.ToolCall{})
require.NoError(t, err)
assert.Equal(t, tc.expected, result.Output)
})
}
}
3 changes: 0 additions & 3 deletions pkg/tui/components/tool/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package tool
import (
"encoding/json"
"fmt"
"log/slog"
"strings"

"github.com/charmbracelet/bubbles/v2/spinner"
Expand Down Expand Up @@ -96,8 +95,6 @@ func (mv *toolModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (mv *toolModel) View() string {
msg := mv.message

slog.Debug("Rendering tool message", "status", msg.ToolStatus, "content", msg.Content, "args", msg.ToolCall.Function.Arguments)
slog.Debug("Tool definition", "name", msg.ToolDefinition.Name, "title", msg.ToolDefinition.Annotations.Title)
displayName := msg.ToolDefinition.DisplayName()

content := fmt.Sprintf("%s %s", icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName))
Expand Down
Loading