feat(pico): add support for tool_calls in chat messages

This commit is contained in:
lc6464
2026-04-25 23:43:10 +08:00
parent 77be169db4
commit 5cd10b594a
20 changed files with 815 additions and 409 deletions
+43 -141
View File
@@ -2,7 +2,6 @@ package api
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"net/http"
@@ -53,6 +52,7 @@ type sessionChatMessage struct {
Kind string `json:"kind,omitempty"`
Media []string `json:"media,omitempty"`
Attachments []sessionChatAttachment `json:"attachments,omitempty"`
ToolCalls []utils.VisibleToolCall `json:"tool_calls,omitempty"`
}
type sessionChatAttachment struct {
@@ -456,7 +456,10 @@ func truncateRunes(s string, maxLen int) string {
}
func sessionChatMessageVisible(msg sessionChatMessage) bool {
return strings.TrimSpace(msg.Content) != "" || len(msg.Media) > 0 || len(msg.Attachments) > 0
return strings.TrimSpace(msg.Content) != "" ||
len(msg.Media) > 0 ||
len(msg.Attachments) > 0 ||
len(msg.ToolCalls) > 0
}
func sessionChatMessagePreview(msg sessionChatMessage) string {
@@ -475,6 +478,9 @@ func sessionChatMessagePreview(msg sessionChatMessage) string {
}
return "[attachment]"
}
if len(msg.ToolCalls) > 0 {
return "[tool call]"
}
return ""
}
@@ -521,25 +527,11 @@ func sessionTranscriptMessages(
}
}
toolSummaryMessages := visibleAssistantToolSummaryMessages(msg.ToolCalls, toolFeedbackMaxArgsLength)
if len(toolSummaryMessages) > 0 {
transcript = append(transcript, toolSummaryMessages...)
}
toolCallsMsg, hasToolCallsMsg := assistantToolCallsMessage(
msg.ToolCalls,
toolFeedbackMaxArgsLength,
)
visibleToolMessages := visibleAssistantToolMessages(msg.ToolCalls)
if len(visibleToolMessages) > 0 {
transcript = append(transcript, visibleToolMessages...)
}
// When assistant content exactly matches the rendered tool summary or
// tool-delivered message, skip it to avoid duplicates. Distinct content
// must remain visible in restored session history.
if len(msg.ToolCalls) > 0 &&
len(msg.Media) == 0 &&
len(attachments) == 0 &&
assistantToolCallContentDuplicated(msg.Content, toolSummaryMessages, visibleToolMessages) {
continue
}
// Pico web chat can persist both visible `message` tool output and a
// later plain assistant reply in the same turn. Hide only the fixed
@@ -547,6 +539,12 @@ func sessionTranscriptMessages(
content := msg.Content
if assistantMessageInternalOnly(msg) {
if len(attachments) == 0 {
if hasToolCallsMsg {
transcript = append(transcript, toolCallsMsg)
}
if len(visibleToolMessages) > 0 {
transcript = append(transcript, visibleToolMessages...)
}
continue
}
content = ""
@@ -559,10 +557,22 @@ func sessionTranscriptMessages(
Attachments: attachments,
}
if !sessionChatMessageVisible(chatMsg) {
if hasToolCallsMsg {
transcript = append(transcript, toolCallsMsg)
}
if len(visibleToolMessages) > 0 {
transcript = append(transcript, visibleToolMessages...)
}
continue
}
transcript = append(transcript, chatMsg)
if hasToolCallsMsg {
transcript = append(transcript, toolCallsMsg)
}
if len(visibleToolMessages) > 0 {
transcript = append(transcript, visibleToolMessages...)
}
}
}
@@ -580,51 +590,6 @@ func filterSessionChatMessages(messages []sessionChatMessage) []sessionChatMessa
return filtered
}
func assistantToolCallContentDuplicated(
content string,
toolSummaryMessages []sessionChatMessage,
visibleToolMessages []sessionChatMessage,
) bool {
content = strings.TrimSpace(content)
if content == "" {
return false
}
for _, msg := range toolSummaryMessages {
if toolSummaryContainsContent(msg.Content, content) {
return true
}
}
for _, msg := range visibleToolMessages {
if strings.TrimSpace(msg.Content) == content {
return true
}
}
return false
}
func toolSummaryContainsContent(summary, content string) bool {
summary = strings.TrimSpace(summary)
content = strings.TrimSpace(content)
if summary == "" || content == "" {
return false
}
if summary == content {
return true
}
_, body, hasBody := strings.Cut(summary, "\n")
if !hasBody {
return false
}
body = strings.TrimSpace(body)
if body == content {
return true
}
firstSection, _, _ := strings.Cut(body, "\n```")
return strings.TrimSpace(firstSection) == content
}
func sessionAttachments(msg providers.Message) []sessionChatAttachment {
if len(msg.Attachments) == 0 {
return nil
@@ -720,80 +685,34 @@ func assistantThoughtMessage(msg providers.Message) (sessionChatMessage, bool) {
}, true
}
func visibleAssistantToolSummaryMessages(
func assistantToolCallsMessage(
toolCalls []providers.ToolCall,
toolFeedbackMaxArgsLength int,
) []sessionChatMessage {
) (sessionChatMessage, bool) {
if len(toolCalls) == 0 {
return nil
return sessionChatMessage{}, false
}
if toolFeedbackMaxArgsLength <= 0 {
toolFeedbackMaxArgsLength = defaultToolFeedbackMaxArgsLength()
}
messages := make([]sessionChatMessage, 0, len(toolCalls))
for _, tc := range toolCalls {
name, argsJSON := toolCallNameAndArguments(tc)
if strings.TrimSpace(name) == "" {
continue
}
if name == "web_search" || name == "web_fetch" {
continue
}
if name == "message" {
if _, ok := parseMessageToolContent(argsJSON); ok {
continue
}
}
messages = append(messages, sessionChatMessage{
Role: "assistant",
Content: utils.FormatToolFeedbackMessage(
name,
visibleAssistantToolFeedbackExplanation(tc, toolFeedbackMaxArgsLength),
visibleAssistantToolArgsPreview(tc, toolFeedbackMaxArgsLength),
),
})
visibleToolCalls := utils.BuildVisibleToolCalls(toolCalls, toolFeedbackMaxArgsLength)
if len(visibleToolCalls) == 0 {
return sessionChatMessage{}, false
}
return messages
}
func visibleAssistantToolFeedbackExplanation(
tc providers.ToolCall,
toolFeedbackMaxArgsLength int,
) string {
if tc.ExtraContent != nil {
if explanation := strings.TrimSpace(tc.ExtraContent.ToolFeedbackExplanation); explanation != "" {
return utils.Truncate(explanation, toolFeedbackMaxArgsLength)
}
}
return ""
return sessionChatMessage{
Role: "assistant",
Kind: "tool_calls",
ToolCalls: visibleToolCalls,
}, true
}
func visibleAssistantToolArgsPreview(
tc providers.ToolCall,
toolFeedbackMaxArgsLength int,
) string {
argsJSON := ""
if tc.Function != nil {
argsJSON = tc.Function.Arguments
}
if strings.TrimSpace(argsJSON) == "" && len(tc.Arguments) > 0 {
if encodedArgs, err := json.MarshalIndent(tc.Arguments, "", " "); err == nil {
argsJSON = string(encodedArgs)
}
}
argsJSON = strings.TrimSpace(argsJSON)
if argsJSON == "" {
return ""
}
var pretty bytes.Buffer
if err := json.Indent(&pretty, []byte(argsJSON), "", " "); err == nil {
argsJSON = pretty.String()
}
return utils.Truncate(argsJSON, toolFeedbackMaxArgsLength)
return utils.VisibleToolCallArgumentsPreview(tc, toolFeedbackMaxArgsLength)
}
func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatMessage {
@@ -803,7 +722,7 @@ func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatM
messages := make([]sessionChatMessage, 0, len(toolCalls))
for _, tc := range toolCalls {
name, argsJSON := toolCallNameAndArguments(tc)
name, argsJSON := utils.VisibleToolCallNameAndArguments(tc)
if name != "message" {
continue
}
@@ -820,23 +739,6 @@ func visibleAssistantToolMessages(toolCalls []providers.ToolCall) []sessionChatM
return messages
}
func toolCallNameAndArguments(tc providers.ToolCall) (string, string) {
name := tc.Name
argsJSON := ""
if tc.Function != nil {
if name == "" {
name = tc.Function.Name
}
argsJSON = tc.Function.Arguments
}
if strings.TrimSpace(argsJSON) == "" && len(tc.Arguments) > 0 {
if encodedArgs, err := json.Marshal(tc.Arguments); err == nil {
argsJSON = string(encodedArgs)
}
}
return name, argsJSON
}
func parseMessageToolContent(argsJSON string) (string, bool) {
var args struct {
Content string `json:"content"`
+84 -121
View File
@@ -32,6 +32,25 @@ func sessionsTestDir(t *testing.T, configPath string) string {
return dir
}
func assertVisibleToolCallMessage(
t *testing.T,
msg sessionChatMessage,
toolName string,
) utils.VisibleToolCall {
t.Helper()
if msg.Role != "assistant" || msg.Kind != "tool_calls" {
t.Fatalf("message = %#v, want assistant/tool_calls", msg)
}
if len(msg.ToolCalls) != 1 {
t.Fatalf("len(message.ToolCalls) = %d, want 1", len(msg.ToolCalls))
}
if got := msg.ToolCalls[0].Function; got == nil || got.Name != toolName {
t.Fatalf("tool call = %#v, want function %q", msg.ToolCalls[0], toolName)
}
return msg.ToolCalls[0]
}
func TestHandleListSessions_JSONLStorage(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@@ -516,11 +535,7 @@ func TestHandleGetSession_SkipsTransientThoughtMessages(t *testing.T) {
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
Kind string `json:"kind"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -569,11 +584,7 @@ func TestHandleGetSession_ReconstructsThoughtFromAssistantReasoningContent(t *te
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
Kind string `json:"kind"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -667,11 +678,7 @@ func TestHandleGetSession_ReconstructsRefreshMatrixForThoughtAndToolSummary(t *t
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
Kind string `json:"kind"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -694,20 +701,14 @@ func TestHandleGetSession_ReconstructsRefreshMatrixForThoughtAndToolSummary(t *t
assertMessage(2, "assistant", "", "plain visible")
assertMessage(3, "user", "", "turn2")
assertMessage(4, "assistant", "thought", "tool thought")
if !strings.Contains(resp.Messages[5].Content, "`read_file`") {
t.Fatalf("messages[5] = %#v, want read_file tool summary", resp.Messages[5])
}
assertVisibleToolCallMessage(t, resp.Messages[5], "read_file")
assertMessage(6, "user", "", "turn3")
if !strings.Contains(resp.Messages[7].Content, "`list_dir`") {
t.Fatalf("messages[7] = %#v, want list_dir tool summary", resp.Messages[7])
}
assertMessage(8, "assistant", "", "tool visible only")
assertMessage(7, "assistant", "", "tool visible only")
assertVisibleToolCallMessage(t, resp.Messages[8], "list_dir")
assertMessage(9, "user", "", "turn4")
assertMessage(10, "assistant", "thought", "tool mixed thought")
if !strings.Contains(resp.Messages[11].Content, "`exec`") {
t.Fatalf("messages[11] = %#v, want exec tool summary", resp.Messages[11])
}
assertMessage(12, "assistant", "", "tool visible and thought")
assertMessage(11, "assistant", "", "tool visible and thought")
assertVisibleToolCallMessage(t, resp.Messages[12], "exec")
}
func TestHandleGetSession_ReconstructsVisibleMessageToolOutputWithoutDuplicateSummary(t *testing.T) {
@@ -758,27 +759,20 @@ func TestHandleGetSession_ReconstructsVisibleMessageToolOutputWithoutDuplicateSu
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(resp.Messages) != 2 {
t.Fatalf("len(resp.Messages) = %d, want 2", len(resp.Messages))
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
}
if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "test" {
t.Fatalf("first message = %#v, want user/test", resp.Messages[0])
}
if resp.Messages[1].Role != "assistant" || resp.Messages[1].Content != "visible tool output" {
t.Fatalf("assistant message = %#v, want visible tool output", resp.Messages[1])
}
for _, msg := range resp.Messages {
if msg.Role == "tool" || strings.Contains(msg.Content, "`message`") {
t.Fatalf("unexpected raw tool or duplicate message-tool summary: %#v", msg)
}
assertVisibleToolCallMessage(t, resp.Messages[1], "message")
if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "visible tool output" {
t.Fatalf("assistant message = %#v, want visible tool output", resp.Messages[2])
}
}
@@ -829,25 +823,23 @@ func TestHandleGetSession_PreservesFinalAssistantReplyAfterMessageToolOutput(t *
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
if len(resp.Messages) != 4 {
t.Fatalf("len(resp.Messages) = %d, want 4", len(resp.Messages))
}
if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "test" {
t.Fatalf("first message = %#v, want user/test", resp.Messages[0])
}
if resp.Messages[1].Role != "assistant" || resp.Messages[1].Content != "visible tool output" {
t.Fatalf("interim assistant message = %#v, want visible tool output", resp.Messages[1])
assertVisibleToolCallMessage(t, resp.Messages[1], "message")
if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "visible tool output" {
t.Fatalf("interim assistant message = %#v, want visible tool output", resp.Messages[2])
}
if resp.Messages[2].Role != "assistant" || resp.Messages[2].Content != "final assistant reply" {
t.Fatalf("final assistant message = %#v, want final assistant reply", resp.Messages[2])
if resp.Messages[3].Role != "assistant" || resp.Messages[3].Content != "final assistant reply" {
t.Fatalf("final assistant message = %#v, want final assistant reply", resp.Messages[3])
}
}
@@ -904,8 +896,8 @@ func TestHandleListSessions_MessageCountUsesVisibleTranscript(t *testing.T) {
if len(items) != 1 {
t.Fatalf("len(items) = %d, want 1", len(items))
}
if items[0].MessageCount != 2 {
t.Fatalf("items[0].MessageCount = %d, want 2", items[0].MessageCount)
if items[0].MessageCount != 3 {
t.Fatalf("items[0].MessageCount = %d, want 3", items[0].MessageCount)
}
}
@@ -959,25 +951,24 @@ func TestHandleGetSession_DoesNotDuplicateAssistantToolCallContent(t *testing.T)
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if len(resp.Messages) != 2 {
t.Fatalf("len(resp.Messages) = %d, want 2", len(resp.Messages))
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
}
if resp.Messages[0].Role != "user" || resp.Messages[0].Content != "check file" {
t.Fatalf("first message = %#v, want user/check file", resp.Messages[0])
}
if !strings.Contains(resp.Messages[1].Content, "`read_file`") {
t.Fatalf("tool summary message = %#v, want read_file summary", resp.Messages[1])
if resp.Messages[1].Content != "Read the file before replying." {
t.Fatalf("assistant content = %#v, want preserved assistant content", resp.Messages[1])
}
if !strings.Contains(resp.Messages[1].Content, "Read the file before replying.") {
t.Fatalf("tool summary message = %#v, want tool explanation", resp.Messages[1])
toolCall := assertVisibleToolCallMessage(t, resp.Messages[2], "read_file")
if toolCall.ExtraContent == nil ||
toolCall.ExtraContent.ToolFeedbackExplanation != "Read the file before replying." {
t.Fatalf("tool call = %#v, want explanation", toolCall)
}
}
@@ -1030,10 +1021,7 @@ func TestHandleGetSession_PreservesDistinctAssistantToolCallContent(t *testing.T
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -1041,13 +1029,11 @@ func TestHandleGetSession_PreservesDistinctAssistantToolCallContent(t *testing.T
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
}
if !strings.Contains(resp.Messages[1].Content, "`read_file`") {
t.Fatalf("tool summary message = %#v, want read_file summary", resp.Messages[1])
}
if resp.Messages[2].Role != "assistant" ||
resp.Messages[2].Content != "I will summarize the findings after reading the file." {
t.Fatalf("assistant content = %#v, want preserved distinct content", resp.Messages[2])
if resp.Messages[1].Role != "assistant" ||
resp.Messages[1].Content != "I will summarize the findings after reading the file." {
t.Fatalf("assistant content = %#v, want preserved distinct content", resp.Messages[1])
}
assertVisibleToolCallMessage(t, resp.Messages[2], "read_file")
}
func TestHandleGetSession_PreservesMediaWhenAssistantToolCallContentDuplicatesSummary(t *testing.T) {
@@ -1100,11 +1086,7 @@ func TestHandleGetSession_PreservesMediaWhenAssistantToolCallContentDuplicatesSu
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
Media []string `json:"media"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -1112,23 +1094,16 @@ func TestHandleGetSession_PreservesMediaWhenAssistantToolCallContentDuplicatesSu
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
}
if !strings.Contains(resp.Messages[1].Content, "`view_image`") {
t.Fatalf("tool summary message = %#v, want view_image summary", resp.Messages[1])
if resp.Messages[1].Role != "assistant" {
t.Fatalf("assistant message role = %q, want assistant", resp.Messages[1].Role)
}
if resp.Messages[2].Role != "assistant" {
t.Fatalf("assistant message role = %q, want assistant", resp.Messages[2].Role)
if resp.Messages[1].Content != "Reviewing the generated screenshot." {
t.Fatalf("assistant content = %q, want preserved duplicated content with media", resp.Messages[1].Content)
}
if resp.Messages[2].Content != "Reviewing the generated screenshot." {
t.Fatalf("assistant content = %q, want preserved duplicated content with media", resp.Messages[2].Content)
}
if len(resp.Messages[2].Media) != 1 || resp.Messages[2].Media[0] != "data:image/png;base64,abc123" {
t.Fatalf("assistant media = %#v, want preserved media", resp.Messages[2].Media)
}
for _, msg := range resp.Messages {
if msg.Role == "tool" || strings.Contains(msg.Content, "raw read_file result") {
t.Fatalf("unexpected raw tool result in history: %#v", msg)
}
if len(resp.Messages[1].Media) != 1 || resp.Messages[1].Media[0] != "data:image/png;base64,abc123" {
t.Fatalf("assistant media = %#v, want preserved media", resp.Messages[1].Media)
}
assertVisibleToolCallMessage(t, resp.Messages[2], "view_image")
}
func TestHandleGetSession_PreservesAttachmentsWhenAssistantToolCallContentDuplicatesSummary(t *testing.T) {
@@ -1198,21 +1173,19 @@ func TestHandleGetSession_PreservesAttachmentsWhenAssistantToolCallContentDuplic
if len(resp.Messages) != 3 {
t.Fatalf("len(resp.Messages) = %d, want 3", len(resp.Messages))
}
if !strings.Contains(resp.Messages[1].Content, "`read_file`") {
t.Fatalf("tool summary message = %#v, want read_file summary", resp.Messages[1])
if resp.Messages[1].Role != "assistant" {
t.Fatalf("assistant message role = %q, want assistant", resp.Messages[1].Role)
}
if resp.Messages[2].Role != "assistant" {
t.Fatalf("assistant message role = %q, want assistant", resp.Messages[2].Role)
if resp.Messages[1].Content != "Reviewing the generated report." {
t.Fatalf("assistant content = %q, want preserved duplicated content", resp.Messages[1].Content)
}
if resp.Messages[2].Content != "Reviewing the generated report." {
t.Fatalf("assistant content = %q, want preserved duplicated content", resp.Messages[2].Content)
if len(resp.Messages[1].Attachments) != 1 {
t.Fatalf("len(assistant.Attachments) = %d, want 1", len(resp.Messages[1].Attachments))
}
if len(resp.Messages[2].Attachments) != 1 {
t.Fatalf("len(assistant.Attachments) = %d, want 1", len(resp.Messages[2].Attachments))
}
if resp.Messages[2].Attachments[0].URL != "https://example.com/report.txt" {
t.Fatalf("attachment url = %q, want report URL", resp.Messages[2].Attachments[0].URL)
if resp.Messages[1].Attachments[0].URL != "https://example.com/report.txt" {
t.Fatalf("attachment url = %q, want report URL", resp.Messages[1].Attachments[0].URL)
}
assertVisibleToolCallMessage(t, resp.Messages[2], "read_file")
}
func TestHandleGetSession_UsesConfiguredToolFeedbackMaxArgsLength(t *testing.T) {
@@ -1273,10 +1246,7 @@ func TestHandleGetSession_UsesConfiguredToolFeedbackMaxArgsLength(t *testing.T)
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
err = json.Unmarshal(rec.Body.Bytes(), &resp)
if err != nil {
@@ -1287,17 +1257,15 @@ func TestHandleGetSession_UsesConfiguredToolFeedbackMaxArgsLength(t *testing.T)
}
wantPreview := utils.Truncate(explanation, 20)
if !strings.Contains(resp.Messages[1].Content, wantPreview) {
t.Fatalf("tool summary = %q, want preview %q", resp.Messages[1].Content, wantPreview)
}
wantArgsPreview := visibleAssistantToolArgsPreview(providers.ToolCall{
Function: &providers.FunctionCall{Arguments: argsJSON},
}, 20)
if !strings.Contains(resp.Messages[1].Content, wantArgsPreview) {
t.Fatalf("tool summary = %q, want args preview %q", resp.Messages[1].Content, wantArgsPreview)
toolCall := assertVisibleToolCallMessage(t, resp.Messages[1], "read_file")
if toolCall.ExtraContent == nil || toolCall.ExtraContent.ToolFeedbackExplanation != wantPreview {
t.Fatalf("tool call = %#v, want preview %q", toolCall, wantPreview)
}
if !strings.Contains(resp.Messages[1].Content, "`read_file`") {
t.Fatalf("tool summary = %q, want read_file summary", resp.Messages[1].Content)
if toolCall.Function == nil || toolCall.Function.Arguments != wantArgsPreview {
t.Fatalf("tool call = %#v, want args preview %q", toolCall, wantArgsPreview)
}
}
@@ -1357,10 +1325,7 @@ func TestHandleGetSession_FallsBackToLegacyToolArgumentsWhenExplanationMissing(t
}
var resp struct {
Messages []struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"messages"`
Messages []sessionChatMessage `json:"messages"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
@@ -1372,11 +1337,9 @@ func TestHandleGetSession_FallsBackToLegacyToolArgumentsWhenExplanationMissing(t
wantPreview := visibleAssistantToolArgsPreview(providers.ToolCall{
Function: &providers.FunctionCall{Arguments: argsJSON},
}, 20)
if !strings.Contains(resp.Messages[1].Content, "`read_file`") {
t.Fatalf("tool summary = %q, want read_file summary", resp.Messages[1].Content)
}
if !strings.Contains(resp.Messages[1].Content, wantPreview) {
t.Fatalf("tool summary = %q, want legacy args preview %q", resp.Messages[1].Content, wantPreview)
toolCall := assertVisibleToolCallMessage(t, resp.Messages[1], "read_file")
if toolCall.Function == nil || toolCall.Function.Arguments != wantPreview {
t.Fatalf("tool call = %#v, want legacy args preview %q", toolCall, wantPreview)
}
}