273 lines
10 KiB
Swift
273 lines
10 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import AgentKit
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
@Model
|
|
public final class PersistentAgentConversation {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var title = ""
|
|
public var customTitle = ""
|
|
public var isPinned = false
|
|
public var archivedAt: Date?
|
|
public var createdAt: Date = Date()
|
|
public var updatedAt: Date = Date()
|
|
|
|
public init(id: UUID, title: String, createdAt: Date = .now, updatedAt: Date = .now) {
|
|
self.id = id
|
|
self.title = title
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentConversationOrder {
|
|
@Attribute(.unique) public var conversationID: UUID = UUID()
|
|
public var sortIndex = 0.0
|
|
|
|
public init(conversationID: UUID, sortIndex: Double) {
|
|
self.conversationID = conversationID
|
|
self.sortIndex = sortIndex
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentRun {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var conversationID: UUID = UUID()
|
|
public var state = AgentRunState.idle.rawValue
|
|
public var permissionMode = AgentPermissionMode.askForApproval.rawValue
|
|
public var startedAt: Date = Date()
|
|
public var finishedAt: Date?
|
|
public var failure: String?
|
|
|
|
public init(_ snapshot: AgentRunSnapshot) {
|
|
id = snapshot.id
|
|
update(snapshot)
|
|
}
|
|
|
|
public func update(_ snapshot: AgentRunSnapshot) {
|
|
conversationID = snapshot.conversationID
|
|
state = snapshot.state.rawValue
|
|
permissionMode = snapshot.permissionMode.rawValue
|
|
startedAt = snapshot.startedAt
|
|
finishedAt = snapshot.finishedAt
|
|
failure = snapshot.failure.map(AgentPersistenceRedactor.visibleText)
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentMessage {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var runID: UUID = UUID()
|
|
public var role = AgentTranscriptRole.user.rawValue
|
|
public var text = ""
|
|
public var authoredText: String?
|
|
/// Encoded native image parts attached to the message.
|
|
public var imagesData = Data()
|
|
/// Provider-authored, display-safe reasoning summaries. Opaque signatures
|
|
/// and encrypted provider state deliberately remain in memory only.
|
|
public var reasoningData = Data()
|
|
public var toolCallsData = Data()
|
|
public var toolCallID: String?
|
|
public var toolName: String?
|
|
public var isError = false
|
|
/// Marks a synthetic compaction summary in replay history.
|
|
public var isCompaction = false
|
|
/// Position inside the run's replay transcript. Timestamps are presentation
|
|
/// data and can collide at database precision, so they cannot be the sole
|
|
/// ordering key for model history.
|
|
public var sequence = 0
|
|
public var createdAt: Date = Date()
|
|
|
|
public init(_ message: AgentTranscriptMessage, runID: UUID, sequence: Int) {
|
|
id = message.id
|
|
self.runID = runID
|
|
role = message.role.rawValue
|
|
text =
|
|
message.role == .tool
|
|
? AgentPersistenceRedactor.toolResultText(message.text, toolName: message.toolName)
|
|
: AgentPersistenceRedactor.visibleText(message.text)
|
|
authoredText = message.authoredText.map(AgentPersistenceRedactor.visibleText)
|
|
imagesData = (try? JSONEncoder().encode(message.images)) ?? Data()
|
|
reasoningData = AgentPersistenceRedactor.encoded(
|
|
message.reasoning.map { block in
|
|
AgentReasoningBlock(
|
|
id: block.id,
|
|
text: AgentPersistenceRedactor.visibleText(block.text),
|
|
createdAt: block.createdAt
|
|
)
|
|
})
|
|
toolCallsData = AgentPersistenceRedactor.encoded(
|
|
message.toolCalls.map { call in
|
|
AgentToolCall(
|
|
id: call.id, name: call.name,
|
|
arguments: AgentPersistenceRedactor.redactedJSON(call.arguments),
|
|
providerItemID: call.providerItemID
|
|
)
|
|
})
|
|
toolCallID = message.toolCallID
|
|
toolName = message.toolName
|
|
isError = message.isError
|
|
isCompaction = message.isCompaction
|
|
self.sequence = sequence
|
|
createdAt = message.createdAt
|
|
}
|
|
}
|
|
|
|
/// A compaction is conversation state, not output produced by an agent run.
|
|
/// Keeping it in its own table preserves the complete run/message graph for the
|
|
/// transcript reader while giving restore an exact model-context boundary.
|
|
@Model
|
|
public final class PersistentAgentCompaction {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var conversationID: UUID = UUID()
|
|
public var summaryMessageID: UUID = UUID()
|
|
public var compactedThroughMessageID: UUID?
|
|
public var sequence = 0
|
|
public var summary = ""
|
|
public var summarizedTokens = 0
|
|
public var keptTokens = 0
|
|
public var displayAnchor: Date = Date()
|
|
public var compactedAt: Date = Date()
|
|
|
|
public init(_ record: AgentCompactionRecord, conversationID: UUID) {
|
|
id = record.id
|
|
self.conversationID = conversationID
|
|
summaryMessageID = record.summaryMessageID
|
|
compactedThroughMessageID = record.compactedThroughMessageID
|
|
sequence = record.sequence
|
|
summary = AgentPersistenceRedactor.visibleText(record.summary)
|
|
summarizedTokens = record.summarizedTokens
|
|
keptTokens = record.keptTokens
|
|
displayAnchor = record.displayAnchor
|
|
compactedAt = record.compactedAt
|
|
}
|
|
|
|
public var value: AgentCompactionRecord {
|
|
AgentCompactionRecord(
|
|
id: id, summaryMessageID: summaryMessageID,
|
|
compactedThroughMessageID: compactedThroughMessageID,
|
|
sequence: sequence, summary: summary,
|
|
summarizedTokens: summarizedTokens, keptTokens: keptTokens,
|
|
displayAnchor: displayAnchor, compactedAt: compactedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
/// V3 storage for original context snapshots. A separate model keeps the V2
|
|
/// message schema frozen and makes the migration an additive lightweight one.
|
|
@Model
|
|
public final class PersistentAgentContextAttachments {
|
|
@Attribute(.unique) public var messageID: UUID = UUID()
|
|
@Attribute(.externalStorage) public var attachmentsData = Data()
|
|
|
|
public init(messageID: UUID, attachments: [AgentContextAttachment]) {
|
|
self.messageID = messageID
|
|
attachmentsData = (try? JSONEncoder().encode(attachments)) ?? Data()
|
|
}
|
|
|
|
public var attachments: [AgentContextAttachment] {
|
|
(try? JSONDecoder().decode(
|
|
[AgentContextAttachment].self, from: attachmentsData
|
|
)) ?? []
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentToolInvocation {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var runID: UUID = UUID()
|
|
public var callID = ""
|
|
public var toolName = ""
|
|
public var argumentsData = Data()
|
|
/// Assistant message that requested the tool invocation, when applicable.
|
|
public var sourceMessageID: UUID?
|
|
public var targetHostID: UUID?
|
|
public var state = "proposed"
|
|
public var createdAt: Date = Date()
|
|
|
|
public init(_ invocation: AgentToolInvocation, state: String) {
|
|
id = invocation.id
|
|
runID = invocation.runID
|
|
callID = invocation.call.id
|
|
toolName = invocation.call.name
|
|
argumentsData = AgentPersistenceRedactor.redactedJSON(invocation.call.arguments).encodedData
|
|
sourceMessageID = invocation.sourceMessageID
|
|
targetHostID = invocation.targetHostID
|
|
self.state = state
|
|
createdAt = invocation.createdAt
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentToolResult {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var runID: UUID = UUID()
|
|
public var invocationID: UUID = UUID()
|
|
public var callID = ""
|
|
public var content = ""
|
|
public var isError = false
|
|
public var isTruncated = false
|
|
public var metadataData = Data()
|
|
|
|
public init(_ result: AgentToolResult, invocation: AgentToolInvocation) {
|
|
runID = invocation.runID
|
|
invocationID = invocation.id
|
|
callID = result.callID
|
|
content = AgentPersistenceRedactor.toolResultText(result.content, toolName: invocation.call.name)
|
|
isError = result.isError
|
|
isTruncated = result.isTruncated
|
|
metadataData = AgentPersistenceRedactor.encoded(result.metadata)
|
|
}
|
|
|
|
var restoredResult: AgentToolResult {
|
|
var result = AgentToolResult(
|
|
callID: callID, content: content, isError: isError, isTruncated: isTruncated,
|
|
metadata: (try? JSONDecoder().decode([String: AgentJSONValue].self, from: metadataData)) ?? [:])
|
|
result.content = AgentPersistenceRedactor.restoredToolResultText(content, fallbacks: [result.modelContent])
|
|
if result.content != content {
|
|
result.isTruncated = true
|
|
if result.content.hasPrefix(AgentToolResult.untrustedDataOpeningMarker) {
|
|
result.metadata["untrusted"] = .bool(true)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
@Model
|
|
public final class PersistentAgentReviewRecord {
|
|
@Attribute(.unique) public var id: UUID = UUID()
|
|
public var runID: UUID = UUID()
|
|
public var invocationID: UUID = UUID()
|
|
public var status = "started"
|
|
public var verdict: String?
|
|
public var risk: String?
|
|
public var userAuthorization: String?
|
|
public var reason: String?
|
|
public var createdAt: Date = Date()
|
|
|
|
public init(runID: UUID, invocationID: UUID, decision: GuardianDecision?) {
|
|
self.runID = runID
|
|
self.invocationID = invocationID
|
|
if let decision {
|
|
status = "finished"
|
|
verdict = decision.verdict.rawValue
|
|
risk = decision.risk.rawValue
|
|
userAuthorization = decision.userAuthorization.rawValue
|
|
reason = AgentPersistenceRedactor.visibleText(decision.reason)
|
|
}
|
|
}
|
|
|
|
public init(runID: UUID, invocationID: UUID, failure: String, timedOut: Bool) {
|
|
self.runID = runID
|
|
self.invocationID = invocationID
|
|
status = timedOut ? "timedOut" : "failed"
|
|
reason = AgentPersistenceRedactor.visibleText(failure)
|
|
}
|
|
}
|