package server import ( "encoding/json" "fmt" "strings" "github.com/daniel/cursor-adapter/internal/sanitize" "github.com/daniel/cursor-adapter/internal/types" ) // buildPromptFromAnthropicMessages flattens an Anthropic Messages request into // a single prompt string suitable for `agent --print`. It: // - renders tool_use / tool_result blocks as readable pseudo-XML so the // model can follow the trajectory of previous tool calls // - embeds the `tools` schema as part of the System block via // toolsToSystemText, so the model knows what tools the outer agent (e.g. // Claude Code) has available // - runs every piece of free text through sanitize.Text to strip Claude Code // branding and telemetry headers that would confuse the Cursor agent func buildPromptFromAnthropicMessages(req types.AnthropicMessagesRequest) string { var systemParts []string for _, block := range req.System { if block.Type == "text" && strings.TrimSpace(block.Text) != "" { systemParts = append(systemParts, sanitize.Text(block.Text)) } } if tools := toolsToSystemText(req.Tools); tools != "" { systemParts = append(systemParts, tools) } var convo []string for _, msg := range req.Messages { text := anthropicContentToText(msg.Content) if text == "" { continue } switch msg.Role { case "assistant": convo = append(convo, "Assistant: "+text) default: convo = append(convo, "User: "+text) } } var prompt strings.Builder if len(systemParts) > 0 { prompt.WriteString("System:\n") prompt.WriteString(strings.Join(systemParts, "\n\n")) prompt.WriteString("\n\n") } prompt.WriteString(strings.Join(convo, "\n\n")) prompt.WriteString("\n\nAssistant:") return prompt.String() } // anthropicContentToText renders a single message's content blocks as a // single string. Unlike the old implementation, this one preserves tool_use // and tool_result blocks so the model sees the full conversation trajectory // rather than mysterious gaps. func anthropicContentToText(content types.AnthropicContent) string { var parts []string for _, block := range content { switch block.Type { case "text": if block.Text != "" { parts = append(parts, sanitize.Text(block.Text)) } case "tool_use": input := strings.TrimSpace(string(block.Input)) if input == "" { input = "{}" } parts = append(parts, fmt.Sprintf( "\n%s\n", block.ID, block.Name, input, )) case "tool_result": body := toolResultBody(block.Content) errAttr := "" if block.IsError { errAttr = ` is_error="true"` } parts = append(parts, fmt.Sprintf( "\n%s\n", block.ToolUseID, errAttr, body, )) case "image": parts = append(parts, "[Image]") case "document": title := block.Title if title == "" { title = "Document" } parts = append(parts, "[Document: "+title+"]") } } return strings.Join(parts, "\n") } // toolResultBody flattens the `content` field of a tool_result block, which // can be either a plain string or an array of `{type, text}` content parts. func toolResultBody(raw json.RawMessage) string { if len(raw) == 0 { return "" } var asString string if err := json.Unmarshal(raw, &asString); err == nil { return sanitize.Text(asString) } var parts []struct { Type string `json:"type"` Text string `json:"text"` } if err := json.Unmarshal(raw, &parts); err == nil { var out []string for _, p := range parts { if p.Type == "text" && p.Text != "" { out = append(out, sanitize.Text(p.Text)) } } return strings.Join(out, "\n") } return string(raw) } // toolsToSystemText renders a tools schema array into a system-prompt chunk // describing each tool. The idea (from cursor-api-proxy) is that since the // Cursor CLI does not expose native tool_call deltas over the proxy, we tell // the model what tools exist so it can reference them in its text output. // // NOTE: This is a one-way passthrough. The proxy cannot turn the model's // textual "I would call Write with {...}" back into structured tool_use // blocks. Callers that need real tool-use routing (e.g. Claude Code's coding // agent) should run tools client-side and feed tool_result back in. func toolsToSystemText(tools []types.AnthropicTool) string { if len(tools) == 0 { return "" } var lines []string lines = append(lines, "Available tools (they belong to the caller, not to you; describe your", "intended call in plain text and the caller will execute it):", "", ) for _, t := range tools { schema := strings.TrimSpace(string(t.InputSchema)) if schema == "" { schema = "{}" } else { var pretty any if err := json.Unmarshal(t.InputSchema, &pretty); err == nil { if out, err := json.MarshalIndent(pretty, "", " "); err == nil { schema = string(out) } } } lines = append(lines, "Function: "+t.Name, "Description: "+sanitize.Text(t.Description), "Parameters: "+schema, "", ) } return strings.TrimRight(strings.Join(lines, "\n"), "\n") }