@preconcurrency import Foundation enum AssistantWorkspaceMutation: Sendable { case saveMemory(MemoryItem) case captureKnowledge(KnowledgeItem) case createHandoff(SessionHandoff) case restoreHandoff(id: UUID, restoredAt: Date) case deactivateHandoff(id: UUID) case addTask(AssistantTaskItem) case completeTask(id: UUID, completedAt: Date) case reopenTask(id: UUID) } enum AssistantWorkspaceMutationResult: Sendable { case memory(MemoryItem) case knowledge(KnowledgeItem) case handoff(SessionHandoff) case task(AssistantTaskItem) } typealias AssistantWorkspaceResultBuilder = @Sendable (AssistantWorkspaceMutationResult) throws -> AgentToolResult /// Closures keep the tool layer independent of the UI's ownership model while /// ensuring all workspace mutations still pass through one serialized owner. struct AgentToolExecutionContext: Sendable { let snapshot: @Sendable () async -> AssistantWorkspace let isApplicationActive: @Sendable () async -> Bool let commitLocalWrite: @Sendable ( AssistantWorkspaceMutation, AgentToolCall, AgentToolDefinition, AssistantWorkspaceResultBuilder ) async throws -> AgentToolResult init( snapshot: @escaping @Sendable () async -> AssistantWorkspace, isApplicationActive: @escaping @Sendable () async -> Bool = { true }, commitLocalWrite: @escaping @Sendable ( AssistantWorkspaceMutation, AgentToolCall, AgentToolDefinition, AssistantWorkspaceResultBuilder ) async throws -> AgentToolResult ) { self.snapshot = snapshot self.isApplicationActive = isApplicationActive self.commitLocalWrite = commitLocalWrite } } enum AgentToolRegistryError: LocalizedError, Sendable { case unknownTool(String) case missingArgument(String) case unexpectedArguments([String]) case invalidArgument(name: String, reason: String) case contextRejected(String) var errorDescription: String? { switch self { case .unknownTool(let name): "Unknown tool: \(name)." case .missingArgument(let name): "Missing required argument: \(name)." case .unexpectedArguments(let names): "Unexpected argument\(names.count == 1 ? "" : "s"): \(names.joined(separator: ", "))." case .invalidArgument(let name, let reason): "Invalid \(name): \(reason)." case .contextRejected(let reason): "The workspace rejected the tool operation: \(reason)." } } } struct AgentToolRegistry: Sendable { static let canonicalDefinitions: [AgentToolDefinition] = [ AgentToolDefinition( id: "current_datetime", displayName: "Current Date & Time", summary: "Read the current local date, time, and time zone. Takes no arguments.", parameters: schema(properties: [:], required: []), risk: .readOnly, maxOutputCharacters: 512 ), AgentToolDefinition( id: "calculator", displayName: "Calculator", summary: "Evaluate arithmetic using numbers, parentheses, and +, -, *, /, or %.", parameters: schema( properties: [ "expression": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(256), ]) ], required: ["expression"] ), risk: .readOnly, maxOutputCharacters: 512 ), AgentToolDefinition( id: "memory_search", displayName: "Search Memory", summary: "Search durable local memories only when the user asks to recall or find saved information. Do not use for greetings or to save a new fact.", parameters: schema( properties: [ "query": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(200), ]), "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(5), ]), ], required: ["query"] ), risk: .readOnly, maxOutputCharacters: 1_200 ), AgentToolDefinition( id: "memory_save", displayName: "Save Memory", summary: "Save one durable local memory when the user explicitly asks to remember, save, store, or not forget something.", parameters: schema( properties: [ "content": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(1_500), ]) ], required: ["content"] ), risk: .localWrite, maxOutputCharacters: 512 ), AgentToolDefinition( id: "knowledge_search", displayName: "Search Project Knowledge", summary: "Search the private local world model with deterministic hybrid lexical ranking. Includes captured knowledge and legacy memories; this is not embedding search.", parameters: schema( properties: [ "query": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(Double(KnowledgeEngine.Limits.maxQueryCharacters)), ]), "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(8), ]), ], required: ["query"] ), risk: .readOnly, maxOutputCharacters: 2_800 ), AgentToolDefinition( id: "knowledge_get", displayName: "Get Knowledge Details", summary: "Read one private local knowledge item by its exact UUID, including provenance, revision, tags, and relationships.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .readOnly, maxOutputCharacters: 3_200 ), AgentToolDefinition( id: "knowledge_capture", displayName: "Capture Project Knowledge", summary: "Capture one typed fact, preference, decision, goal, code note, context note, or insight in the private local world model.", parameters: schema( properties: [ "kind": .object([ "type": .string("string"), "enum": .array([ .string("fact"), .string("preference"), .string("decision"), .string("goal"), .string("code"), .string("context"), .string("insight"), ]), ]), "title": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(Double(KnowledgeEngine.Limits.maxItemTitleCharacters)), ]), "content": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(4_000), ]), "tags": stringArraySchema( maximumItems: 8, maximumLength: KnowledgeEngine.Limits.maxTagCharacters ), "related_ids": uuidArraySchema(maximumItems: 8), ], required: ["kind", "title", "content"] ), risk: .localWrite, maxOutputCharacters: 700 ), AgentToolDefinition( id: "project_context", displayName: "Synthesize Project Context", summary: "Build a bounded, deterministic context digest from captured knowledge, open tasks, and the active handoff. Takes no arguments and does not save model-generated text.", parameters: schema(properties: [:], required: []), risk: .readOnly, maxOutputCharacters: 3_200 ), AgentToolDefinition( id: "project_review", displayName: "Review Project State", summary: "Produce a deterministic, evidence-backed review from local knowledge, open tasks, the active handoff, and proactive insight rules. This does not run a model or save data.", parameters: schema( properties: [ "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(8), ]) ], required: [] ), risk: .readOnly, maxOutputCharacters: 4_000 ), AgentToolDefinition( id: "handoff_create", displayName: "Create Session Handoff", summary: "Create a typed local continuation package with focus, synthesized context, next steps, evidence links, and open tasks.", parameters: schema( properties: [ "title": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(120), ]), "focus": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(500), ]), ], required: ["title", "focus"] ), risk: .localWrite, maxOutputCharacters: 900 ), AgentToolDefinition( id: "handoff_list", displayName: "List Session Handoffs", summary: "List recent typed continuation packages and indicate which handoff is active.", parameters: schema( properties: [ "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(10), ]) ], required: [] ), risk: .readOnly, maxOutputCharacters: 2_400 ), AgentToolDefinition( id: "handoff_get", displayName: "Get Session Handoff", summary: "Read one saved continuation package by UUID, including its focus, summary, next steps, and evidence links. Reading it never replays an action.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .readOnly, maxOutputCharacters: 3_600 ), AgentToolDefinition( id: "handoff_restore", displayName: "Restore Session Handoff", summary: "Select a saved handoff as active local continuity context. This never replays old actions.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .localWrite, maxOutputCharacters: 700 ), AgentToolDefinition( id: "handoff_deactivate", displayName: "Deactivate Session Handoff", summary: "Deactivate the exact currently active handoff by UUID. The saved checkpoint is preserved and no prior action is replayed.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .localWrite, maxOutputCharacters: 512 ), AgentToolDefinition( id: "insight_list", displayName: "Proactive Insights", summary: "Derive bounded local follow-up suggestions from gaps, unfinished tasks, decisions, and handoff freshness. Takes no background model action.", parameters: schema( properties: [ "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(8), ]) ], required: [] ), risk: .readOnly, maxOutputCharacters: 2_400 ), AgentToolDefinition( id: "code_analyze", displayName: "Analyze Code Snippet", summary: "Perform bounded, deterministic static heuristics on supplied source text without executing or saving it.", parameters: schema( properties: [ "code": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(5_000), ]), "language": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(Double(KnowledgeEngine.Limits.maxTagCharacters)), ]), ], required: ["code"] ), risk: .readOnly, maxOutputCharacters: 2_800 ), AgentToolDefinition( id: "task_list", displayName: "List Tasks", summary: "Read local tasks, optionally including completed tasks.", parameters: schema( properties: [ "include_completed": .object(["type": .string("boolean")]), "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(10), ]), ], required: [] ), risk: .readOnly, maxOutputCharacters: 1_200 ), AgentToolDefinition( id: "task_search", displayName: "Search Tasks", summary: "Search local task titles with deterministic lexical ranking.", parameters: schema( properties: [ "query": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(200), ]), "include_completed": .object(["type": .string("boolean")]), "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(10), ]), ], required: ["query"] ), risk: .readOnly, maxOutputCharacters: 1_800 ), AgentToolDefinition( id: "task_add", displayName: "Add Task", summary: "Add one task to the durable local task list.", parameters: schema( properties: [ "title": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(300), ]) ], required: ["title"] ), risk: .localWrite, maxOutputCharacters: 512 ), AgentToolDefinition( id: "task_complete", displayName: "Complete Task", summary: "Mark a local task complete using its UUID from task_list.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .localWrite, maxOutputCharacters: 512 ), AgentToolDefinition( id: "task_reopen", displayName: "Reopen Task", summary: "Reopen one completed local task using its exact UUID.", parameters: schema( properties: [ "id": .object([ "type": .string("string"), "format": .string("uuid"), ]) ], required: ["id"] ), risk: .localWrite, maxOutputCharacters: 512 ), AgentToolDefinition( id: "calendar_events", displayName: "Calendar Events", summary: "Read calendar event titles and times for today, tomorrow, or the next 7 days. Requires the user-enabled Calendar capability and one-time approval.", parameters: schema( properties: [ "window": .object([ "type": .string("string"), "enum": .array([ .string(CalendarQueryWindow.today.rawValue), .string(CalendarQueryWindow.tomorrow.rawValue), .string(CalendarQueryWindow.nextSevenDays.rawValue), ]), ]), "limit": .object([ "type": .string("integer"), "minimum": .number(1), "maximum": .number(10), ]), ], required: ["window"] ), risk: .sensitiveRead, requiredCapability: .calendarRead, maxOutputCharacters: 2_400 ), ] private static let definitionsByID = Dictionary( uniqueKeysWithValues: canonicalDefinitions.map { ($0.id, $0) } ) let definitions: [AgentToolDefinition] private let dateProvider: any DateProviding private let calendarReader: any SystemCalendarReading private let calendar: Calendar init( dateProvider: any DateProviding = SystemDateProvider(), calendarReader: any SystemCalendarReading = UnavailableSystemCalendarReader(), calendar: Calendar = .autoupdatingCurrent ) { definitions = Self.canonicalDefinitions self.dateProvider = dateProvider self.calendarReader = calendarReader self.calendar = calendar } func definition(named id: String) -> AgentToolDefinition? { Self.definitionsByID[id] } /// Performs an exact key, type, and bound check. The agent loop can call this /// before showing approval so the approved arguments are the executable ones. @discardableResult func validate(_ call: AgentToolCall) throws -> AgentToolDefinition { _ = try normalize(call) guard let definition = definition(named: call.name) else { throw AgentToolRegistryError.unknownTool(call.name) } return definition } /// Returns the exact canonical call used for duplicate checks, approval, and /// execution. No normalization occurs after an approval is displayed. func normalize(_ call: AgentToolCall) throws -> AgentToolCall { guard definition(named: call.name) != nil else { throw AgentToolRegistryError.unknownTool(call.name) } let normalized: [String: JSONValue] switch call.name { case "current_datetime": _ = try ExactArguments(call.arguments, allowed: [], required: []) normalized = [:] case "calculator": let arguments = try ExactArguments( call.arguments, allowed: ["expression"], required: ["expression"] ) normalized = [ "expression": .string( try arguments.string("expression", maximumLength: 256) ) ] case "memory_search": let arguments = try ExactArguments( call.arguments, allowed: ["query", "limit"], required: ["query"] ) normalized = [ "query": .string( try arguments.string("query", maximumLength: 200) ), "limit": .number( Double(try arguments.integer("limit", default: 5, range: 1...5)) ), ] case "memory_save": let arguments = try ExactArguments( call.arguments, allowed: ["content"], required: ["content"] ) let content = KnowledgeEngine.canonicalMemoryContent( try arguments.string( "content", maximumLength: KnowledgeEngine.Limits.maxLegacyMemoryCharacters ) ) guard !content.isEmpty else { throw AgentToolRegistryError.invalidArgument( name: "content", reason: "must contain visible text after safety normalization" ) } normalized = ["content": .string(content)] case "knowledge_search": let arguments = try ExactArguments( call.arguments, allowed: ["query", "limit"], required: ["query"] ) normalized = [ "query": .string( try arguments.string( "query", maximumLength: KnowledgeEngine.Limits.maxQueryCharacters ) ), "limit": .number( Double(try arguments.integer("limit", default: 5, range: 1...8)) ), ] case "knowledge_get": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "knowledge_capture": let arguments = try ExactArguments( call.arguments, allowed: ["kind", "title", "content", "tags", "related_ids"], required: ["kind", "title", "content"] ) let kind = try arguments.string("kind", maximumLength: 20) guard [ "fact", "preference", "decision", "goal", "code", "context", "insight", ].contains(kind) else { throw AgentToolRegistryError.invalidArgument( name: "kind", reason: "expected fact, preference, decision, goal, code, context, or insight" ) } let fields = KnowledgeEngine.canonicalCaptureFields( title: try arguments.string( "title", maximumLength: KnowledgeEngine.Limits.maxItemTitleCharacters ), content: try arguments.string( "content", maximumLength: KnowledgeEngine.Limits.maxItemContentCharacters ), tags: try arguments.stringArray( "tags", default: [], maximumCount: 8, maximumElementLength: KnowledgeEngine.Limits.maxTagCharacters ), relatedItemIDs: try arguments.uuidArray( "related_ids", default: [], maximumCount: 8 ) ) normalized = [ "kind": .string(kind), "title": .string(fields.title), "content": .string(fields.content), "tags": .array(fields.tags.map(JSONValue.string)), "related_ids": .array( fields.relatedItemIDs.map { .string($0.uuidString) } ), ] case "project_context": _ = try ExactArguments(call.arguments, allowed: [], required: []) normalized = [:] case "project_review": let arguments = try ExactArguments( call.arguments, allowed: ["limit"], required: [] ) normalized = [ "limit": .number( Double(try arguments.integer("limit", default: 5, range: 1...8)) ) ] case "handoff_create": let arguments = try ExactArguments( call.arguments, allowed: ["title", "focus"], required: ["title", "focus"] ) let fields = KnowledgeEngine.canonicalHandoffFields( title: try arguments.string( "title", maximumLength: KnowledgeEngine.Limits.maxItemTitleCharacters ), focus: try arguments.string( "focus", maximumLength: KnowledgeEngine.Limits.maxHandoffFocusCharacters ) ) guard !fields.focus.isEmpty else { throw AgentToolRegistryError.invalidArgument( name: "focus", reason: "must contain visible text after safety normalization" ) } normalized = [ "title": .string(fields.title), "focus": .string(fields.focus), ] case "handoff_list": let arguments = try ExactArguments( call.arguments, allowed: ["limit"], required: [] ) normalized = [ "limit": .number( Double(try arguments.integer("limit", default: 5, range: 1...10)) ) ] case "handoff_get": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "handoff_restore": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "handoff_deactivate": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "insight_list": let arguments = try ExactArguments( call.arguments, allowed: ["limit"], required: [] ) normalized = [ "limit": .number( Double(try arguments.integer("limit", default: 5, range: 1...8)) ) ] case "code_analyze": let arguments = try ExactArguments( call.arguments, allowed: ["code", "language"], required: ["code"] ) normalized = [ "code": .string(try arguments.string("code", maximumLength: 5_000)), "language": .string( try arguments.optionalString( "language", default: "unknown", maximumLength: KnowledgeEngine.Limits.maxTagCharacters ) ), ] case "task_list": let arguments = try ExactArguments( call.arguments, allowed: ["include_completed", "limit"], required: [] ) normalized = [ "include_completed": .bool( try arguments.boolean("include_completed", default: false) ), "limit": .number( Double( try arguments.integer("limit", default: 10, range: 1...10) ) ), ] case "task_search": let arguments = try ExactArguments( call.arguments, allowed: ["query", "include_completed", "limit"], required: ["query"] ) normalized = [ "query": .string( try arguments.string("query", maximumLength: 200) ), "include_completed": .bool( try arguments.boolean("include_completed", default: false) ), "limit": .number( Double(try arguments.integer("limit", default: 10, range: 1...10)) ), ] case "task_add": let arguments = try ExactArguments( call.arguments, allowed: ["title"], required: ["title"] ) let title = KnowledgeEngine.canonicalTaskTitle( try arguments.string( "title", maximumLength: KnowledgeEngine.Limits.maxTaskTitleCharacters ) ) guard !title.isEmpty else { throw AgentToolRegistryError.invalidArgument( name: "title", reason: "must contain visible text after safety normalization" ) } normalized = ["title": .string(title)] case "task_complete": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "task_reopen": let arguments = try ExactArguments( call.arguments, allowed: ["id"], required: ["id"] ) normalized = ["id": .string(try arguments.uuid("id").uuidString)] case "calendar_events": let arguments = try ExactArguments( call.arguments, allowed: ["window", "limit"], required: ["window"] ) let rawWindow = try arguments.string("window", maximumLength: 32) guard CalendarQueryWindow(rawValue: rawWindow) != nil else { throw AgentToolRegistryError.invalidArgument( name: "window", reason: "expected today, tomorrow, or next_7_days" ) } normalized = [ "window": .string(rawWindow), "limit": .number( Double(try arguments.integer("limit", default: 10, range: 1...10)) ), ] default: throw AgentToolRegistryError.unknownTool(call.name) } return AgentToolCall(id: call.id, name: call.name, arguments: normalized) } /// Validation and execution are deliberately one operation. Validation /// failures become bounded tool observations instead of escaping into UI code. func execute( _ call: AgentToolCall, context: AgentToolExecutionContext ) async -> AgentToolResult { let outputLimit = definition(named: call.name)?.maxOutputCharacters ?? 512 do { let normalizedCall = try normalize(call) guard let definition = definition(named: normalizedCall.name) else { throw AgentToolRegistryError.unknownTool(normalizedCall.name) } let result = try await executeValidated( normalizedCall, definition: definition, context: context ) return result.bounded( toMaximumCharacters: definition.maxOutputCharacters ) } catch is CancellationError { return AgentToolResult( modelText: "Tool execution was cancelled.", displayText: "Cancelled", succeeded: false ).bounded(toMaximumCharacters: outputLimit) } catch { let message = error.localizedDescription return AgentToolResult( modelText: "Tool error: \(message)", displayText: message, succeeded: false ).bounded(toMaximumCharacters: outputLimit) } } private func executeValidated( _ call: AgentToolCall, definition: AgentToolDefinition, context: AgentToolExecutionContext ) async throws -> AgentToolResult { let arguments = call.arguments switch call.name { case "current_datetime": let now = dateProvider.now() let iso8601 = Self.iso8601(now) return AgentToolResult( modelText: JSONValue.object([ "iso8601": .string(iso8601), "time_zone": .string(TimeZone.current.identifier), "unix_time": .number(now.timeIntervalSince1970), ]).canonicalJSON, displayText: iso8601, succeeded: true ) case "knowledge_get": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") let workspace = await context.snapshot() guard let item = workspace.knowledgeItems.first(where: { $0.id == id }) else { throw AgentToolRegistryError.contextRejected( "No local knowledge item matches that UUID." ) } return AgentToolResult( modelText: Self.boundedKnowledgeDetailPayload( item, maximumCharacters: definition.maxOutputCharacters ), displayText: "Knowledge details: \(item.title)", succeeded: true ) case "calculator": let values = try ExactArguments( arguments, allowed: ["expression"], required: ["expression"] ) let expression = try values.string("expression", maximumLength: 256) let result = try SafeCalculator.evaluate(expression) let formatted = Self.formatNumber(result) return AgentToolResult( modelText: JSONValue.object([ "expression": .string(expression), "result": .string(formatted), ]).canonicalJSON, displayText: formatted, succeeded: true ) case "memory_search": let values = try ExactArguments( arguments, allowed: ["query", "limit"], required: ["query"] ) let query = try values.string("query", maximumLength: 200) let limit = try values.integer("limit", default: 5, range: 1...5) let workspace = await context.snapshot() let matches = Self.memoryMatches( query: query, memories: workspace.memories, limit: limit + 1 ) let visibleMatches = Array(matches.prefix(limit)) let payload = visibleMatches.map { item in JSONValue.object([ "id": .string(item.id.uuidString), "content": .string(Self.clipped(item.content, maximumCharacters: 300)), "created_at": .string(Self.iso8601(item.createdAt)), ]) } return AgentToolResult( modelText: Self.boundedMemorySearchPayload( query: query, values: Array(payload), maximumCharacters: 1_200, alreadyTruncated: matches.count > limit ), displayText: payload.isEmpty ? "No matching memories" : "Found \(payload.count) memor\(payload.count == 1 ? "y" : "ies")", succeeded: true ) case "memory_save": let values = try ExactArguments( arguments, allowed: ["content"], required: ["content"] ) let content = try values.string("content", maximumLength: 1_500) let proposed = MemoryItem(content: content, createdAt: dateProvider.now()) return try await context.commitLocalWrite( .saveMemory(proposed), call, definition ) { mutationResult in guard case .memory(let saved) = mutationResult else { throw AgentToolRegistryError.contextRejected( "memory_save returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "saved": .bool(true), "id": .string(saved.id.uuidString), ]).canonicalJSON, displayText: "Memory saved", succeeded: true ) } case "knowledge_search": let values = try ExactArguments( arguments, allowed: ["query", "limit"], required: ["query"] ) let query = try values.string( "query", maximumLength: KnowledgeEngine.Limits.maxQueryCharacters ) let limit = try values.integer("limit", default: 5, range: 1...8) let workspace = await context.snapshot() let matches = KnowledgeEngine().search( query: query, in: KnowledgeEngine.searchableKnowledge( typedItems: workspace.knowledgeItems, legacyMemories: workspace.memories ), limit: limit + 1 ) let visible = Array(matches.prefix(limit)) let payload = visible.map { item in JSONValue.object([ "content": .string(Self.clipped(item.content, maximumCharacters: 420)), "created_at": .string(Self.iso8601(item.createdAt)), "id": .string(item.id.uuidString), "kind": .string(item.kind.rawValue), "revision": .number(Double(item.revision)), "source": .string(item.source.rawValue), "tags": .array(item.tags.map(JSONValue.string)), "title": .string(item.title), ]) } return AgentToolResult( modelText: Self.boundedKnowledgeSearchPayload( query: query, values: payload, maximumCharacters: definition.maxOutputCharacters, alreadyTruncated: matches.count > limit ), displayText: visible.isEmpty ? "No matching project knowledge" : "Found \(visible.count) knowledge item\(visible.count == 1 ? "" : "s")", succeeded: true ) case "knowledge_capture": let values = try ExactArguments( arguments, allowed: ["kind", "title", "content", "tags", "related_ids"], required: ["kind", "title", "content"] ) let rawKind = try values.string("kind", maximumLength: 20) guard let kind = KnowledgeKind(rawValue: rawKind) else { throw AgentToolRegistryError.invalidArgument( name: "kind", reason: "unsupported knowledge kind" ) } let item = KnowledgeItem( kind: kind, title: try values.string( "title", maximumLength: KnowledgeEngine.Limits.maxItemTitleCharacters ), content: try values.string( "content", maximumLength: KnowledgeEngine.Limits.maxItemContentCharacters ), tags: try values.stringArray( "tags", default: [], maximumCount: 8, maximumElementLength: KnowledgeEngine.Limits.maxTagCharacters ), relatedItemIDs: try values.uuidArray( "related_ids", default: [], maximumCount: 8 ), source: .user, createdAt: dateProvider.now() ) let workspace = await context.snapshot() let knownIDs = Set(workspace.knowledgeItems.map(\.id)) guard item.relatedItemIDs.allSatisfy(knownIDs.contains) else { throw AgentToolRegistryError.invalidArgument( name: "related_ids", reason: "every related item must already exist in local knowledge" ) } return try await context.commitLocalWrite( .captureKnowledge(item), call, definition ) { mutationResult in guard case .knowledge(let saved) = mutationResult else { throw AgentToolRegistryError.contextRejected( "knowledge_capture returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "captured": .bool(true), "id": .string(saved.id.uuidString), "kind": .string(saved.kind.rawValue), "revision": .number(Double(saved.revision)), ]).canonicalJSON, displayText: "Captured \(saved.kind.rawValue): \(saved.title)", succeeded: true ) } case "project_context": _ = try ExactArguments(arguments, allowed: [], required: []) let workspace = await context.snapshot() let activeHandoff = workspace.activeHandoffID.flatMap { id in workspace.handoffs.first { $0.id == id } } let digest = KnowledgeEngine().synthesizeContext( knowledge: workspace.knowledgeItems, tasks: workspace.tasks, handoff: activeHandoff, trigger: "tool request", createdAt: dateProvider.now(), maxCharacters: 1_600 ) return AgentToolResult( modelText: Self.boundedProjectContextPayload( digest: digest, activeHandoffID: activeHandoff?.id, maximumCharacters: definition.maxOutputCharacters ), displayText: "Project context synthesized from \(digest.evidenceItemIDs.count) evidence item\(digest.evidenceItemIDs.count == 1 ? "" : "s")", succeeded: true ) case "project_review": let values = try ExactArguments( arguments, allowed: ["limit"], required: [] ) let limit = try values.integer("limit", default: 5, range: 1...8) let workspace = await context.snapshot() let activeHandoff = workspace.activeHandoffID.flatMap { id in workspace.handoffs.first { $0.id == id } } let engine = KnowledgeEngine() let digest = engine.synthesizeContext( knowledge: workspace.knowledgeItems, tasks: workspace.tasks, handoff: activeHandoff, trigger: "project review", createdAt: dateProvider.now(), maxCharacters: 1_200 ) let insights = Array(engine.deriveProactiveInsights( knowledge: workspace.knowledgeItems, tasks: workspace.tasks, handoff: activeHandoff, now: dateProvider.now() ).prefix(limit)) return AgentToolResult( modelText: Self.boundedProjectReviewPayload( digest: digest, insights: insights, workspace: workspace, activeHandoffID: activeHandoff?.id, maximumCharacters: definition.maxOutputCharacters ), displayText: "Project review derived from \(digest.evidenceItemIDs.count) evidence item\(digest.evidenceItemIDs.count == 1 ? "" : "s")", succeeded: true ) case "handoff_create": let values = try ExactArguments( arguments, allowed: ["title", "focus"], required: ["title", "focus"] ) let workspace = await context.snapshot() let handoff = KnowledgeEngine().makeHandoff( title: try values.string( "title", maximumLength: KnowledgeEngine.Limits.maxItemTitleCharacters ), focus: try values.string( "focus", maximumLength: KnowledgeEngine.Limits.maxHandoffFocusCharacters ), knowledge: workspace.knowledgeItems, tasks: workspace.tasks, recentMessages: workspace.messages.suffix(6).map(\.content), createdAt: dateProvider.now() ) return try await context.commitLocalWrite( .createHandoff(handoff), call, definition ) { mutationResult in guard case .handoff(let saved) = mutationResult else { throw AgentToolRegistryError.contextRejected( "handoff_create returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "active": .bool(true), "created": .bool(true), "id": .string(saved.id.uuidString), "knowledge_count": .number(Double(saved.knowledgeItemIDs.count)), "next_step_count": .number(Double(saved.nextSteps.count)), "task_count": .number(Double(saved.taskIDs.count)), ]).canonicalJSON, displayText: "Session handoff created: \(saved.title)", succeeded: true ) } case "handoff_list": let values = try ExactArguments( arguments, allowed: ["limit"], required: [] ) let limit = try values.integer("limit", default: 5, range: 1...10) let workspace = await context.snapshot() let ordered = workspace.handoffs.sorted { first, second in (first.restoredAt ?? first.createdAt) > (second.restoredAt ?? second.createdAt) } let visible = Array(ordered.prefix(limit)) let payload = visible.map { handoff in JSONValue.object([ "active": .bool(workspace.activeHandoffID == handoff.id), "created_at": .string(Self.iso8601(handoff.createdAt)), "focus": .string(Self.clipped(handoff.focus, maximumCharacters: 300)), "id": .string(handoff.id.uuidString), "restored_at": handoff.restoredAt.map { .string(Self.iso8601($0)) } ?? .null, "title": .string(handoff.title), ]) } return AgentToolResult( modelText: Self.boundedCollectionPayload( key: "handoffs", values: payload, maximumCharacters: definition.maxOutputCharacters, alreadyTruncated: ordered.count > limit ), displayText: visible.isEmpty ? "No session handoffs" : "Found \(visible.count) session handoff\(visible.count == 1 ? "" : "s")", succeeded: true ) case "handoff_get": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") let workspace = await context.snapshot() guard let handoff = workspace.handoffs.first(where: { $0.id == id }) else { throw AgentToolRegistryError.contextRejected( "No saved session handoff matches that UUID." ) } return AgentToolResult( modelText: Self.boundedHandoffDetailPayload( handoff, isActive: workspace.activeHandoffID == id, maximumCharacters: definition.maxOutputCharacters ), displayText: "Saved handoff details: \(handoff.title)", succeeded: true ) case "handoff_restore": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") return try await context.commitLocalWrite( .restoreHandoff(id: id, restoredAt: dateProvider.now()), call, definition ) { mutationResult in guard case .handoff(let restored) = mutationResult else { throw AgentToolRegistryError.contextRejected( "handoff_restore returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "active": .bool(true), "id": .string(restored.id.uuidString), "restored": .bool(true), ]).canonicalJSON, displayText: "Restored handoff: \(restored.title). No action was replayed", succeeded: true ) } case "handoff_deactivate": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") return try await context.commitLocalWrite( .deactivateHandoff(id: id), call, definition ) { mutationResult in guard case .handoff(let handoff) = mutationResult else { throw AgentToolRegistryError.contextRejected( "handoff_deactivate returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "deactivated": .bool(true), "id": .string(handoff.id.uuidString), "title": .string(handoff.title), ]).canonicalJSON, displayText: "Handoff deactivated: \(handoff.title). The checkpoint remains saved", succeeded: true ) } case "insight_list": let values = try ExactArguments( arguments, allowed: ["limit"], required: [] ) let limit = try values.integer("limit", default: 5, range: 1...8) let workspace = await context.snapshot() let activeHandoff = workspace.activeHandoffID.flatMap { id in workspace.handoffs.first { $0.id == id } } let insights = KnowledgeEngine().deriveProactiveInsights( knowledge: workspace.knowledgeItems, tasks: workspace.tasks, handoff: activeHandoff, now: dateProvider.now() ) let visible = Array(insights.prefix(limit)) let payload = visible.map { insight in JSONValue.object([ "detail": .string(insight.detail), "evidence_ids": .array( insight.evidenceItemIDs.map { .string($0.uuidString) } ), "id": .string(insight.id.uuidString), "priority": .string(insight.priority.rawValue), "title": .string(insight.title), ]) } return AgentToolResult( modelText: Self.boundedCollectionPayload( key: "insights", values: payload, maximumCharacters: definition.maxOutputCharacters, alreadyTruncated: insights.count > limit ), displayText: visible.isEmpty ? "No proactive insights" : "Derived \(visible.count) proactive insight\(visible.count == 1 ? "" : "s")", succeeded: true ) case "code_analyze": let values = try ExactArguments( arguments, allowed: ["code", "language"], required: ["code"] ) let code = try values.string("code", maximumLength: 5_000) let language = try values.optionalString( "language", default: "unknown", maximumLength: KnowledgeEngine.Limits.maxTagCharacters ) let analysis = KnowledgeEngine().analyzeCode( code, language: language ) let findings = analysis.findings.map { finding in JSONValue.object([ "kind": .string(finding.kind.rawValue), "line": finding.line.map { .number(Double($0)) } ?? .null, "message": .string(finding.message), "severity": .string(finding.severity.rawValue), ]) } return AgentToolResult( modelText: Self.boundedCodeAnalysisPayload( analysis: analysis, findings: findings, inputSHA256: AgentInputDigest.sha256(code), maximumCharacters: definition.maxOutputCharacters ), displayText: analysis.findings.isEmpty ? "No deterministic code findings" : "Found \(analysis.findings.count) deterministic code observation\(analysis.findings.count == 1 ? "" : "s")", succeeded: true ) case "task_list": let values = try ExactArguments( arguments, allowed: ["include_completed", "limit"], required: [] ) let includeCompleted = try values.boolean( "include_completed", default: false ) let limit = try values.integer("limit", default: 10, range: 1...10) let workspace = await context.snapshot() let matchingTasks = workspace.tasks .filter { includeCompleted || !$0.isCompleted } .sorted { $0.createdAt > $1.createdAt } let tasks = matchingTasks.prefix(limit) let payload = tasks.map { item in JSONValue.object([ "id": .string(item.id.uuidString), "title": .string(Self.clipped(item.title, maximumCharacters: 200)), "completed": .bool(item.isCompleted), ]) } return AgentToolResult( modelText: Self.boundedCollectionPayload( key: "tasks", values: Array(payload), maximumCharacters: 1_200, alreadyTruncated: matchingTasks.count > limit ), displayText: payload.isEmpty ? "No tasks" : "Listed \(payload.count) task\(payload.count == 1 ? "" : "s")", succeeded: true ) case "task_search": let values = try ExactArguments( arguments, allowed: ["query", "include_completed", "limit"], required: ["query"] ) let query = try values.string("query", maximumLength: 200) let includeCompleted = try values.boolean( "include_completed", default: false ) let limit = try values.integer("limit", default: 10, range: 1...10) let workspace = await context.snapshot() let matches = KnowledgeEngine().searchTasks( query: query, in: workspace.tasks, includeCompleted: includeCompleted, limit: limit + 1 ) let visible = Array(matches.prefix(limit)) let payload = visible.map { item in JSONValue.object([ "completed": .bool(item.isCompleted), "completed_at": item.completedAt.map { .string(Self.iso8601($0)) } ?? .null, "created_at": .string(Self.iso8601(item.createdAt)), "id": .string(item.id.uuidString), "title": .string(Self.clipped(item.title, maximumCharacters: 300)), ]) } return AgentToolResult( modelText: Self.boundedTaskSearchPayload( query: query, values: payload, maximumCharacters: definition.maxOutputCharacters, alreadyTruncated: matches.count > limit ), displayText: visible.isEmpty ? "No matching tasks" : "Found \(visible.count) matching task\(visible.count == 1 ? "" : "s")", succeeded: true ) case "task_add": let values = try ExactArguments( arguments, allowed: ["title"], required: ["title"] ) let title = try values.string("title", maximumLength: 300) let proposed = AssistantTaskItem( title: title, createdAt: dateProvider.now() ) return try await context.commitLocalWrite( .addTask(proposed), call, definition ) { mutationResult in guard case .task(let saved) = mutationResult else { throw AgentToolRegistryError.contextRejected( "task_add returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "added": .bool(true), "id": .string(saved.id.uuidString), "title": .string( Self.clipped(saved.title, maximumCharacters: 300) ), ]).canonicalJSON, displayText: "Task added", succeeded: true ) } case "task_complete": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") return try await context.commitLocalWrite( .completeTask(id: id, completedAt: dateProvider.now()), call, definition ) { mutationResult in guard case .task(let saved) = mutationResult else { throw AgentToolRegistryError.contextRejected( "task_complete returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "completed": .bool(true), "id": .string(saved.id.uuidString), "title": .string( Self.clipped(saved.title, maximumCharacters: 300) ), ]).canonicalJSON, displayText: "Task completed", succeeded: true ) } case "task_reopen": let values = try ExactArguments( arguments, allowed: ["id"], required: ["id"] ) let id = try values.uuid("id") return try await context.commitLocalWrite( .reopenTask(id: id), call, definition ) { mutationResult in guard case .task(let task) = mutationResult else { throw AgentToolRegistryError.contextRejected( "task_reopen returned an unexpected result" ) } return AgentToolResult( modelText: JSONValue.object([ "id": .string(task.id.uuidString), "reopened": .bool(true), "title": .string( Self.clipped(task.title, maximumCharacters: 300) ), ]).canonicalJSON, displayText: "Task reopened: \(task.title)", succeeded: true ) } case "calendar_events": let values = try ExactArguments( arguments, allowed: ["window", "limit"], required: ["window"] ) let rawWindow = try values.string("window", maximumLength: 32) guard let window = CalendarQueryWindow(rawValue: rawWindow) else { throw AgentToolRegistryError.invalidArgument( name: "window", reason: "expected today, tomorrow, or next_7_days" ) } let limit = try values.integer("limit", default: 10, range: 1...10) guard await context.isApplicationActive() else { throw AgentToolRegistryError.contextRejected( "Calendar can be read only while Dolphin is in the foreground." ) } let authorization = await calendarReader.authorizationStatus() guard authorization == .fullAccess else { throw SystemCalendarReaderError.fullAccessRequired(authorization) } let interval = window.dateInterval( containing: dateProvider.now(), calendar: calendar ) let page = try await calendarReader.readEvents( in: interval, limit: limit + 1 ) try Task.checkCancellation() guard await context.isApplicationActive() else { throw AgentToolRegistryError.contextRejected( "Dolphin left the foreground before the Calendar read completed." ) } let matchingEvents = page.events .filter { event in event.startsAt < interval.end && event.endsAt > interval.start } .sorted { first, second in if first.startsAt != second.startsAt { return first.startsAt < second.startsAt } if first.endsAt != second.endsAt { return first.endsAt < second.endsAt } return first.title < second.title } let visibleEvents = Array(matchingEvents.prefix(limit)) let payloadEvents = visibleEvents.map { event in JSONValue.object([ "all_day": .bool(event.isAllDay), "ends_at": .string(Self.iso8601(event.endsAt)), "starts_at": .string(Self.iso8601(event.startsAt)), "title": .string( Self.calendarTitle(event.title, maximumCharacters: 160) ), ]) } let payload = Self.boundedCalendarPayload( window: window, timeZoneID: calendar.timeZone.identifier, events: payloadEvents, maximumCharacters: definition.maxOutputCharacters, alreadyTruncated: page.hasMore || matchingEvents.count > limit ) return AgentToolResult( modelText: payload, displayText: visibleEvents.isEmpty ? "No calendar events \(window.displayLabel)" : "Found \(visibleEvents.count) calendar event\(visibleEvents.count == 1 ? "" : "s")", succeeded: true ) default: throw AgentToolRegistryError.unknownTool(call.name) } } private static func schema( properties: [String: JSONValue], required: [String] ) -> JSONValue { .object([ "type": .string("object"), "properties": .object(properties), "required": .array(required.map(JSONValue.string)), "additionalProperties": .bool(false), ]) } private static func stringArraySchema( maximumItems: Int, maximumLength: Int ) -> JSONValue { .object([ "type": .string("array"), "items": .object([ "type": .string("string"), "minLength": .number(1), "maxLength": .number(Double(maximumLength)), ]), "maxItems": .number(Double(maximumItems)), ]) } private static func uuidArraySchema(maximumItems: Int) -> JSONValue { .object([ "type": .string("array"), "items": .object([ "type": .string("string"), "format": .string("uuid"), ]), "maxItems": .number(Double(maximumItems)), ]) } private static func iso8601(_ date: Date) -> String { ISO8601DateFormatter().string(from: date) } private static func formatNumber(_ value: Double) -> String { if value == 0 { return "0" } if value.rounded() == value, abs(value) <= 9_007_199_254_740_991 { return String(Int64(value)) } return String( format: "%.15g", locale: Locale(identifier: "en_US_POSIX"), value ) } private static func boundedCollectionPayload( key: String, values: [JSONValue], maximumCharacters: Int, alreadyTruncated: Bool = false ) -> String { var included: [JSONValue] = [] for value in values { let candidate = included + [value] let encoded = JSONValue.object([ "count": .number(Double(candidate.count)), key: .array(candidate), "truncated": .bool( alreadyTruncated || candidate.count < values.count ), ]).canonicalJSON guard encoded.count <= maximumCharacters else { break } included = candidate } return JSONValue.object([ "count": .number(Double(included.count)), key: .array(included), "truncated": .bool( alreadyTruncated || included.count < values.count ), ]).canonicalJSON } private static func boundedKnowledgeSearchPayload( query: String, values: [JSONValue], maximumCharacters: Int, alreadyTruncated: Bool ) -> String { var included: [JSONValue] = [] for value in values { let candidate = included + [value] let encoded = knowledgeSearchPayload( query: query, values: candidate, truncated: alreadyTruncated || candidate.count < values.count ) guard encoded.count <= maximumCharacters else { break } included = candidate } return knowledgeSearchPayload( query: query, values: included, truncated: alreadyTruncated || included.count < values.count ) } private static func boundedMemorySearchPayload( query: String, values: [JSONValue], maximumCharacters: Int, alreadyTruncated: Bool ) -> String { var included: [JSONValue] = [] for value in values { let candidate = included + [value] let encoded = memorySearchPayload( query: query, values: candidate, truncated: alreadyTruncated || candidate.count < values.count ) guard encoded.count <= maximumCharacters else { break } included = candidate } return memorySearchPayload( query: query, values: included, truncated: alreadyTruncated || included.count < values.count ) } private static func memorySearchPayload( query: String, values: [JSONValue], truncated: Bool ) -> String { JSONValue.object([ "count": .number(Double(values.count)), "memories": .array(values), "query": .string(query), "truncated": .bool(truncated), ]).canonicalJSON } private static func knowledgeSearchPayload( query: String, values: [JSONValue], truncated: Bool ) -> String { JSONValue.object([ "count": .number(Double(values.count)), "items": .array(values), "query": .string(query), "retrieval": .string("local_hybrid_lexical"), "truncated": .bool(truncated), ]).canonicalJSON } private static func boundedKnowledgeDetailPayload( _ item: KnowledgeItem, maximumCharacters: Int ) -> String { var contentLimit = min(item.content.count, 1_800) while true { let content = clipped( item.content, maximumCharacters: max(contentLimit, 64) ) let payload = JSONValue.object([ "content": .string(content), "content_truncated": .bool(content != item.content), "created_at": .string(iso8601(item.createdAt)), "id": .string(item.id.uuidString), "kind": .string(item.kind.rawValue), "related_ids": .array( item.relatedItemIDs.map { .string($0.uuidString) } ), "revision": .number(Double(item.revision)), "source": .string(item.source.rawValue), "tags": .array(item.tags.map(JSONValue.string)), "title": .string(item.title), "updated_at": .string(iso8601(item.updatedAt)), ]).canonicalJSON if payload.count <= maximumCharacters || contentLimit <= 64 { return payload } contentLimit = max(64, contentLimit - 192) } } private static func boundedProjectReviewPayload( digest: KnowledgeDigest, insights: [ProactiveKnowledgeInsight], workspace: AssistantWorkspace, activeHandoffID: UUID?, maximumCharacters: Int ) -> String { let summary = clipped(digest.summary, maximumCharacters: 900) let insightValues = insights.map { insight in JSONValue.object([ "detail": .string(clipped(insight.detail, maximumCharacters: 300)), "evidence_ids": .array( insight.evidenceItemIDs.prefix(6).map { .string($0.uuidString) } ), "id": .string(insight.id.uuidString), "priority": .string(insight.priority.rawValue), "title": .string(insight.title), ]) } var priorities: [String] = [] var questions: [String] = [] var evidenceIDs: [UUID] = [] var includedInsights: [JSONValue] = [] func payload(truncated: Bool) -> String { JSONValue.object([ "active_handoff_id": activeHandoffID.map { .string($0.uuidString) } ?? .null, "evidence_ids": .array( evidenceIDs.map { .string($0.uuidString) } ), "generated_at": .string(iso8601(digest.createdAt)), "handoff_count": .number(Double(workspace.handoffs.count)), "insights": .array(includedInsights), "knowledge_count": .number(Double(workspace.knowledgeItems.count)), "open_questions": .array(questions.map(JSONValue.string)), "open_task_count": .number( Double(workspace.tasks.filter { !$0.isCompleted }.count) ), "priorities": .array(priorities.map(JSONValue.string)), "summary": .string(summary), "truncated": .bool(truncated), ]).canonicalJSON } for priority in digest.priorities.prefix(6) { let previous = priorities priorities.append(clipped(priority, maximumCharacters: 180)) if payload(truncated: true).count > maximumCharacters { priorities = previous break } } for question in digest.openQuestions.prefix(4) { let previous = questions questions.append(clipped(question, maximumCharacters: 220)) if payload(truncated: true).count > maximumCharacters { questions = previous break } } for id in digest.evidenceItemIDs.prefix(20) { let previous = evidenceIDs evidenceIDs.append(id) if payload(truncated: true).count > maximumCharacters { evidenceIDs = previous break } } for insight in insightValues { let previous = includedInsights includedInsights.append(insight) if payload(truncated: true).count > maximumCharacters { includedInsights = previous break } } let truncated = summary != digest.summary || priorities.count != min(digest.priorities.count, 6) || questions.count != min(digest.openQuestions.count, 4) || evidenceIDs.count != min(digest.evidenceItemIDs.count, 20) || includedInsights.count != insightValues.count return payload(truncated: truncated) } private static func boundedHandoffDetailPayload( _ handoff: SessionHandoff, isActive: Bool, maximumCharacters: Int ) -> String { let summary = clipped(handoff.summary, maximumCharacters: 900) var nextSteps: [String] = [] var knowledgeIDs: [UUID] = [] var taskIDs: [UUID] = [] func payload(truncated: Bool) -> String { JSONValue.object([ "active": .bool(isActive), "created_at": .string(iso8601(handoff.createdAt)), "focus": .string(handoff.focus), "id": .string(handoff.id.uuidString), "knowledge_ids": .array( knowledgeIDs.map { .string($0.uuidString) } ), "next_steps": .array(nextSteps.map(JSONValue.string)), "restored_at": handoff.restoredAt.map { .string(iso8601($0)) } ?? .null, "summary": .string(summary), "summary_truncated": .bool(summary != handoff.summary), "task_ids": .array(taskIDs.map { .string($0.uuidString) }), "title": .string(handoff.title), "truncated": .bool(truncated), ]).canonicalJSON } for step in handoff.nextSteps { let previous = nextSteps nextSteps.append(step) if payload(truncated: true).count > maximumCharacters { nextSteps = previous break } } for id in handoff.knowledgeItemIDs { let previous = knowledgeIDs knowledgeIDs.append(id) if payload(truncated: true).count > maximumCharacters { knowledgeIDs = previous break } } for id in handoff.taskIDs { let previous = taskIDs taskIDs.append(id) if payload(truncated: true).count > maximumCharacters { taskIDs = previous break } } return payload( truncated: summary != handoff.summary || nextSteps.count != handoff.nextSteps.count || knowledgeIDs.count != handoff.knowledgeItemIDs.count || taskIDs.count != handoff.taskIDs.count ) } private static func boundedTaskSearchPayload( query: String, values: [JSONValue], maximumCharacters: Int, alreadyTruncated: Bool ) -> String { var included: [JSONValue] = [] for value in values { let candidate = included + [value] let encoded = taskSearchPayload( query: query, values: candidate, truncated: alreadyTruncated || candidate.count < values.count ) guard encoded.count <= maximumCharacters else { break } included = candidate } return taskSearchPayload( query: query, values: included, truncated: alreadyTruncated || included.count < values.count ) } private static func taskSearchPayload( query: String, values: [JSONValue], truncated: Bool ) -> String { JSONValue.object([ "count": .number(Double(values.count)), "query": .string(query), "tasks": .array(values), "truncated": .bool(truncated), ]).canonicalJSON } private static func boundedProjectContextPayload( digest: KnowledgeDigest, activeHandoffID: UUID?, maximumCharacters: Int ) -> String { let summary = clipped( digest.summary, maximumCharacters: min(1_200, max(256, maximumCharacters / 2)) ) var priorities: [String] = [] var questions: [String] = [] func payload( priorities: [String], questions: [String], truncated: Bool ) -> String { JSONValue.object([ "active_handoff_id": activeHandoffID.map { .string($0.uuidString) } ?? .null, "evidence_count": .number(Double(digest.evidenceItemIDs.count)), "generated_at": .string(iso8601(digest.createdAt)), "open_questions": .array(questions.map(JSONValue.string)), "priorities": .array(priorities.map(JSONValue.string)), "summary": .string(summary), "truncated": .bool(truncated), ]).canonicalJSON } for priority in digest.priorities { let candidate = priorities + [priority] guard payload( priorities: candidate, questions: questions, truncated: true ).count <= maximumCharacters else { break } priorities = candidate } for question in digest.openQuestions { let candidate = questions + [question] guard payload( priorities: priorities, questions: candidate, truncated: true ).count <= maximumCharacters else { break } questions = candidate } return payload( priorities: priorities, questions: questions, truncated: summary != digest.summary || priorities.count != digest.priorities.count || questions.count != digest.openQuestions.count ) } private static func boundedCodeAnalysisPayload( analysis: CodeAnalysis, findings: [JSONValue], inputSHA256: String, maximumCharacters: Int ) -> String { var included: [JSONValue] = [] for finding in findings { let candidate = included + [finding] let encoded = codeAnalysisPayload( analysis: analysis, findings: candidate, inputSHA256: inputSHA256, truncated: analysis.wasTruncated || candidate.count < findings.count ) guard encoded.count <= maximumCharacters else { break } included = candidate } return codeAnalysisPayload( analysis: analysis, findings: included, inputSHA256: inputSHA256, truncated: analysis.wasTruncated || included.count < findings.count ) } private static func codeAnalysisPayload( analysis: CodeAnalysis, findings: [JSONValue], inputSHA256: String, truncated: Bool ) -> String { JSONValue.object([ "analyzed_characters": .number(Double(analysis.analyzedCharacterCount)), "findings": .array(findings), "input_sha256": .string(inputSHA256), "language": .string(analysis.language), "line_count": .number(Double(analysis.lineCount)), "max_nesting": .number(Double(analysis.maxNestingApproximation)), "nonempty_line_count": .number(Double(analysis.nonemptyLineCount)), "truncated": .bool(truncated), ]).canonicalJSON } private static func boundedCalendarPayload( window: CalendarQueryWindow, timeZoneID: String, events: [JSONValue], maximumCharacters: Int, alreadyTruncated: Bool ) -> String { var included: [JSONValue] = [] for event in events { let candidate = included + [event] let encoded = calendarPayload( window: window, timeZoneID: timeZoneID, events: candidate, truncated: alreadyTruncated || candidate.count < events.count ) guard encoded.count <= maximumCharacters else { break } included = candidate } return calendarPayload( window: window, timeZoneID: timeZoneID, events: included, truncated: alreadyTruncated || included.count < events.count ) } private static func calendarPayload( window: CalendarQueryWindow, timeZoneID: String, events: [JSONValue], truncated: Bool ) -> String { JSONValue.object([ "count": .number(Double(events.count)), "events": .array(events), "time_zone": .string(timeZoneID), "truncated": .bool(truncated), "window": .string(window.rawValue), ]).canonicalJSON } private static func calendarTitle( _ title: String, maximumCharacters: Int ) -> String { let sanitized = sanitizeUntrusted(title) .split(whereSeparator: { $0.isWhitespace }) .joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) let fallback = sanitized.isEmpty ? "Untitled event" : sanitized return clipped(fallback, maximumCharacters: maximumCharacters) } private struct RankedMemory { let item: MemoryItem let score: Int } private static let memorySearchStopWords: Set = [ "a", "about", "am", "an", "and", "are", "did", "do", "does", "for", "i", "is", "it", "me", "my", "of", "please", "recall", "remember", "s", "search", "tell", "the", "to", "was", "were", "what", "whats", "who", "you", "your", ] private static func memoryMatches( query: String, memories: [MemoryItem], limit: Int ) -> [MemoryItem] { let normalizedQuery = normalizedSearchText(query) let queryTerms = searchTerms(in: normalizedQuery) let ranked = memories.compactMap { item -> RankedMemory? in let normalizedContent = normalizedSearchText(item.content) let contentTerms = searchTerms(in: normalizedContent) let overlap = queryTerms.intersection(contentTerms).count let exactMatch = !normalizedQuery.isEmpty && normalizedContent.contains(normalizedQuery) guard exactMatch || overlap > 0 else { return nil } return RankedMemory( item: item, score: (exactMatch ? 100 : 0) + overlap * 10 ) }.sorted { first, second in if first.score != second.score { return first.score > second.score } return first.item.createdAt > second.item.createdAt } return Array(ranked.prefix(limit).map(\.item)) } private static func normalizedSearchText(_ text: String) -> String { text.folding( options: [.caseInsensitive, .diacriticInsensitive], locale: Locale(identifier: "en_US_POSIX") ).split { character in !character.isLetter && !character.isNumber }.joined(separator: " ") } private static func searchTerms(in text: String) -> Set { Set( text.split(separator: " ").map(String.init).filter { token in token.count > 1 && !memorySearchStopWords.contains(token) } ) } private static func clipped( _ text: String, maximumCharacters: Int ) -> String { guard text.count > maximumCharacters else { return text } let suffix = "… [truncated]" let prefixCount = max(0, maximumCharacters - suffix.count) return String(text.prefix(prefixCount)) + suffix } private static func sanitizeUntrusted(_ text: String) -> String { let allowedControls = CharacterSet(charactersIn: "\n\r\t") let scalars = text.unicodeScalars.filter { scalar in !CharacterSet.controlCharacters.contains(scalar) || allowedControls.contains(scalar) } return String(String.UnicodeScalarView(scalars)) .replacingOccurrences(of: "<", with: "‹") .replacingOccurrences(of: ">", with: "›") } } private struct ExactArguments { private let values: [String: JSONValue] init( _ values: [String: JSONValue], allowed: Set, required: Set ) throws { let unexpected = Set(values.keys).subtracting(allowed).sorted() guard unexpected.isEmpty else { throw AgentToolRegistryError.unexpectedArguments(unexpected) } for key in required.sorted() where values[key] == nil { throw AgentToolRegistryError.missingArgument(key) } self.values = values } func string(_ name: String, maximumLength: Int) throws -> String { guard let value = values[name] else { throw AgentToolRegistryError.missingArgument(name) } guard case .string(let raw) = value else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected a string" ) } let result = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !result.isEmpty else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "must not be empty" ) } guard result.count <= maximumLength else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "must be at most \(maximumLength) characters" ) } return result } func optionalString( _ name: String, default defaultValue: String, maximumLength: Int ) throws -> String { guard values[name] != nil else { return defaultValue } return try string(name, maximumLength: maximumLength) } func stringArray( _ name: String, default defaultValue: [String], maximumCount: Int, maximumElementLength: Int ) throws -> [String] { guard let value = values[name] else { return defaultValue } guard case .array(let rawValues) = value else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected an array of strings" ) } guard rawValues.count <= maximumCount else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "must contain at most \(maximumCount) values" ) } var result: [String] = [] for rawValue in rawValues { guard case .string(let raw) = rawValue else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected an array of strings" ) } let cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty, cleaned.count <= maximumElementLength else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "each value must contain 1...\(maximumElementLength) characters" ) } if !result.contains(cleaned) { result.append(cleaned) } } return result } func uuidArray( _ name: String, default defaultValue: [UUID], maximumCount: Int ) throws -> [UUID] { let rawValues = try stringArray( name, default: defaultValue.map(\.uuidString), maximumCount: maximumCount, maximumElementLength: 36 ) return try rawValues.map { raw in guard let value = UUID(uuidString: raw) else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected UUID strings" ) } return value } } func integer( _ name: String, default defaultValue: Int, range: ClosedRange ) throws -> Int { guard let value = values[name] else { return defaultValue } guard case .number(let number) = value, number.isFinite, number.rounded(.towardZero) == number, number >= Double(Int.min), number <= Double(Int.max) else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected an integer" ) } let result = Int(number) guard range.contains(result) else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected \(range.lowerBound)...\(range.upperBound)" ) } return result } func boolean(_ name: String, default defaultValue: Bool) throws -> Bool { guard let value = values[name] else { return defaultValue } guard case .bool(let result) = value else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected a boolean" ) } return result } func uuid(_ name: String) throws -> UUID { let raw = try string(name, maximumLength: 36) guard let result = UUID(uuidString: raw) else { throw AgentToolRegistryError.invalidArgument( name: name, reason: "expected a UUID" ) } return result } } private enum SafeCalculatorError: LocalizedError, Sendable { case invalidExpression case tooComplex case divisionByZero case nonFiniteResult var errorDescription: String? { switch self { case .invalidExpression: "The arithmetic expression is invalid." case .tooComplex: "The arithmetic expression is too complex." case .divisionByZero: "Division by zero is not allowed." case .nonFiniteResult: "The arithmetic result is outside the supported range." } } } private enum SafeCalculator { static func evaluate(_ expression: String) throws -> Double { var parser = Parser(characters: Array(expression)) let value = try parser.parseExpression() parser.skipWhitespace() guard parser.isAtEnd else { throw SafeCalculatorError.invalidExpression } guard value.isFinite else { throw SafeCalculatorError.nonFiniteResult } return value } private struct Parser { let characters: [Character] var index = 0 var operationCount = 0 var nestingDepth = 0 var isAtEnd: Bool { index >= characters.count } mutating func parseExpression() throws -> Double { var value = try parseTerm() while true { skipWhitespace() if consume("+") { value = try checked(value + parseTerm()) } else if consume("-") { value = try checked(value - parseTerm()) } else { return value } try countOperation() } } mutating func parseTerm() throws -> Double { var value = try parseUnary() while true { skipWhitespace() if consume("*") { value = try checked(value * parseUnary()) } else if consume("/") { let divisor = try parseUnary() guard divisor != 0 else { throw SafeCalculatorError.divisionByZero } value = try checked(value / divisor) } else if consume("%") { let divisor = try parseUnary() guard divisor != 0 else { throw SafeCalculatorError.divisionByZero } value = try checked(value.truncatingRemainder(dividingBy: divisor)) } else { return value } try countOperation() } } mutating func parseUnary() throws -> Double { skipWhitespace() if consume("+") { try countOperation() return try parseUnary() } if consume("-") { try countOperation() return try checked(-(parseUnary())) } return try parsePrimary() } mutating func parsePrimary() throws -> Double { skipWhitespace() if consume("(") { nestingDepth += 1 guard nestingDepth <= 20 else { throw SafeCalculatorError.tooComplex } let result = try parseExpression() skipWhitespace() guard consume(")") else { throw SafeCalculatorError.invalidExpression } nestingDepth -= 1 return result } return try parseNumber() } mutating func parseNumber() throws -> Double { skipWhitespace() let start = index var digitCount = 0 while let character = peek(), Self.isDigit(character) { digitCount += 1 index += 1 } if consume(".") { while let character = peek(), Self.isDigit(character) { digitCount += 1 index += 1 } } guard digitCount > 0 else { throw SafeCalculatorError.invalidExpression } if let character = peek(), character == "e" || character == "E" { index += 1 if let sign = peek(), sign == "+" || sign == "-" { index += 1 } let exponentStart = index while let digit = peek(), Self.isDigit(digit) { index += 1 } guard index > exponentStart else { throw SafeCalculatorError.invalidExpression } } let raw = String(characters[start.. Bool { guard peek() == expected else { return false } index += 1 return true } func peek() -> Character? { isAtEnd ? nil : characters[index] } mutating func countOperation() throws { operationCount += 1 guard operationCount <= 100 else { throw SafeCalculatorError.tooComplex } } func checked(_ value: Double) throws -> Double { guard value.isFinite else { throw SafeCalculatorError.nonFiniteResult } return value } static func isDigit(_ character: Character) -> Bool { character >= "0" && character <= "9" } } }