diff --git a/internal/adapter/claude/handler_util_test.go b/internal/adapter/claude/handler_util_test.go index 3b45d61..82302f0 100644 --- a/internal/adapter/claude/handler_util_test.go +++ b/internal/adapter/claude/handler_util_test.go @@ -319,8 +319,7 @@ func TestBuildClaudeToolPromptSupportsOpenAIStyleFunctionTool(t *testing.T) { "name": "search", "description": "Search via function tool", "parameters": map[string]any{ - "type": "object", - "required": []any{"q"}, + "type": "object", "properties": map[string]any{ "q": map[string]any{"type": "string"}, }, @@ -335,8 +334,8 @@ func TestBuildClaudeToolPromptSupportsOpenAIStyleFunctionTool(t *testing.T) { if !containsStr(prompt, "Search via function tool") { t.Fatalf("expected OpenAI-style function tool description in prompt, got: %q", prompt) } - if !containsStr(prompt, "MUST INCLUDE: q") { - t.Fatalf("expected required-field summary in prompt, got: %q", prompt) + if !containsStr(prompt, "\"q\"") { + t.Fatalf("expected parameters schema serialized in prompt, got: %q", prompt) } } diff --git a/internal/adapter/claude/handler_utils.go b/internal/adapter/claude/handler_utils.go index 6b3a601..fef1194 100644 --- a/internal/adapter/claude/handler_utils.go +++ b/internal/adapter/claude/handler_utils.go @@ -90,7 +90,8 @@ func buildClaudeToolPrompt(tools []any) string { continue } names = append(names, name) - toolSchemas = append(toolSchemas, util.FormatToolSchemaAttentionBlock(name, desc, schemaObj)) + schema, _ := json.Marshal(schemaObj) + toolSchemas = append(toolSchemas, fmt.Sprintf("Tool: %s\nDescription: %s\nParameters: %s", name, desc, schema)) } if len(toolSchemas) == 0 { return "" diff --git a/internal/adapter/openai/handler_toolcall_format.go b/internal/adapter/openai/handler_toolcall_format.go index 5942d69..c11a3c7 100644 --- a/internal/adapter/openai/handler_toolcall_format.go +++ b/internal/adapter/openai/handler_toolcall_format.go @@ -2,6 +2,7 @@ package openai import ( "encoding/json" + "fmt" "strings" "github.com/google/uuid" @@ -43,7 +44,11 @@ func injectToolPrompt(messages []map[string]any, tools []any, policy util.ToolCh continue } names = append(names, name) - toolSchemas = append(toolSchemas, util.FormatToolSchemaAttentionBlock(name, desc, schema)) + if desc == "" { + desc = "No description available" + } + b, _ := json.Marshal(schema) + toolSchemas = append(toolSchemas, fmt.Sprintf("Tool: %s\nDescription: %s\nParameters: %s", name, desc, string(b))) } if len(toolSchemas) == 0 { return messages, names diff --git a/internal/adapter/openai/prompt_build_test.go b/internal/adapter/openai/prompt_build_test.go index 7539763..223689b 100644 --- a/internal/adapter/openai/prompt_build_test.go +++ b/internal/adapter/openai/prompt_build_test.go @@ -34,11 +34,7 @@ func TestBuildOpenAIFinalPrompt_HandlerPathIncludesToolRoundtripSemantics(t *tes "name": "get_weather", "description": "Get weather", "parameters": map[string]any{ - "type": "object", - "required": []any{"city"}, - "properties": map[string]any{ - "city": map[string]any{"type": "string"}, - }, + "type": "object", }, }, }, @@ -57,9 +53,6 @@ func TestBuildOpenAIFinalPrompt_HandlerPathIncludesToolRoundtripSemantics(t *tes if !strings.Contains(finalPrompt, "get_weather") { t.Fatalf("handler finalPrompt should include tool name history: %q", finalPrompt) } - if !strings.Contains(finalPrompt, "MUST INCLUDE: city") { - t.Fatalf("handler finalPrompt should front-load required fields: %q", finalPrompt) - } } func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t *testing.T) { @@ -74,11 +67,7 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t * "name": "search", "description": "search docs", "parameters": map[string]any{ - "type": "object", - "required": []any{"query"}, - "properties": map[string]any{ - "query": map[string]any{"type": "string"}, - }, + "type": "object", }, }, }, @@ -94,9 +83,6 @@ func TestBuildOpenAIFinalPrompt_VercelPreparePathKeepsFinalAnswerInstruction(t * if !strings.Contains(finalPrompt, "TOOL CALL FORMAT") { t.Fatalf("vercel prepare finalPrompt missing xml format instruction: %q", finalPrompt) } - if !strings.Contains(finalPrompt, "MUST INCLUDE: query") { - t.Fatalf("vercel prepare finalPrompt missing required-field summary: %q", finalPrompt) - } if !strings.Contains(finalPrompt, "Do NOT wrap the XML in markdown code fences") { t.Fatalf("vercel prepare finalPrompt missing no-fence xml instruction: %q", finalPrompt) } diff --git a/internal/util/tool_prompt.go b/internal/util/tool_prompt.go index 8d9fd59..a801286 100644 --- a/internal/util/tool_prompt.go +++ b/internal/util/tool_prompt.go @@ -1,11 +1,6 @@ package util -import ( - "encoding/json" - "fmt" - "sort" - "strings" -) +import "strings" // BuildToolCallInstructions generates the unified tool-calling instruction block // used by all adapters (OpenAI, Claude, Gemini). It uses attention-optimized @@ -59,17 +54,12 @@ RULES: 6) Parameters MUST use the exact field names from the selected tool schema. 7) CRITICAL: Do NOT invent or add any extra fields (such as "_raw", "_xml"). Use ONLY the fields strictly defined in the schema. Extra fields will cause execution failure. -ATTENTION CHECKLIST BEFORE YOU EMIT A TOOL CALL: -- Read the tool block above first. -- If the tool block says MUST INCLUDE, every such field must be present. -- If any required field is missing or uncertain, ask a clarifying question instead of guessing. - ❌ WRONG — Do NOT do these: Wrong 1 — mixed text and XML: I'll read the file for you. ... Wrong 2 — describing tool calls in text: [调用 Bash] {"command": "ls"} -Wrong 3 — empty or missing required parameters: +Wrong 3 — missing wrapper: ` + ex1 + `{} Wrong 4 — extra/invented fields: {"_raw": "...", "command": "ls"} @@ -108,40 +98,6 @@ Example C — Tool with complex nested JSON parameters: Remember: Output ONLY the ... XML block when calling tools.` } -// FormatToolSchemaAttentionBlock renders a compact, attention-friendly tool -// summary for prompt injection. It front-loads required fields so the model can -// spot them before the full format rules and examples. -func FormatToolSchemaAttentionBlock(name, description string, schema any) string { - lines := make([]string, 0, 4) - - name = strings.TrimSpace(name) - if name != "" { - lines = append(lines, "Tool: "+name) - } - - description = strings.TrimSpace(description) - if description != "" { - lines = append(lines, "Description: "+description) - } - - required, optional := summarizeToolSchemaFields(schema) - switch { - case len(required) > 0: - lines = append(lines, "MUST INCLUDE: "+strings.Join(required, ", ")) - if len(optional) > 0 { - lines = append(lines, "OPTIONAL: "+strings.Join(optional, ", ")) - } - case len(optional) > 0: - lines = append(lines, "FIELDS: "+strings.Join(optional, ", ")) - case schema != nil: - if b, err := json.Marshal(schema); err == nil && len(b) > 0 { - lines = append(lines, "Schema: "+string(b)) - } - } - - return strings.TrimSpace(strings.Join(lines, "\n")) -} - func matchAny(name string, candidates ...string) bool { for _, c := range candidates { if name == c { @@ -185,75 +141,3 @@ func exampleInteractiveParams(name string) string { return `{"question":"Which approach do you prefer?","follow_up":[{"text":"Option A"},{"text":"Option B"}]}` } } - -func summarizeToolSchemaFields(schema any) (required []string, optional []string) { - obj, ok := schema.(map[string]any) - if !ok || len(obj) == 0 { - return nil, nil - } - - requiredSet := map[string]struct{}{} - for _, name := range anySliceToStrings(obj["required"]) { - requiredSet[name] = struct{}{} - } - - propNames := map[string]struct{}{} - if props, ok := obj["properties"].(map[string]any); ok { - for k := range props { - name := strings.TrimSpace(k) - if name == "" { - continue - } - propNames[name] = struct{}{} - } - } - - required = make([]string, 0, len(requiredSet)) - for name := range requiredSet { - required = append(required, name) - } - sort.Strings(required) - - if len(propNames) == 0 { - return required, nil - } - - optional = make([]string, 0, len(propNames)) - for name := range propNames { - if _, ok := requiredSet[name]; ok { - continue - } - optional = append(optional, name) - } - sort.Strings(optional) - return required, optional -} - -func anySliceToStrings(v any) []string { - switch x := v.(type) { - case []string: - out := make([]string, 0, len(x)) - for _, item := range x { - item = strings.TrimSpace(item) - if item != "" { - out = append(out, item) - } - } - return out - case []any: - out := make([]string, 0, len(x)) - for _, item := range x { - s := strings.TrimSpace(fmt.Sprintf("%v", item)) - if s != "" && s != "" { - out = append(out, s) - } - } - return out - default: - s := strings.TrimSpace(fmt.Sprintf("%v", v)) - if s == "" || s == "" { - return nil - } - return []string{s} - } -} diff --git a/internal/util/tool_prompt_test.go b/internal/util/tool_prompt_test.go index ddb90d6..e10f176 100644 --- a/internal/util/tool_prompt_test.go +++ b/internal/util/tool_prompt_test.go @@ -24,28 +24,3 @@ func TestBuildToolCallInstructions_ExecuteCommandUsesCommandExample(t *testing.T t.Fatalf("expected command parameter example for execute_command, got: %s", out) } } - -func TestFormatToolSchemaAttentionBlockPrioritizesRequiredFields(t *testing.T) { - schema := map[string]any{ - "type": "object", - "required": []any{ - "command", - }, - "properties": map[string]any{ - "command": map[string]any{"type": "string"}, - "cwd": map[string]any{"type": "string"}, - "timeout": map[string]any{"type": "integer"}, - }, - } - - out := FormatToolSchemaAttentionBlock("execute_command", "Run a command", schema) - if !strings.Contains(out, "Tool: execute_command") { - t.Fatalf("expected tool name in summary, got: %s", out) - } - if !strings.Contains(out, "MUST INCLUDE: command") { - t.Fatalf("expected required field summary, got: %s", out) - } - if !strings.Contains(out, "OPTIONAL: cwd, timeout") { - t.Fatalf("expected optional field summary, got: %s", out) - } -}