diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index f87eaab..79cafca 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -696,6 +696,60 @@ func TestSynthesizesTemplateToolCallKeepsStructuredCommandArgs(t *testing.T) { } } +func TestSynthesizesTemplateToolCallFollowsClineUnionSchemaWithShellStrings(t *testing.T) { + content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'git', 'args': ['-C', '/config/workspace/iop', 'status']}, {'command': 'pwd && ls -la /config/workspace/'}])}}" + tools := []any{map[string]any{ + "type": "function", + "function": map[string]any{ + "name": "run_commands", + "parameters": map[string]any{ + "type": "object", + "properties": map[string]any{ + "commands": map[string]any{ + "type": "array", + "items": map[string]any{ + "anyOf": []any{ + map[string]any{ + "type": "object", + "properties": map[string]any{ + "command": map[string]any{"type": "string"}, + "args": map[string]any{ + "type": "array", + "items": map[string]any{"type": "string"}, + }, + }, + }, + map[string]any{"type": "string"}, + }, + }, + }, + }, + }, + }, + }} + + _, toolCalls := synthesizeToolCallsFromText(content, tools, "run-test") + + if len(toolCalls) != 1 { + t.Fatalf("tool_calls len: got %d", len(toolCalls)) + } + call := toolCalls[0].(map[string]any) + fn := call["function"].(map[string]any) + var args map[string][]string + if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil { + t.Fatalf("arguments JSON: %v", err) + } + if len(args["commands"]) != 2 { + t.Fatalf("commands len: %+v", args["commands"]) + } + if args["commands"][0] != "git -C /config/workspace/iop status" { + t.Fatalf("first command: %+v", args["commands"][0]) + } + if args["commands"][1] != "pwd && ls -la /config/workspace/" { + t.Fatalf("second command: %+v", args["commands"][1]) + } +} + func TestSynthesizesTemplateToolCallConvertsStructuredShellOperatorsToString(t *testing.T) { content := "확인합니다.\n\n{{run_commands(commands=[{'command': 'cd', 'args': ['/config/workspace/iop', '&&', 'git', 'status', '&&', 'git', 'diff', '--stat']}, {'command': 'ls', 'args': ['-la', '/config/workspace/iop', '&&', 'pwd']}])}}" tools := []any{map[string]any{ diff --git a/apps/edge/internal/openai/types.go b/apps/edge/internal/openai/types.go index 51e4030..1c75c17 100644 --- a/apps/edge/internal/openai/types.go +++ b/apps/edge/internal/openai/types.go @@ -802,6 +802,9 @@ func normalizeCommandArgument(command any, schema commandArgSchema) (any, bool) } if args, ok := stringSliceArgument(m["args"]); ok { commandText = strings.TrimSpace(commandText) + if schema.allowsString { + return renderShellCommand(commandText, args), true + } if commandExecutableNeedsShell(commandText) || commandArgsNeedShell(args) { if schema.allowsString || !schema.allowsObject { return renderShellCommand(commandText, args), true @@ -816,6 +819,9 @@ func normalizeCommandArgument(command any, schema commandArgSchema) (any, bool) } commandText = strings.TrimSpace(commandText) if commandText != "" { + if schema.allowsString { + return commandText, true + } if commandStringNeedsShell(commandText) { return commandText, true }