import Foundation enum DolphinPromptMessage: Sendable, Equatable { case user(String) case assistant(String) case assistantToolCall(AgentToolCall) case toolResponse(String) } struct DolphinPromptRenderer: Sendable { static func render( systemPrompt: String, messages: [DolphinPromptMessage], tools: [AgentToolDefinition], addGenerationPrompt: Bool = true ) -> String { var prompt = renderSystemPrompt( sanitize(systemPrompt), tools: tools ) for message in messages { switch message { case .user(let content): prompt += chatMessage(role: "user", content: sanitize(content)) case .assistant(let content): prompt += chatMessage(role: "assistant", content: sanitize(content)) case .assistantToolCall(let call): prompt += "<|im_start|>assistant\n" prompt += "\n" prompt += sanitize(toolCallJSON(call)) prompt += "\n<|im_end|>\n" case .toolResponse(let content): prompt += "<|im_start|>user\n" prompt += "\n" prompt += sanitize(content) prompt += "\n<|im_end|>\n" } } if addGenerationPrompt { prompt += "<|im_start|>assistant\n" } return prompt } static func toolCallJSON(_ call: AgentToolCall) -> String { JSONValue.object([ "arguments": .object(call.arguments), "name": .string(call.name), ]).canonicalJSON } private static func renderSystemPrompt( _ systemPrompt: String, tools: [AgentToolDefinition] ) -> String { let governedPrompt = systemPrompt + """ # Runtime protocol - Use no tool for greetings, introductions, thanks, or ordinary conversation. - Use memory_save only for an explicit request to remember or store information. - Use memory_search only when the user asks to recall or search saved information. - After a tool result, answer naturally and concisely. - Keep internal reasoning private. Return only the next tool call or the final answer; never emit analysis, chain-of-thought, or blocks. - Never reproduce or imitate internal receipts, call IDs, argument JSON, observations, or Activity logs in the answer. - If no tools are listed, answer without a tool call. """ guard !tools.isEmpty else { return chatMessage(role: "system", content: governedPrompt) } var value = "<|im_start|>system\n\(governedPrompt)" value += "\n\n# Tools\n\n" value += "You may call one function to assist with the user query.\n\n" value += "You are provided with function signatures within XML tags:\n" for tool in tools.sorted(by: { $0.id < $1.id }) { value += "\n\(toolDefinitionJSON(tool))" } value += "\n\n\n" value += "For a function call, return one JSON object with function name and arguments within XML tags:\n" value += "\n{\"name\": , \"arguments\": }\n<|im_end|>\n" return value } private static func toolDefinitionJSON(_ tool: AgentToolDefinition) -> String { JSONValue.object([ "function": .object([ "description": .string(tool.summary), "name": .string(tool.id), "parameters": tool.parameters, ]), "type": .string("function"), ]).canonicalJSON } private static func chatMessage(role: String, content: String) -> String { "<|im_start|>\(role)\n\(content)<|im_end|>\n" } private static func sanitize(_ value: String) -> String { var result = value while let start = result.range(of: "<|"), let end = result.range( of: "|>", range: start.upperBound..", with: "[tool_call]") .replacingOccurrences(of: "", with: "[/tool_call]") .replacingOccurrences(of: "", with: "[tool_response]") .replacingOccurrences(of: "", with: "[/tool_response]") .replacingOccurrences(of: "", with: "[tools]") .replacingOccurrences(of: "", with: "[/tools]") } }