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)
- }
-}