Files
nucleic/Sources/NucleicProtocol/CoreIdentifiers.swift
T
NucleicandClaude Opus 4.8 609ce70539 Migrate Grok backend to ACP (grok agent stdio)
Replace the streaming-json CLI + generated PreToolUse-hook Grok adapter with grok's native ACP mode (JSON-RPC 2.0 over stdio). Adds GrokACPBackend/GrokACPDecoder/GrokACPDecisionMapping (a near-twin of the Codex app-server adapter); generalizes CodexJSONRPC into a shared JSONRPCConnection (jsonrpc-header flag, default off so Codex is byte-identical, plus endNotifications); deletes GrokBuildBackend, GrokStreamDecoder, GrokHookConfig and strips the /grok-hook HTTP bridge from MCPApprovalServer. Claude and Codex are left native. Preserves conflict-lock parity, native resume (session/load), and the hermetic fake-grok contract test. Full suite: 525 tests green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 02:52:24 -07:00

246 lines
9.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
// Pure, platform-agnostic value types shared by the macOS host (NucleicCore) and the
// iPhone client (NucleicRemote). These are the *exact* identifiers the sync wire layer
// projects (SYNC_PROTOCOL §5). They live here, not in NucleicCore, so the protocol layer
// compiles cleanly for iOS without dragging in GRDB / Foundation.Process / Network.
//
// Extracted from Backend.swift (identifiers), Session.swift (SessionStatus), and
// WorktreeManager.swift (DiffStat); NucleicCore re-exports this module (ProtocolExports.swift)
// so existing host code keeps referencing these unqualified.
// MARK: - Identifiers
public struct SessionID: Hashable, Sendable, Codable, CustomStringConvertible {
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public static func generate() -> SessionID { SessionID(rawValue: UUID().uuidString) }
public init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
public var description: String { rawValue }
/// Short prefix for logs — structural metadata only (OBSERVABILITY A.1).
public var short: String { String(rawValue.prefix(8)) }
}
public struct ApprovalID: Hashable, Sendable, Codable, CustomStringConvertible {
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public static func generate() -> ApprovalID { ApprovalID(rawValue: UUID().uuidString) }
public init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
public var description: String { rawValue }
}
public enum BackendID: String, Sendable, Codable {
case claudeCode
case codex
case codexExec
/// xAI's Grok coding agent over **ACP** (`grok agent stdio`) — standards JSON-RPC 2.0 over
/// stdio, baked into the binary. A sibling of `codex`: interactive approvals arrive as native
/// `session/request_permission` requests (GROK_ADAPTER §34). Host-only in v1.
case grok
/// Infer the backend from a model SKU so the model picker doubles as the backend
/// selector: Claude SKUs → `claudeCode`, OpenAI/Codex SKUs (`gpt-*`, `o3*`, `o4*`, or
/// anything containing `codex`) → `codex`, xAI SKUs (`grok-*`) → `grok`. `nil` when
/// unrecognized, so callers fall back to the project/app default.
public static func forModel(_ model: String?) -> BackendID? {
guard let model = model?.lowercased() else { return nil }
if model.hasPrefix("claude") { return .claudeCode }
if model.hasPrefix("grok") { return .grok }
if model.hasPrefix("gpt") || model.hasPrefix("o3") || model.hasPrefix("o4")
|| model.contains("codex")
{
return .codex
}
return nil
}
/// True for the OpenAI/Codex backends (`codex`, `codexExec`) — the GPT-family agents,
/// as opposed to `claudeCode`/`grok`. Used to bucket sessions when keeping Claude and GPT
/// agents in separate sandboxes (the two can otherwise compete and kill each other's
/// processes). Grok is not part of this family (it runs host-only in v1).
public var isCodexFamily: Bool {
switch self {
case .codex, .codexExec: return true
case .claudeCode, .grok: return false
}
}
}
public struct ProjectID: Hashable, Sendable, Codable, CustomStringConvertible {
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public static func generate() -> ProjectID { ProjectID(rawValue: UUID().uuidString) }
public init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
public var description: String { rawValue }
}
public struct TodoID: Hashable, Sendable, Codable, Identifiable, CustomStringConvertible {
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public static func generate() -> TodoID { TodoID(rawValue: UUID().uuidString) }
public var id: String { rawValue }
public init(from decoder: Decoder) throws {
rawValue = try decoder.singleValueContainer().decode(String.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
public var description: String { rawValue }
}
/// Lifecycle of a captured idea (the to-do inbox).
public enum TodoStatus: String, Sendable, Codable {
case open // captured, not yet acted on
case dispatched // an agent was dispatched for it
case done // manually completed / dismissed
}
public typealias WorktreePath = String
// MARK: - AgentInput (user turn payload; carried over the wire by `sendInput`)
public struct AgentInput: Sendable, Codable, Equatable {
public enum Part: Sendable, Codable, Equatable {
case text(String)
case context(label: String, body: String)
private enum CodingKeys: String, CodingKey { case type, text, label, body }
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
switch try c.decode(String.self, forKey: .type) {
case "text":
self = .text(try c.decode(String.self, forKey: .text))
case "context":
self = .context(
label: try c.decode(String.self, forKey: .label),
body: try c.decode(String.self, forKey: .body))
case let other:
throw DecodingError.dataCorruptedError(
forKey: .type, in: c, debugDescription: "Unknown AgentInput.Part \(other)")
}
}
public func encode(to encoder: Encoder) throws {
var c = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try c.encode("text", forKey: .type)
try c.encode(text, forKey: .text)
case .context(let label, let body):
try c.encode("context", forKey: .type)
try c.encode(label, forKey: .label)
try c.encode(body, forKey: .body)
}
}
}
public let parts: [Part]
public init(parts: [Part]) { self.parts = parts }
public init(text: String) { self.parts = [.text(text)] }
/// Flattened user-facing text (for the injected `userText` transcript event).
public var plainText: String? {
let pieces = parts.map { part -> String in
switch part {
case .text(let text): return text
case .context(let label, let body): return "[\(label)]\n\(body)"
}
}
let joined = pieces.joined(separator: "\n")
return joined.isEmpty ? nil : joined
}
}
// MARK: - SessionStatus (derived state; PLAN "Core domain model", RUNTIME §2)
/// The `SessionController` is the only writer; everyone else (including the phone) observes.
public enum SessionStatus: String, Sendable, Codable {
/// Created, not yet started.
case idle
/// Worktree being created / setup script running.
case provisioning
/// Agent actively working a turn.
case running
/// Blocked on a human approval decision.
case awaitingApproval
/// Turn finished; waiting for the user's next input (interactive run still open).
case awaitingInput
/// Run completed normally.
case finished
/// Interrupted (cooperative) — resumable.
case interrupted
/// Fatal error.
case error
public var isTerminal: Bool {
switch self {
case .finished, .interrupted, .error: return true
default: return false
}
}
public var displayName: String {
switch self {
case .idle: "Idle"
case .provisioning: "Provisioning"
case .running: "Running"
case .awaitingApproval: "Awaiting approval"
case .awaitingInput: "Awaiting input"
case .finished: "Finished"
case .interrupted: "Interrupted"
case .error: "Error"
}
}
}
/// How an agent's most recent turn *ended*, refining `.awaitingInput` so the UI can tell
/// "the agent is done" from "the agent needs you". A soft, presentation-only signal (not part
/// of the state machine); `nil` = not-yet-classified.
public enum TurnDisposition: String, Sendable, Codable {
/// The turn ended by asking the user something or needing a decision from them.
case awaitingInput
/// The turn finished the requested work; nothing is required from the user.
case completed
}
// MARK: - DiffStat (worktree diff summary; carried in SessionSummary)
public struct DiffStat: Sendable, Codable, Equatable {
public let filesChanged: Int
public let added: Int
public let removed: Int
public init(filesChanged: Int, added: Int, removed: Int) {
self.filesChanged = filesChanged
self.added = added
self.removed = removed
}
public static let empty = DiffStat(filesChanged: 0, added: 0, removed: 0)
}