| package proxy |
|
|
| import ( |
| "encoding/json" |
| "fmt" |
| "log" |
| "regexp" |
| "strings" |
| ) |
|
|
| |
| |
| |
|
|
| type modelFamily int |
|
|
| const ( |
| familyAnthropic modelFamily = iota |
| familyOpenAI |
| familyGemini |
| familyOther |
| ) |
|
|
| func detectModelFamily(model string) modelFamily { |
| m := strings.ToLower(model) |
| switch { |
| case strings.HasPrefix(m, "opus") || strings.HasPrefix(m, "sonnet") || strings.HasPrefix(m, "haiku") || strings.Contains(m, "claude"): |
| return familyAnthropic |
| case strings.HasPrefix(m, "gpt") || strings.HasPrefix(m, "o1") || strings.HasPrefix(m, "o3") || strings.HasPrefix(m, "o4"): |
| return familyOpenAI |
| case strings.HasPrefix(m, "gemini"): |
| return familyGemini |
| default: |
| return familyOther |
| } |
| } |
|
|
| |
| |
| |
|
|
| |
| func buildAnthropicToolsBlock(tools []Tool) string { |
| type anthropicTool struct { |
| Name string `json:"name"` |
| Description string `json:"description,omitempty"` |
| InputSchema interface{} `json:"input_schema"` |
| } |
| var defs []anthropicTool |
| for _, t := range tools { |
| schema := t.Function.Parameters |
| if schema == nil { |
| schema = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}} |
| } |
| defs = append(defs, anthropicTool{ |
| Name: t.Function.Name, |
| Description: t.Function.Description, |
| InputSchema: schema, |
| }) |
| } |
| data, _ := json.MarshalIndent(defs, "", " ") |
| return fmt.Sprintf("<tools>\n%s\n</tools>", string(data)) |
| } |
|
|
| |
| func buildOpenAIToolsBlock(tools []Tool) string { |
| type openaiFunc struct { |
| Name string `json:"name"` |
| Description string `json:"description,omitempty"` |
| Parameters interface{} `json:"parameters"` |
| } |
| var funcs []openaiFunc |
| for _, t := range tools { |
| params := t.Function.Parameters |
| if params == nil { |
| params = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}} |
| } |
| funcs = append(funcs, openaiFunc{ |
| Name: t.Function.Name, |
| Description: t.Function.Description, |
| Parameters: params, |
| }) |
| } |
| data, _ := json.MarshalIndent(funcs, "", " ") |
| return fmt.Sprintf("## Functions\n```json\n%s\n```", string(data)) |
| } |
|
|
| |
| func buildGeminiToolsBlock(tools []Tool) string { |
| type geminiFunc struct { |
| Name string `json:"name"` |
| Description string `json:"description,omitempty"` |
| Parameters interface{} `json:"parameters"` |
| } |
| var funcs []geminiFunc |
| for _, t := range tools { |
| params := t.Function.Parameters |
| if params == nil { |
| params = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}} |
| } |
| funcs = append(funcs, geminiFunc{ |
| Name: t.Function.Name, |
| Description: t.Function.Description, |
| Parameters: params, |
| }) |
| } |
| data, _ := json.MarshalIndent(funcs, "", " ") |
| return fmt.Sprintf("Available function declarations:\n%s", string(data)) |
| } |
|
|
| |
| |
| |
| func buildToolsBlock(tools []Tool, family modelFamily) string { |
| return buildOpenAIToolsBlock(tools) |
| } |
|
|
| |
| |
| |
|
|
| |
| func buildToolList(tools []Tool) string { |
| var sb strings.Builder |
| for _, t := range tools { |
| sb.WriteString(fmt.Sprintf("Function: %s", t.Function.Name)) |
| if t.Function.Description != "" { |
| sb.WriteString(fmt.Sprintf(" - %s", t.Function.Description)) |
| } |
| if t.Function.Parameters != nil { |
| params, _ := json.Marshal(t.Function.Parameters) |
| sb.WriteString(fmt.Sprintf("\nParameters: %s", string(params))) |
| } |
| sb.WriteString("\n") |
| } |
| return sb.String() |
| } |
|
|
| |
| |
| |
| func buildCompactToolList(tools []Tool) string { |
| var sb strings.Builder |
| for _, t := range tools { |
| sb.WriteString(fmt.Sprintf("- %s", t.Function.Name)) |
| |
| if t.Function.Parameters != nil { |
| paramNames := extractParamSignature(t.Function.Parameters) |
| if paramNames != "" { |
| sb.WriteString(fmt.Sprintf("(%s)", paramNames)) |
| } |
| } |
| if t.Function.Description != "" { |
| desc := t.Function.Description |
| if len(desc) > 80 { |
| desc = desc[:80] + "..." |
| } |
| sb.WriteString(fmt.Sprintf(" — %s", desc)) |
| } |
| sb.WriteString("\n") |
| } |
| return sb.String() |
| } |
|
|
| |
| |
| |
| func extractParamSignature(schema interface{}) string { |
| obj, ok := schema.(map[string]interface{}) |
| if !ok { |
| return "" |
| } |
| props, ok := obj["properties"].(map[string]interface{}) |
| if !ok { |
| return "" |
| } |
| |
| requiredSet := map[string]bool{} |
| if req, ok := obj["required"].([]interface{}); ok { |
| for _, r := range req { |
| if s, ok := r.(string); ok { |
| requiredSet[s] = true |
| } |
| } |
| } |
| var parts []string |
| for name, v := range props { |
| typeName := "any" |
| if pm, ok := v.(map[string]interface{}); ok { |
| if t, ok := pm["type"].(string); ok { |
| switch t { |
| case "string": |
| typeName = "str" |
| case "integer": |
| typeName = "int" |
| case "number": |
| typeName = "num" |
| case "boolean": |
| typeName = "bool" |
| case "array": |
| typeName = "arr" |
| case "object": |
| typeName = "obj" |
| default: |
| typeName = t |
| } |
| } |
| } |
| if requiredSet[name] { |
| parts = append(parts, fmt.Sprintf("%s: %s", name, typeName)) |
| } else { |
| parts = append(parts, fmt.Sprintf("%s?: %s", name, typeName)) |
| } |
| } |
| return strings.Join(parts, ", ") |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| var coreToolNames = map[string]bool{ |
| "Bash": true, "Read": true, "Edit": true, "Write": true, |
| "Glob": true, "Grep": true, "WebSearch": true, |
| |
| |
| |
| } |
|
|
| |
| |
| var nativeSearchToolNames = map[string]bool{ |
| "WebSearch": true, "WebFetch": true, |
| } |
|
|
| |
| |
| |
| |
| func filterNativeSearchTools(tools []Tool) ([]Tool, bool) { |
| var filtered []Tool |
| hasWebSearch := false |
| for _, t := range tools { |
| switch t.Function.Name { |
| case "WebFetch": |
| |
| continue |
| case "WebSearch": |
| hasWebSearch = true |
| } |
| filtered = append(filtered, t) |
| } |
| return filtered, hasWebSearch |
| } |
|
|
| |
| |
| |
| func stripWebSearchHistory(messages []ChatMessage) []ChatMessage { |
| |
| webSearchIDs := map[string]bool{} |
| for _, m := range messages { |
| if m.Role == "assistant" { |
| for _, tc := range m.ToolCalls { |
| if nativeSearchToolNames[tc.Function.Name] { |
| webSearchIDs[tc.ID] = true |
| } |
| } |
| } |
| } |
| if len(webSearchIDs) == 0 { |
| return messages |
| } |
|
|
| var result []ChatMessage |
| for _, m := range messages { |
| switch m.Role { |
| case "assistant": |
| |
| var keptCalls []ToolCall |
| for _, tc := range m.ToolCalls { |
| if !nativeSearchToolNames[tc.Function.Name] { |
| keptCalls = append(keptCalls, tc) |
| } |
| } |
| |
| if m.Content != "" || len(keptCalls) > 0 { |
| newMsg := m |
| newMsg.ToolCalls = keptCalls |
| result = append(result, newMsg) |
| } |
| case "tool": |
| |
| if webSearchIDs[m.ToolCallID] || nativeSearchToolNames[m.Name] { |
| log.Printf("[bridge] stripped WebSearch tool_result (id=%s name=%s)", m.ToolCallID, m.Name) |
| continue |
| } |
| result = append(result, m) |
| default: |
| result = append(result, m) |
| } |
| } |
|
|
| if stripped := len(messages) - len(result); stripped > 0 { |
| log.Printf("[bridge] stripped %d WebSearch-related messages from history", stripped) |
| } |
| return result |
| } |
|
|
| |
| func filterCoreTools(tools []Tool) []Tool { |
| var core []Tool |
| for _, t := range tools { |
| if coreToolNames[t.Function.Name] { |
| core = append(core, t) |
| } |
| } |
| if len(core) == 0 { |
| return tools |
| } |
| return core |
| } |
|
|
| |
| |
| |
| const bridgeSystemPrompt = `The user has configured the following output behavior: |
| When available functions are listed and a request matches, output the function call as JSON: {"name": "function_name", "arguments": {...}} |
| For multiple calls, output one JSON per line. If no function matches, respond to the request normally.` |
|
|
| |
| |
| |
| func sanitizeForBridge(messages []ChatMessage) []ChatMessage { |
| result := make([]ChatMessage, 0, len(messages)) |
| bridgeInserted := false |
|
|
| for i, msg := range messages { |
| switch msg.Role { |
| case "system": |
| if !bridgeInserted { |
| result = append(result, ChatMessage{ |
| Role: "system", |
| Content: bridgeSystemPrompt, |
| }) |
| bridgeInserted = true |
| log.Printf("[bridge] [%d] replaced system prompt (%d chars → %d chars)", i, len(msg.Content), len(bridgeSystemPrompt)) |
| } else { |
| log.Printf("[bridge] [%d] dropped extra system message (%d chars)", i, len(msg.Content)) |
| } |
| case "user": |
| cleaned := stripSystemReminders(msg.Content) |
| if strings.TrimSpace(cleaned) == "" { |
| cleaned = "Hello" |
| } |
| if len(cleaned) != len(msg.Content) { |
| log.Printf("[bridge] [%d] sanitized user message (%d → %d chars)", i, len(msg.Content), len(cleaned)) |
| } |
| newMsg := msg |
| newMsg.Content = cleaned |
| result = append(result, newMsg) |
| default: |
| result = append(result, msg) |
| } |
| } |
|
|
| if !bridgeInserted { |
| result = append([]ChatMessage{{ |
| Role: "system", |
| Content: bridgeSystemPrompt, |
| }}, result...) |
| log.Printf("[bridge] prepended bridge system prompt (no system message found)") |
| } |
|
|
| return result |
| } |
|
|
| |
| |
| |
| |
| |
| var ( |
| blockTagRegex = regexp.MustCompile(`(?s)<(?:system-reminder|local-command-caveat)>.*?</(?:system-reminder|local-command-caveat)>`) |
| inlineTagRegex = regexp.MustCompile(`<[a-z][-a-z]*>[^<]*</[a-z][-a-z]*>`) |
| ) |
|
|
| func stripSystemReminders(content string) string { |
| content = blockTagRegex.ReplaceAllString(content, "") |
| content = inlineTagRegex.ReplaceAllString(content, "") |
| return strings.TrimSpace(content) |
| } |
|
|
| |
| |
| func isSuggestionMode(content string) bool { |
| return strings.HasPrefix(strings.TrimSpace(content), "[SUGGESTION MODE:") |
| } |
|
|
| |
| |
| |
| func injectToolsIntoMessages(messages []ChatMessage, tools []Tool, model string, session *Session, toolChoice ...interface{}) []ChatMessage { |
| if len(tools) == 0 { |
| return messages |
| } |
|
|
| |
| |
| if detectModelFamily(model) != familyAnthropic { |
| log.Printf("[tool] model %s is not Claude — tools stripped, passing through as plain chat", model) |
| return messages |
| } |
|
|
| result := make([]ChatMessage, 0, len(messages)+1) |
|
|
| |
| toolChoiceMode := "auto" |
| if len(toolChoice) > 0 && toolChoice[0] != nil { |
| switch v := toolChoice[0].(type) { |
| case string: |
| toolChoiceMode = v |
| case map[string]interface{}: |
| |
| if fn, ok := v["function"].(map[string]interface{}); ok { |
| if name, ok := fn["name"].(string); ok { |
| toolChoiceMode = "force:" + name |
| } |
| } |
| |
| if t, ok := v["type"].(string); ok { |
| switch t { |
| case "any": |
| toolChoiceMode = "required" |
| case "tool": |
| if name, ok := v["name"].(string); ok { |
| toolChoiceMode = "force:" + name |
| } |
| case "auto": |
| toolChoiceMode = "auto" |
| } |
| } |
| } |
| } |
|
|
| toolList := buildToolList(tools) |
|
|
| |
| toolCallIDMap := make(map[string]string) |
| for _, msg := range messages { |
| if msg.Role == "assistant" && len(msg.ToolCalls) > 0 { |
| for _, tc := range msg.ToolCalls { |
| if tc.ID != "" && tc.Function.Name != "" { |
| toolCallIDMap[tc.ID] = tc.Function.Name |
| } |
| } |
| } |
| } |
|
|
| |
| lastUserIdx := -1 |
| for i := len(messages) - 1; i >= 0; i-- { |
| if messages[i].Role == "user" && messages[i].ToolCallID == "" { |
| lastUserIdx = i |
| break |
| } |
| } |
|
|
| |
| var formatInstruction string |
| if toolChoiceMode == "none" { |
| |
| return messages |
| } |
|
|
| |
| |
| family := detectModelFamily(model) |
| isAdvancedAnthropic := family == familyAnthropic && !strings.Contains(strings.ToLower(model), "haiku") |
|
|
| |
| |
| |
| |
| useLargeToolSet := len(tools) > 5 |
|
|
| |
| var chainCompactList string |
|
|
| if useLargeToolSet { |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| for i := range messages { |
| if messages[i].Role == "user" || messages[i].Role == "tool" { |
| orig := messages[i].Content |
| cleaned := stripSystemReminders(orig) |
| if len(cleaned) != len(orig) { |
| log.Printf("[bridge] [%d] sanitized user message (%d → %d chars)", i, len(orig), len(cleaned)) |
| } |
| messages[i].Content = cleaned |
| } |
| } |
|
|
| |
| |
| var extractedCwd string |
| cwdRe := regexp.MustCompile(`<cwd>([^<]+)</cwd>`) |
|
|
| |
| |
| var filtered []ChatMessage |
| for _, m := range messages { |
| if m.Role == "system" { |
| if match := cwdRe.FindStringSubmatch(m.Content); len(match) >= 2 { |
| extractedCwd = match[1] |
| log.Printf("[bridge] extracted CWD from system prompt: %s", extractedCwd) |
| } |
| log.Printf("[bridge] dropped system message (%d chars)", len(m.Content)) |
| } else if m.Role == "user" && strings.TrimSpace(m.Content) == "" && m.ToolCallID == "" && len(m.ToolCalls) == 0 { |
| log.Printf("[bridge] dropped empty wrapper-only user message after sanitization") |
| } else { |
| filtered = append(filtered, m) |
| } |
| } |
| messages = filtered |
|
|
| |
| lastUserIdx = -1 |
| for i := len(messages) - 1; i >= 0; i-- { |
| if messages[i].Role == "user" && messages[i].ToolCallID == "" { |
| lastUserIdx = i |
| break |
| } |
| } |
|
|
| |
| if lastUserIdx >= 0 && isSuggestionMode(messages[lastUserIdx].Content) { |
| log.Printf("[bridge] SUGGESTION MODE detected — skipping tool injection") |
| return messages |
| } |
|
|
| |
| |
| coreTools := filterCoreTools(tools) |
| compactList := buildCompactToolList(coreTools) |
| chainCompactList = compactList |
| if lastUserIdx >= 0 { |
| } |
| log.Printf("[bridge] large tool set: %d→%d core tools, compact %d chars", |
| len(tools), len(coreTools), len(compactList)) |
|
|
| |
| |
| |
| isChainContinuation := len(messages) > 0 && messages[len(messages)-1].Role == "tool" |
| if isChainContinuation { |
| |
| |
| |
| |
| |
| if session != nil && session.TurnCount > 0 { |
| return buildSessionChainFollowUp(messages, compactList, extractedCwd) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| tcMap := make(map[string]string) |
| for _, m := range messages { |
| for _, tc := range m.ToolCalls { |
| tcMap[tc.ID] = tc.Function.Name |
| } |
| } |
| resolveName := func(m ChatMessage) string { |
| if m.Name != "" { |
| return m.Name |
| } |
| if m.ToolCallID != "" { |
| if n, ok := tcMap[m.ToolCallID]; ok { |
| return n |
| } |
| } |
| return "tool" |
| } |
| |
| var userQuery string |
| userQueryIdx := -1 |
| for i := len(messages) - 1; i >= 0; i-- { |
| if messages[i].Role == "user" && messages[i].ToolCallID == "" { |
| userQuery = messages[i].Content |
| userQueryIdx = i |
| break |
| } |
| } |
| |
| |
| var lastRoundResults strings.Builder |
| var prevRoundSummary strings.Builder |
| needsReadNarrowing := false |
| |
| lastAssistantIdx := -1 |
| for i := len(messages) - 1; i >= 0; i-- { |
| if messages[i].Role == "assistant" && i > userQueryIdx { |
| lastAssistantIdx = i |
| break |
| } |
| } |
| for i, m := range messages { |
| if m.Role != "tool" || i <= userQueryIdx { |
| continue |
| } |
| name := resolveName(m) |
| if i > lastAssistantIdx && lastAssistantIdx >= 0 { |
| |
| content := m.Content |
| if name == "Read" && strings.Contains(content, "exceeds maximum allowed tokens") { |
| needsReadNarrowing = true |
| } |
| if len(content) > 800 { |
| content = content[:800] + "..." |
| } |
| if lastRoundResults.Len() > 0 { |
| lastRoundResults.WriteString("\n") |
| } |
| lastRoundResults.WriteString(fmt.Sprintf("[%s]: %s", name, content)) |
| } else { |
| |
| status := "ok" |
| if strings.Contains(m.Content, "error") || strings.Contains(m.Content, "Error") { |
| status = "error" |
| } |
| if prevRoundSummary.Len() > 0 { |
| prevRoundSummary.WriteString(", ") |
| } |
| prevRoundSummary.WriteString(fmt.Sprintf("%s(%s)", name, status)) |
| } |
| } |
| |
| var dataStr string |
| if prevRoundSummary.Len() > 0 { |
| dataStr = fmt.Sprintf("Done so far: %s\nLatest:\n%s", prevRoundSummary.String(), lastRoundResults.String()) |
| } else { |
| dataStr = lastRoundResults.String() |
| } |
| cwdLine := "" |
| if extractedCwd != "" { |
| cwdLine = fmt.Sprintf("Working directory: %s\n", extractedCwd) |
| } |
| readGuardLine := "" |
| if needsReadNarrowing { |
| readGuardLine = "The previous Read call was too large. Do NOT repeat the same full-file Read. Use Grep to narrow scope or call Read with both offset and limit.\n" |
| } |
| collapsed := fmt.Sprintf( |
| "I'm writing a unit test for an API router.\n%s%sAvailable functions:\n%s- __done__(result: str) — call when no more steps needed\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\n\nAlready executed (do NOT re-run):\n%s\n\nInput: \"%s\"\n\nIf the results above answer the input, output: {\"name\": \"__done__\", \"arguments\": {\"result\": \"natural language answer based on results\"}}\nOtherwise output the JSON for the NEXT DIFFERENT function call.", |
| cwdLine, readGuardLine, compactList, dataStr, userQuery) |
| log.Printf("[bridge] chain: collapsed %d messages to single message (%d chars)", len(messages), len(collapsed)) |
| return []ChatMessage{{Role: "user", Content: collapsed}} |
| } |
|
|
| |
| |
| if lastUserIdx >= 0 { |
| userQuery := messages[lastUserIdx].Content |
|
|
| |
| |
| |
| |
| var prevSearchContext string |
| for i := len(messages) - 1; i >= 0; i-- { |
| m := messages[i] |
| if m.Role == "assistant" && strings.Contains(m.Content, "---\nSources:") { |
| ctx := m.Content |
| if len(ctx) > 600 { |
| ctx = ctx[:600] + "..." |
| } |
| prevSearchContext = ctx |
| break |
| } |
| } |
|
|
| cwdLine := "" |
| if extractedCwd != "" { |
| cwdLine = fmt.Sprintf("Working directory: %s\n", extractedCwd) |
| } |
|
|
| if strings.HasPrefix(toolChoiceMode, "force:") { |
| forcedName := strings.TrimPrefix(toolChoiceMode, "force:") |
| messages[lastUserIdx].Content = fmt.Sprintf( |
| "I'm writing a unit test. Generate the expected JSON output.\n%sFunction: %s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.\n\nInput: \"%s\"", |
| cwdLine, forcedName, userQuery) |
| } else if toolChoiceMode == "required" { |
| messages[lastUserIdx].Content = fmt.Sprintf( |
| "I'm writing a unit test for an API router. Generate the expected JSON output.\n%sAvailable functions:\n%s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.\n\nInput: \"%s\"", |
| cwdLine, compactList, userQuery) |
| } else if prevSearchContext != "" { |
| |
| |
| messages[lastUserIdx].Content = fmt.Sprintf( |
| "I'm writing a unit test for an API router. Generate the expected JSON output for this test case.\n%sAvailable functions:\n%s- __done__(result: str) — answer the question using context below, or call WebSearch for new topics\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nAlways output exactly one JSON object.\n\nPrevious search results:\n%s\n\nInput: \"%s\"", |
| cwdLine, compactList, prevSearchContext, userQuery) |
| log.Printf("[bridge] included previous search context (%d chars) in framing", len(prevSearchContext)) |
| } else { |
| messages[lastUserIdx].Content = fmt.Sprintf( |
| "I'm writing a unit test for an API router. Generate the expected JSON output for this test case.\n%sAvailable functions:\n%s- __done__(result: str) — respond naturally to the user's message\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nAlways output exactly one JSON object.\n\nInput: \"%s\"", |
| cwdLine, compactList, userQuery) |
| } |
| log.Printf("[bridge] embedded query in unit test framing (%d chars)", len(messages[lastUserIdx].Content)) |
| } |
|
|
| |
| formatInstruction = "" |
| } else if isAdvancedAnthropic { |
| |
| if strings.HasPrefix(toolChoiceMode, "force:") { |
| forcedName := strings.TrimPrefix(toolChoiceMode, "force:") |
| formatInstruction = fmt.Sprintf("\n\nI'm writing a unit test. Generate the expected JSON output.\nFunction: %s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.", forcedName) |
| } else if toolChoiceMode == "required" { |
| formatInstruction = fmt.Sprintf("\n\nI'm writing a unit test for an API router. Generate the expected JSON output.\nAvailable functions:\n%s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.", toolList) |
| } else { |
| formatInstruction = fmt.Sprintf("\n\nI'm writing a unit test for an API router. Generate the expected JSON output.\nAvailable functions:\n%s\n__done__(result: str) — respond naturally to the user's message\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nAlways output exactly one JSON object.", toolList) |
| } |
| } else { |
| |
| if strings.HasPrefix(toolChoiceMode, "force:") { |
| forcedName := strings.TrimPrefix(toolChoiceMode, "force:") |
| formatInstruction = fmt.Sprintf("\n\nTranslate this request into a JSON function call.\nFunction to use: %s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.", forcedName) |
| } else if toolChoiceMode == "required" { |
| formatInstruction = fmt.Sprintf("\n\nTranslate this request into a JSON function call using one of these available functions:\n%s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nOutput only the JSON.", toolList) |
| } else { |
| formatInstruction = fmt.Sprintf("\n\nTranslate this request into a JSON function call if it matches one of these available functions:\n%s\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nIf a function matches, output only the JSON. Otherwise, respond normally.", toolList) |
| } |
| } |
|
|
| |
| resolveToolName := func(m ChatMessage) string { |
| if m.Name != "" { |
| return m.Name |
| } |
| if m.ToolCallID != "" { |
| if name, ok := toolCallIDMap[m.ToolCallID]; ok { |
| return name |
| } |
| } |
| return "unknown_tool" |
| } |
|
|
| |
| var pendingToolResults strings.Builder |
|
|
| |
| for i := 0; i < len(messages); i++ { |
| msg := messages[i] |
| switch msg.Role { |
| case "system": |
| result = append(result, msg) |
| case "tool": |
| if isAdvancedAnthropic { |
| |
| |
| toolName := resolveToolName(msg) |
| if pendingToolResults.Len() > 0 { |
| pendingToolResults.WriteString("\n\n") |
| } |
| pendingToolResults.WriteString(fmt.Sprintf("Results from %s:\n%s", toolName, msg.Content)) |
|
|
| |
| if i+1 < len(messages) && messages[i+1].Role == "tool" { |
| continue |
| } |
|
|
| |
| summary := pendingToolResults.String() |
| pendingToolResults.Reset() |
| lastToolSummary := summary |
|
|
| |
| |
| |
| merged := false |
| for j := len(result) - 1; j >= 0; j-- { |
| if result[j].Role == "assistant" { |
| result[j].Content = "I'll help with that.\n\n" + summary |
| merged = true |
| break |
| } |
| } |
| if !merged { |
| |
| if i+1 >= len(messages) { |
| var fallbackContent string |
| if chainCompactList != "" { |
| fallbackContent = fmt.Sprintf( |
| "Output:\n%s\n\nContinue. Available:\n%s\nFormat: {\"name\": \"function_name\", \"arguments\": {...}}", |
| summary, chainCompactList) |
| log.Printf("[bridge] chain: re-injected tool list in !merged follow-up (%d chars)", len(fallbackContent)) |
| } else { |
| fallbackContent = summary + "\n\nPlease summarize these results." |
| } |
| result = append(result, ChatMessage{ |
| Role: "user", |
| Content: fallbackContent, |
| }) |
| } |
| } else if i+1 >= len(messages) { |
| |
| var followUp string |
| if chainCompactList != "" { |
| followUp = fmt.Sprintf( |
| "Output:\n%s\n\nContinue. Available:\n%s\nFormat: {\"name\": \"function_name\", \"arguments\": {...}}", |
| lastToolSummary, chainCompactList) |
| log.Printf("[bridge] chain: re-injected tool list in follow-up (%d chars)", len(followUp)) |
| } else { |
| followUp = "Here is the output:\n\n" + lastToolSummary + "\n\nPresent this as a clean, concise summary." |
| } |
| result = append(result, ChatMessage{ |
| Role: "user", |
| Content: followUp, |
| }) |
| } |
| } else { |
| |
| toolName := resolveToolName(msg) |
| if pendingToolResults.Len() > 0 { |
| pendingToolResults.WriteString("\n\n") |
| } |
| pendingToolResults.WriteString(fmt.Sprintf("[Data from %s]:\n%s", toolName, msg.Content)) |
| if i+1 >= len(messages) { |
| var haikuFollowUp string |
| if chainCompactList != "" { |
| haikuFollowUp = fmt.Sprintf( |
| "Output:\n%s\n\nContinue. Available:\n%s\nFormat: {\"name\": \"function_name\", \"arguments\": {...}}", |
| pendingToolResults.String(), chainCompactList) |
| log.Printf("[bridge] chain(haiku): re-injected tool list in follow-up") |
| } else { |
| haikuFollowUp = pendingToolResults.String() + "\n\nPlease summarize these results." |
| } |
| result = append(result, ChatMessage{ |
| Role: "user", |
| Content: haikuFollowUp, |
| }) |
| pendingToolResults.Reset() |
| } |
| } |
| case "assistant": |
| if len(msg.ToolCalls) > 0 { |
| if isAdvancedAnthropic { |
| |
| var content strings.Builder |
| if msg.Content != "" { |
| content.WriteString(msg.Content) |
| } else { |
| content.WriteString("I'll help with that.") |
| } |
| result = append(result, ChatMessage{ |
| Role: "assistant", |
| Content: content.String(), |
| }) |
| } else { |
| |
| var content strings.Builder |
| if msg.Content != "" { |
| content.WriteString(msg.Content) |
| content.WriteString("\n") |
| } |
| for _, tc := range msg.ToolCalls { |
| call := map[string]interface{}{ |
| "name": tc.Function.Name, |
| "arguments": json.RawMessage(tc.Function.Arguments), |
| } |
| data, _ := json.Marshal(call) |
| content.WriteString("```json\n") |
| content.Write(data) |
| content.WriteString("\n```\n") |
| } |
| result = append(result, ChatMessage{ |
| Role: "assistant", |
| Content: strings.TrimSpace(content.String()), |
| }) |
| } |
| } else { |
| result = append(result, msg) |
| } |
| case "user": |
| var userContent string |
| if pendingToolResults.Len() > 0 { |
| userContent = pendingToolResults.String() + "\n\n" + msg.Content |
| pendingToolResults.Reset() |
| } else { |
| userContent = msg.Content |
| } |
| if i == lastUserIdx { |
| userContent += formatInstruction |
| } |
| result = append(result, ChatMessage{ |
| Role: "user", |
| Content: userContent, |
| }) |
| default: |
| result = append(result, msg) |
| } |
| } |
|
|
| return result |
| } |
|
|
| |
| |
| |
| |
| |
| func buildSessionChainFollowUp(messages []ChatMessage, compactList string, cwd string) []ChatMessage { |
| |
| tcMap := make(map[string]string) |
| for _, m := range messages { |
| for _, tc := range m.ToolCalls { |
| tcMap[tc.ID] = tc.Function.Name |
| } |
| } |
| resolveName := func(m ChatMessage) string { |
| if m.Name != "" { |
| return m.Name |
| } |
| if m.ToolCallID != "" { |
| if n, ok := tcMap[m.ToolCallID]; ok { |
| return n |
| } |
| } |
| return "tool" |
| } |
|
|
| |
| lastAssistantIdx := -1 |
| for i := len(messages) - 1; i >= 0; i-- { |
| if messages[i].Role == "assistant" { |
| lastAssistantIdx = i |
| break |
| } |
| } |
|
|
| |
| var results strings.Builder |
| resultCount := 0 |
| needsReadNarrowing := false |
| for i, m := range messages { |
| if m.Role != "tool" || i <= lastAssistantIdx { |
| continue |
| } |
| name := resolveName(m) |
| content := m.Content |
| if name == "Read" && strings.Contains(content, "exceeds maximum allowed tokens") { |
| needsReadNarrowing = true |
| } |
| if len(content) > 4000 { |
| content = content[:4000] + "\n... (truncated)" |
| } |
| if results.Len() > 0 { |
| results.WriteString("\n") |
| } |
| results.WriteString(fmt.Sprintf("[%s]: %s", name, content)) |
| resultCount++ |
| } |
|
|
| cwdLine := "" |
| if cwd != "" { |
| cwdLine = fmt.Sprintf("Working directory: %s\n", cwd) |
| } |
| readGuardLine := "" |
| if needsReadNarrowing { |
| readGuardLine = "The previous Read call was too large. Do NOT repeat the same full-file Read. Use Grep to narrow scope or call Read with both offset and limit.\n" |
| } |
|
|
| followUp := fmt.Sprintf( |
| "Results from executed function(s):\n%s\n\n%s%sAvailable functions:\n%s- __done__(result: str) — call when no more steps needed\nOutput format: {\"name\": \"function_name\", \"arguments\": {...}}\nIf these results answer the question, use __done__. Otherwise output the next function call.", |
| results.String(), cwdLine, readGuardLine, compactList) |
|
|
| log.Printf("[bridge] session chain: follow-up for partial transcript (%d chars, %d tool results)", |
| len(followUp), resultCount) |
|
|
| return []ChatMessage{{Role: "user", Content: followUp}} |
| } |
|
|
| |
| |
| |
|
|
| |
| func nativeToolUseToOpenAI(entries []AgentValueEntry) []ToolCall { |
| var calls []ToolCall |
| for i, e := range entries { |
| if e.Type != "tool_use" || e.Name == "" { |
| continue |
| } |
| argsStr := "{}" |
| if len(e.Input) > 0 && json.Valid(e.Input) { |
| argsStr = string(e.Input) |
| } |
| calls = append(calls, ToolCall{ |
| ID: e.ID, |
| Type: "function", |
| Function: ToolCallFunction{ |
| Name: e.Name, |
| Arguments: argsStr, |
| }, |
| }) |
| _ = i |
| } |
| return calls |
| } |
|
|
| |
| var toolCallXMLRegex = regexp.MustCompile(`(?s)<tool_call>\s*(\{.*?\})\s*</tool_call>`) |
| var mdFenceRegex = regexp.MustCompile("(?s)```(?:json|tool_call)?\\s*\\n?(.*?)\\n?```") |
| var jsonToolCallRegex = regexp.MustCompile(`(?s)\{"tool_call"\s*:\s*(\{.*?\})\s*\}`) |
|
|
| |
| |
| func parseToolCalls(content string) ([]ToolCall, string, bool) { |
| var toolCalls []ToolCall |
| remaining := content |
|
|
| |
| xmlMatches := toolCallXMLRegex.FindAllStringSubmatch(content, -1) |
| for i, match := range xmlMatches { |
| remaining = strings.Replace(remaining, match[0], "", 1) |
| tc := parseToolCallJSON(match[1], i) |
| if tc != nil { |
| toolCalls = append(toolCalls, *tc) |
| } |
| } |
| if len(toolCalls) > 0 { |
| return toolCalls, strings.TrimSpace(remaining), true |
| } |
|
|
| |
| remaining = content |
| mdMatches := mdFenceRegex.FindAllStringSubmatch(content, -1) |
| for i, match := range mdMatches { |
| fenced := strings.TrimSpace(match[1]) |
| tc := parseToolCallJSON(fenced, i) |
| if tc != nil { |
| toolCalls = append(toolCalls, *tc) |
| remaining = strings.Replace(remaining, match[0], "", 1) |
| } |
| } |
| if len(toolCalls) > 0 { |
| return toolCalls, strings.TrimSpace(remaining), true |
| } |
|
|
| |
| remaining = content |
| stripped := strings.TrimSpace(content) |
|
|
| |
| var direct struct { |
| Name string `json:"name"` |
| Arguments json.RawMessage `json:"arguments"` |
| } |
| if err := json.Unmarshal([]byte(stripped), &direct); err == nil && direct.Name != "" { |
| argsStr := string(direct.Arguments) |
| if !json.Valid(direct.Arguments) { |
| argsStr = "{}" |
| } |
| toolCalls = append(toolCalls, ToolCall{ |
| ID: fmt.Sprintf("call_0_%s", generateUUIDv4()[:8]), |
| Type: "function", |
| Function: ToolCallFunction{ |
| Name: direct.Name, |
| Arguments: argsStr, |
| }, |
| }) |
| return toolCalls, "", true |
| } |
|
|
| |
| var wrapper struct { |
| ToolCall *struct { |
| Name string `json:"name"` |
| Arguments json.RawMessage `json:"arguments"` |
| } `json:"tool_call"` |
| } |
| if err := json.Unmarshal([]byte(stripped), &wrapper); err == nil && wrapper.ToolCall != nil { |
| argsStr := string(wrapper.ToolCall.Arguments) |
| if !json.Valid(wrapper.ToolCall.Arguments) { |
| argsStr = "{}" |
| } |
| toolCalls = append(toolCalls, ToolCall{ |
| ID: fmt.Sprintf("call_0_%s", generateUUIDv4()[:8]), |
| Type: "function", |
| Function: ToolCallFunction{ |
| Name: wrapper.ToolCall.Name, |
| Arguments: argsStr, |
| }, |
| }) |
| return toolCalls, "", true |
| } |
|
|
| |
| |
| lines := strings.Split(stripped, "\n") |
| var multiCalls []ToolCall |
| var nonToolLines []string |
| for _, line := range lines { |
| line = strings.TrimSpace(line) |
| if line == "" { |
| continue |
| } |
| var lineCall struct { |
| Name string `json:"name"` |
| Arguments json.RawMessage `json:"arguments"` |
| } |
| if err := json.Unmarshal([]byte(line), &lineCall); err == nil && lineCall.Name != "" { |
| argsStr := string(lineCall.Arguments) |
| if !json.Valid(lineCall.Arguments) { |
| argsStr = "{}" |
| } |
| multiCalls = append(multiCalls, ToolCall{ |
| ID: fmt.Sprintf("call_%d_%s", len(multiCalls), generateUUIDv4()[:8]), |
| Type: "function", |
| Function: ToolCallFunction{ |
| Name: lineCall.Name, |
| Arguments: argsStr, |
| }, |
| }) |
| } else { |
| nonToolLines = append(nonToolLines, line) |
| } |
| } |
| if len(multiCalls) > 0 { |
| return multiCalls, strings.TrimSpace(strings.Join(nonToolLines, "\n")), true |
| } |
|
|
| return nil, content, false |
| } |
|
|
| func parseToolCallJSON(jsonStr string, index int) *ToolCall { |
| var call struct { |
| Name string `json:"name"` |
| Arguments json.RawMessage `json:"arguments"` |
| } |
| if err := json.Unmarshal([]byte(jsonStr), &call); err != nil { |
| return nil |
| } |
| argsStr := string(call.Arguments) |
| if !json.Valid(call.Arguments) { |
| argsStr = "{}" |
| } |
| return &ToolCall{ |
| ID: fmt.Sprintf("call_%d_%s", index, generateUUIDv4()[:8]), |
| Type: "function", |
| Function: ToolCallFunction{ |
| Name: call.Name, |
| Arguments: argsStr, |
| }, |
| } |
| } |
|
|