| import Foundation |
|
|
| enum AgentRunCheckpointUpdate: Sendable, Equatable { |
| case phase(AgentRunPhase) |
| case activeCall(AgentToolCall, phase: AgentRunPhase) |
| case counters(toolCallsUsed: Int, modelTurns: Int) |
| case completedReceipt( |
| AgentToolReceipt, |
| toolCallsUsed: Int, |
| modelTurns: Int |
| ) |
| } |
|
|
| struct AgentLoopCallbacks: Sendable { |
| let normalizeTool: @Sendable (AgentToolCall) async throws -> AgentToolCall |
| let executeTool: @Sendable (AgentToolCall) async throws -> AgentToolResult |
| let requestApproval: |
| @MainActor @Sendable (ToolApprovalRequest) async -> Bool |
| let emit: @MainActor @Sendable (AgentEvent) -> Void |
| let modelProgress: |
| @MainActor @Sendable (DolphinGenerationProgress) -> Void |
| let checkpoint: |
| @MainActor @Sendable (AgentRunCheckpointUpdate) async throws -> Void |
|
|
| init( |
| normalizeTool: @escaping @Sendable (AgentToolCall) async throws -> AgentToolCall, |
| executeTool: @escaping @Sendable (AgentToolCall) async throws -> AgentToolResult, |
| requestApproval: @escaping @MainActor @Sendable ( |
| ToolApprovalRequest |
| ) async -> Bool, |
| emit: @escaping @MainActor @Sendable (AgentEvent) -> Void, |
| modelProgress: @escaping @MainActor @Sendable ( |
| DolphinGenerationProgress |
| ) -> Void, |
| checkpoint: @escaping @MainActor @Sendable ( |
| AgentRunCheckpointUpdate |
| ) async throws -> Void = { _ in } |
| ) { |
| self.normalizeTool = normalizeTool |
| self.executeTool = executeTool |
| self.requestApproval = requestApproval |
| self.emit = emit |
| self.modelProgress = modelProgress |
| self.checkpoint = checkpoint |
| } |
| } |
|
|
| enum AgentRunOutcome: Sendable, Equatable { |
| case succeeded |
| case failed |
| } |
|
|
| struct AgentRunResult: Sendable, Equatable { |
| let finalText: String |
| let outcome: AgentRunOutcome |
| let modelTurns: Int |
| let toolCalls: Int |
| let trustedToolResults: Int |
| let failedToolCalls: Int |
| } |
|
|
| private struct AgentFinalPresentation: Sendable { |
| let text: String |
| let outcome: AgentRunOutcome |
| let status: AgentEventStatus |
| let title: String |
| let detail: String |
| } |
|
|
| enum AgentLoopError: LocalizedError, Sendable, Equatable { |
| case timeLimitReached |
| case emptyFinalResponse |
|
|
| var errorDescription: String? { |
| switch self { |
| case .timeLimitReached: |
| "The assistant reached its time limit." |
| case .emptyFinalResponse: |
| "The model returned an empty final response." |
| } |
| } |
| } |
|
|
| actor AgentLoop { |
| private let runtime: any DolphinModelRuntimeProtocol |
| private let dateProvider: any DateProviding |
|
|
| init( |
| runtime: any DolphinModelRuntimeProtocol, |
| dateProvider: any DateProviding = SystemDateProvider() |
| ) { |
| self.runtime = runtime |
| self.dateProvider = dateProvider |
| } |
|
|
| func run( |
| runID: UUID, |
| conversation: [AssistantMessage], |
| memoryContext: [MemoryItem] = [], |
| settings: AgentSettings, |
| toolDefinitions: [AgentToolDefinition], |
| callbacks: AgentLoopCallbacks |
| ) async throws -> AgentRunResult { |
| try Task.checkCancellation() |
| let limits = sanitized(settings) |
| let clock = ContinuousClock() |
| let deadline = clock.now.advanced( |
| by: .seconds(limits.maxRunSeconds) |
| ) |
| let networkEligibleTools = toolDefinitions.filter { |
| $0.risk != .networkRead || limits.networkAccessEnabled |
| } |
| let latestUserText = conversation.last(where: { $0.role == .user })?.content ?? "" |
| var requestPlan = AgentRequestRouter.plan(for: latestUserText) |
| let availableToolIDs = Set(networkEligibleTools.map(\.id)) |
| let canonicalAvailableIDs = availableToolIDs.intersection( |
| AgentRequestRouter.canonicalToolIDs |
| ) |
| |
| |
| |
| if canonicalAvailableIDs.isEmpty, !availableToolIDs.isEmpty { |
| requestPlan = AgentRequestPlan( |
| initialToolCall: nil, |
| eligibleToolIDs: availableToolIDs |
| ) |
| } |
| var selectedToolIDs = requestPlan.eligibleToolIDs |
| selectedToolIDs.formUnion(requestPlan.plannedToolCalls.map(\.name)) |
| |
| |
| if canonicalAvailableIDs.isEmpty { |
| selectedToolIDs = availableToolIDs |
| } else { |
| selectedToolIDs.formUnion( |
| availableToolIDs.subtracting(AgentRequestRouter.canonicalToolIDs) |
| ) |
| } |
| let tools = networkEligibleTools.filter { |
| selectedToolIDs.contains($0.id) |
| } |
| let definitionsByID = Dictionary( |
| uniqueKeysWithValues: tools.map { ($0.id, $0) } |
| ) |
| var plannedToolCalls = requestPlan.plannedToolCalls |
| let plannedToolIDs = Set(plannedToolCalls.map(\.name)) |
| let modelTools = tools.filter { definition in |
| !plannedToolIDs.contains(definition.id) |
| } |
| let emitter = AgentEventEmitter(runID: runID, sink: callbacks.emit) |
| var scratchpad: [DolphinPromptMessage] = [] |
| var signatures: Set<String> = [] |
| var modelTurns = 0 |
| var toolCalls = 0 |
| var trustedToolResults = 0 |
| var failedToolCalls = 0 |
| var receipts: [AgentToolReceipt] = [] |
| var attemptedToolAction = false |
|
|
| await emitter.emit( |
| kind: .run, |
| status: .running, |
| title: "Run started", |
| detail: "Bounded to \(limits.maxToolSteps) tool steps and \(limits.maxNewTokens) output tokens per turn. Cancellation is requested at the \(limits.maxRunSeconds)-second deadline." |
| ) |
| try await callbacks.checkpoint(.phase(.routing)) |
| try Task.checkCancellation() |
|
|
| await emitter.emit( |
| kind: .run, |
| status: .information, |
| title: "Plan prepared", |
| detail: requestPlan.auditSummary |
| ) |
|
|
| if requestPlan.mode == .deterministic, |
| plannedToolCalls.count > limits.maxToolSteps |
| { |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| await emitter.emit( |
| kind: .run, |
| status: .failed, |
| title: "Plan exceeds action limit", |
| detail: "The deterministic plan requires \(plannedToolCalls.count) steps, but this run allows \(limits.maxToolSteps). No action was executed." |
| ) |
| return AgentRunResult( |
| finalText: "This request needs \(plannedToolCalls.count) verified actions, but the current limit is \(limits.maxToolSteps). Increase the tool-step limit or split the request.", |
| outcome: .failed, |
| modelTurns: 0, |
| toolCalls: 0, |
| trustedToolResults: 0, |
| failedToolCalls: 0 |
| ) |
| } |
|
|
| for _ in 0..<limits.maxToolSteps { |
| try Task.checkCancellation() |
| try checkDeadline(deadline) |
|
|
| if requestPlan.mode == .deterministic, plannedToolCalls.isEmpty { |
| return try await finishDeterministicPlan( |
| requestPlan, |
| receipts: receipts, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls, |
| emitter: emitter, |
| callbacks: callbacks |
| ) |
| } |
|
|
| let call: AgentToolCall |
| if !plannedToolCalls.isEmpty { |
| let routedCall = plannedToolCalls.removeFirst() |
| call = routedCall |
| await emitter.emit( |
| kind: .run, |
| status: .information, |
| title: "Request routed", |
| detail: definitionsByID[routedCall.name]?.displayName |
| ?? "Unavailable required tool: \(routedCall.name)" |
| ) |
| try await callbacks.checkpoint( |
| .activeCall(routedCall, phase: .routing) |
| ) |
| try Task.checkCancellation() |
| } else { |
| try await callbacks.checkpoint(.phase(.modelGeneration)) |
| try Task.checkCancellation() |
| let turn = try await generateTurn( |
| conversation: conversation, |
| memoryContext: memoryContext, |
| scratchpad: scratchpad, |
| systemPrompt: limits.systemPrompt, |
| tools: modelTools, |
| maxNewTokens: limits.maxNewTokens, |
| emitter: emitter, |
| progress: callbacks.modelProgress |
| ) |
| modelTurns += 1 |
| try await callbacks.checkpoint( |
| .counters(toolCallsUsed: toolCalls, modelTurns: modelTurns) |
| ) |
| try Task.checkCancellation() |
| try checkDeadline(deadline) |
|
|
| let parsedCall: AgentToolCall? |
| do { |
| parsedCall = try ToolCallParser.parse(turn.text) |
| } catch { |
| attemptedToolAction = true |
| toolCalls += 1 |
| failedToolCalls += 1 |
| try await callbacks.checkpoint( |
| .counters(toolCallsUsed: toolCalls, modelTurns: modelTurns) |
| ) |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Invalid tool request", |
| detail: error.localizedDescription |
| ) |
| scratchpad.append(.assistant(turn.text)) |
| scratchpad.append( |
| .user( |
| "Tool protocol error: \(error.localizedDescription) Return either one valid tool call or a final answer." |
| ) |
| ) |
| continue |
| } |
|
|
| guard let parsedCall else { |
| if attemptedToolAction && failedToolCalls > 0 { |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| let safeFailure = partialFailureText( |
| "I couldn’t complete every requested action safely. Review Activity for the denied or invalid step, then adjust permissions or try again.", |
| receipts: receipts |
| ) |
| await emitter.emit( |
| kind: .run, |
| status: .failed, |
| title: "Run stopped safely", |
| detail: "At least one requested action did not produce a trusted result." |
| ) |
| return AgentRunResult( |
| finalText: safeFailure, |
| outcome: .failed, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
| let finalText = try presentationModelText( |
| from: turn.text, |
| receipts: receipts |
| ) |
| let presentation = finalPresentation( |
| modelText: finalText, |
| receipts: receipts |
| ) |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| await emitter.emit( |
| kind: .run, |
| status: presentation.status, |
| title: presentation.title, |
| detail: presentation.detail + " · " + completionDetail( |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| ) |
| return AgentRunResult( |
| finalText: presentation.text, |
| outcome: presentation.outcome, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
| call = parsedCall |
| } |
|
|
| attemptedToolAction = true |
| toolCalls += 1 |
| try await callbacks.checkpoint( |
| .counters(toolCallsUsed: toolCalls, modelTurns: modelTurns) |
| ) |
| try Task.checkCancellation() |
| guard let definition = definitionsByID[call.name] else { |
| failedToolCalls += 1 |
| scratchpad.append(.assistantToolCall(call)) |
| await emitter.emit( |
| kind: .tool, |
| status: .denied, |
| title: "Unknown tool blocked", |
| detail: call.name |
| ) |
| scratchpad.append( |
| .toolResponse(errorObservation("Unknown tool: \(call.name)")) |
| ) |
| continue |
| } |
|
|
| let normalizedCall: AgentToolCall |
| do { |
| normalizedCall = try await callbacks.normalizeTool(call) |
| } catch { |
| failedToolCalls += 1 |
| scratchpad.append(.assistantToolCall(call)) |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Invalid arguments", |
| detail: "\(definition.displayName): \(error.localizedDescription)" |
| ) |
| scratchpad.append( |
| .toolResponse(errorObservation(error.localizedDescription)) |
| ) |
| continue |
| } |
| scratchpad.append(.assistantToolCall(normalizedCall)) |
| try await callbacks.checkpoint( |
| .activeCall(normalizedCall, phase: .routing) |
| ) |
| try Task.checkCancellation() |
|
|
| guard signatures.insert(normalizedCall.signature).inserted else { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .denied, |
| title: "Duplicate action blocked", |
| detail: definition.displayName |
| ) |
| scratchpad.append( |
| .toolResponse(errorObservation("Duplicate tool call blocked.")) |
| ) |
| continue |
| } |
|
|
| switch AgentPolicy.decision(for: definition, settings: limits) { |
| case .deny(let reason): |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .denied, |
| title: "Action denied by policy", |
| detail: "\(definition.displayName): \(reason)" |
| ) |
| scratchpad.append(.toolResponse(errorObservation(reason))) |
| continue |
|
|
| case .requireApproval(let reason): |
| let createdAt = dateProvider.now() |
| let request = ToolApprovalRequest( |
| runID: runID, |
| call: normalizedCall, |
| toolName: definition.displayName, |
| reason: reason, |
| createdAt: createdAt, |
| expiresAt: min( |
| createdAt.addingTimeInterval(10 * 60), |
| createdAt.addingTimeInterval( |
| remainingSeconds(until: deadline, clock: clock) |
| ) |
| ) |
| ) |
| await emitter.emit( |
| kind: .approval, |
| status: .awaitingApproval, |
| title: "Approval requested", |
| detail: definition.displayName |
| ) |
| try await callbacks.checkpoint( |
| .activeCall(normalizedCall, phase: .awaitingApproval) |
| ) |
| try Task.checkCancellation() |
| let approved = await callbacks.requestApproval(request) |
| try Task.checkCancellation() |
| guard approved, !request.isExpired(at: dateProvider.now()) else { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .approval, |
| status: .denied, |
| title: approved ? "Approval expired" : "Action denied", |
| detail: definition.displayName |
| ) |
| scratchpad.append( |
| .toolResponse( |
| errorObservation( |
| approved ? "Approval expired." : "The user denied this action." |
| ) |
| ) |
| ) |
| continue |
| } |
| await emitter.emit( |
| kind: .approval, |
| status: .succeeded, |
| title: "Approved once", |
| detail: definition.displayName |
| ) |
|
|
| case .allow: |
| break |
| } |
|
|
| try checkDeadline(deadline) |
| try await callbacks.checkpoint( |
| .activeCall(normalizedCall, phase: .toolExecution) |
| ) |
| try Task.checkCancellation() |
| await emitter.emit( |
| kind: .tool, |
| status: .running, |
| title: "Running tool", |
| detail: definition.displayName |
| ) |
| do { |
| let rawResult = try await callbacks.executeTool(normalizedCall) |
| |
| |
| |
| switch definition.risk { |
| case .readOnly, .sensitiveRead, .networkRead: |
| try Task.checkCancellation() |
| case .localWrite: |
| break |
| } |
| let result = rawResult.bounded( |
| toMaximumCharacters: definition.maxOutputCharacters |
| ) |
| if result.succeeded { |
| guard |
| let receipt = AgentToolReceipt( |
| runID: runID, |
| call: normalizedCall, |
| definition: definition, |
| result: result, |
| completedAt: dateProvider.now() |
| ) |
| else { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Tool result rejected", |
| detail: "The successful result could not be recorded safely." |
| ) |
| scratchpad.append( |
| .toolResponse( |
| errorObservation( |
| "The tool result could not be recorded safely." |
| ) |
| ) |
| ) |
| try Task.checkCancellation() |
| continue |
| } |
| guard AgentVerifiedAnswerRenderer.acceptsAsTrustedReceipt(receipt) |
| else { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Tool result rejected", |
| detail: "The canonical observation did not match the normalized call and expected schema." |
| ) |
| scratchpad.append( |
| .toolResponse( |
| errorObservation( |
| "The tool observation failed canonical schema or call-correlation validation." |
| ) |
| ) |
| ) |
| try Task.checkCancellation() |
| continue |
| } |
| trustedToolResults += 1 |
| receipts.append(receipt) |
| await emitter.emit( |
| kind: .tool, |
| status: .succeeded, |
| title: "Tool completed", |
| detail: receipt.evidenceJSON, |
| receipt: receipt |
| ) |
| try await callbacks.checkpoint( |
| .completedReceipt( |
| receipt, |
| toolCallsUsed: toolCalls, |
| modelTurns: modelTurns |
| ) |
| ) |
| } else { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Tool failed", |
| detail: result.displayText |
| ) |
| } |
| scratchpad.append(.toolResponse(result.modelText)) |
| |
| |
| try Task.checkCancellation() |
| } catch is CancellationError { |
| throw CancellationError() |
| } catch { |
| failedToolCalls += 1 |
| await emitter.emit( |
| kind: .tool, |
| status: .failed, |
| title: "Tool failed", |
| detail: error.localizedDescription |
| ) |
| scratchpad.append( |
| .toolResponse(errorObservation(error.localizedDescription)) |
| ) |
| } |
| } |
|
|
| if requestPlan.mode == .deterministic { |
| return try await finishDeterministicPlan( |
| requestPlan, |
| receipts: receipts, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls, |
| emitter: emitter, |
| callbacks: callbacks |
| ) |
| } |
|
|
| try checkDeadline(deadline) |
| await emitter.emit( |
| kind: .run, |
| status: .information, |
| title: "Action limit reached", |
| detail: "Generating a final answer without additional tools." |
| ) |
| try await callbacks.checkpoint(.phase(.modelGeneration)) |
| try Task.checkCancellation() |
| let finalTurn = try await generateTurn( |
| conversation: conversation, |
| memoryContext: memoryContext, |
| scratchpad: scratchpad, |
| systemPrompt: limits.systemPrompt, |
| tools: [], |
| maxNewTokens: limits.maxNewTokens, |
| emitter: emitter, |
| progress: callbacks.modelProgress |
| ) |
| modelTurns += 1 |
| try await callbacks.checkpoint( |
| .counters(toolCallsUsed: toolCalls, modelTurns: modelTurns) |
| ) |
| try Task.checkCancellation() |
| try checkDeadline(deadline) |
|
|
| let finalRequestedAnotherTool: Bool |
| do { |
| finalRequestedAnotherTool = try ToolCallParser.parse(finalTurn.text) != nil |
| } catch { |
| finalRequestedAnotherTool = true |
| } |
| if finalRequestedAnotherTool { |
| failedToolCalls += 1 |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| let safeFailure = partialFailureText( |
| "I reached the action limit before I could finish. Review Activity and try a narrower request.", |
| receipts: receipts |
| ) |
| await emitter.emit( |
| kind: .run, |
| status: .failed, |
| title: "Run limit reached", |
| detail: "A further tool request was blocked." |
| ) |
| return AgentRunResult( |
| finalText: safeFailure, |
| outcome: .failed, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
|
|
| if attemptedToolAction && failedToolCalls > 0 { |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| let safeFailure = partialFailureText( |
| "I couldn’t complete every requested action safely. Review Activity for the denied or invalid step, then adjust permissions or try again.", |
| receipts: receipts |
| ) |
| await emitter.emit( |
| kind: .run, |
| status: .failed, |
| title: "Run stopped safely", |
| detail: "At least one requested action did not produce a trusted result." |
| ) |
| return AgentRunResult( |
| finalText: safeFailure, |
| outcome: .failed, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
|
|
| let finalText = try presentationModelText( |
| from: finalTurn.text, |
| receipts: receipts |
| ) |
| let presentation = finalPresentation(modelText: finalText, receipts: receipts) |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| await emitter.emit( |
| kind: .run, |
| status: presentation.status, |
| title: presentation.title, |
| detail: presentation.detail + " · " + completionDetail( |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| ) |
| return AgentRunResult( |
| finalText: presentation.text, |
| outcome: presentation.outcome, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
|
|
| private func finishDeterministicPlan( |
| _ plan: AgentRequestPlan, |
| receipts: [AgentToolReceipt], |
| modelTurns: Int, |
| toolCalls: Int, |
| trustedToolResults: Int, |
| failedToolCalls: Int, |
| emitter: AgentEventEmitter, |
| callbacks: AgentLoopCallbacks |
| ) async throws -> AgentRunResult { |
| let receivedCallIDs = Set(receipts.map { $0.call.id }) |
| let missingCalls = plan.plannedToolCalls.filter { |
| !receivedCallIDs.contains($0.id) |
| } |
| guard failedToolCalls == 0, missingCalls.isEmpty else { |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| let missingNames = missingCalls.map(\.name).joined(separator: ", ") |
| let reason = missingNames.isEmpty |
| ? "At least one deterministic action failed or was denied." |
| : "Missing verified receipt(s) for: \(missingNames)." |
| let text = partialFailureText( |
| "I couldn’t complete the full verified plan. \(reason) Review Activity for the failed or denied step.", |
| receipts: receipts |
| ) |
| await emitter.emit( |
| kind: .run, |
| status: .failed, |
| title: "Verified plan incomplete", |
| detail: reason |
| ) |
| return AgentRunResult( |
| finalText: text, |
| outcome: .failed, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: max(failedToolCalls, missingCalls.count) |
| ) |
| } |
|
|
| let presentation = finalPresentation(modelText: "", receipts: receipts) |
| try await callbacks.checkpoint(.phase(.finalizing)) |
| await emitter.emit( |
| kind: .run, |
| status: presentation.status, |
| title: presentation.title, |
| detail: presentation.detail + " · " + completionDetail( |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| ) |
| return AgentRunResult( |
| finalText: presentation.text, |
| outcome: presentation.outcome, |
| modelTurns: modelTurns, |
| toolCalls: toolCalls, |
| trustedToolResults: trustedToolResults, |
| failedToolCalls: failedToolCalls |
| ) |
| } |
|
|
| private func generateTurn( |
| conversation: [AssistantMessage], |
| memoryContext: [MemoryItem], |
| scratchpad: [DolphinPromptMessage], |
| systemPrompt: String, |
| tools: [AgentToolDefinition], |
| maxNewTokens: Int, |
| emitter: AgentEventEmitter, |
| progress: @MainActor @Sendable (DolphinGenerationProgress) -> Void |
| ) async throws -> DolphinGenerationResult { |
| let prompt = try await AgentContextWindowBuilder.build( |
| systemPrompt: systemPrompt, |
| conversation: conversation, |
| memoryContext: memoryContext, |
| scratchpad: scratchpad, |
| tools: tools, |
| maxNewTokens: maxNewTokens, |
| runtime: runtime |
| ) |
| var detail = |
| "Prompt \(prompt.promptTokens) tokens · output budget \(maxNewTokens) tokens" |
| if prompt.droppedMessages > 0 { |
| detail += " · dropped \(prompt.droppedMessages) older messages" |
| } |
| if prompt.droppedScratchpadMessages > 0 { |
| detail += " · dropped \(prompt.droppedScratchpadMessages) older action messages" |
| } |
| if prompt.droppedMemoryItems > 0 { |
| detail += " · dropped \(prompt.droppedMemoryItems) older memories" |
| } |
| await emitter.emit( |
| kind: .model, |
| status: .running, |
| title: "Model turn", |
| detail: detail |
| ) |
| let result = try await runtime.generate( |
| renderedPrompt: prompt.renderedPrompt, |
| maxNewTokens: maxNewTokens, |
| progress: progress |
| ) |
| let information = await runtime.information() |
| await emitter.emit( |
| kind: .model, |
| status: .succeeded, |
| title: "Model response", |
| detail: |
| "\(result.generatedTokens) tokens · first token \(format(result.timeToFirstTokenSeconds)) s · \(format(result.tokensPerSecond)) tok/s · \(information.modelIdentifier)" |
| ) |
| return result |
| } |
|
|
| private func checkDeadline(_ deadline: ContinuousClock.Instant) throws { |
| try Task.checkCancellation() |
| guard ContinuousClock.now < deadline else { |
| throw AgentLoopError.timeLimitReached |
| } |
| } |
|
|
| private func remainingSeconds( |
| until deadline: ContinuousClock.Instant, |
| clock: ContinuousClock |
| ) -> TimeInterval { |
| let duration = clock.now.duration(to: deadline) |
| let seconds = Double(duration.components.seconds) |
| + Double(duration.components.attoseconds) |
| / 1_000_000_000_000_000_000 |
| return min(max(seconds, 0), 10 * 60) |
| } |
|
|
| private func sanitized(_ settings: AgentSettings) -> AgentSettings { |
| settings.sanitized() |
| } |
|
|
| |
| |
| |
| |
| private func presentationModelText( |
| from rawText: String, |
| receipts: [AgentToolReceipt] |
| ) throws -> String { |
| let value = AssistantChatText.cleaned(rawText) |
| if !value.isEmpty { return value } |
| if AgentVerifiedAnswerRenderer.requiresDeterministicAnswer(for: receipts) |
| || verifiedWriteText(receipts) != nil |
| { |
| return "" |
| } |
| throw AgentLoopError.emptyFinalResponse |
| } |
|
|
| private func errorObservation(_ message: String) -> String { |
| JSONValue.object([ |
| "error": .string(String(message.prefix(500))), |
| "ok": .bool(false), |
| ]).canonicalJSON |
| } |
|
|
| private func finalPresentation( |
| modelText: String, |
| receipts: [AgentToolReceipt] |
| ) -> AgentFinalPresentation { |
| guard !receipts.isEmpty else { |
| return AgentFinalPresentation( |
| text: modelText, |
| outcome: .succeeded, |
| status: .succeeded, |
| title: "Answer completed", |
| detail: "No tool was needed" |
| ) |
| } |
|
|
| let localWriteCount = receipts.count { $0.risk == .localWrite } |
| let canonicalReadRequired = |
| AgentVerifiedAnswerRenderer.requiresDeterministicAnswer(for: receipts) |
| let canonicalReadText = canonicalReadRequired |
| ? AgentVerifiedAnswerRenderer.readAnswer(for: receipts) |
| : nil |
| if canonicalReadRequired, canonicalReadText == nil { |
| return AgentFinalPresentation( |
| text: partialFailureText( |
| "A verified result couldn’t be displayed safely.", |
| receipts: receipts |
| ), |
| outcome: .failed, |
| status: .failed, |
| title: "Verified result rejected", |
| detail: "A canonical tool observation did not match its trusted schema" |
| ) |
| } |
|
|
| let writeText = verifiedWriteText(receipts) |
| if localWriteCount > 0, writeText == nil { |
| return AgentFinalPresentation( |
| text: partialFailureText( |
| "A completed local change couldn’t be summarized safely.", |
| receipts: receipts |
| ), |
| outcome: .failed, |
| status: .failed, |
| title: "Verified action summary unavailable", |
| detail: "The local change remains receipted in Activity" |
| ) |
| } |
|
|
| let authoritativeText = groundedReceiptText(receipts) ?? modelText |
| let readOnlyCount = receipts.count - localWriteCount |
| return AgentFinalPresentation( |
| text: authoritativeText, |
| outcome: .succeeded, |
| status: .succeeded, |
| title: localWriteCount > 0 |
| ? "Verified actions completed" |
| : "Verified read completed", |
| detail: "\(receipts.count) trusted result(s): \(localWriteCount) local change(s), \(readOnlyCount) read-only" |
| ) |
| } |
|
|
| private func partialFailureText( |
| _ message: String, |
| receipts: [AgentToolReceipt] |
| ) -> String { |
| guard !receipts.isEmpty else { return message } |
| if let groundedText = groundedReceiptText(receipts) { |
| return "\(message)\n\nVerified before the stop:\n\(groundedText)" |
| } |
| let hasLocalWrites = receipts.contains { $0.risk == .localWrite } |
| let completed = hasLocalWrites |
| ? "Some local changes completed before the stop." |
| : "Some read-only steps completed before the stop." |
| return "\(message)\n\n\(completed) Review Activity for the verified details." |
| } |
|
|
| private func groundedReceiptText( |
| _ receipts: [AgentToolReceipt] |
| ) -> String? { |
| var parts: [String] = [] |
| if let writeText = verifiedWriteText(receipts) { |
| parts.append(writeText) |
| } |
| if let readText = AgentVerifiedAnswerRenderer.readAnswer(for: receipts) { |
| parts.append(readText) |
| } |
| return parts.isEmpty ? nil : parts.joined(separator: "\n") |
| } |
|
|
| private func verifiedWriteText( |
| _ receipts: [AgentToolReceipt] |
| ) -> String? { |
| let lines = receipts.compactMap { receipt -> String? in |
| guard receipt.risk == .localWrite else { return nil } |
| switch receipt.call.name { |
| case "memory_save": |
| guard let content = receipt.call.arguments["content"]?.stringValue else { |
| return "Memory saved." |
| } |
| return "Got it — I’ll remember: “\(content)”." |
| case "task_add": |
| guard let title = receipt.call.arguments["title"]?.stringValue else { |
| return "Task added." |
| } |
| return "Added “\(title)” to your tasks." |
| case "task_complete": |
| return "Task marked complete." |
| default: |
| let text = receipt.displayText.trimmingCharacters( |
| in: .whitespacesAndNewlines |
| ) |
| return text.isEmpty ? nil : text + (text.hasSuffix(".") ? "" : ".") |
| } |
| } |
| return lines.isEmpty ? nil : lines.joined(separator: "\n") |
| } |
|
|
| private func completionDetail( |
| modelTurns: Int, |
| toolCalls: Int, |
| trustedToolResults: Int, |
| failedToolCalls: Int |
| ) -> String { |
| "\(modelTurns) model turns · \(toolCalls) tool calls · \(trustedToolResults) trusted results · \(failedToolCalls) failed or denied" |
| } |
|
|
| private func format(_ value: Double) -> String { |
| String(format: "%.2f", value) |
| } |
| } |
|
|
| private actor AgentEventEmitter { |
| private let runID: UUID |
| private let sink: @MainActor @Sendable (AgentEvent) -> Void |
| private var sequence = 0 |
|
|
| init( |
| runID: UUID, |
| sink: @escaping @MainActor @Sendable (AgentEvent) -> Void |
| ) { |
| self.runID = runID |
| self.sink = sink |
| } |
|
|
| func emit( |
| kind: AgentEventKind, |
| status: AgentEventStatus, |
| title: String, |
| detail: String, |
| receipt: AgentToolReceipt? = nil |
| ) async { |
| sequence += 1 |
| let event = AgentEvent( |
| runID: runID, |
| sequence: sequence, |
| kind: kind, |
| status: status, |
| title: title, |
| detail: detail, |
| receipt: receipt |
| ) |
| await sink(event) |
| } |
| } |
|
|