├── .gitignore ├── Package.swift ├── LICENSE ├── Sources └── PocketFlow │ └── PocketFlow.swift ├── Tests └── PocketFlowTests │ └── PocketFlowTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PocketFlow", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "PocketFlow", 16 | targets: ["PocketFlow"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "PocketFlow"), 23 | .testTarget( 24 | name: "PocketFlowTests", 25 | dependencies: ["PocketFlow"] 26 | ), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PocketFlow-Swift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Sources/PocketFlow/PocketFlow.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Action Alias 4 | 5 | public typealias Action = String 6 | 7 | // MARK: - Protocol 8 | 9 | public protocol FlowNode: AnyObject { 10 | associatedtype Shared 11 | associatedtype Prep: Sendable 12 | associatedtype Exec: Sendable 13 | associatedtype Act: Hashable & Sendable = Action 14 | 15 | func prep(shared: inout Shared) async throws -> Prep 16 | func exec(prep: Prep) async throws -> Exec 17 | func post(shared: inout Shared, prep: Prep, exec: Exec) async throws -> Act? 18 | } 19 | 20 | extension FlowNode { 21 | public func _run(shared: inout Shared) async throws -> Act? { 22 | let p = try await prep(shared: &shared) 23 | let e = try await exec(prep: p) 24 | return try await post(shared: &shared, prep: p, exec: e) 25 | } 26 | } 27 | 28 | // MARK: - Base Node 29 | 30 | open class BaseNode: FlowNode { 31 | public typealias Shared = [String: Sendable] 32 | public typealias Prep = PrepRes 33 | public typealias Exec = ExecRes 34 | public typealias Act = Action 35 | 36 | public var maxRetries: Int = 1 37 | public var waitInSeconds: UInt64 = 0 38 | public var successors: [Action: any AnyNode] = [:] 39 | public var params: [String: Sendable] = [:] 40 | 41 | public init() {} 42 | 43 | open func prep(shared: inout Shared) async throws -> PrepRes { 44 | fatalError("Must override prep") 45 | } 46 | 47 | open func exec(prep: PrepRes) async throws -> ExecRes { 48 | fatalError("Must override exec") 49 | } 50 | 51 | open func post(shared: inout Shared, prep: PrepRes, exec: ExecRes) async throws -> Action? { 52 | fatalError("Must override post") 53 | } 54 | 55 | open func execFallback(prep: PrepRes, error: any Error) async throws -> ExecRes { 56 | throw error 57 | } 58 | 59 | public func _exec(prep: PrepRes) async throws -> ExecRes { 60 | for i in 0.. 0 { 68 | try await Task.sleep(nanoseconds: UInt64(waitInSeconds) * 1_000_000_000) 69 | } 70 | } 71 | } 72 | throw NSError(domain: "RetryFailed", code: -1) 73 | } 74 | 75 | public func setNext(_ action: Action = "default", _ node: some AnyNode) { 76 | successors[action] = node 77 | } 78 | } 79 | 80 | // MARK: - Type Erasure 81 | 82 | public protocol AnyNode: AnyObject { 83 | var params: [String: Sendable] { get set } 84 | var successors: [Action: any AnyNode] { get set } 85 | func _run(shared: inout [String: Sendable]) async throws -> Action? 86 | } 87 | 88 | extension BaseNode: AnyNode { 89 | public func _run(shared: inout [String: Sendable]) async throws -> Action? { 90 | let prep = try await prep(shared: &shared) 91 | let exec = try await _exec(prep: prep) 92 | return try await post(shared: &shared, prep: prep, exec: exec) 93 | } 94 | } 95 | 96 | // MARK: - Conditional Transition 97 | 98 | public struct _ConditionalTransition { 99 | let node: BaseNode 100 | let action: Action 101 | 102 | public init(node: BaseNode, action: Action) { 103 | self.node = node 104 | self.action = action 105 | } 106 | } 107 | 108 | // MARK: - Flow 109 | 110 | public final class Flow { 111 | public var startNode: any AnyNode 112 | 113 | public init(start: any AnyNode) { 114 | self.startNode = start 115 | } 116 | 117 | public func run(shared: inout [String: Sendable]) async throws -> Action? { 118 | var current: AnyNode? = startNode 119 | var lastAction: Action? = nil 120 | 121 | while let node = current { 122 | lastAction = try await node._run(shared: &shared) 123 | current = node.successors[lastAction ?? "default"] 124 | } 125 | 126 | return lastAction 127 | } 128 | } 129 | 130 | // MARK: - Operators 131 | 132 | precedencegroup FlowConnectPrecedence { 133 | associativity: left 134 | } 135 | 136 | precedencegroup FlowTransitionPrecedence { 137 | associativity: left 138 | higherThan: FlowConnectPrecedence 139 | } 140 | 141 | infix operator >>> : FlowConnectPrecedence 142 | infix operator <> : FlowTransitionPrecedence 143 | 144 | @discardableResult 145 | public func >>> ( 146 | lhs: BaseNode, 147 | rhs: N 148 | ) -> N { 149 | lhs.setNext("default", rhs) 150 | return rhs 151 | } 152 | 153 | @discardableResult 154 | public func >>> ( 155 | lhs: _ConditionalTransition, 156 | rhs: N 157 | ) -> N { 158 | lhs.node.setNext(lhs.action, rhs) 159 | return rhs 160 | } 161 | 162 | public func <> (lhs: BaseNode, action: Action) 163 | -> _ConditionalTransition 164 | { 165 | _ConditionalTransition(node: lhs, action: action) 166 | } 167 | -------------------------------------------------------------------------------- /Tests/PocketFlowTests/PocketFlowTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | 3 | @testable import PocketFlow 4 | 5 | struct FlowTests { 6 | @Test func testLinearFlow() async throws { 7 | final class A: BaseNode { 8 | override func prep(shared: inout BaseNode.Shared) async throws -> String 9 | { 10 | return "A_PREP_RESULT" 11 | } 12 | 13 | override func exec(prep: String) async throws -> String { 14 | return "A_EXEC_RESULT" 15 | } 16 | 17 | override func post(shared: inout Shared, prep: String, exec: String) async throws 18 | -> Action? 19 | { 20 | shared["a_exec_result"] = exec 21 | return "default" 22 | } 23 | } 24 | 25 | final class B: BaseNode { 26 | override func prep(shared: inout BaseNode.Shared) async throws -> String 27 | { 28 | return "B_PREP_RESULT" 29 | } 30 | 31 | override func exec(prep: String) async throws -> String { 32 | return "B_EXEC_RESULT" 33 | } 34 | 35 | override func post(shared: inout Shared, prep: String, exec: String) async throws 36 | -> Action? 37 | { 38 | shared["b_exec_result"] = exec 39 | return "default" 40 | } 41 | } 42 | 43 | let a = A() 44 | let b = B() 45 | a >>> b 46 | 47 | let flow = Flow(start: a) 48 | var shared = [String: Sendable]() 49 | _ = try await flow.run(shared: &shared) 50 | 51 | guard let aExecResult = shared["a_exec_result"] as? String else { 52 | Issue.record("shared[\"a_exec_result\"] not found") 53 | return 54 | } 55 | #expect(aExecResult == "A_EXEC_RESULT") 56 | 57 | guard let bExecResult = shared["b_exec_result"] as? String else { 58 | Issue.record("shared[\"b_exec_result\"] not found") 59 | return 60 | } 61 | #expect(bExecResult == "B_EXEC_RESULT") 62 | } 63 | 64 | @Test func testBranching() async throws { 65 | final class Review: BaseNode { 66 | override func prep(shared: inout BaseNode.Shared) async throws -> String 67 | { 68 | return "REVIEW_PREP_RESULT" 69 | } 70 | 71 | override func exec(prep: String) async throws -> String { 72 | return "REVIEW_EXEC_RESULT" 73 | } 74 | 75 | override func post(shared: inout Shared, prep: String, exec: String) async throws 76 | -> Action? 77 | { 78 | shared["review_exec_result"] = exec 79 | return "approve" 80 | } 81 | } 82 | 83 | final class Publish: BaseNode { 84 | override func prep(shared: inout BaseNode.Shared) async throws -> String 85 | { 86 | return "PUBLISH_PREP_RESULT" 87 | } 88 | 89 | override func exec(prep: String) async throws -> String { 90 | return "PUBLISH_EXEC_RESULT" 91 | } 92 | 93 | override func post( 94 | shared: inout BaseNode.Shared, prep: String, exec: String 95 | ) async throws -> Action? { 96 | shared["publish_exec_result"] = exec 97 | return nil 98 | } 99 | } 100 | 101 | final class Draft: BaseNode { 102 | override func prep(shared: inout BaseNode.Shared) async throws -> String 103 | { 104 | return "DRAFT_PREP_RESULT" 105 | } 106 | 107 | override func exec(prep: String) async throws -> String { 108 | return "drafted" 109 | } 110 | 111 | override func post( 112 | shared: inout BaseNode.Shared, prep: String, exec: String 113 | ) async throws -> Action? { 114 | shared["DRAFT_EXEC_RESULT"] = exec 115 | return nil 116 | } 117 | } 118 | 119 | let review = Review() 120 | let publish = Publish() 121 | let draft = Draft() 122 | 123 | review <> "approve" >>> publish 124 | review <> "revise" >>> draft 125 | 126 | let flow = Flow(start: review) 127 | var shared = [String: Sendable]() 128 | _ = try await flow.run(shared: &shared) 129 | 130 | guard let reviewExecResult = shared["review_exec_result"] as? String else { 131 | Issue.record("shared[\"a_exec_result\"] not found") 132 | return 133 | } 134 | #expect(reviewExecResult == "REVIEW_EXEC_RESULT") 135 | 136 | guard let publishExecResult = shared["publish_exec_result"] as? String else { 137 | Issue.record("shared[\"publish_exec_result\"] not found") 138 | return 139 | } 140 | #expect(publishExecResult == "PUBLISH_EXEC_RESULT") 141 | 142 | #expect(shared["draft_exec_result"] == nil) 143 | } 144 | 145 | @Test func testLooping() async throws { 146 | final class Loop: BaseNode { 147 | override func prep(shared: inout Shared) async throws -> Int { 148 | guard let count = shared["count"] as? Int else {return 0} 149 | return count 150 | } 151 | 152 | override func exec(prep: Int) async throws -> Int { 153 | return prep + 1 154 | } 155 | 156 | override func post(shared: inout Shared, prep: Int, exec: Int) async throws -> Action? { 157 | let count = exec 158 | shared["count"] = count 159 | return count >= 3 ? "done" : "loop" 160 | } 161 | } 162 | 163 | final class Done: BaseNode { 164 | override func prep(shared: inout BaseNode.Shared) async throws -> String { 165 | return "DONE_PREP_RESULT" 166 | } 167 | 168 | override func exec(prep: String) async throws -> String { 169 | return "DONE" 170 | } 171 | 172 | override func post(shared: inout BaseNode.Shared, prep: String, exec: String) async throws -> Action? { 173 | shared["done_exec_result"] = exec 174 | return nil 175 | } 176 | } 177 | 178 | let loop = Loop() 179 | let done = Done() 180 | 181 | loop <> "loop" >>> loop 182 | loop <> "done" >>> done 183 | 184 | let flow = Flow(start: loop) 185 | var shared = [String: Sendable]() 186 | _ = try await flow.run(shared: &shared) 187 | 188 | guard let countResult = shared["count"] as? Int else { 189 | Issue.record("shared[\"count\"] not found") 190 | return 191 | } 192 | #expect(countResult == 3) 193 | 194 | guard let doneExecResult = shared["done_exec_result"] as? String else { 195 | Issue.record("shared[\"done_exec_result\"] not found") 196 | return 197 | } 198 | #expect(doneExecResult == "DONE") 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PocketFlow-Swift 2 | 3 | [![Swift](https://img.shields.io/badge/Swift-6.1+-orange.svg)](https://swift.org) 4 | [![Platforms](https://img.shields.io/badge/Platforms-iOS%2013%2B%20%7C%20macOS%2010.15%2B-blue.svg)](https://swift.org) 5 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 6 | [![Lines of Code](https://img.shields.io/badge/Lines%20of%20Code-~165-brightgreen.svg)](Sources/PocketFlow/PocketFlow.swift) 7 | [![SPM Compatible](https://img.shields.io/badge/SPM-Compatible-success.svg)](Package.swift) 8 | 9 | A Swift port of the minimalist LLM workflow framework [PocketFlow](https://github.com/The-Pocket/PocketFlow). 10 | 11 | > **Lightweight**: Just ~165 lines. Zero bloat, zero dependencies, zero vendor lock-in. 12 | > **Expressive**: Everything you love—Agents, Workflows, branching, and looping. 13 | > **Agentic Coding**: Let AI Agents build Agents—10x productivity boost! 14 | 15 | ## About 16 | 17 | PocketFlow-Swift is a Swift implementation inspired by the original [PocketFlow](https://github.com/The-Pocket/PocketFlow) Python framework. While the original focuses on LLM applications, this Swift port provides a general-purpose workflow orchestration framework that can be used for any asynchronous task coordination. 18 | 19 | ## Installation 20 | 21 | ### Swift Package Manager 22 | 23 | Add PocketFlow-Swift to your `Package.swift`: 24 | 25 | ```swift 26 | dependencies: [ 27 | .package(url: "https://github.com/phucledien/PocketFlow-Swift.git", from: "1.0.1") 28 | ] 29 | ``` 30 | 31 | ### Manual Installation 32 | 33 | Simply copy the `Sources/PocketFlow/PocketFlow.swift` file into your project. It's only ~165 lines! 34 | 35 | ## Quick Start 36 | 37 | ### 1. Define Your Nodes 38 | 39 | Create custom nodes by inheriting from `BaseNode`: 40 | 41 | ```swift 42 | import PocketFlow 43 | 44 | final class ProcessData: BaseNode { 45 | override func prep(shared: inout Shared) async throws -> String { 46 | // Preparation phase - setup input data 47 | return "input_data" 48 | } 49 | 50 | override func exec(prep: String) async throws -> [String] { 51 | // Execution phase - main logic 52 | return prep.components(separatedBy: "_") 53 | } 54 | 55 | override func post(shared: inout Shared, prep: String, exec: [String]) async throws -> Action? { 56 | // Post-processing phase - store results and determine next action 57 | shared["processed_data"] = exec 58 | return "success" 59 | } 60 | } 61 | ``` 62 | 63 | ### 2. Build Your Workflow 64 | 65 | Connect nodes using operators: 66 | 67 | ```swift 68 | let processData = ProcessData() 69 | let saveResults = SaveResults() 70 | let handleError = HandleError() 71 | 72 | // Linear flow: processData -> saveResults 73 | processData >>> saveResults 74 | 75 | // Conditional flow: processData -> saveResults (on "success") or handleError (on "error") 76 | processData <> "success" >>> saveResults 77 | processData <> "error" >>> handleError 78 | ``` 79 | 80 | ### 3. Execute Your Flow 81 | 82 | ```swift 83 | let flow = Flow(start: processData) 84 | var shared = [String: Sendable]() 85 | let result = try await flow.run(shared: &shared) 86 | ``` 87 | 88 | ## Core Concepts 89 | 90 | ### BaseNode 91 | 92 | The building block of workflows. Every node has three phases: 93 | 94 | - **`prep`**: Prepare input data from shared state 95 | - **`exec`**: Execute the main logic (with automatic retry support) 96 | - **`post`**: Process results and determine the next action 97 | 98 | ### Flow 99 | 100 | Orchestrates the execution of connected nodes, maintaining shared state throughout the workflow. 101 | 102 | ### Operators 103 | 104 | - **`>>>`**: Sequential connection (default path) 105 | - **`<>`**: Conditional connection (specific action path) 106 | 107 | ## Workflow Patterns 108 | 109 | ### Linear Workflow 110 | 111 | ```swift 112 | final class StepA: BaseNode { 113 | override func prep(shared: inout Shared) async throws -> String { 114 | return "input" 115 | } 116 | 117 | override func exec(prep: String) async throws -> String { 118 | return "processed_\(prep)" 119 | } 120 | 121 | override func post(shared: inout Shared, prep: String, exec: String) async throws -> Action? { 122 | shared["step_a_result"] = exec 123 | return "default" 124 | } 125 | } 126 | 127 | final class StepB: BaseNode { 128 | override func prep(shared: inout Shared) async throws -> String { 129 | return shared["step_a_result"] as? String ?? "" 130 | } 131 | 132 | override func exec(prep: String) async throws -> String { 133 | return "final_\(prep)" 134 | } 135 | 136 | override func post(shared: inout Shared, prep: String, exec: String) async throws -> Action? { 137 | shared["final_result"] = exec 138 | return nil // End of workflow 139 | } 140 | } 141 | 142 | let stepA = StepA() 143 | let stepB = StepB() 144 | stepA >>> stepB 145 | 146 | let flow = Flow(start: stepA) 147 | var shared = [String: Sendable]() 148 | _ = try await flow.run(shared: &shared) 149 | ``` 150 | 151 | ### Branching Workflow 152 | 153 | ```swift 154 | final class DecisionNode: BaseNode { 155 | override func exec(prep: String) async throws -> String { 156 | return prep 157 | } 158 | 159 | override func post(shared: inout Shared, prep: String, exec: String) async throws -> Action? { 160 | // Return different actions based on logic 161 | return exec.contains("success") ? "approve" : "reject" 162 | } 163 | } 164 | 165 | let decision = DecisionNode() 166 | let approveNode = ApproveNode() 167 | let rejectNode = RejectNode() 168 | 169 | decision <> "approve" >>> approveNode 170 | decision <> "reject" >>> rejectNode 171 | ``` 172 | 173 | ### Looping Workflow 174 | 175 | ```swift 176 | final class LoopNode: BaseNode { 177 | override func prep(shared: inout Shared) async throws -> Int { 178 | return shared["counter"] as? Int ?? 0 179 | } 180 | 181 | override func exec(prep: Int) async throws -> Int { 182 | return prep + 1 183 | } 184 | 185 | override func post(shared: inout Shared, prep: Int, exec: Int) async throws -> Action? { 186 | shared["counter"] = exec 187 | return exec >= 5 ? "done" : "continue" 188 | } 189 | } 190 | 191 | let loop = LoopNode() 192 | let done = DoneNode() 193 | 194 | loop <> "continue" >>> loop // Self-loop 195 | loop <> "done" >>> done 196 | ``` 197 | 198 | ## Advanced Features 199 | 200 | ### Retry Logic 201 | 202 | Configure automatic retries and wait periods: 203 | 204 | ```swift 205 | let unreliableNode = UnreliableNode() 206 | unreliableNode.maxRetries = 3 207 | unreliableNode.waitInSeconds = 1 208 | 209 | // Override execFallback for custom error handling 210 | class UnreliableNode: BaseNode { 211 | override func execFallback(prep: String, error: any Error) async throws -> String { 212 | return "fallback_result" 213 | } 214 | } 215 | ``` 216 | 217 | ### Shared State 218 | 219 | Pass data between nodes using the shared dictionary: 220 | 221 | ```swift 222 | override func post(shared: inout Shared, prep: PrepType, exec: ExecType) async throws -> Action? { 223 | shared["key"] = "value" 224 | shared["results"] = computedResults 225 | return "next_action" 226 | } 227 | ``` 228 | 229 | ## Examples 230 | 231 | Check out the test files for complete examples: 232 | 233 | - **Linear Flow**: Sequential processing with state sharing 234 | - **Branching Flow**: Conditional routing based on node results 235 | - **Looping Flow**: Iterative processing with termination conditions 236 | 237 | ## Comparison with Original PocketFlow 238 | 239 | | Feature | Original PocketFlow (Python) | PocketFlow-Swift | 240 | |---------|-------------------------------|------------------| 241 | | **Purpose** | LLM workflow orchestration | General async workflow orchestration | 242 | | **Lines of Code** | ~100 | ~165 | 243 | | **Dependencies** | Zero | Zero | 244 | | **Type Safety** | Runtime | Compile-time | 245 | | **Concurrency** | asyncio | Swift async/await | 246 | | **Operators** | `>>`, `~>` | `>>>`, `<>` | 247 | 248 | ## Contributing 249 | 250 | 1. Fork the repository 251 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 252 | 3. Commit your changes (`git commit -m 'Add amazing feature'`) 253 | 4. Push to the branch (`git push origin feature/amazing-feature`) 254 | 5. Open a Pull Request 255 | 256 | ## License 257 | 258 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 259 | 260 | ## Acknowledgments 261 | 262 | - Original [PocketFlow](https://github.com/The-Pocket/PocketFlow) by [The Pocket](https://github.com/The-Pocket) 263 | - Inspired by the minimalist philosophy of the original framework 264 | - Built for the Swift ecosystem with modern async/await support 265 | 266 | ## Related Projects 267 | 268 | - [PocketFlow](https://github.com/The-Pocket/PocketFlow) - Original Python implementation 269 | - [PocketFlow TypeScript](https://github.com/The-Pocket/PocketFlow) - TypeScript port 270 | - [PocketFlow Java](https://github.com/The-Pocket/PocketFlow) - Java port 271 | - [PocketFlow C++](https://github.com/The-Pocket/PocketFlow) - C++ port 272 | - [PocketFlow Go](https://github.com/The-Pocket/PocketFlow) - Go port 273 | 274 | --- 275 | 276 | **Happy Flowing! 🚀** --------------------------------------------------------------------------------