native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
21 kB · 692 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693// ProwlShared/InputModels.swift// Typed input models matching input.md contract
import Foundation
public struct OpenInput: Codable, Sendable { /// Normalized absolute path, or nil for bare `prowl` (bring to front). public let path: String?
/// Invocation kind: "bare", "implicit-open", or "open-subcommand". /// Optional — handler derives a default if absent. public let invocation: String?
/// `true` when the CLI had to launch Prowl before sending this command. /// The handler copies this value into the response's `app_launched` field. public let appLaunched: Bool
public init(path: String? = nil, invocation: String? = nil, appLaunched: Bool = false) { self.path = path self.invocation = invocation self.appLaunched = appLaunched }}
public struct ListInput: Codable, Sendable { public init() {}}
public struct AgentsInput: Codable, Sendable { public init() {}}
public enum DispatchCompletionOutcome: String, Codable, CaseIterable, Sendable { case succeeded case failed}
public struct DispatchCompleteInput: Codable, Sendable { nonisolated public static let environmentKey = "PROWL_DISPATCH_ID" public static let maximumSummaryBytes = 32 * 1_024
/// The launch-scoped `PROWL_DISPATCH_ID` when the caller still carries one. The app /// resolves the receipt from the caller pane's current pending dispatch, so this value /// is compatibility diagnostics only: a process launched with an older id still /// completes the pane's current record. public let dispatchID: String? public let outcome: DispatchCompletionOutcome public let summary: String
enum CodingKeys: String, CodingKey { case dispatchID = "dispatch_id" case outcome case summary }
public init(dispatchID: String?, outcome: DispatchCompletionOutcome, summary: String) { self.dispatchID = dispatchID self.outcome = outcome self.summary = summary }
public var validationErrorMessage: String? { dispatchID.flatMap { CLIInputTextValidator.validateDispatchID($0, name: DispatchCompleteInput.environmentKey) } ?? CLIInputTextValidator.validate( summary, name: "--summary", maximumBytes: Self.maximumSummaryBytes ) }}
/// `prowl agents dispatch <pane> --prompt -`: a new pending dispatch for an agent that/// already runs in an existing pane. The prompt reaches the runtime through the pane's/// input path as one bracketed paste followed by Enter, so newlines and tabs survive but/// every other control character would be stripped or reinterpreted before delivery.public struct DispatchInput: Codable, Sendable, Equatable { public static let maximumPromptUTF8ByteCount = CreateLaunchInput.maximumPromptUTF8ByteCount
public let pane: String public let prompt: String
public init(pane: String, prompt: String) { self.pane = pane self.prompt = prompt }
public var validationErrorMessage: String? { guard !pane.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return "agents dispatch requires a pane handle (pN) or pane UUID." } guard !prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return "The dispatch prompt is empty." } guard prompt.utf8.count <= Self.maximumPromptUTF8ByteCount else { return "The dispatch prompt exceeds the 256 KiB UTF-8 limit." } guard !prompt.unicodeScalars.contains(where: Self.isDisallowedScalar) else { return "The dispatch prompt must not contain control characters other than newlines and tabs." } return nil }
private static func isDisallowedScalar(_ scalar: Unicode.Scalar) -> Bool { scalar != "\n" && scalar != "\t" && CharacterSet.controlCharacters.contains(scalar) }}
public struct DispatchAbandonInput: Codable, Sendable { public static let maximumReasonBytes = 32 * 1_024
public let dispatchID: String public let reason: String
enum CodingKeys: String, CodingKey { case dispatchID = "dispatch_id" case reason }
public init(dispatchID: String, reason: String) { self.dispatchID = dispatchID self.reason = reason }
public var validationErrorMessage: String? { CLIInputTextValidator.validateDispatchID(dispatchID, name: "--dispatch") ?? CLIInputTextValidator.validate( reason, name: "--reason", maximumBytes: Self.maximumReasonBytes ) }}
public enum AgentWaitMode: String, Codable, Sendable { case dispatch case condition}
public enum AgentWaitCondition: String, Codable, CaseIterable, Sendable { case idle case blocked case changed case exit}
public enum AgentWaitMinimumConfidence: String, Codable, CaseIterable, Sendable { case auto case exact case high case heuristic}
public struct AgentWaitInput: Codable, Sendable { public static let defaultTimeoutSeconds = 600 public static let maximumTimeoutSeconds = 600 public static let maximumScreenLines = 200
public let mode: AgentWaitMode public let dispatchID: String? public let pane: String? public let condition: AgentWaitCondition? public let timeoutSeconds: Int public let minimumConfidence: AgentWaitMinimumConfidence? public let includeScreenLines: Int?
enum CodingKeys: String, CodingKey { case mode case dispatchID = "dispatch_id" case pane case condition case timeoutSeconds = "timeout_seconds" case minimumConfidence = "minimum_confidence" case includeScreenLines = "include_screen_lines" }
public init( mode: AgentWaitMode, dispatchID: String? = nil, pane: String? = nil, condition: AgentWaitCondition? = nil, timeoutSeconds: Int = Self.defaultTimeoutSeconds, minimumConfidence: AgentWaitMinimumConfidence? = nil, includeScreenLines: Int? = nil ) { self.mode = mode self.dispatchID = dispatchID self.pane = pane self.condition = condition self.timeoutSeconds = timeoutSeconds self.minimumConfidence = minimumConfidence self.includeScreenLines = includeScreenLines }}
private enum CLIInputTextValidator { static func validateDispatchID(_ value: String, name: String) -> String? { validate(value, name: name, maximumBytes: 256) }
static func validate(_ value: String, name: String, maximumBytes: Int) -> String? { guard !value.isEmpty else { return "\(name) must not be empty." } guard value.utf8.count <= maximumBytes else { return "\(name) must be at most \(maximumBytes) UTF-8 bytes." } guard !value.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains) else { return "\(name) must not contain control characters." } return nil }}
nonisolated public enum AgentSignalEvent: String, Codable, CaseIterable, Hashable, Sendable { case turnEnded = "turn-ended" case needsInput = "needs-input" case sessionStart = "session-start" case sessionEnd = "session-end" case progress}
nonisolated public struct AgentSignalInput: Codable, Sendable { public static let maximumSessionIDBytes = 256 public static let maximumOriginBytes = 256 public static let maximumDetailBytes = 32 * 1_024
public let event: AgentSignalEvent public let progress: Int? public let origin: String? public let sessionID: String? public let detail: String?
enum CodingKeys: String, CodingKey { case event case progress case origin case sessionID = "session_id" case detail }
public init( event: AgentSignalEvent, progress: Int? = nil, origin: String? = nil, sessionID: String? = nil, detail: String? = nil ) { self.event = event self.progress = progress self.origin = origin self.sessionID = sessionID self.detail = detail }
public var validationErrorMessage: String? { if event != .progress, progress != nil { return "--progress is only valid with the 'progress' event." } if let progress, !(0...100).contains(progress) { return "--progress must be between 0 and 100." } if let message = Self.validateText( sessionID, name: "--session", maximumBytes: Self.maximumSessionIDBytes ) { return message } if let message = Self.validateText( origin, name: "--origin", maximumBytes: Self.maximumOriginBytes ) { return message } return Self.validateText( detail, name: "--detail", maximumBytes: Self.maximumDetailBytes ) }
private static func validateText(_ value: String?, name: String, maximumBytes: Int) -> String? { guard let value else { return nil } guard !value.isEmpty else { return "\(name) must not be empty." } guard value.utf8.count <= maximumBytes else { return "\(name) must be at most \(maximumBytes) UTF-8 bytes." } guard !value.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains) else { return "\(name) must not contain control characters." } return nil }}
public struct ProfilesInput: Codable, Sendable { public init() {}}
public struct AgentReadInput: Codable, Sendable { public static let defaultMaxBytes = 1_024 * 1_024 public static let maximumMaxBytes = 4 * 1_024 * 1_024
public let pane: String public let maxBytes: Int public let resultOnly: Bool
enum CodingKeys: String, CodingKey { case pane case maxBytes = "max_bytes" case resultOnly = "result_only" }
public init(pane: String, maxBytes: Int = Self.defaultMaxBytes, resultOnly: Bool = false) { self.pane = pane self.maxBytes = maxBytes self.resultOnly = resultOnly }}
public struct FocusInput: Codable, Sendable { public let selector: TargetSelector
public init(selector: TargetSelector = .none) { self.selector = selector }}
public enum InputSource: String, Codable, Sendable { case argv case stdin}
public struct SendInput: Codable, Sendable { public let selector: TargetSelector public let text: String public let trailingEnter: Bool public let source: InputSource public let wait: Bool public let timeoutSeconds: Int? public let captureOutput: Bool
enum CodingKeys: String, CodingKey { case selector case text case trailingEnter = "trailing_enter" case source case wait case timeoutSeconds = "timeout_seconds" case captureOutput = "capture_output" }
public init( selector: TargetSelector = .none, text: String, trailingEnter: Bool = true, source: InputSource = .argv, wait: Bool = true, timeoutSeconds: Int? = nil, captureOutput: Bool = false ) { self.selector = selector self.text = text self.trailingEnter = trailingEnter self.source = source self.wait = wait self.timeoutSeconds = timeoutSeconds self.captureOutput = captureOutput }}
public struct KeyInput: Codable, Sendable { public let selector: TargetSelector /// The user's original token after trimming (for `requested.token` in response). public let rawToken: String /// The canonical normalized token (for execution and `key.normalized` in response). public let token: String public let repeatCount: Int
enum CodingKeys: String, CodingKey { case selector case rawToken = "raw_token" case token case repeatCount = "repeat_count" }
public init( selector: TargetSelector = .none, rawToken: String, token: String, repeatCount: Int = 1 ) { self.selector = selector self.rawToken = rawToken self.token = token self.repeatCount = repeatCount }}
public enum ReadInputSource: String, Codable, Sendable { case viewport case detection}
public struct ReadInput: Codable, Sendable { public let selector: TargetSelector public let last: Int? /// The terminal buffer requested by the caller. public let source: ReadInputSource /// When true, the app re-reads the pane until its output stops changing before responding. public let waitStable: Bool /// Sampling interval in milliseconds while waiting for stable output (nil → app default). public let stableIntervalMs: Int? /// Output must stay unchanged for this many milliseconds to count as stable (nil → app default). public let stablePeriodMs: Int? /// Maximum seconds to keep waiting for stable output before returning the latest snapshot (nil → app default). public let waitTimeoutSeconds: Int?
enum CodingKeys: String, CodingKey { case selector case last case source case waitStable case stableIntervalMs case stablePeriodMs case waitTimeoutSeconds }
public init( selector: TargetSelector = .none, last: Int? = nil, source: ReadInputSource = .viewport, waitStable: Bool = false, stableIntervalMs: Int? = nil, stablePeriodMs: Int? = nil, waitTimeoutSeconds: Int? = nil ) { self.selector = selector self.last = last self.source = source self.waitStable = waitStable self.stableIntervalMs = stableIntervalMs self.stablePeriodMs = stablePeriodMs self.waitTimeoutSeconds = waitTimeoutSeconds }
public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.selector = try container.decodeIfPresent(TargetSelector.self, forKey: .selector) ?? .none self.last = try container.decodeIfPresent(Int.self, forKey: .last) self.source = try container.decodeIfPresent(ReadInputSource.self, forKey: .source) ?? .viewport self.waitStable = try container.decodeIfPresent(Bool.self, forKey: .waitStable) ?? false self.stableIntervalMs = try container.decodeIfPresent(Int.self, forKey: .stableIntervalMs) self.stablePeriodMs = try container.decodeIfPresent(Int.self, forKey: .stablePeriodMs) self.waitTimeoutSeconds = try container.decodeIfPresent(Int.self, forKey: .waitTimeoutSeconds) }
public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(selector, forKey: .selector) try container.encodeIfPresent(last, forKey: .last) try container.encode(source, forKey: .source) try container.encode(waitStable, forKey: .waitStable) try container.encodeIfPresent(stableIntervalMs, forKey: .stableIntervalMs) try container.encodeIfPresent(stablePeriodMs, forKey: .stablePeriodMs) try container.encodeIfPresent(waitTimeoutSeconds, forKey: .waitTimeoutSeconds) }}
public enum LifecycleResource: String, Codable, Sendable, Equatable { case tab case pane}
public enum CreatePaneDirection: String, Codable, CaseIterable, Sendable, Equatable { case right case left case upward = "up" case down}
public struct CreateLaunchInput: Codable, Sendable, Equatable { /// Keeps the surface-shell spawn and prompted child exec comfortably below /// macOS ARG_MAX, including the inherited environment and configured argv. public static let maximumPromptUTF8ByteCount = 256 * 1_024
public let profile: String public let prompt: String?
public init(profile: String, prompt: String? = nil) { self.profile = profile self.prompt = prompt }}
public struct CreateInput: Codable, Sendable { public let resource: LifecycleResource public let selector: TargetSelector public let path: String? public let direction: CreatePaneDirection? public let launch: CreateLaunchInput? public let background: Bool
enum CodingKeys: String, CodingKey { case resource case selector case path case direction case launch case background }
public init( resource: LifecycleResource, selector: TargetSelector, path: String? = nil, direction: CreatePaneDirection? = nil, launch: CreateLaunchInput? = nil, background: Bool = false ) { self.resource = resource self.selector = selector self.path = path self.direction = direction self.launch = launch self.background = background }
public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) resource = try container.decode(LifecycleResource.self, forKey: .resource) selector = try container.decode(TargetSelector.self, forKey: .selector) path = try container.decodeIfPresent(String.self, forKey: .path) direction = try container.decodeIfPresent(CreatePaneDirection.self, forKey: .direction) launch = try container.decodeIfPresent(CreateLaunchInput.self, forKey: .launch) background = try container.decodeIfPresent(Bool.self, forKey: .background) ?? false }}
public struct CloseInput: Codable, Sendable { public let selector: TargetSelector public let force: Bool
public init(selector: TargetSelector, force: Bool = false) { self.selector = selector self.force = force }}
public enum TabAction: String, Codable, Sendable { case create case close}
public struct TabInput: Codable, Sendable { public let action: TabAction public let selector: TargetSelector public let path: String? public let force: Bool
enum CodingKeys: String, CodingKey { case action case selector case path case force }
public init(action: TabAction, selector: TargetSelector = .none, path: String? = nil, force: Bool = false) { self.action = action self.selector = selector self.path = path self.force = force }
public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.action = try container.decode(TabAction.self, forKey: .action) self.selector = try container.decode(TargetSelector.self, forKey: .selector) self.path = try container.decodeIfPresent(String.self, forKey: .path) self.force = try container.decodeIfPresent(Bool.self, forKey: .force) ?? false }}
public enum PaneAction: String, Codable, Sendable { case close}
public struct PaneInput: Codable, Sendable { public let action: PaneAction public let selector: TargetSelector public let force: Bool
enum CodingKeys: String, CodingKey { case action case selector case force }
public init(action: PaneAction, selector: TargetSelector = .none, force: Bool = false) { self.action = action self.selector = selector self.force = force }
public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.action = try container.decode(PaneAction.self, forKey: .action) self.selector = try container.decode(TargetSelector.self, forKey: .selector) self.force = try container.decodeIfPresent(Bool.self, forKey: .force) ?? false }}
// MARK: - Workflow
nonisolated public enum WorkflowInputAction: String, Codable, Sendable { case read case list case run case status case deliver case cancel}
/// The wire request for the workflow command family (docs-ai 063 B1/B3). Fields are/// action-specific; the app handler ignores the ones that do not belong to `action`.nonisolated public struct WorkflowInput: Codable, Sendable { public let action: WorkflowInputAction /// `list`: the worktree whose repo source is searched. `run`: the source pane (a workflow with a /// `current` role) or worktree. `.none` = the caller's pane, then the focused worktree (`list`). public let target: TargetSelector /// `run`: workflow id or unique name. public let workflow: String? public let invocation: Int? public let contentOffset: Int64? public let contentResource: String? public let testAction: String? public let actionInputs: [String: WorkflowJSONValue]? /// `run`: `<role>=<binding>` overrides (dsl-spec §9). public let roleBindings: [String] /// `run`: `<name>=<value>` inputs. public let inputValues: [String] /// `run`: step ids skipped at start. public let skippedSteps: [String] /// `status` / `cancel`: the run; `deliver`: the manual target together with `stepID`. public let runID: String? public let stepID: String? /// `deliver`: the delivered output body (already read by the CLI). public let body: String? public let verdict: String? /// `deliver`: `--token` or `$PROWL_WORKFLOW_TOKEN`; correlation only, never authentication. public let token: String? /// `deliver`: deliver to the explicit target even when the caller pane belongs to another step. public let force: Bool
public init( action: WorkflowInputAction = .list, target: TargetSelector = .none, workflow: String? = nil, invocation: Int? = nil, contentOffset: Int64? = nil, contentResource: String? = nil, testAction: String? = nil, actionInputs: [String: WorkflowJSONValue]? = nil, roleBindings: [String] = [], inputValues: [String] = [], skippedSteps: [String] = [], runID: String? = nil, stepID: String? = nil, body: String? = nil, verdict: String? = nil, token: String? = nil, force: Bool = false ) { self.action = action self.target = target self.workflow = workflow self.invocation = invocation self.contentOffset = contentOffset self.contentResource = contentResource self.testAction = testAction self.actionInputs = actionInputs self.roleBindings = roleBindings self.inputValues = inputValues self.skippedSteps = skippedSteps self.runID = runID self.stepID = stepID self.body = body self.verdict = verdict self.token = token self.force = force }}