native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496// supacode/Domain/Workflow/WorkflowRun.swift// The state of one workflow run (docs-ai 063 B2, dsl-spec §5/§8/§10): frozen context and// bindings, the position cursor, invocations and activations, deliveries, and the attention// vocabulary the panel renders. Transitions live in WorkflowRunMachine.
import CryptoKitimport Foundationimport ProwlCLIShared
// MARK: - Identity and bindings
/// A pane a role is bound to; the handle is the short `pN` form templates expose.nonisolated struct WorkflowPaneIdentity: Equatable, Sendable, Codable { let surfaceID: UUID let tabID: UUID? let handle: String /// The pane's launch-profile name when Prowl launched it, otherwise the detected agent's /// display name (or `shell` when none). let displayName: String /// The detected agent token; nil for a bare shell. let agent: String? var sessionIdentity: String?
enum CodingKeys: String, CodingKey { case surfaceID = "surface_id" case tabID = "tab_id" case handle case displayName = "display_name" case agent case sessionIdentity = "session_identity" }}
/// The profile frozen into a `launch` role: identity and agent token only — the launch plan/// (with its environment) stays with the wiring layer and never reaches `run.json`.nonisolated struct WorkflowProfileBinding: Equatable, Sendable, Codable { let id: UUID let name: String let agent: String}
nonisolated enum WorkflowRoleBinding: Equatable, Sendable { case current(WorkflowPaneIdentity) case pick(WorkflowPaneIdentity) case launch(WorkflowProfileBinding, pane: WorkflowPaneIdentity?)
var source: WorkflowRoleSource { switch self { case .current: .current case .pick: .pick case .launch: .launch } }
var pane: WorkflowPaneIdentity? { switch self { case .current(let pane), .pick(let pane): pane case .launch(_, let pane): pane } }
var profile: WorkflowProfileBinding? { if case .launch(let profile, _) = self { return profile } return nil }
/// `roles.<r>.name` / `roles.<r>.agent` as dsl-spec §6 defines them. var displayName: String { switch self { case .current(let pane), .pick(let pane): pane.displayName case .launch(let profile, _): profile.name } }
var agent: String { switch self { case .current(let pane), .pick(let pane): pane.agent ?? "" case .launch(let profile, _): profile.agent } }
func binding(pane: WorkflowPaneIdentity) -> WorkflowRoleBinding { switch self { case .current: .current(pane) case .pick: .pick(pane) case .launch(let profile, _): .launch(profile, pane: pane) } }}
// MARK: - Context
nonisolated enum WorkflowRunScope: Equatable, Sendable, Codable { case bundle case user case repo(repositoryID: String)
/// The scope key of the binding memory (dsl-spec §3): `bundle`, `user`, or `repo:<repository id>`. var key: String { switch self { case .bundle: "bundle" case .user: "user" case .repo(let repositoryID): "repo:\(repositoryID)" } }
var workflowScope: WorkflowScope { switch self { case .bundle: .bundle case .user: .user case .repo: .repo } }
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let key = try container.decode(String.self) switch key { case "bundle": self = .bundle case "user": self = .user default: guard key.hasPrefix("repo:") else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unknown workflow scope '\(key)'.") } self = .repo(repositoryID: String(key.dropFirst("repo:".count))) } }
func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(key) }}
nonisolated struct WorkflowRunWorktree: Equatable, Sendable, Codable { let id: String let name: String let branch: String /// The worktree root every run-relative path hangs from. let path: String
var rootURL: URL { URL(filePath: path, directoryHint: .isDirectory) }}
nonisolated struct WorkflowRunContext: Equatable, Sendable { let scope: WorkflowRunScope let definitionPath: String? let worktree: WorkflowRunWorktree var bundle: WorkflowPreparedBundle? var sourcePaneID: UUID? var sourceSessionIdentity: String? var sourceTabID: UUID? var literalActionInputs = false var historyDirectory: URL? var occupancy: WorkflowRunOccupancy?}
/// Every path under a run directory, derived from validated slugs and the run UUID only.nonisolated enum WorkflowRunPaths { static func runsDirectory(root: URL) -> URL { let storage = WorkflowHistoryStorage.configured return storage.baseURL.appending(path: storage.rootKey(root)) }
static func runDirectory(root: URL, runID: UUID, createdAt: Date? = nil) -> URL { let storage = WorkflowHistoryStorage.configured if createdAt == nil, let existing = try? storage.find(runID) { return existing } return storage.directory(root: root, createdAt: createdAt ?? Date(), runID: runID) }
static func promptURL(runDirectory: URL, stepID: String, ordinal: Int) -> URL { runDirectory.appending(path: "prompts", directoryHint: .isDirectory) .appending(path: "\(stepID).\(ordinal).md", directoryHint: .notDirectory) }
/// Content-addressed snapshots preserve each submission when an invocation is corrected. static func submissionURL(runDirectory: URL, name: String, ordinal: Int, body: String) -> URL { let digest = SHA256.hash(data: Data(body.utf8)).map { String(format: "%02x", $0) }.joined() return runDirectory.appending(path: "deliveries/\(name).\(ordinal).\(digest).md") }
static func deliveryURL(runDirectory: URL, name: String, ordinal: Int?) -> URL { let file = ordinal.map { "\(name).\($0).md" } ?? "\(name).md" return runDirectory.appending(path: "deliveries", directoryHint: .isDirectory) .appending(path: file, directoryHint: .notDirectory) }
static func skillDirectory(runDirectory: URL, skillID: String) -> URL { runDirectory.appending(path: "skills", directoryHint: .isDirectory) .appending(path: skillID, directoryHint: .isDirectory) }
static func path(_ url: URL) -> String { AgentProfileLaunchPlanner.pathString(url) }}
// MARK: - Invocations and activations
nonisolated enum WorkflowActivationState: String, Equatable, Sendable, Codable { case waiting /// A validated delivery is being written to the run directory; the record completes once it is. case persisting /// The delivery is on disk but had issues a non-strict step tolerates; the user decides. case provisional case delivered case skipped case revoked}
/// A waiting invocation (dsl-spec §5): the token correlates a delivery, the dispatch id is/// the record in the shared dispatch store once the activation is open.nonisolated struct WorkflowActivation: Equatable, Sendable { let ordinal: Int let stepID: String let role: String let token: String let expect: WorkflowExpectation let deliveryName: String var dispatchID: String? var state: WorkflowActivationState /// `expect.timeout` as an absolute deadline, fixed when the activation opened; a re-armed /// watchdog receives the remaining time, never a fresh cap. var deadline: Date? /// The validated body while the delivery is being persisted (never written to `run.json`). var pendingDelivery: WorkflowValidatedDelivery?
var completion: WorkflowCompletionCommand { WorkflowCompletionCommand(token: token, verdicts: expect.verdicts) }}
nonisolated enum WorkflowInvocationKind: String, Equatable, Sendable, Codable { case message case launch}
nonisolated struct WorkflowInvocation: Equatable, Sendable { let ordinal: Int let stepID: String /// 1-based iteration when the step sits inside a loop. let iteration: Int? let role: String let kind: WorkflowInvocationKind let startedAt: Date var promptPath: String? var target: WorkflowRunRecord.Binding? var content: WorkflowTaskContent? var activation: WorkflowActivation? var endedAt: Date?}
nonisolated struct WorkflowDeliveryRecord: Equatable, Sendable, Codable { let name: String let ordinal: Int /// `deliveries/<name>.<ordinal>.md`. let path: String /// `deliveries/<name>.md`, the atomically replaced latest view. let latestPath: String let verdict: String? let deliveredAt: Date
enum CodingKeys: String, CodingKey { case name case ordinal case path case latestPath = "latest_path" case verdict case deliveredAt = "delivered_at" }}
nonisolated struct WorkflowHistorySubmission: Codable, Equatable, Sendable { var delivery: WorkflowDeliveryRecord var accepted: Bool var issues: [String] var statusLabel: String { if accepted { return String(localized: "Accepted") } return issues.isEmpty ? String(localized: "Not accepted") : String(localized: "Needs correction") }}
// MARK: - Position and step records
nonisolated enum WorkflowStepState: String, Equatable, Sendable, Codable { case active case completed case skipped case failed}
nonisolated struct WorkflowStepRecord: Equatable, Sendable { let stepID: String let iteration: Int? var state: WorkflowStepState var ordinal: Int? var iterationPath: [String]? var branchExcluded: Bool? var title: String? var error: String? var outputs: [String: WorkflowJSONValue]? var delivery: WorkflowDeliveryRecord? var submissions: [WorkflowHistorySubmission]? var actionExecutionID: String? var summary: String?}
// MARK: - Attention and status
nonisolated enum WorkflowAttentionAction: String, Equatable, Sendable, Codable, CaseIterable { case focusPane = "focus_pane" case nudge case keepWaiting = "keep_waiting" case retry case relaunch /// Keep a provisional delivery as it is. case acceptDelivery = "accept_delivery" /// Keep a provisional delivery and supply the verdict it lacks (one of the declared values). case acceptWithVerdict = "accept_with_verdict" /// Type the step's requirements into the role's pane again and keep waiting. case askAgain = "ask_again" case skip case cancel}
nonisolated enum WorkflowAgentGoneReason: String, Equatable, Sendable, Codable { case sessionEnded = "session_ended" case paneClosed = "pane_closed" case processGone = "process_gone" /// The role has no pane: its launch was skipped or never succeeded. case notLaunched = "not_launched"}
/// Why an injection did not deliver the line.nonisolated enum WorkflowInjectionFailure: Equatable, Sendable { /// The role is working or blocked again; the step returns to its idle wait. case roleBusy case roleBlocked case surfaceMissing case insertFailed /// The insert succeeded; the line may sit unsubmitted in the pane's input. case submitFailed case activationUnavailable(String)}
nonisolated enum WorkflowAttentionReason: Equatable, Sendable { case needsInput case idleWithoutDelivery case blocked case agentGone(WorkflowAgentGoneReason) case injectionFailed(WorkflowInjectionFailure) case launchFailed(String) case renderedTextInvalid case actionFailed(String) /// The validated output could not be written to the run directory. case persistFailed(String) /// A non-strict delivery is on disk with these issues; the user accepts, asks again, or skips. case deliveryIssues([WorkflowDeliveryIssue]) case timeout}
nonisolated struct WorkflowAttention: Equatable, Sendable { let reason: WorkflowAttentionReason let stepID: String let role: String? let ordinal: Int? let actions: [WorkflowAttentionAction] /// Panel copy (decision H7 of docs-ai 063.007); C1 renders it as is. let message: String}
nonisolated enum WorkflowRunStatus: Equatable, Sendable { case running case needsAttention(WorkflowAttention) case completed case cancelled case skipped(step: String, dependent: String) case iterationLimitReached case interrupted
var isTerminal: Bool { switch self { case .running, .needsAttention: false case .completed, .cancelled, .skipped, .iterationLimitReached, .interrupted: true } }
var attention: WorkflowAttention? { if case .needsAttention(let attention) = self { return attention } return nil }}
/// What the run is doing inside the current step.nonisolated enum WorkflowRunPhase: Equatable, Sendable { case idle case waitingForRole(role: String, ordinal: Int) case injecting(ordinal: Int) case launching(ordinal: Int) case waitingForDelivery(ordinal: Int) case runningAction(stepID: String)}
// MARK: - Run
nonisolated struct WorkflowRun: Equatable, Sendable { let id: UUID let definition: WorkflowDefinition let context: WorkflowRunContext let inputs: [String: String] let startedAt: Date var updatedAt: Date var finishedAt: Date? var bindings: [String: WorkflowRoleBinding] var status: WorkflowRunStatus = .running var phase: WorkflowRunPhase = .idle var invocations: [WorkflowInvocation] = [] /// Latest delivered output per name (latest wins across steps). var deliveries: [String: WorkflowDeliveryRecord] = [:] var actionOutputs: [String: [String: WorkflowJSONValue]] = [:] /// Metadata for non-revocable writes; cancellation can discard activation bodies but not their attribution. var pendingHistorySubmissions: [Int: WorkflowHistorySubmission] = [:] var controlCursor: WorkflowControlCursor? var stepValues: [String: WorkflowJSONValue] = [:] var observations: [String: WorkflowJSONValue] = [:] var actionExecutionID: String? var actionAttempts: [String: Int] = [:] /// Delivery name → the step whose skip made it missing. var skippedDeliveries: [String: String] = [:] /// Steps skipped at start (`--skip` / the start sheet). let preSkippedSteps: Set<String> var historyIsPartial = false var participants: [String: [WorkflowPaneIdentity]] = [:] var stepRecords: [WorkflowStepRecord] = [] var nextOrdinal = 1 /// The first step's rendered line when the run was started from the `current` role's own /// pane: returned to the caller instead of being typed (dsl-spec §9). var selfInitiatedLine: String?
mutating func captureParticipantSessions() { for (role, binding) in bindings { guard var pane = binding.pane, case .object(let fields) = observations[pane.surfaceID.uuidString], case .string(let identity) = fields["session_identity"] else { continue } pane.sessionIdentity = identity if !participants[role, default: []].contains(pane) { participants[role, default: []].append(pane) } } }
var runDirectory: URL { context.historyDirectory ?? WorkflowRunPaths.runDirectory(root: context.worktree.rootURL, runID: id, createdAt: startedAt) }
var currentInvocation: WorkflowInvocation? { switch phase { case .waitingForRole(_, let ordinal), .injecting(let ordinal), .launching(let ordinal), .waitingForDelivery(let ordinal): return invocations.first { $0.ordinal == ordinal } case .idle, .runningAction: return nil } }
/// The activation currently waiting for a delivery, if any. var currentActivation: WorkflowActivation? { guard case .waitingForDelivery(let ordinal) = phase else { return nil } return invocations.first { $0.ordinal == ordinal }?.activation }
/// The activation of the invocation in flight, whatever its state. var activeActivation: WorkflowActivation? { currentInvocation?.activation }
func activation(forDispatchID dispatchID: String) -> WorkflowActivation? { invocations.lazy.compactMap(\.activation).first { $0.dispatchID == dispatchID } }
/// The step the position cursor points at; nil past the end of the sequence it is in. var currentStep: WorkflowStepDefinition? { controlCursor?.currentStep } var currentIteration: Int? { controlCursor?.iteration }
/// Whether the runner will deliver a `message` to the `current` role (dsl-spec §3). func deliversToCurrentRole() -> Bool { guard let current = definition.roles.first(where: { $0.source == .current }) else { return false } return definition.flattenedSteps.contains { step in if case .message(let role, _, _) = step.action, role == current.name { return !preSkippedSteps.contains(step.id) } return false } }}