2026-10-14 13:34:37 -07:00
import Foundation
struct AgentFailure : LocalizedError , Sendable {
let message : String
var errorDescription : String ? { message }
}
// Compare decimal strings without losing precision through Double or overflowing Int.
enum AgentCursor {
static func normalize ( _ value : String ) throws -> String {
guard ! value . isEmpty , value . utf8 . allSatisfy ({ ( 48. .. 57 ). contains ( $0 ) }) else {
throw AgentFailure ( message : "Invalid Agent cursor." )
}
let trimmed = value . drop ( while : { $0 == "0" })
return trimmed . isEmpty ? "0" : String ( trimmed )
}
static func less ( _ a : String , _ b : String ) -> Bool {
a . count == b . count ? a < b : a . count < b . count
}
static func advance ( _ current : String , _ incoming : String ) throws -> String {
let value = try normalize ( incoming )
return less ( current , value ) ? value : current
}
}
struct AgentActivity : Codable , Equatable , Sendable {
struct Tool : Codable , Equatable , Sendable {
var id : String
var name : String
var status : String
var output : String ?
}
struct Wait : Codable , Equatable , Sendable {
var due : Double
var reason : String ?
}
var state : String = "idle"
var isThinking = false
var queued = 0
var backgroundWorkers = 0
var tools : [ Tool ] = []
var queuedMessages : [ Message ] = []
var wait : Wait ?
var error : String ?
var isBusy : Bool { isThinking || backgroundWorkers > 0 }
}
struct AgentDescriptor : Decodable , Sendable {
var id : String
var title : String
var type : String
var model : String
var ` protocol `: String
var activity : AgentActivity
var websocket : String
2026-10-14 19:51:27 -07:00
var capabilities : AgentCapabilities ? = nil
2026-10-14 13:34:37 -07:00
}
struct AgentPage : Codable , Sendable {
var conversationId : String
var messages : [ Message ]
var cursor : String ?
var beforeCursor : String ?
var hasMore : Bool
func validate () throws {
guard ! conversationId . isEmpty , messages . allSatisfy ({ ! $0 . id . isEmpty }) else {
throw AgentFailure ( message : "Invalid Agent history." )
}
if let cursor { _ = try AgentCursor . normalize ( cursor ) }
if let beforeCursor { _ = try AgentCursor . normalize ( beforeCursor ) }
}
}
struct AgentReadState : Codable , Sendable {
var cursor : String
var updatedAt : String ?
2026-10-14 22:03:21 -07:00
var sourceId : String ? = nil
2026-10-14 13:34:37 -07:00
}
struct AgentReceipt : Codable , Sendable {
var id : Int64
var status : String
var messageId : String
2026-10-14 19:51:27 -07:00
var attachmentIds : [ String ]? = nil
func validate ( requestID : String , attachmentIDs : [ String ] = []) throws {
2026-10-14 13:34:37 -07:00
guard id > 0 , id <= 9_007_199_254_740_991 ,
[ "queued" , "placed" , "done" , "unanswered" ]. contains ( status ),
messageId == "inbound:sybil: \( requestID ) " else {
throw AgentFailure ( message : "Invalid Agent receipt. Retry to confirm delivery." )
}
2026-10-14 19:51:27 -07:00
if ! attachmentIDs . isEmpty , attachmentIds != attachmentIDs {
throw AgentAttachmentIssue . local ( "Agent receipt did not confirm the exact ordered attachments. Retry this message to confirm delivery." , code : "invalid_upstream_response" )
}
2026-10-14 13:34:37 -07:00
}
}
enum AgentEvent : Sendable {
case ready ( String ?)
case messages ( AgentPage )
case activity ( String , AgentActivity )
2026-10-14 22:03:21 -07:00
case readState ( AgentReadState )
2026-10-14 13:34:37 -07:00
case heartbeat
case error ( String )
static func decode ( _ data : Data ) throws -> AgentEvent ? {
struct Envelope : Decodable {
var type : String
var conversationId : String ?
var activity : AgentActivity ?
var error : String ?
}
let envelope = try AgentCoding . decoder (). decode ( Envelope . self , from : data )
switch envelope . type {
case "ready" : return . ready ( envelope . conversationId )
case "messages" :
let page = try AgentCoding . decoder (). decode ( AgentPage . self , from : data )
try page . validate ()
return . messages ( page )
case "activity" :
guard let id = envelope . conversationId , let activity = envelope . activity else {
throw AgentFailure ( message : "Invalid Agent activity frame." )
}
return . activity ( id , activity )
case "pong" , "heartbeat" : return . heartbeat
2026-10-14 22:03:21 -07:00
case "read_state" :
// Additive optional frame: malformed new metadata must not tear down
// an otherwise compatible Agent connection.
guard let read = try ? AgentCoding . decoder (). decode ( AgentReadState . self , from : data ),
AgentPushPayload . validCursor ( read . cursor ) != nil ,
read . sourceId . map ( AgentPushPayload . validSource ) ?? true else { return nil }
return . readState ( read )
2026-10-14 13:34:37 -07:00
case "error" : return . error ( envelope . error ?? "Agent connection error." )
default : return nil
}
}
}
enum AgentCoding {
static func decoder () -> JSONDecoder {
let decoder = JSONDecoder ()
decoder . dateDecodingStrategy = . custom { decoder in
let container = try decoder . singleValueContainer ()
let text = try container . decode ( String . self )
let formatter = ISO8601DateFormatter ()
formatter . formatOptions = [. withInternetDateTime , . withFractionalSeconds ]
if let date = formatter . date ( from : text ) { return date }
formatter . formatOptions = [. withInternetDateTime ]
guard let date = formatter . date ( from : text ) else {
throw DecodingError . dataCorruptedError ( in : container , debugDescription : "Invalid message date" )
}
return date
}
return decoder
}
}
extension Message {
var agentEntryCursor : String ? {
let value = metadata ?. objectValue ?[ "entryId" ]
if let text = value ?. stringValue { return try ? AgentCursor . normalize ( text ) }
if let number = value ?. numberValue , number >= 0 , number <= 9_007_199_254_740_991 , number . rounded () == number {
return String ( Int64 ( number ))
}
return nil
}
var isAgentPublicReply : Bool {
role == . assistant && metadata ?. objectValue ?[ "kind" ]?. stringValue == "message"
}
var agentToolID : String { toolCallMetadata ?. toolCallId ?? id }
}
struct AgentRoomState : Equatable , Sendable {
var conversationID : String ?
var messages : [ Message ] = []
var cursor = "0"
var readCursor = "0"
var activity = AgentActivity ()
var unreadCount : Int {
messages . filter { $0 . isAgentPublicReply && $0 . agentEntryCursor . map { AgentCursor . less ( readCursor , $0 ) } == true }. count
}
mutating func checkConversation ( _ id : String ) throws {
guard conversationID == nil || conversationID == id else {
throw AgentFailure ( message : "The Agent room changed. Reconnect in Settings to reload it." )
}
conversationID = id
}
mutating func apply ( _ page : AgentPage ) throws {
try page . validate ()
var next = self
try next . checkConversation ( page . conversationId )
next . cursor = try page . cursor . map { try AgentCursor . advance ( cursor , $0 ) } ?? cursor
next . messages = Self . merge ( messages , page . messages )
self = next
}
static func merge ( _ previous : [ Message ], _ incoming : [ Message ]) -> [ Message ] {
var byID = Dictionary ( previous . map { ( $0 . id , $0 ) }, uniquingKeysWith : { _ , last in last })
for var message in incoming {
if let old = byID [ message . id ] {
let prior = old . metadata ?. objectValue ?? [:]
let next = message . metadata ?. objectValue ?? [:]
if prior [ "status" ] == . string ( "complete" ) && next [ "status" ] == . string ( "queued" ) { continue }
if old . toolCallMetadata != nil , message . toolCallMetadata != nil {
let stale = [ "completed" , "failed" ]. contains ( old . toolCallMetadata ?. status ?? "" ) && message . toolCallMetadata ?. status == "initiated"
var merged = prior . merging ( next , uniquingKeysWith : { a , b in stale ? a : b })
if next [ "startedAt" ] == nil || next [ "startedAt" ] == . null { merged [ "startedAt" ] = prior [ "startedAt" ] }
if let args = prior [ "args" ], next [ "args" ]?. objectValue ?. isEmpty != false { merged [ "args" ] = args }
message . metadata = . object ( merged )
message . createdAt = min ( old . createdAt , message . createdAt )
if stale { message . content = old . content ; message . name = old . name }
}
}
byID [ message . id ] = message
}
return byID . values . sorted {
if $0 . createdAt != $1 . createdAt { return $0 . createdAt < $1 . createdAt }
if let a = $0 . agentEntryCursor , let b = $1 . agentEntryCursor , a != b { return AgentCursor . less ( a , b ) }
return $0 . id < $1 . id
}
}
}
2026-10-14 15:31:24 -07:00
// Filter only the transcript projection, before windowing. Keep the room's raw
// messages and cursors intact for upserts, replay, unread state and activity.
struct AgentTranscript {
let messages : [ Message ]
let startIndex : Int
var displayed : [ Message ] { Array ( messages . dropFirst ( startIndex )) }
init ( messages : [ Message ], startID : String ? = nil , limit : Int = 100 ) {
let visible = messages . filter {
$0 . role != . tool && $0 . metadata ?. objectValue ?[ "kind" ]?. stringValue != "tool_call"
}
self . messages = visible
startIndex = startID . flatMap { id in visible . firstIndex ( where : { $0 . id == id }) }
?? max ( 0 , visible . count - limit )
}
}
2026-10-14 13:34:37 -07:00
// The transcript latch is independent of the truthful sidebar/worker activity.
struct AgentPresentation {
private ( set ) var suppressed = false
private ( set ) var busySince : Date ?
private var seenTools : Set < String > = []
static let quietTools : Set < String > = [ "user_message" , "notify_parent" , "clock_wait" , "event_decision" ]
mutating func receive ( _ event : AgentEvent , before : AgentRoomState , after : AgentRoomState , replay : Bool , now : Date ) {
switch event {
case let . messages ( page ):
let existing = Set ( before . messages . map ( \ . id ))
let fresh = page . messages . filter { ! existing . contains ( $0 . id ) && $0 . agentEntryCursor . map { AgentCursor . less ( before . cursor , $0 ) } == true }
. sorted { AgentCursor . less ( $0 . agentEntryCursor ?? "0" , $1 . agentEntryCursor ?? "0" ) }
if ! replay {
for message in fresh {
if message . isAgentPublicReply && ! message . content . trimmingCharacters ( in : . whitespacesAndNewlines ). isEmpty { suppressed = true }
else if message . role == . user { suppressed = false }
else if message . toolCallMetadata ?. status == "initiated" ,
! Self . quietTools . contains ( message . toolCallMetadata ?. toolName ?? "" ), ! seenTools . contains ( message . agentToolID ) { suppressed = false }
}
}
for message in page . messages where message . role == . tool { seenTools . insert ( message . agentToolID ) }
case let . activity ( _ , activity ):
if ! activity . isThinking || activity . tools . contains ( where : { ! seenTools . contains ( $0 . id ) && ! Self . quietTools . contains ( $0 . name ) }) { suppressed = false }
seenTools . formUnion ( activity . tools . map ( \ . id ))
default : break
}
reconcile ( activity : after . activity , now : now )
}
mutating func sent ( now : Date , activity : AgentActivity ) { suppressed = false ; reconcile ( activity : activity , now : now ) }
private mutating func reconcile ( activity : AgentActivity , now : Date ) {
if suppressed || ! activity . isBusy { busySince = nil }
else if busySince == nil { busySince = now }
}
func isTyping ( now : Date ) -> Bool { busySince . map { now . timeIntervalSince ( $0 ) >= 0.25 } ?? false }
}