native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
38 kB · 904 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905import Foundationimport ProwlCLIShared
nonisolated struct AgentSignalHookCapability: Equatable, Sendable { let runtime: AgentNativeHookRuntime let nativeEvents: [String: AgentSignalEvent] let coveredEvents: [AgentSignalEvent]
init(runtime: AgentNativeHookRuntime, nativeEvents: [String: AgentSignalEvent]) { self.runtime = runtime self.nativeEvents = nativeEvents self.coveredEvents = Array(Set(nativeEvents.values)).sorted { $0.rawValue < $1.rawValue } }}
/// Interactive and headless launch behavior for one supported runtime./// Handoff briefing is authored by the live source agent and does not resume/// native sessions through this adapter boundary (docs-ai 055).nonisolated protocol AgentRuntimeAdapter: Sendable { var runtime: AgentProfileRuntime { get } var displayName: String { get } var supportsModelSelection: Bool { get } var supportsReasoningEffort: Bool { get } /// Empty when the runtime cannot represent both persisted modes honestly. /// Adapters exposing the picker must render every listed mode, including /// inverse guarded flags for CLIs whose default is auto-approved. var executionModeOptions: [AgentExecutionMode] { get } var accountIsolation: AgentProfileHomeRelocation? { get } var signalHooks: AgentSignalHookCapability? { get } var reasoningEffortSuggestions: [String] { get } var modelSuggestions: [String] { get }
func observe(arguments: [String]) -> AgentLaunchObservation func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation}
nonisolated extension AgentRuntimeAdapter { var supportsAccountIsolation: Bool { accountIsolation != nil } var accountHomeEnvironmentVariable: String? { accountIsolation?.environmentVariable } var supportsModelSelection: Bool { false } var supportsReasoningEffort: Bool { false } var executionModeOptions: [AgentExecutionMode] { [] } var accountIsolation: AgentProfileHomeRelocation? { nil } var signalHooks: AgentSignalHookCapability? { nil } var reasoningEffortSuggestions: [String] { [] } var modelSuggestions: [String] { [] }
/// User arguments remain last-wins for ordinary options. A managed home is /// appended after them because account binding is an identity invariant: /// extra arguments must not silently redirect state outside the UUID home. func finalizedOptions(_ generated: [String], request: AgentStartRequest) -> [String] { var options = generated + request.configuration.extraArguments if let home = request.dedicatedHome, let accountIsolation { options += accountIsolation.arguments(for: home) } return options }}
/// One path-valued CLI option in a managed-home relocation contract.nonisolated struct AgentProfileHomePathArgument: Equatable, Sendable { let option: String /// Empty means the managed home itself; otherwise a child path. let relativePath: String}
/// Verified full-state relocation for a runtime. Environment and CLI-argument/// mechanisms share one model so the planner, editor, cleanup, and session/// resolver agree on the same managed root (docs-ai 055).nonisolated struct AgentProfileHomeRelocation: Equatable, Sendable { let environmentVariable: String? let pathArguments: [AgentProfileHomePathArgument] let sessionRootRelativePath: String let reservedEnvironmentVariables: Set<String>
init( environmentVariable: String? = nil, pathArguments: [AgentProfileHomePathArgument] = [], sessionRootRelativePath: String = "", reservedEnvironmentVariables: Set<String> = [] ) { self.environmentVariable = environmentVariable self.pathArguments = pathArguments self.sessionRootRelativePath = sessionRootRelativePath self.reservedEnvironmentVariables = reservedEnvironmentVariables.union( environmentVariable.map { [$0] } ?? [] ) }
func arguments(for home: URL) -> [String] { pathArguments.flatMap { argument in let url = argument.relativePath.isEmpty ? home : home.appending(path: argument.relativePath, directoryHint: .isDirectory) return [argument.option, AgentProfileLaunchPlanner.pathString(url)] } }
func sessionConfigRoot(for home: URL) -> URL { guard !sessionRootRelativePath.isEmpty else { return home } return home.appending(path: sessionRootRelativePath, directoryHint: .isDirectory) }}
nonisolated enum AgentExecutionMode: String, Codable, CaseIterable, Equatable, Identifiable, Sendable { case standard case unrestricted
var id: String { rawValue }
var title: String { switch self { case .standard: "Standard" case .unrestricted: "Unrestricted" } }}
/// How an agent process should begin. An empty prompt sentinel is forbidden by/// construction: interactive-with-no-prompt and prompted starts are distinct/// CLI semantics (docs-ai 053).nonisolated enum AgentStartIntent: Equatable, Sendable { case interactive case prompt(String) case headless(String)
var promptText: String? { switch self { case .interactive: nil case .prompt(let prompt), .headless(let prompt): prompt } }}
nonisolated struct AgentLaunchConfiguration: Codable, Equatable, Sendable { var model: String? var executionMode: AgentExecutionMode var reasoningEffort: String? /// Literal argv tokens appended after adapter-generated options (last-wins) /// and before managed-home arguments and any positional prompt. var extraArguments: [String]
init( model: String? = nil, executionMode: AgentExecutionMode = .standard, reasoningEffort: String? = nil, extraArguments: [String] = [] ) { self.model = model self.executionMode = executionMode self.reasoningEffort = reasoningEffort self.extraArguments = extraArguments }
private enum CodingKeys: String, CodingKey { case model, executionMode, reasoningEffort, extraArguments }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) model = try container.decodeIfPresent(String.self, forKey: .model) executionMode = try container.decodeIfPresent(AgentExecutionMode.self, forKey: .executionMode) ?? .standard reasoningEffort = try container.decodeIfPresent(String.self, forKey: .reasoningEffort) extraArguments = try container.decodeIfPresent([String].self, forKey: .extraArguments) ?? [] }}
/// Options observed from a live agent process. Nil denotes an unknown effective/// setting, never an inferred safe default from the absence of an argv flag.nonisolated struct AgentLaunchObservation: Equatable, Sendable { let model: String? let executionMode: AgentExecutionMode?
init(model: String? = nil, executionMode: AgentExecutionMode? = nil) { self.model = model self.executionMode = executionMode }}
nonisolated struct AgentStartRequest: Equatable, Sendable { let runtime: AgentProfileRuntime let intent: AgentStartIntent let configuration: AgentLaunchConfiguration /// Set only by Agent Profile planning. Handoff and ordinary starts are pure /// invocations and therefore leave this nil. let dedicatedHome: URL?
var agent: DetectedAgent { runtime.agent }
init( runtime: AgentProfileRuntime, intent: AgentStartIntent, configuration: AgentLaunchConfiguration = .init(), dedicatedHome: URL? = nil ) { self.runtime = runtime self.intent = intent self.configuration = configuration self.dedicatedHome = dedicatedHome }
init( agent: DetectedAgent, intent: AgentStartIntent, configuration: AgentLaunchConfiguration = .init() ) { self.init(runtime: AgentProfileRuntime(agent: agent), intent: intent, configuration: configuration) }}
nonisolated struct AgentInvocation: Equatable, Sendable { let executable: String let arguments: [String]
init(executable: String, arguments: [String]) { self.executable = executable self.arguments = arguments }
var terminalInput: String { terminalInput(replacingArgumentsWithEnvironmentVariables: [:]) }
/// Profile plans can carry arbitrary argv values through the surface /// environment instead of typing them into a canonical PTY. The logical /// invocation retains the real values while shell rendering substitutes /// quoted variable references at the declared indexes. func terminalInput( replacingArgumentsWithEnvironmentVariables replacements: [Int: String] ) -> String { var tokens = arguments.map(Self.shellQuote) for (index, variable) in replacements where tokens.indices.contains(index) { tokens[index] = "\"$\(variable)\"" } return ([Self.shellQuote(executable)] + tokens).joined(separator: " ") }
func terminalInput(replacingFinalArgumentWithEnvironmentVariable variable: String?) -> String { guard let variable, !arguments.isEmpty else { return terminalInput } return terminalInput( replacingArgumentsWithEnvironmentVariables: [arguments.index(before: arguments.endIndex): variable] ) }
static func shellQuote(_ argument: String) -> String { "'" + argument.replacing("'", with: "'\"'\"'") + "'" }}
nonisolated enum AgentRuntimeError: Error, Equatable, Sendable { case unsupportedAgent(DetectedAgent) case unsupportedStartIntent(AgentProfileRuntime, AgentStartIntent)}
nonisolated enum AgentRuntimeAdapterRegistry { static func profileAdapter(for runtime: AgentProfileRuntime) -> (any AgentRuntimeAdapter)? { switch runtime { case .claude: ClaudeCodeRuntimeAdapter() case .codex: CodexRuntimeAdapter() case .gemini: GeminiRuntimeAdapter() case .cursor: CursorRuntimeAdapter() case .cline: ClineRuntimeAdapter() case .opencode: OpenCodeRuntimeAdapter() case .copilot: CopilotRuntimeAdapter() case .kimi: KimiRuntimeAdapter() case .droid: DroidRuntimeAdapter() case .amp: AmpRuntimeAdapter() case .qoder: QoderRuntimeAdapter() case .qwen: QwenRuntimeAdapter() case .grok: GrokRuntimeAdapter() case .pi: PiRuntimeAdapter() case .omp: OMPRuntimeAdapter() } }
/// Canonical launch adapter for one detected runtime. static func adapter(for agent: DetectedAgent) -> (any AgentRuntimeAdapter)? { profileAdapter(for: AgentProfileRuntime(agent: agent)) }
static var launchableAgents: [DetectedAgent] { DetectedAgent.allCases.filter { adapter(for: $0) != nil } }
static func displayName(for runtime: AgentProfileRuntime) -> String { profileAdapter(for: runtime)?.displayName ?? runtime.rawValue }
static func displayName(for agent: DetectedAgent) -> String { adapter(for: agent)?.displayName ?? agent.displayName }
static func canStart(_ agent: DetectedAgent) -> Bool { adapter(for: agent) != nil }
static func observe(runtime: AgentProfileRuntime, arguments: [String]) -> AgentLaunchObservation { profileAdapter(for: runtime)?.observe(arguments: arguments) ?? .init() }
static func observe(agent: DetectedAgent, arguments: [String]) -> AgentLaunchObservation { return adapter(for: agent)?.observe(arguments: arguments) ?? .init() }
static func inheritedConfiguration( from sourceAgent: DetectedAgent, observation: AgentLaunchObservation?, to destinationAgent: DetectedAgent ) -> AgentLaunchConfiguration { AgentLaunchConfiguration( model: sourceAgent == destinationAgent ? observation?.model : nil, executionMode: observation?.executionMode ?? .standard ) }
static func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { guard let adapter = profileAdapter(for: request.runtime) else { throw AgentRuntimeError.unsupportedAgent(request.agent) } return try adapter.makeStartInvocation(request) }
}
// MARK: - Launch adapters
nonisolated private struct CodexRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .codex let displayName = "Codex" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation(environmentVariable: "CODEX_HOME") let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .codex, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .codex) ) let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh", "max"] let modelSuggestions = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.containsAny("--dangerously-bypass-approvals-and-sandbox", "--yolo") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["-c", "model_reasoning_effort=\(effort)"] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-bypass-approvals-and-sandbox") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "codex", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "codex", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "codex", arguments: ["exec"] + options + [prompt]) } }}
nonisolated private struct ClaudeCodeRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .claude let displayName = "Claude Code" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "CLAUDE_CONFIG_DIR" ) let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .claude, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .claude) ) let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh", "max"] let modelSuggestions = [ "claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-haiku-4-5-20251001", ]
func observe(arguments: [String]) -> AgentLaunchObservation { let bypasses = arguments.contains("--dangerously-skip-permissions") || arguments.optionValue(long: "--permission-mode") == "bypassPermissions" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypasses ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-skip-permissions") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "claude", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "claude", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "claude", arguments: ["-p"] + options + [prompt]) } }}
nonisolated private struct GeminiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .gemini let displayName = "Gemini CLI" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "GEMINI_CLI_HOME", sessionRootRelativePath: ".gemini" )
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.contains("--yolo") || arguments.optionValue(long: "--approval-mode") == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: approves && arguments.booleanOptionIsFalse("--sandbox") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated += ["--approval-mode", "yolo", "--sandbox=false"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "gemini", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "gemini", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "gemini", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct CursorRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .cursor let displayName = "Cursor Agent" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.containsAny("--force", "--yolo") return AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: approves && arguments.optionValue(long: "--sandbox") == "disabled" ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated += ["--yolo", "--sandbox", "disabled"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "cursor-agent", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "cursor-agent", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "cursor-agent", arguments: options + ["--print", prompt]) } }}
nonisolated private struct ClineRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .cline let displayName = "Cline" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( pathArguments: [ AgentProfileHomePathArgument(option: "--config", relativePath: "config"), AgentProfileHomePathArgument(option: "--data-dir", relativePath: "data"), AgentProfileHomePathArgument(option: "--hooks-dir", relativePath: "hooks"), ], sessionRootRelativePath: "data", reservedEnvironmentVariables: ["CLINE_DATA_DIR"] ) let reasoningEffortSuggestions = ["none", "low", "medium", "high", "xhigh"]
func observe(arguments: [String]) -> AgentLaunchObservation { let autoApprove = arguments.optionValue(long: "--auto-approve") let executionMode: AgentExecutionMode? = switch autoApprove { case "true": .unrestricted case "false": .standard default: nil } return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: executionMode ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } generated += [ "--auto-approve", request.configuration.executionMode == .unrestricted ? "true" : "false", ] let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "cline", arguments: options + ["--tui"]) case .prompt(let prompt): AgentInvocation(executable: "cline", arguments: options + ["--tui", prompt]) case .headless(let prompt): AgentInvocation(executable: "cline", arguments: options + [prompt]) } }}
nonisolated private struct OpenCodeRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .opencode let displayName = "OpenCode" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases /// Relayed by the bundled `agent-hooks/opencode/prowl-hooks.ts` plugin. OpenCode creates its /// session lazily at the first prompt and emits nothing on `/new` or resume, so the runtime is /// non-announcing like Codex: the first `session.idle` verifies the channel. `permission.asked` /// is dropped per launch under `--auto`, where OpenCode auto-replies in the same millisecond. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .opencode, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .opencode) )
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.contains("--auto") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--variant", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--auto") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "opencode", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "opencode", arguments: options + ["--prompt", prompt]) case .headless(let prompt): AgentInvocation(executable: "opencode", arguments: ["run"] + options + [prompt]) } }}
nonisolated private struct CopilotRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .copilot let displayName = "GitHub Copilot" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "COPILOT_HOME" ) /// `PermissionRequest` is excluded on purpose: it fires whenever a tool enters the /// permission service, including when `--allow-all-tools` auto-approves and nobody is /// waiting. `Notification` is the only event that means a human is actually blocked. /// `subagentStop` is excluded because a subagent finishing is not the main turn ending. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .copilot, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .copilot) ) let reasoningEffortSuggestions = ["none", "minimal", "low", "medium", "high", "xhigh", "max"]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: arguments.contains("--allow-all") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--allow-all") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "copilot", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "copilot", arguments: options + ["--interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "copilot", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct KimiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .kimi let displayName = "Kimi CLI" let supportsModelSelection = true let executionModeOptions = AgentExecutionMode.allCases
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: arguments.containsAny("--yolo", "--yes") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if request.configuration.executionMode == .unrestricted { generated.append("--yolo") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "kimi", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "kimi", arguments: options + ["--prompt", prompt]) case .headless(let prompt): AgentInvocation(executable: "kimi", arguments: options + ["--print", "--prompt", prompt]) } }}
nonisolated private struct DroidRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .droid let displayName = "Droid" /// Droid has no `PermissionRequest` event; attention arrives through `Notification`. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .droid, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .droid) )
func observe(arguments _: [String]) -> AgentLaunchObservation { .init() }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { let options = finalizedOptions([], request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "droid", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "droid", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "droid", arguments: ["exec"] + options + [prompt]) } }}
nonisolated private struct AmpRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .amp let displayName = "Amp" let supportsReasoningEffort = true let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments _: [String]) -> AgentLaunchObservation { .init() }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let effort = request.configuration.reasoningEffort { generated += ["--effort", effort] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "amp", arguments: options) case .prompt: throw AgentRuntimeError.unsupportedStartIntent(.amp, request.intent) case .headless(let prompt): AgentInvocation(executable: "amp", arguments: options + ["--execute", prompt]) } }}
nonisolated private struct QoderRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .qoder let displayName = "Qoder CLI" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( pathArguments: [AgentProfileHomePathArgument(option: "--config-dir", relativePath: "")] ) /// `PermissionRequest` is excluded on purpose: Qoder was measured firing it under /// `--permission-mode accept_edits` while the write was auto-approved and nobody was /// waiting. `StopFailure` is included because Qoder reports a failed turn that way. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .qoder, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .qoder) ) let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments: [String]) -> AgentLaunchObservation { let bypasses = arguments.contains("--dangerously-skip-permissions") || arguments.optionValue(long: "--permission-mode") == "bypass_permissions" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypasses ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated.append("--dangerously-skip-permissions") } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "qodercli", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "qodercli", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "qodercli", arguments: options + ["--print", prompt]) } }}
nonisolated private struct QwenRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .qwen let displayName = "Qwen Code" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation(environmentVariable: "QWEN_HOME") let reasoningEffortSuggestions = ["low", "medium", "high", "xhigh"]
func observe(arguments: [String]) -> AgentLaunchObservation { let approves = arguments.contains("--yolo") || arguments.optionValue(long: "--approval-mode") == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: approves && arguments.booleanOptionIsFalse("--sandbox") ? .unrestricted : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } if request.configuration.executionMode == .unrestricted { generated += ["--approval-mode", "yolo", "--sandbox=false"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "qwen", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "qwen", arguments: options + ["--prompt-interactive", prompt]) case .headless(let prompt): AgentInvocation(executable: "qwen", arguments: options + ["--prompt", prompt]) } }}
nonisolated private struct GrokRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .grok let displayName = "Grok Build" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let reasoningEffortSuggestions = ["low", "medium", "high"]
func observe(arguments: [String]) -> AgentLaunchObservation { let permissionMode = arguments.optionValue(long: "--permission-mode") let bypassesPermissions = permissionMode == "bypassPermissions" || arguments.contains("--always-approve") let sandboxIsOff = arguments.optionValue(long: "--sandbox") == "off" return AgentLaunchObservation( model: arguments.optionValue(long: "--model", short: "-m"), executionMode: bypassesPermissions && sandboxIsOff ? .unrestricted : permissionMode == "default" ? .standard : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--reasoning-effort", effort] } switch request.configuration.executionMode { case .standard: generated += ["--permission-mode", "default"] case .unrestricted: generated += ["--permission-mode", "bypassPermissions", "--sandbox", "off"] } let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "grok", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "grok", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "grok", arguments: options + ["--single", prompt]) } }}
nonisolated private struct PiRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .pi let displayName = "Pi" let supportsModelSelection = true let supportsReasoningEffort = true let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "PI_CODING_AGENT_DIR" ) /// Relayed by the bundled `agent-hooks/pi/prowl-hooks.ts` extension (`-e`). `agent_settled` /// is Pi's documented idle point (`agent_end` precedes it); Pi has no permission system. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .pi, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .pi) ) let reasoningEffortSuggestions = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
func observe(arguments: [String]) -> AgentLaunchObservation { AgentLaunchObservation(model: arguments.optionValue(long: "--model")) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { let options = piOptions(request) return switch request.intent { case .interactive: AgentInvocation(executable: "pi", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "pi", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "pi", arguments: options + ["--print", prompt]) } }
private func piOptions(_ request: AgentStartRequest) -> [String] { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } return finalizedOptions(generated, request: request) }}
nonisolated private struct OMPRuntimeAdapter: AgentRuntimeAdapter { let runtime: AgentProfileRuntime = .omp let displayName = "Oh My Pi" let supportsModelSelection = true let supportsReasoningEffort = true let executionModeOptions = AgentExecutionMode.allCases let accountIsolation: AgentProfileHomeRelocation? = AgentProfileHomeRelocation( environmentVariable: "PI_CODING_AGENT_DIR" ) /// Relayed by the bundled `agent-hooks/omp/prowl-hooks.ts` extension (`--hook`). /// `session_stop` is documented main-session only, whereas `agent_end` was measured firing /// once per in-process `task` sub-agent; `/new` rotates through `session_switch`. let signalHooks: AgentSignalHookCapability? = AgentSignalHookCapability( runtime: .omp, nativeEvents: AgentNativeHookDecoder.nativeEvents(for: .omp) ) let reasoningEffortSuggestions = ["off", "minimal", "low", "medium", "high", "xhigh", "max", "auto"]
func observe(arguments: [String]) -> AgentLaunchObservation { let approvalMode = arguments.optionValue(long: "--approval-mode") let bypasses = arguments.contains("--auto-approve") || approvalMode == "yolo" return AgentLaunchObservation( model: arguments.optionValue(long: "--model"), executionMode: bypasses ? .unrestricted : approvalMode == "always-ask" ? .standard : nil ) }
func makeStartInvocation(_ request: AgentStartRequest) throws -> AgentInvocation { var generated: [String] = [] if let model = request.configuration.model { generated += ["--model", model] } if let effort = request.configuration.reasoningEffort { generated += ["--thinking", effort] } generated += [ "--approval-mode", request.configuration.executionMode == .unrestricted ? "yolo" : "always-ask", ] let options = finalizedOptions(generated, request: request) return switch request.intent { case .interactive: AgentInvocation(executable: "omp", arguments: options) case .prompt(let prompt): AgentInvocation(executable: "omp", arguments: options + [prompt]) case .headless(let prompt): AgentInvocation(executable: "omp", arguments: options + ["--print", prompt]) } }}
nonisolated extension [String] { fileprivate func optionValue(long: String, short: String? = nil) -> String? { for index in indices.reversed() { let argument = self[index] if argument == long || short == argument { let next = self.index(after: index) guard next < endIndex else { return nil } return self[next] } if argument.hasPrefix(long + "=") { return String(argument.dropFirst(long.count + 1)) } } return nil }
fileprivate func containsAny(_ values: String...) -> Bool { values.contains(where: contains) }
fileprivate func booleanOptionIsFalse(_ option: String) -> Bool { contains("\(option)=false") || optionValue(long: option) == "false" }}
nonisolated extension String { fileprivate var lastPathComponent: String { URL(fileURLWithPath: self).lastPathComponent }}