// // ToolTypes.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The normalized tool-calling vocabulary shared by the agent loop, the tool // registry, and every provider client (docs/PROVIDER-REUSE.md §5.3). Clients // translate these to/from each wire dialect (OpenAI `tools`/`tool_calls`, // Anthropic `tools`/`tool_use`/`tool_result`) — nothing provider-specific // leaks above this layer. // import Foundation /// A tool offered to the model, as handed to the client by the ToolRegistry. struct ToolSpec: Codable, Hashable { var name: String var description: String /// Canonical JSON Schema (an `object` schema) for the tool's parameters, /// serialized as a JSON string. Clients embed it as raw JSON in requests. var parametersJSONSchema: String init(name: String, description: String, parametersJSONSchema: String) { self.name = name self.description = description self.parametersJSONSchema = parametersJSONSchema } } /// One tool invocation emitted by the model. struct ToolCall: Codable, Identifiable, Hashable { /// Provider call ID (`toolu_…` / `call_…`); synthesized when a provider omits it. var id: String var name: String /// Raw accumulated JSON string of the arguments; parsed and validated /// against the ToolSpec schema by the agent loop before execution. var argumentsJSON: String /// Gemini 3+ thought signature (`extra_content.google.thought_signature` /// on the compat endpoint). Opaque; captured from responses and echoed /// back verbatim when the call is threaded into history — Gemini rejects /// tool results whose originating call lost its signature (verified live, /// Phase 7.1). Nil for every other provider. var thoughtSignature: String? init(id: String, name: String, argumentsJSON: String, thoughtSignature: String? = nil) { self.id = id self.name = name self.argumentsJSON = argumentsJSON self.thoughtSignature = thoughtSignature } /// Arguments parsed to a dictionary; nil when the model produced /// malformed JSON (the loop re-prompts on that). var argumentsDictionary: [String: Any]? { guard let data = argumentsJSON.data(using: .utf8), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } return obj } } /// The outcome of executing one tool call, threaded back on the next request. struct ToolResult: Codable, Hashable { var toolCallID: String /// Stringified output (stdout/stderr summary, file text, error message…). var content: String var isError: Bool init(toolCallID: String, content: String, isError: Bool = false) { self.toolCallID = toolCallID self.content = content self.isError = isError } } /// How the model is allowed to use the offered tools. enum ToolChoice: Hashable { /// Model decides freely (the default; omitted from the wire when possible). case auto /// Tools are declared but must not be called. case none /// The model must call some tool (OpenAI "required" / Anthropic "any"). case required /// The model must call this specific tool. case named(String) } /// Normalized stop condition, derived from the provider's raw finish/stop /// reason. The agent loop branches only on this — never on raw strings. enum StopReason: Hashable { /// Natural end of the assistant turn ("stop", "end_turn", "eos", …). case endTurn /// The model is waiting for tool results ("tool_calls" / "tool_use"). case toolUse /// Output truncated by the token limit ("length" / "max_tokens"). case maxTokens /// The model refused ("refusal" / "content_filter"). case refusal /// Anything else (provider-specific values like DeepSeek's /// "insufficient_system_resource"), with the raw string preserved. case other(String?) /// Maps every observed finish_reason/stop_reason vocabulary (OpenAI schema /// and Anthropic Messages) to the normalized enum. `hasToolCalls` covers /// providers that stream tool calls but report a plain "stop". static func normalize(_ raw: String?, hasToolCalls: Bool = false) -> StopReason { switch raw { case "tool_calls", "tool_use", "function_call": return .toolUse case "stop", "end_turn", "eos", "stop_sequence", "pause_turn": return hasToolCalls ? .toolUse : .endTurn case "length", "max_tokens", "model_context_window_exceeded": return .maxTokens case "refusal", "content_filter": return .refusal case nil: return hasToolCalls ? .toolUse : .endTurn default: return hasToolCalls ? .toolUse : .other(raw) } } }