Skip to content

Commit 1422d70

Browse files
authored
Merge pull request #1110 from rumpl/tool-meta
Add a "Meta" property to the tool results
2 parents 9ef3406 + 1d37a7c commit 1422d70

17 files changed

Lines changed: 241 additions & 403 deletions

File tree

pkg/runtime/event.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,20 @@ func ToolCallConfirmation(toolCall tools.ToolCall, toolDefinition tools.Tool, ag
8282
}
8383

8484
type ToolCallResponseEvent struct {
85-
Type string `json:"type"`
86-
ToolCall tools.ToolCall `json:"tool_call"`
87-
ToolDefinition tools.Tool `json:"tool_definition"`
88-
Response string `json:"response"`
85+
Type string `json:"type"`
86+
ToolCall tools.ToolCall `json:"tool_call"`
87+
ToolDefinition tools.Tool `json:"tool_definition"`
88+
Response string `json:"response"`
89+
Result *tools.ToolCallResult `json:"result,omitempty"`
8990
AgentContext
9091
}
9192

92-
func ToolCallResponse(toolCall tools.ToolCall, toolDefinition tools.Tool, response, agentName string) Event {
93+
func ToolCallResponse(toolCall tools.ToolCall, toolDefinition tools.Tool, result *tools.ToolCallResult, response, agentName string) Event {
9394
return &ToolCallResponseEvent{
9495
Type: "tool_call_response",
9596
ToolCall: toolCall,
9697
Response: response,
98+
Result: result,
9799
ToolDefinition: toolDefinition,
98100
AgentContext: AgentContext{AgentName: agentName},
99101
}

pkg/runtime/runtime.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ func (r *LocalRuntime) runTool(ctx context.Context, tool tools.Tool, toolCall to
11381138
slog.Debug("Agent tool call completed", "tool", toolCall.Function.Name, "output_length", len(res.Output))
11391139
}
11401140

1141-
events <- ToolCallResponse(toolCall, tool, res.Output, a.Name())
1141+
events <- ToolCallResponse(toolCall, tool, res, res.Output, a.Name())
11421142

11431143
// Ensure tool response content is not empty for API compatibility
11441144
content := res.Output
@@ -1173,29 +1173,23 @@ func (r *LocalRuntime) runAgentTool(ctx context.Context, handler ToolHandlerFunc
11731173

11741174
telemetry.RecordToolCall(ctx, toolCall.Function.Name, sess.ID, a.Name(), duration, err)
11751175

1176-
var output string
11771176
if err != nil {
11781177
if errors.Is(err, context.Canceled) || errors.Is(ctx.Err(), context.Canceled) {
11791178
slog.Debug("Runtime tool handler canceled by context", "tool", toolCall.Function.Name, "agent", a.Name(), "session_id", sess.ID)
11801179
// Synthesize a cancellation response so the transcript remains consistent
1181-
output = "The tool call was canceled by the user."
1180+
res.Output = "The tool call was canceled by the user."
11821181
span.SetStatus(codes.Ok, "runtime tool handler canceled by user")
11831182
} else {
11841183
span.RecordError(err)
11851184
span.SetStatus(codes.Error, "runtime tool handler error")
1186-
output = fmt.Sprintf("error calling tool: %v", err)
11871185
slog.Error("Error executing tool", "tool", toolCall.Function.Name, "error", err)
11881186
}
1189-
} else {
1190-
output = res.Output
1191-
span.SetStatus(codes.Ok, "runtime tool handler completed")
1192-
slog.Debug("Tool executed successfully", "tool", toolCall.Function.Name)
11931187
}
11941188

1195-
events <- ToolCallResponse(toolCall, tool, output, a.Name())
1189+
events <- ToolCallResponse(toolCall, tool, res, res.Output, a.Name())
11961190

11971191
// Ensure tool response content is not empty for API compatibility
1198-
content := output
1192+
content := res.Output
11991193
if strings.TrimSpace(content) == "" {
12001194
content = "(no output)"
12011195
}
@@ -1215,7 +1209,9 @@ func (r *LocalRuntime) addToolRejectedResponse(ctx context.Context, sess *sessio
12151209

12161210
result := "The user rejected the tool call."
12171211

1218-
events <- ToolCallResponse(toolCall, tool, result, a.Name())
1212+
events <- ToolCallResponse(toolCall, tool, &tools.ToolCallResult{
1213+
Output: result,
1214+
}, result, a.Name())
12191215

12201216
toolResponseMsg := chat.Message{
12211217
Role: chat.MessageRoleTool,
@@ -1232,7 +1228,9 @@ func (r *LocalRuntime) addToolCancelledResponse(ctx context.Context, sess *sessi
12321228

12331229
result := "The tool call was canceled by the user."
12341230

1235-
events <- ToolCallResponse(toolCall, tool, result, a.Name())
1231+
events <- ToolCallResponse(toolCall, tool, &tools.ToolCallResult{
1232+
Output: result,
1233+
}, result, a.Name())
12361234

12371235
toolResponseMsg := chat.Message{
12381236
Role: chat.MessageRoleTool,

pkg/tools/builtin/filesystem.go

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,17 @@ type ReadMultipleFilesArgs struct {
124124
JSON bool `json:"json,omitempty" jsonschema:"Whether to return the result as JSON"`
125125
}
126126

127+
type ReadMultipleFilesEntry struct {
128+
Path string `json:"path"`
129+
Content string `json:"content"`
130+
LineCount int `json:"lineCount"`
131+
Error string `json:"error,omitempty"`
132+
}
133+
134+
type ReadMultipleFilesMeta struct {
135+
Files []ReadMultipleFilesEntry `json:"files"`
136+
}
137+
127138
type SearchFilesArgs struct {
128139
Path string `json:"path" jsonschema:"The starting directory path"`
129140
Pattern string `json:"pattern" jsonschema:"The glob pattern to match file names against"`
@@ -141,6 +152,12 @@ type ListDirectoryArgs struct {
141152
Path string `json:"path" jsonschema:"The directory path to list"`
142153
}
143154

155+
type ListDirectoryMeta struct {
156+
Files []string `json:"files"`
157+
Dirs []string `json:"dirs"`
158+
Truncated bool `json:"truncated"`
159+
}
160+
144161
type ReadFileArgs struct {
145162
Path string `json:"path" jsonschema:"The file path to read"`
146163
}
@@ -549,27 +566,33 @@ func (t *FilesystemTool) handleListDirectory(_ context.Context, args ListDirecto
549566
}
550567

551568
var result strings.Builder
569+
meta := ListDirectoryMeta{}
552570
count := 0
553571
for _, entry := range entries {
554-
// Check if entry should be ignored by VCS rules
555572
entryPath := filepath.Join(args.Path, entry.Name())
556573
if t.shouldIgnorePath(entryPath) {
557574
continue
558575
}
559576

560577
if entry.IsDir() {
561578
result.WriteString(fmt.Sprintf("DIR %s\n", entry.Name()))
579+
meta.Dirs = append(meta.Dirs, entry.Name())
562580
} else {
563581
result.WriteString(fmt.Sprintf("FILE %s\n", entry.Name()))
582+
meta.Files = append(meta.Files, entry.Name())
564583
}
565584
count++
566585
if count >= maxFiles {
567586
result.WriteString("...output truncated due to file limit...\n")
587+
meta.Truncated = true
568588
break
569589
}
570590
}
571591

572-
return tools.ResultSuccess(result.String()), nil
592+
return &tools.ToolCallResult{
593+
Output: result.String(),
594+
Meta: meta,
595+
}, nil
573596
}
574597

575598
func (t *FilesystemTool) handleReadFile(_ context.Context, args ReadFileArgs) (*tools.ToolCallResult, error) {
@@ -592,50 +615,66 @@ func (t *FilesystemTool) handleReadMultipleFiles(ctx context.Context, args ReadM
592615
}
593616

594617
var contents []PathContent
618+
meta := ReadMultipleFilesMeta{}
595619

596620
for _, path := range args.Paths {
597621
if ctx.Err() != nil {
598622
return nil, ctx.Err()
599623
}
600624

625+
entry := ReadMultipleFilesEntry{Path: path}
626+
601627
if err := t.isPathAllowed(path); err != nil {
628+
errMsg := fmt.Sprintf("Error: %s", err)
602629
contents = append(contents, PathContent{
603630
Path: path,
604-
Content: fmt.Sprintf("Error: %s", err),
631+
Content: errMsg,
605632
})
633+
entry.Error = errMsg
634+
meta.Files = append(meta.Files, entry)
606635
continue
607636
}
608637

609638
content, err := os.ReadFile(path)
610639
if err != nil {
640+
errMsg := fmt.Sprintf("Error reading file: %s", err)
611641
contents = append(contents, PathContent{
612642
Path: path,
613-
Content: fmt.Sprintf("Error reading file: %s", err),
643+
Content: errMsg,
614644
})
645+
entry.Error = errMsg
646+
meta.Files = append(meta.Files, entry)
615647
continue
616648
}
617649

618650
contents = append(contents, PathContent{
619651
Path: path,
620652
Content: string(content),
621653
})
654+
entry.Content = string(content)
655+
entry.LineCount = strings.Count(string(content), "\n") + 1
656+
meta.Files = append(meta.Files, entry)
622657
}
623658

659+
var output string
624660
if args.JSON {
625661
jsonResult, err := json.MarshalIndent(contents, "", " ")
626662
if err != nil {
627663
return tools.ResultError(fmt.Sprintf("Error formatting JSON: %s", err)), nil
628664
}
629-
630-
return tools.ResultSuccess(string(jsonResult)), nil
631-
}
632-
633-
var result strings.Builder
634-
for _, content := range contents {
635-
result.WriteString(fmt.Sprintf("=== %s ===\n%s\n\n", content.Path, content.Content))
665+
output = string(jsonResult)
666+
} else {
667+
var result strings.Builder
668+
for _, content := range contents {
669+
result.WriteString(fmt.Sprintf("=== %s ===\n%s\n\n", content.Path, content.Content))
670+
}
671+
output = result.String()
636672
}
637673

638-
return tools.ResultSuccess(result.String()), nil
674+
return &tools.ToolCallResult{
675+
Output: output,
676+
Meta: meta,
677+
}, nil
639678
}
640679

641680
func (t *FilesystemTool) handleSearchFiles(_ context.Context, args SearchFilesArgs) (*tools.ToolCallResult, error) {

pkg/tools/builtin/todo.go

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ type todoHandler struct {
4848
todos *concurrent.Map[string, Todo]
4949
}
5050

51+
func (h *todoHandler) allTodos() []Todo {
52+
var todos []Todo
53+
h.todos.Range(func(_ string, todo Todo) bool {
54+
todos = append(todos, todo)
55+
return true
56+
})
57+
return todos
58+
}
59+
5160
var NewSharedTodoTool = sync.OnceValue(NewTodoTool)
5261

5362
func NewTodoTool() *TodoTool {
@@ -81,17 +90,20 @@ This toolset is REQUIRED for maintaining task state and ensuring all steps are c
8190

8291
func (h *todoHandler) createTodo(_ context.Context, params CreateTodoArgs) (*tools.ToolCallResult, error) {
8392
id := fmt.Sprintf("todo_%d", h.todos.Length()+1)
84-
h.todos.Store(id, Todo{
93+
todo := Todo{
8594
ID: id,
8695
Description: params.Description,
8796
Status: "pending",
88-
})
97+
}
98+
h.todos.Store(id, todo)
8999

90-
return tools.ResultSuccess(fmt.Sprintf("Created todo [%s]: %s", id, params.Description)), nil
100+
return &tools.ToolCallResult{
101+
Output: fmt.Sprintf("Created todo [%s]: %s", id, params.Description),
102+
Meta: h.allTodos(),
103+
}, nil
91104
}
92105

93106
func (h *todoHandler) createTodos(_ context.Context, params CreateTodosArgs) (*tools.ToolCallResult, error) {
94-
ids := make([]string, len(params.Descriptions))
95107
start := h.todos.Length()
96108
for i, desc := range params.Descriptions {
97109
id := fmt.Sprintf("todo_%d", start+i+1)
@@ -100,43 +112,51 @@ func (h *todoHandler) createTodos(_ context.Context, params CreateTodosArgs) (*t
100112
Description: desc,
101113
Status: "pending",
102114
})
103-
ids[i] = id
104115
}
105116

106-
output := fmt.Sprintf("Created %d todos: ", len(params.Descriptions))
107-
for i, id := range ids {
117+
var output strings.Builder
118+
fmt.Fprintf(&output, "Created %d todos: ", len(params.Descriptions))
119+
for i := range params.Descriptions {
108120
if i > 0 {
109-
output += ", "
121+
output.WriteString(", ")
110122
}
111-
output += fmt.Sprintf("[%s]", id)
123+
fmt.Fprintf(&output, "[todo_%d]", start+i+1)
112124
}
113125

114-
return tools.ResultSuccess(output), nil
126+
return &tools.ToolCallResult{
127+
Output: output.String(),
128+
Meta: h.allTodos(),
129+
}, nil
115130
}
116131

117132
func (h *todoHandler) updateTodo(_ context.Context, params UpdateTodoArgs) (*tools.ToolCallResult, error) {
118133
todo, exists := h.todos.Load(params.ID)
119134
if !exists {
120-
return nil, fmt.Errorf("todo [%s] not found", params.ID)
135+
return tools.ResultError(fmt.Sprintf("todo [%s] not found", params.ID)), nil
121136
}
122137

123138
todo.Status = params.Status
124139
h.todos.Store(params.ID, todo)
125140

126-
return tools.ResultSuccess(fmt.Sprintf("Updated todo [%s] to status: [%s]", params.ID, params.Status)), nil
141+
return &tools.ToolCallResult{
142+
Output: fmt.Sprintf("Updated todo [%s] to status: [%s]", params.ID, params.Status),
143+
Meta: h.allTodos(),
144+
}, nil
127145
}
128146

129-
func (h *todoHandler) listTodos(_ context.Context, _ map[string]any) (*tools.ToolCallResult, error) {
147+
func (h *todoHandler) listTodos(_ context.Context, _ tools.ToolCall) (*tools.ToolCallResult, error) {
130148
var output strings.Builder
131149
output.WriteString("Current todos:\n")
132150

133-
h.todos.Range(func(_ string, todo Todo) bool {
134-
output.WriteString(fmt.Sprintf("- [%s] %s (Status: %s)\n",
135-
todo.ID, todo.Description, todo.Status))
136-
return true
137-
})
151+
todos := h.allTodos()
152+
for _, todo := range todos {
153+
fmt.Fprintf(&output, "- [%s] %s (Status: %s)\n", todo.ID, todo.Description, todo.Status)
154+
}
138155

139-
return tools.ResultSuccess(output.String()), nil
156+
return &tools.ToolCallResult{
157+
Output: output.String(),
158+
Meta: todos,
159+
}, nil
140160
}
141161

142162
func (t *TodoTool) Tools(context.Context) ([]tools.Tool, error) {
@@ -182,7 +202,7 @@ func (t *TodoTool) Tools(context.Context) ([]tools.Tool, error) {
182202
Category: "todo",
183203
Description: "List all current todos with their status",
184204
OutputSchema: tools.MustSchemaFor[string](),
185-
Handler: NewHandler(t.handler.listTodos),
205+
Handler: t.handler.listTodos,
186206
Annotations: tools.ToolAnnotations{
187207
Title: "List TODOs",
188208
ReadOnlyHint: true,

0 commit comments

Comments
 (0)