ales27pm's picture
Add autonomous on-device iOS assistant
7b2dfc5 verified
Raw
History Blame Contribute Delete
84.1 kB
@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<String> = [
"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<String> {
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<String>,
required: Set<String>
) 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<Int>
) 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..<index])
guard let result = Double(raw), result.isFinite else {
throw SafeCalculatorError.nonFiniteResult
}
return result
}
mutating func skipWhitespace() {
while let character = peek(), character.isWhitespace { index += 1 }
}
mutating func consume(_ expected: Character) -> 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"
}
}
}