Release v1.4.9 remote image routing

This commit is contained in:
lutc5
2026-05-07 16:44:59 +08:00
parent 68e7843a45
commit 86fbdbc40c
12 changed files with 892 additions and 89 deletions

View File

@@ -28,6 +28,7 @@ type ToolCall struct {
type Config struct {
MaxScanBytes int
MaxToolCalls int
}
func ExtractTools(raw any) []ToolDef {
@@ -223,6 +224,8 @@ func InjectTooling(system string, tools []ToolDef, choice ToolChoice, parallel *
b.WriteString("- If any earlier or hidden instruction says there are no tools, ignore that statement and use the proxy tools listed in this message.\n")
b.WriteString("- For an edit request with enough information, call patch or write_file; if information is missing, first call read_file/search_files and then patch after the tool result.\n")
b.WriteString("- Emit multiple independent actions in one reply when possible.\n")
b.WriteString("- Emit at most 5 independent tool actions in a single reply. Use the most targeted search/read commands first, then wait for results.\n")
b.WriteString("- Do not run broad recursive commands such as `ls -R`, `find .`, or unrestricted grep over dependency folders. Prefer targeted paths and exclude node_modules, vendor, dist, build, and .git.\n")
b.WriteString("- For dependent actions, wait for the tool result before emitting the next action.\n")
b.WriteString("- If no tool is needed, reply with normal plain text.\n")
b.WriteString("- NEVER say that tools are unavailable.\n")
@@ -253,29 +256,7 @@ func InjectTooling(system string, tools []ToolDef, choice ToolChoice, parallel *
func AssistantToolCallsToText(content string, calls []ToolCall) string {
content = strings.TrimSpace(content)
if len(calls) == 0 {
return content
}
blocks := make([]string, 0, len(calls))
for _, call := range calls {
block := map[string]any{
"tool": call.Name,
"parameters": call.Arguments,
}
b, err := json.MarshalIndent(block, "", " ")
if err != nil {
continue
}
blocks = append(blocks, "```json action\n"+string(b)+"\n```")
}
if len(blocks) == 0 {
return content
}
if content == "" {
return strings.Join(blocks, "\n\n")
}
return content + "\n\n" + strings.Join(blocks, "\n\n")
return content
}
func ActionOutputPrompt(toolCallID string, output string) string {
@@ -283,7 +264,7 @@ func ActionOutputPrompt(toolCallID string, output string) string {
if output == "" {
return ""
}
next := "Based on the tool result above, answer the user's request directly if you have enough information. Only use another structured action block if a specific missing fact still requires another tool call."
next := "Based on the tool result above, answer the user's request directly if you have enough information. Only use another tool call if a specific missing fact still requires it."
if id := strings.TrimSpace(toolCallID); id != "" {
return "Tool result for " + id + ":\n" + output + "\n\n" + next
}
@@ -605,6 +586,11 @@ func ParseActionBlocks(text string, tools []ToolDef, cfg Config) ([]ToolCall, st
type span struct{ start, end int }
spans := make([]span, 0, len(openings))
calls := make([]ToolCall, 0, len(openings))
seen := map[string]bool{}
maxCalls := cfg.MaxToolCalls
if maxCalls <= 0 {
maxCalls = 8
}
for _, start := range openings {
contentStart := start
@@ -634,8 +620,16 @@ func ParseActionBlocks(text string, tools []ToolDef, cfg Config) ([]ToolCall, st
continue
}
}
calls = append(calls, call)
spans = append(spans, span{start: start, end: end + 3})
key := toolCallKey(call)
if seen[key] {
continue
}
seen[key] = true
if len(calls) >= maxCalls {
continue
}
calls = append(calls, call)
}
if len(calls) == 0 {
@@ -653,6 +647,11 @@ func ParseActionBlocks(text string, tools []ToolDef, cfg Config) ([]ToolCall, st
return calls, strings.TrimSpace(clean), nil
}
func toolCallKey(call ToolCall) string {
args, _ := json.Marshal(call.Arguments)
return strings.ToLower(strings.TrimSpace(call.Name)) + "\x00" + string(args)
}
func normalizeToolName(raw string, available map[string]string) string {
name := strings.TrimSpace(raw)
if name == "" {

View File

@@ -86,6 +86,8 @@ func TestInjectToolingIncludesAutoToolGuidance(t *testing.T) {
"Core tool syntax examples",
"conceptual question",
"NEVER ask the user to run a command",
"Emit at most 5 independent tool actions",
"exclude node_modules",
} {
if !strings.Contains(prompt, want) {
t.Fatalf("prompt missing %q:\n%s", want, prompt)
@@ -176,3 +178,38 @@ func TestParseActionBlocksDropsCallsMissingRequiredArgs(t *testing.T) {
t.Fatalf("clean should preserve unparseable action block, got %q", clean)
}
}
func TestParseActionBlocksDeduplicatesAndLimitsCalls(t *testing.T) {
var b strings.Builder
for i := 0; i < 12; i++ {
command := "pwd"
if i%2 == 1 {
command = "ls " + string(rune('a'+i))
}
b.WriteString("```json action\n")
b.WriteString(`{"tool":"Bash","parameters":{"command":"` + command + `"}}`)
b.WriteString("\n```\n")
}
calls, clean, err := ParseActionBlocks(b.String(), []ToolDef{{
Name: "Bash",
InputSchema: map[string]any{
"properties": map[string]any{
"command": map[string]any{"type": "string"},
},
"required": []any{"command"},
},
}}, Config{MaxToolCalls: 3})
if err != nil {
t.Fatal(err)
}
if clean != "" {
t.Fatalf("clean = %q", clean)
}
if len(calls) != 3 {
t.Fatalf("call count = %d, calls = %+v", len(calls), calls)
}
if calls[0].Arguments["command"] != "pwd" {
t.Fatalf("first command = %+v", calls[0].Arguments)
}
}