{
  "authors" : [
    {
      "name" : "Alejandro M. P."
    }
  ],
  "description" : "Alejandro M.P. official site",
  "favicon" : "https://alejandromp.com/favicon-32x32.png",
  "feed_url" : "https://alejandromp.com/development/feed.json",
  "home_page_url" : "https://alejandromp.com/development",
  "icon" : "https://alejandromp.com/apple-touch-icon-152x152.png",
  "items" : [
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>A few weeks ago I read <a href=\"https://www.cocoanetics.com/2026/05/an-interpreter-for-swift/\">An Interpreter for Swift</a>, from Cocoanetics, and I had that nice feeling of somebody else having already articulated the thing that was vaguely floating in your head.</p><blockquote><p>Let me just say it outright: I get very sad every time somebody insists that TypeScript is the future of agentic coding. So I kept sending my wish to the universe that Swift should be also in the race for that. Now – finally – I was able to manifest the missing piece: an interpreter for the language I love. - Cocoanetics</p></blockquote><p>That resounded inside me as I’ve always thought that Swift was the ideal language for this. And if I am already building a tiny Swift AI harness to learn how these systems work, then this felt like the perfect place to test the idea. So I immediately knew I had to try using this Swift interpreter in the harness. Even if the project is still tiny and mostly for learning, I thought it could still be useful.</p><h2 id=\"the-old-bash\">The old Bash</h2><p>In the <a href=\"https://alejandromp.com/development/blog/do-we-even-need-multiple-tools/\">previous post</a> I had already replaced the little file toolbox with <code>bash</code>, mostly to see what happened if I stopped pretending those file tools were anything other than wrappers around shell commands.</p><p>So we were already in a good position to perform the next part of the experiment and answer, what if the generic tool was not <code>bash</code>, but <code>Swift</code>?</p><p>Instead of letting the model write shell, let it write whatever top-level Swift it needs to accomplish the task. That sounds much nicer to me already.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div><h2 id=\"the-first-fork-in-the-road\">The first fork in the road</h2><p>The most obvious version of the idea is very simple. Keep the harness the same. Just replace <code>bash</code> with a <code>swift</code> tool. Let the model write top-level Swift, and call out to the compiler to execute it.</p><p>That would definitely work, but running real Swift brings the whole toolchain with it. Module caches. Compiler behavior. We all know that running Swift means going through a compilation step, and that is not the fastest thing in the world, especially if we need to start a new compiler process every time. And given LLMs might roundtrip many times to accomplish a task, any gains we get here are worth it. It is the usual reality of “this is a language toolchain”, not “this is a tiny embedded runtime”.</p><p>It also means we have the security issues we had with Bash, we can still use Seatbelt of course, but maybe we can take this chance to do better.</p><p>And finally, I just wanted to use the Swift Interpreter. Despite not being an official solution, it looks very promising and useful, and like something that should have been taken more seriously over the years. I still think Swift is the best language even for scripting, but performance is not there. So in my head there have always been two options: increase the performance of the toolchain and be as fast as Jai, or accept the limits and make a faster interpreter. So what we have here now, despite not coming from the core team, lets us prove the second idea.</p><h2 id=\"embedding-swift\">Embedding Swift</h2><p>That is where <a href=\"https://github.com/Cocoanetics/SwiftScript\">SwiftScript</a> becomes interesting.</p><p>SwiftScript is a tree-walking Swift interpreter that you can embed as a library. No <code>swiftc</code>. No shelling out to <code>swift -e</code>. Just parse Swift and interpret it.</p><p>Now the <code>swift</code> tool is not another subprocess. It can just be part of the harness itself. So we just make a new <code>ToolDefinition</code> that looks very similar to the <code>bash</code> one.</p><pre><code class=\"language-swift\">static func swift() -&gt; Self {\n    ToolDefinition(\n        name: &quot;swift&quot;,\n        description: &quot;Run top-level Swift code directly inside the harness process, using SwiftScript as the execution engine and ShellKit for workspace confinement.&quot;,\n        arguments: [&quot;code&quot;],\n        run: { arguments, workspaceRoot in\n            let code = arguments[&quot;code&quot;] ?? &quot;&quot;\n            let result = await SwiftScriptToolRunner.executeInProcess(\n                code: code,\n                at: workspaceRoot\n            )\n\n            return if result.exitCode == &quot;ExitStatus(0)&quot;,\n                      let stdout = result.stdout.nonEmpty {\n                stdout\n            } else {\n                executionResultJSON(\n                    stdout: result.stdout,\n                    stderr: result.stderr,\n                    exitCode: result.exitCode\n                )\n            }\n        }\n    )\n}\n</code></pre><h2 id=\"where-does-the-interpreter-run?\">Where does the interpreter run?</h2><p>This was the next question I had to understand. I have experience writing interpreters and things are easy when you just need to exercise pure code. Interpreting a syntax tree that wants a mathematical computation is trivial, but things get trickier when you start talking about I/O and other features that need to interact with an “environment”.</p><p>So we have an interpreter that we can embed and it knows how to run Swift, but what is the environment around it? Where does <code>print</code> go? What is the current directory? How do file APIs know what to allow?</p><p>This is where <a href=\"https://github.com/Cocoanetics/ShellKit\">ShellKit</a>, another library from the same author, enters the picture.</p><p>SwiftScript’s interpreter defaults to using <code>Shell.current</code> for output and runtime context, and ShellKit lets an embedder install a custom <code>Shell</code> for the current async task with <code>withCurrent { ... }</code>.</p><p>So the actual execution path ends up looking like this:</p><pre><code class=\"language-swift\">let shell = Shell(\n    stdout: stdoutSink,\n    stderr: stderrSink,\n    environment: .synthetic(workingDirectory: workspaceRoot.path),\n    scriptName: &quot;&lt;swift-tool&gt;&quot;,\n    sandbox: try? makeSandbox(...)\n)\n\nlet interpreter = Interpreter()\n\nlet status = try await shell.withCurrent {\n    try await interpreter.evalScript(code, fileName: &quot;&lt;swift-tool&gt;&quot;)\n}\n</code></pre><p>That <code>withCurrent</code> call is the key. It binds our synthetic shell as <code>Shell.current</code> for the duration of the script.</p><p>So when interpreted Swift does things that route through the ShellKit bridge, it sees:</p><ul><li>our stdout and stderr sinks</li><li>our working directory</li><li>our sandbox policy</li></ul><p>That was one of those nice moments where the implementation becomes less magical after you read it. The interpreter is not doing anything mysterious. It is just running inside a runtime context we provide.</p><h2 id=\"more-runtime,-less-terminal\">More Runtime, Less Terminal</h2><p>This change is already feeling better than just running <code>bash</code>. Not because Swift is “safer” in some abstract sense, but because the execution model is narrower and more controlled.</p><p>With <code>bash</code>, the natural shape of the tool is “here is a shell, good luck, have fun”. With the embedded SwiftScript, the natural shape is “here is an interpreter we host, and here is the runtime context we chose to expose”. That is a very different starting point.</p><p>And of course this could be accomplished too by using some bash shell virtualizer, but that’s not why we are here today ^^.</p><p>If the one-tool experiment is supposed to give the model a general-purpose language, Swift feels like a great candidate.</p><h2 id=\"does-that-mean-we-no-longer-need-sandboxing?\">Does that mean we no longer need sandboxing?</h2><p>The good part is that the interpreter is not just floating there in the void. It runs inside the virtualized shell we control, and <a href=\"https://github.com/Cocoanetics/ShellKit\">ShellKit</a> gives that shell a sandbox too.</p><p>So when we build the shell for the <code>swift</code> tool, we can attach a policy like this:</p><pre><code class=\"language-swift\">return Sandbox(\n    documentsDirectory: workspaceRoot,\n    downloadsDirectory: workspaceRoot,\n    libraryDirectory: workspaceRoot.appendingPathComponent(&quot;.swift-script-library&quot;, isDirectory: true),\n    temporaryDirectory: temporaryDirectory,\n    homeDirectory: workspaceRoot,\n    authorize: { url in\n        guard url.isFileURL else {\n            guard let host = url.host, host.isEmpty == false else {\n                throw Sandbox.Denial(url: url, reason: &quot;non-file URL has no host to authorize&quot;)\n            }\n            return\n        }\n\n        let resolved = url.standardizedFileURL.resolvingSymlinksInPath()\n        let path = resolved.path\n\n        if path == gitDirectory.path || path.hasPrefix(gitDirectory.path + &quot;/&quot;) {\n            throw Sandbox.Denial(url: url, reason: &quot;the Swift tool cannot access .git&quot;)\n        }\n\n        if isAllowed(path: path, under: workspaceRoot.path)\n            || isAllowed(path: path, under: globalSkillsRoot.path)\n            || isAllowed(path: path, under: temporaryDirectory.path) {\n            return\n        }\n\n        throw Sandbox.Denial(url: url, reason: &quot;file URL is outside the Swift tool sandbox&quot;)\n    }\n)\n</code></pre><p>That is already a much nicer situation than <code>bash</code>.</p><p>The interpreter gets a view of the world that we shape. It can read the workspace, the user-level skills directory, a temp directory, and it can also make network requests through the bridged APIs. It cannot touch <code>.git</code>. It cannot just wander off into the rest of the machine through the bridged file APIs.</p><p>So due to this, my first instinct was that SwiftScript plus ShellKit might already be enough. Once the execution model moves from <code>bash</code> to an embedded interpreter, the whole thing already feels much more controlled. There is no arbitrary shell pipeline by default. No compiler subprocess. No “just spawn more tools and see what happens” as the natural path.</p><p>But that still does not make the interpreter a hard security boundary.</p><p>The important distinction is that <code>ShellKit.Sandbox</code> is an in-process policy layer. It is very useful, but is different from an OS-enforced sandbox. It is also still part of the same process and the same runtime we are controlling so it also exposes our harness and the quality of our code to security attacks.</p><p>Another approach I took, but discarded to keep things simple, was to use the same embedded interpreter and shell sandbox, but spawning it in a separate process just like we did with bash. If we do that, then we can wrap that separate process in the same Seatbelt system and get yet an extra, OS-enforced layer, of protection.</p><pre><code class=\"language-bash\">sandbox-exec -p &#39;&lt;seatbelt profile&gt;&#39; swiftagentharness swift-script-helper &#39;print(40 + 2)&#39;\n</code></pre><p>This reminds me of when browsers started going multi-process for isolation. Even though for this learning harness keeping it simple seems enough thanks to ShellKit and the interpreter, it’s still interesting to see how an out-of-process system would play in practice.</p><h2 id=\"so-what-does-this-look-like-in-practice?\">So what does this look like in practice?</h2><p>At that point, the only interesting question left was the practical one. If I now run the harness with this <code>swift</code> tool, does it actually behave the way I want?</p><p>The answer was yes, at least for the kind of things that matter in this series.</p><p>The LLM can call the <code>swift</code> tool and just execute Swift:</p><pre><code class=\"language-session\">&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;swift&quot;,&quot;arguments&quot;:{&quot;code&quot;:&quot;print(40 + 2)&quot;}}\ntool-success: swift -&gt; 42\n&lt;Step 2&gt;\nAssistant: The Swift tool worked. It printed 42 from top-level Swift.\n</code></pre><p>It can also use <code>Foundation</code>, inspect the workspace, and read files:</p><pre><code class=\"language-session\">&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;swift&quot;,&quot;arguments&quot;:{&quot;code&quot;:&quot;import Foundation ...&quot;}}\ntool-success: swift -&gt; cwd=/Users/me/code/swiftagentharness\ntop_level_count=11\ntop_level_sample=.agents,.build,.git,.gitignore,AGENTS.md,Package.resolved\npackage_lines=34\nagents_lines=6\n&lt;Step 2&gt;\nAssistant: The Swift tool worked and returned the requested result.\n</code></pre><p>So from the LLM point of view, this already behaves much more like a real general-purpose language tool than a terminal wrapper.</p><p>And the sandbox does kick in when the script tries to escape:</p><pre><code class=\"language-session\">&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;swift&quot;,&quot;arguments&quot;:{&quot;code&quot;:&quot;import Foundation ... try \\&quot;hello\\&quot;.write(toFile: \\&quot;../escape.txt\\&quot;, atomically: true, encoding: .utf8)&quot;}}\ntool-success: swift -&gt; {&quot;stdout&quot;:&quot;&quot;,&quot;stderr&quot;:&quot;file URL is outside the Swift tool sandbox\\n&quot;,&quot;exit_code&quot;:&quot;interpreter-error&quot;}\n</code></pre><p>Which is exactly the kind of behavior I wanted. The model can read and write files, but only inside the world we decided to expose.</p><p>Of course, because this is still an interpreter library built by a single author and not some giant official runtime, there are parts of the surface that are not fully complete yet. That is fine. It still proves the point very well. The important thing is not that it supports every possible Swift API today. The important thing is that the one-tool experiment already feels real with Swift.</p><h2 id=\"where-i-land-on-it\">Where I land on it</h2><p>Replacing <code>bash</code> with embedded Swift is not just a gimmick. It genuinely changes the shape of the harness, and makes the one-tool experiment feel much more deliberate.</p><p>And I also like that the final answer is not perfectly clean.</p><p>The experiment did not end with “Swift replaces everything and all concerns disappear.” It ended somewhere more honest. Swift is a much nicer one-tool language than bash, embedding the interpreter is the interesting version of the idea, ShellKit gives that interpreter a controlled runtime context, and the question of hard sandboxing becomes smaller, but not completely gone.</p><p>Huge kudos to <a href=\"https://x.com/cocoanetics\">@cocoanetics</a>, because this is already an impressive foundation for exploring ideas like this in practice. Like him, I wish the Swift project had an officially supported embeddable interpreter. It’s been many <a href=\"https://alejandromp.com/development/blog/wwdc21-first-impressions/\">WWDCs</a> now that I expected a “SwiftCoreKit” as an alternative to JavaScriptCore.</p><p>I’m very pleased with this exploration, not because I found the final perfect tool, but because the model around tool execution, shells and interpreters is clearer in my head now. I also had fun exploring Swift interpretation and out-of-process execution. For this series, that is the whole point.</p>",
      "date_published" : "2026-05-21T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/replacing-bash-with-swift-in-an-ai-harness",
      "summary" : "A few weeks ago I read <a href=\"https://www.cocoanetics.com/2026/05/an-interpreter-for-swift/\">An Interpreter for Swift</a>, from Cocoanetics, and I had that nice feeling of somebody else having already articulated the thing that was vaguely floating in your head.",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "Replacing Bash with Swift in an AI Harness",
      "url" : "https://alejandromp.com/development/blog/replacing-bash-with-swift-in-an-ai-harness"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Skills are everywhere right now. People talk about them as if they were some magical incantation that will make AI more powerful while also keeping it under control.</p><p>But as with pretty much everything else in this space, once you look closely, it is much less mystical than it sounds. It is mostly markdown.</p><p>If you already understood how <a href=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/\">AGENTS.md works</a>, then skills are not that different. They are just a more structured and reusable way of doing a very similar thing.</p><p>So the question I wanted to answer was simple. What does it actually take to teach skills to a tiny AI harness?</p><h2 id=\"so-what-is-a-skill\">So what is a skill</h2><p>In its most basic form a skill is just a markdown file containing a reusable prompt for the LLM. Just like it happened with the AGENTS.md, the goal is to fill the context with useful information so the back and forth with the model improves.</p><p>In practice, a skill has a bit more. It also contains a bit of metadata and can contain extra files. For this, there is a <a href=\"https://agentskills.io/specification\">standard for skills</a>, which is honestly great because it means we are not talking only about one product’s private feature, but something compatible across harnesses.</p><p>The spec describes a skill as a folder with a markdown file called <code>SKILL.md</code>. That file contains the actual instructions in markdown, plus a bit of metadata like a name and a description as front matter.</p><p>That extra metadata is one of the most important aspects of skills since it lets harnesses follow a progressive disclosure principle. Skills can be discovered and loaded immediately, but just their name and description. Then, only when the skill is needed, the entire instruction content is loaded. This means the LLM context is not filled with unnecessary tokens. That progressive disclosure part is one of my favorite ideas here, unsurprisingly as it’s one of the core tenets of Swift language design.</p><p>It is funny too, because progressive disclosure is one of those principles that make so much sense for humans. Don’t dump everything at once. Show the right thing at the right moment. And it is interesting to see the same instinct apply to LLMs too.</p><h2 id=\"discovery\">Discovery</h2><p>The first actual implementation step is to find the skills themselves. As the standard and the harnesses I looked at support two locations, I wanted to implement the same. This would allow me to already test the harness with the real skills I’m using everywhere else.</p><p>There are two main locations where skills can live:</p><ul><li>a repo-local <code>.agents/skills</code> folder, for workflows that belong to the project</li><li>a user-level <code>~/.agents/skills</code> folder, for global workflows available everywhere</li></ul><p>But discovery is not only finding the right files and folders. It is also policy. And although the security of loading skills is not something we will get into here, it’s a can of worms that is worth considering. A repo-local skill that I wrote for the project is not the same thing as a skill I dumped into my home folder. So the harness has to decide not only where to scan, but also what kind of instructions it is willing to trust and surface automatically.</p><p>For our purposes, the actual code ended up being a small dedicated type for this. We can make a catalog type so the harness has specific knowledge and control of the skills.</p><pre><code class=\"language-swift\">struct SkillCatalog {\n    let skills: [SkillDefinition]\n    let workspaceSkillCount: Int\n    let userSkillCount: Int\n\n    static func load(workspaceRoot: URL, homeDirectory: URL = URL(fileURLWithPath: NSHomeDirectory())) -&gt; Self {\n        let workspaceSkills = loadSkills(\n            in: workspaceRoot.appendingPathComponent(&quot;.agents/skills&quot;, isDirectory: true),\n            source: .workspace\n        )\n        let userSkills = loadSkills(\n            in: homeDirectory.appendingPathComponent(&quot;.agents/skills&quot;, isDirectory: true),\n            source: .user\n        )\n\n        var discoveredByName = [String: SkillDefinition]()\n\n        // Workspace skills load first, so they win name collisions.\n        for skill in workspaceSkills + userSkills {\n            let key = skill.name.lowercased()\n            guard discoveredByName[key] == nil else { continue }\n            discoveredByName[key] = skill\n        }\n\n        return SkillCatalog(\n            skills: discoveredByName.values.sorted { ... },\n            workspaceSkillCount: workspaceSkills.count,\n            userSkillCount: userSkills.count\n        )\n    }\n}\n</code></pre><p>The loader walks both directories recursively, looks for files named exactly <code>SKILL.md</code>, and builds an in-memory catalog.</p><p>That final step looks like this:</p><pre><code class=\"language-swift\">private static func loadSkill(at url: URL, source: SkillSource) -&gt; SkillDefinition? {\n    guard let content = try? String(contentsOf: url, encoding: .utf8) else {\n        return nil\n    }\n\n    let parsed = parseSkillFile(content)\n    let fallbackName = url.deletingLastPathComponent().lastPathComponent\n    let name = parsed.metadata[&quot;name&quot;]?.nonEmpty ?? fallbackName\n    let description = parsed.metadata[&quot;description&quot;]?.nonEmpty ?? &quot;No description provided.&quot;\n\n    return SkillDefinition(\n        name: trimQuotes(from: name),\n        description: trimQuotes(from: description),\n        path: url,\n        source: source\n    )\n}\n</code></pre><p>As you can see, the loop that loads the skills just skips entries whose name already exists. Since workspace skills are loaded first, that means they win collisions. If a repository wants to provide its own version of a skill, that should override my personal default for that session.</p><p>I also made the harness print what it discovered at startup, just like it already did for <code>AGENTS.md</code>. That way I can immediately verify that discovery is working before even asking the model to do anything:</p><pre><code class=\"language-session\">Loaded workspace skills (2)\nLoaded user skills (29)\nDiscovered skills: harness-super-test, harness-swift-helper, agent-browser, ..., find-skills, ...\nLoaded AGENTS.md (11 lines)\n</code></pre><h2 id=\"parsing\">Parsing</h2><p>Once the harness finds the skills, it has to parse them. The good part is that there is nothing special about it, if you’ve ever worked with a <a href=\"https://alejandromp.com/development/blog/back-to-the-basics-with-genesis/\">markdown-based static site generator</a>, you will be familiar with <a href=\"https://alejandromp.com/development/blog/github-action-to-create-new-posts/\">YAML front matter</a>. That’s what needs to be extracted, just the <code>name</code> and <code>description</code>. Maybe validate them a bit. But at this stage the harness does not keep the rest of the markdown in memory. It only keeps the catalog metadata and the path to the file.</p><p>Each discovered <code>SKILL.md</code> gets loaded as text. Then the harness looks for the front matter delimiters, parses the little metadata block, extracts <code>name</code> and <code>description</code>, and stores just enough information to build the catalog.</p><pre><code class=\"language-swift\">private static func parseSkillFile(_ content: String) -&gt; (metadata: [String: String], instructions: String) {\n    let lines = content.components(separatedBy: .newlines)\n\n    guard lines.first == &quot;---&quot; else {\n        return ([:], content.trimmingCharacters(in: .whitespacesAndNewlines))\n    }\n\n    var metadata = [String: String]()\n\n    for line in lines.dropFirst() {\n        if line == &quot;---&quot; { break }\n        guard let separator = line.firstIndex(of: &quot;:&quot;) else { continue }\n\n        let key = line[..&lt;separator].trimmingCharacters(in: .whitespaces)\n        let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespaces)\n\n        guard key.isEmpty == false, value.isEmpty == false else { continue }\n        metadata[key] = value\n    }\n\n    return (metadata, content)\n}\n</code></pre><p>We already have skills on disk. We already know what they are. We already know enough to tell the model they exist. But we have not paid the token cost of loading them all. Not yet.</p><h2 id=\"updating-the-system-prompt\">Updating the system prompt</h2><p>We now have a catalog with the metadata of the available skills. The harness knows them, but the LLM still doesn’t. So just like with <code>AGENTS.md</code>, we now need to inject the catalog into the system prompt so it is available in the model context.</p><pre><code class=\"language-swift\">let catalog = skillCatalog.skills\n    .map(\\.promptBlock)\n    .joined(separator: &quot;\\n&quot;)\n\nskillsPrompt = &quot;&quot;&quot;\nAvailable skills:\nThese skills provide specialized instructions for specific tasks. When a task matches a skill&#39;s description, read the `SKILL.md` at the listed location before proceeding.\n&lt;available_skills&gt;\n\\(catalog)\n&lt;/available_skills&gt;\n&quot;&quot;&quot;\n</code></pre><pre><code class=\"language-swift\">var promptBlock: String {\n    &quot;&quot;&quot;\n    &lt;skill&gt;\n    name: \\(name)\n    description: \\(description)\n    location: \\(path.path)\n    &lt;/skill&gt;\n    &quot;&quot;&quot;\n}\n</code></pre><p>That last field matters more than it may seem.</p><p>The <code>location</code> is what lets the model bridge the gap between “I know this skill exists” and “now I want to actually use it”. Without that field, the model would know the skill exists but would still need some other mechanism to get to the file.</p><p>And once that prompt change exists, we can finally test the interesting part.</p><h2 id=\"implicit-activation\">Implicit activation</h2><p>Once the skills are in context, implicit activation is almost the purest version of the idea. The model sees that a skill exists, sees what it is for, and decides to load it when the task matches. We will look at explicit activation later, but this is the best place to start.</p><p>For this harness, that also means something very concrete. I am not going to add a special <code>activate_skill</code> tool right now. If the point of the project is learning, then I would rather keep the mechanism visible. The model sees a skill in the catalog, notices the path, and reads the file with the normal file-reading tool.</p><div class=\"callout-tip\"><p>A dedicated activation tool is still a valid design, and maybe even the better one for a more serious agent. It gives tighter control, better permissions, and cleaner analytics. But for this harness it would hide the mechanism too early. I want to see the model load the markdown directly.</p></div><p>To make that work in practice, I had to do one small but important change. The harness already knew how to discover user-level skills in <code>~/.agents/skills</code>, but the original implementation of the <a href=\"https://alejandromp.com/development/blog/do-we-even-need-multiple-tools/\"><code>read_file</code> tool</a> and the <a href=\"https://alejandromp.com/development/blog/sandboxing-an-ai-harness-on-macos/\">sandbox</a> would limit access to that directory because it sits outside the workspace.</p><p>So I updated two things:</p><ul><li>the <code>read_file</code> tool can now read both the workspace and <code>~/.agents/skills</code></li><li>the sandbox profile for <code>bash</code> can now read <code>~/.agents/skills</code> too</li></ul><pre><code class=\"language-swift\">let globalSkillsRoot = URL(fileURLWithPath: NSHomeDirectory())\n    .appendingPathComponent(&quot;.agents/skills&quot;, isDirectory: true)\n    .standardizedFileURL\n\nlet isInsideWorkspace = resolvedPath == rootPath || resolvedPath.hasPrefix(rootPath + &quot;/&quot;)\nlet isInsideGlobalSkills = resolvedPath == globalSkillsPath || resolvedPath.hasPrefix(globalSkillsPath + &quot;/&quot;)\n\nguard isInsideWorkspace || isInsideGlobalSkills else {\n    throw HarnessError.workspaceViolation(path)\n}\n</code></pre><pre><code class=\"language-scheme\">;; Global skills\n(allow file-read*\n    (subpath &quot;/Users/me/.agents/skills&quot;))\n</code></pre><p>That keeps the mechanism simple and visible. The model sees a skill in the catalog, notices the path, and reads the markdown like any other file.</p><p>Here is the local skill case:</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Can you use the local harness-super-test skill?\n&lt;Step 1&gt;\nAssistant: I can use it, and I&#39;ve loaded the local `harness-super-test` skill instructions. What would you like me to do with it?\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.agents/skills/harness-super-test/SKILL.md&quot;}}\ntool-success: read_file -&gt; 10 lines\n&lt;Step 2&gt;\nAssistant: You now have super powers, you are a super harness.\n  That phrasing came from the repo-local skill in `.agents/skills/harness-super-test`.\n</code></pre><p>And here is the global skill case:</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Use the find-skills skill to search for the caveman skill, and read the skill file before proceeding.\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;~/.agents/skills/find-skills/SKILL.md&quot;}}\ntool-success: read_file -&gt; 89 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;npx skills find caveman&quot;}}\ntool-success: bash -&gt; command succeeded\n&lt;Step 3&gt;\nAssistant: I used the `find-skills` skill and searched for `caveman`. I read the local `find-skills` skill first, then followed its workflow. The best match was...\n</code></pre><p>That second example is important because it proves the whole chain in a very ordinary way. The catalog exposes a user-level skill, the model reads the file directly, and that file then guides the next action. There is no hidden magic layer in the middle. It is still just context plus tools.</p><h2 id=\"extra-resources\">Extra resources</h2><p>A nice thing about the skills spec is that it allows for a skill to package extra resources. You might have noticed the skill shape: it is not a markdown file named as the skill, no. It’s a folder named as the skill, with a markdown file generically named <code>SKILL.md</code> inside. This is because there can be more.</p><p>The skill folder can contain other subfolders and files inside, which the main skill file can point to. More resources, reference docs, and even scripts. And again, all without polluting the context on load, just when it is needed. This is just progressive disclosure, one level deeper.</p><p>The previous local skill was intentionally tiny, just enough to make activation obvious. That was useful, but still a bit toy-like. So I added a second repo-local skill that feels a bit more like a real workflow:</p><p>It has a <code>SKILL.md</code>, a small reference note, and a tiny Swift helper script. That gave me a much better test, because now the skill was not just shaping the answer. It was guiding the model toward a concrete project-specific action using plain files and plain tools.</p><p>Here is the transcript:</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Use the harness-swift-helper skill to summarize this package. Read the skill file first, follow its execution note, run the Swift helper, and then tell me the result.\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.agents/skills/harness-swift-helper/SKILL.md&quot;}}\ntool-success: read_file -&gt; 15 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.agents/skills/harness-swift-helper/references/RUNNING.md&quot;}}\ntool-success: read_file -&gt; 16 lines\n&lt;Step 3&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;swift .agents/skills/harness-swift-helper/scripts/package_summary.swift&quot;}}\ntool-success: bash -&gt; command succeeded\n&lt;Step 4&gt;\nAssistant: I read the `harness-swift-helper` skill, followed its execution note, ran the Swift helper, and here is the actual output:\n  Package: swiftagentharness\n  Platform...\n</code></pre><p>That is exactly the kind of behavior I wanted to see. The helper script and the execution note were not sitting in the initial context from the start. The model loaded them only when the skill became relevant, and then used them to complete the task.</p><p>And this is yet another proof that skills are nothing magical, just reusable prompts that can easily package extra resources.</p><h2 id=\"explicit-activation\">Explicit activation</h2><p>Doing explicit activation, where the user directly invokes a skill, can quickly turn into terminal UI and UX work instead of learning about AI and harnesses. I did not want to go down that rabbit hole here. But it still felt useful to dip our toes into it, if only to prove that explicit activation is also not magical.</p><p>So I built the smallest version worth having:</p><pre><code class=\"language-text\">/skill-name [task]\n</code></pre><p>That is it. No autocomplete. No picker. No search. No fancy terminal behavior.</p><p>When the harness sees an input line that starts with <code>/</code>, it checks whether the first token matches a discovered skill name. If it does, the harness reads that <code>SKILL.md</code>, injects it into the conversation, and then the loop continues as normal.</p><pre><code class=\"language-swift\">conversation.append(Message(role: &quot;user&quot;, content: input))\nif let explicitSkillActivation = try resolveExplicitSkillActivation(from: input) {\n    ui.printLine(\n        &quot;skill-load: /\\(explicitSkillActivation.skill.name) -&gt; \\(explicitSkillActivation.skill.path.path)&quot;\n    )\n    conversation.append(Message(\n        role: &quot;system&quot;,\n        content: try explicitSkillSystemMessage(for: explicitSkillActivation)\n    ))\n}\n</code></pre><p>And because I wanted this to be debuggable, the harness also prints a visible confirmation line in the terminal:</p><pre><code class=\"language-text\">skill-load: /harness-swift-helper -&gt; /Users/.../SKILL.md\n</code></pre><p>That matters because we do not have to guess whether the harness explicitly loaded the skill, or whether the model later decided to read the file on its own. We can see it.</p><p>This is also the useful difference with implicit loading. There, we still depend on the model noticing that a skill is relevant. Here, the user decides first, and the harness makes sure those desires come true.</p><p>This is an important insight. It reinforces, again, that skills are nothing special, just text dumped into the context. Before, it was the LLM using the read tool to load that text. Now it is the harness doing it manually. After that, the loop just continues as usual. The fact that there are no other changes necessary clarifies a lot.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: /find-skills caveman\nskill-load: /find-skills -&gt; ~/.agents/skills/find-skills/SKILL.md\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;npx skills find caveman&quot;}}\ntool-success: bash -&gt; command succeeded\n&lt;Step 2&gt;\nAssistant: I found several `caveman`-related skills, including `juliusbrussee/caveman@caveman` and `mattpocock/skills@caveman`.\n</code></pre><p>We can see how there was no read tool call. It was not necessary because the skill was already in the context, so the LLM could directly go and use <code>bash</code> as told by the skill. That means explicit invocation can save a round-trip, tokens, and cost.</p><p>This also affects the cost and behavior of different harnesses. For example, what happens if in the next turn we ask for something related?</p><pre><code class=\"language-session\">&lt;Turn 2&gt;\nYou: and can you find the &quot;web-search&quot; one?\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;npx skills find web-search&quot;}}\ntool-success: bash -&gt; command succeeded\n&lt;Step 2&gt;\nAssistant: I found several web-search-related skills, including: ...\n</code></pre><p>You can see how the model decided to use the skill again to answer our request. This time it did not have to load it again, because the information was already in the context. In this harness, that happens because the skill stays in the conversation. That is just a decision I made during the implementation. Some harnesses may keep the skill only on the demanded turn, clean it after a few turns, or reload it every time. These small decisions are part of what makes each harness feel distinct.</p><div class=\"callout-tip\"><p>Better harnesses usually go much further here. They offer autocomplete, skill listing, filtering, search, and richer terminal interactions. I am intentionally stopping earlier. The point of this post is to demystify skills, not to deep dive into terminal UI. That can be its own journey later.</p></div><h2 id=\"back-to-reality\">Back to reality</h2><p>After walking through discovery, parsing, prompt disclosure, implicit loading, extra resources, and even a tiny explicit activation flow, I think the main lesson is simple.</p><p>Skills are not some new magical agent primitive. They are reusable prompts with a folder convention.</p><p>They can be great. They are also, at the end of the day, still mostly markdown. So think twice before jumping into the <a href=\"https://alejandromp.com/development/blog/development-dogma-is-back-a-i-agents/\">dogma hype train</a>.</p><p>That is not diminishing them. If anything, I think that makes them more useful, because now the value is obvious. The harness is not growing a mysterious new organ. We are just giving the model a structured way to discover the right instructions, the right note, the right script, and only load them when they become relevant.</p><p>And because of that, all the usual context problems still apply. Pollution. Distraction. Conflicting instructions. Stale workflows. Too much noise. Skills do not escape any of that. They are just a cleaner way of doing context engineering.</p><p>The security side matters too. Pulling a random skill from the internet is letting somebody else inject instructions into an agent that may already have access to your files, your shell, your browser, whatever tools you gave it. We should all talk about that more.</p><p>So my recommendation is simple. Write your own skills. And if you find a good one online, take the core of the idea, read it carefully, tailor it, make it yours. The nice part is that once you stop treating skills like magic, that becomes trivial. They are mostly markdown, and the LLM can help you write them.</p><p>That is the real win for me. Not that skills are fancy. Not that they feel powerful. But that once you understand what they really are, you can shape them on purpose.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div>",
      "date_published" : "2026-05-09T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/teaching-skills-to-an-ai-harness",
      "summary" : "Skills are everywhere right now. People talk about them as if they were some magical incantation that will make AI more powerful while also keeping it under control.",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "Teaching Skills to an AI Harness",
      "url" : "https://alejandromp.com/development/blog/teaching-skills-to-an-ai-harness"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>In the <a href=\"https://alejandromp.com/development/blog/do-we-even-need-multiple-tools/\">previous post</a> we gave superpowers to the harness in the form of a <code>bash</code> tool. Even doing it just for the learning experience, it was already very eye-opening. It made this little Swift harness feel much closer to the real thing.</p><p>And that is exactly why I don’t want to hand-wave the next part away.</p><p>We haven’t spent the time to make it production-ready, and we won’t because that’s not the purpose of this series. There are no carefully tuned prompts. No tailored rules for different models. No approval flows. No guardrails worth the name. It is just a tiny learning project.</p><p>But learning projects need to teach real lessons, and one of them is that giving an LLM shell access changes the nature of the problem immediately. It is no longer just “can the model inspect the project better?” It is also “what can this thing touch now, and what did I accidentally give it access to?”</p><p>So before happily moving on with the harness, I wanted to stop and look directly at that danger.</p><h2 id=\"the-dangers-of-free-execution\">The dangers of free execution</h2><p>It’s easy to see what power we have given to the LLM:</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Inspect this project and the surrounding projects so you can understand the environment better.\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;pwd &amp;&amp; echo &#39;--- ROOT ---&#39; &amp;&amp; ls -la &amp;&amp; echo &#39;--- PARENT ---&#39; &amp;&amp; ls -la .. &amp;&amp; echo &#39;--- GRANDPARENT ---&#39; &amp;&amp; ls -la ../../&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;\\/Users\\/me\\/code\\/swiftagentharness\\n--- ROOT --...\n&lt;Step 2&gt;\nAssistant: I inspected the current project and nearby directories.\nCurrent project:\n- `swiftagentharness`\n...\nSibling projects in `../`:\n...\nHigher-level directories in `../../`:\n...\n</code></pre><p>We can see how even with the workspace guidance in the prompt, the model can easily use <code>bash</code> to escape the workspace.</p><p>It’s not hard to imagine what would happen if the model decided to use <code>rm</code> or inspect files that should be totally private, like credentials.</p><h2 id=\"what-are-the-options-then\">What are the options then</h2><p><img src=\"https://alejandromp.com/development/blog/sandboxing-an-ai-harness-on-macos/giphy.gif\" alt=\"\"></p><p>A common typical answer for this is to bring the cannons and use Docker to create a fully virtual and controlled environment so the AI can’t harm anything outside of it. And although that is the prevailing mentality across many developer communities, it’s not the only solution, nor the best or most elegant one.</p><p>So just for the sake of exploration and learning, let’s try something else. Let’s use native operating system tooling to sandbox execution. After all, if the entire revolution of mobile apps could happen with system sandboxes, surely we can benefit from the same work.</p><h2 id=\"what-other-harnesses-do\">What other harnesses do</h2><p>Before going further, I wanted to see whether this was one of those cases where I was overthinking it and everybody else had already moved on to some obvious better approach.</p><p>The short answer is no. The pattern I kept seeing was much more pragmatic.</p><p>OpenAI’s own <a href=\"https://developers.openai.com/api/docs/guides/tools-local-shell\">local shell guide</a> explicitly warns that running arbitrary shell commands can be dangerous and says you should sandbox execution or add strict allow and deny lists. That already tells us something important. Even if your product is all about agentic shell use, the default assumption is still that free execution is too much.</p><p><a href=\"https://code.claude.com/docs/en/sandboxing\">Claude Code’s sandboxing docs</a> are probably the clearest public explanation of the approach. On macOS they use Seatbelt. On Linux they use <code>bubblewrap</code>. Filesystem access and network access are treated as two different problems, and all child processes inherit the same restrictions.</p><p>Anthropic also extracted that work into an open-source project, <a href=\"https://github.com/anthropic-experimental/sandbox-runtime\">sandbox-runtime</a>. Their README is very explicit about what it does: native OS sandboxing primitives plus proxy-based network filtering. That last bit is worth noticing. The serious versions of this don’t stop at filesystem restrictions.</p><p><a href=\"https://goose-docs.ai/docs/guides/sandbox/\">Goose Desktop’s macOS sandbox docs</a> show a very similar shape. Again, native sandboxing on macOS, plus extra machinery around network control.</p><p>And even when looking at OpenAI Codex in the wild, you can see the same reality leak through. OpenAI points to <a href=\"https://github.com/openai/codex\">Codex CLI</a> as a reference implementation from the shell guide, and their issue tracker has real macOS reports that mention the Seatbelt sandbox directly, like <a href=\"https://github.com/openai/codex/issues/10390\">this network-related issue</a>.</p><p>So the pattern seems pretty clear:</p><ul><li>If you want the strongest isolation, people reach for containers.</li><li>If you want native process sandboxing on macOS, people still seem to reach for Seatbelt through <code>sandbox-exec</code>.</li><li>If you care about network too, the more serious tools add a proxy on top.</li></ul><p>For me that is enough indication that is worth spending some time understanding Seatbelt and native sandboxing and seeing how it can improve our tiny harness.</p><h2 id=\"seatbelt\">Seatbelt</h2><p>Seatbelt is the low-level sandboxing facility in macOS. It is one of the pieces behind the strong sandboxing model Apple has used for years across macOS and iOS. So when we talk about app sandboxes and processes being limited by the operating system, this is the machinery that is under the hood.</p><p>That was already a good sign for this little experiment. If the whole mobile app world has been able to rely on OS sandboxing for such a long time, there is probably something useful here for us too, even if our use case is much smaller and much messier than a normal app. Apple’s own documentation around <a href=\"https://developer.apple.com/documentation/security/app_sandbox\">App Sandbox</a> and the macOS <a href=\"https://developer.apple.com/documentation/xcode/configuring-the-macos-app-sandbox\">App Sandbox configuration</a> is mostly focused on proper app development, entitlements, containers, helper tools, and that whole world. That is not exactly our situation, but it tells us the underlying model is very real.</p><p>The slightly confusing part is that there is a difference between the modern Apple recommendation and the practical tool we can use from a little CLI harness.</p><p>The modern recommendation is App Sandbox. That is the official, entitlement-based way to sandbox a macOS app. But our harness is not a signed app with entitlements and containers. It is just a command-line tool that spawns <code>/bin/bash</code>.</p><p>That is where <code>sandbox-exec</code> comes in.</p><p><a href=\"https://manp.gs/mac/1/sandbox-exec\"><code>sandbox-exec</code></a> is a command-line wrapper around that lower-level <a href=\"https://manp.gs/mac/7/sandbox\">sandbox facility</a>. You give it a policy, then tell it which command to run, and it launches that command under those restrictions. That is why it keeps showing up in agent harnesses and AI tools on macOS. It fits the shape of the problem much better than App Sandbox does.</p><p>The idea is very simple:</p><pre><code class=\"language-bash\">sandbox-exec -p &#39;(version 1) ...rules here...&#39; /bin/bash -lc &#39;pwd&#39;\n</code></pre><p>That <code>-p</code> argument is an inline sandbox profile. You can also load profiles from files, but for our tiny harness an inline profile is probably enough.</p><p>And this matters too: child processes inherit the sandbox. That means if we launch <code>bash</code> under Seatbelt restrictions, and then <code>bash</code> launches <code>git</code>, <code>swift</code>, <code>ls</code>, or anything else, those processes keep running inside the same boundary. That is exactly what we want. It would be quite useless to sandbox only the first command and then accidentally let the rest of the process tree escape 🤣.</p><p>There is one awkward detail though. Apple marks <code>sandbox-exec</code> as deprecated in the man page. So this is not me saying “this is the shiny modern blessed API.” It is more like “this is the old but still practical door into Seatbelt for command-line process sandboxing.” For our purposes, and also looking at what other harnesses are doing, that is still good enough to learn something real from it.</p><h2 id=\"let’s-wire-it-into-the-harness\">Let’s wire it into the harness</h2><p>We already had a <code>bash</code> tool that used <code>Subprocess.run</code>. So we don’t have to redesign the whole harness around sandboxing. We just need a small change that can prove the idea.</p><p>Let’s start with preparing the sandbox as a custom method on <code>Executable</code>:</p><pre><code class=\"language-swift\">extension Executable {\n    func runSandboxed&lt;\n        Input: InputProtocol,\n        Output: OutputProtocol,\n        Error: ErrorOutputProtocol\n    &gt;(\n        arguments: [String] = [],\n        environment: Environment = .inherit,\n        workingDirectory: FilePath? = nil,\n        input: Input = .none,\n        output: Output,\n        error: Error = .discarded,\n        sandboxProfile: String\n    ) async throws -&gt; ExecutionRecord&lt;Output, Error&gt; {\n        let resolvedExecutable = try self.resolveExecutablePath(in: environment)\n        let wrappedArguments = [&quot;-p&quot;, sandboxProfile, resolvedExecutable.string] + arguments\n\n        return try await Subprocess.run(\n            .path(&quot;/usr/bin/sandbox-exec&quot;),\n            arguments: Arguments(wrappedArguments),\n            environment: environment,\n            workingDirectory: workingDirectory,\n            input: input,\n            output: output,\n            error: error\n        )\n    }\n}\n</code></pre><p>This part is actually quite simple. We are still just spawning a process, but with the difference that instead of spawning <code>/bin/bash</code> directly, we spawn <code>sandbox-exec</code>, give it the Seatbelt profile, and then tell it to launch the real executable inside that sandbox.</p><p>Then the <code>bash</code> tool can just call this instead of the original <code>Subprocess.run</code>. I’ve added an argument to the harness to be able to use the sandbox only when desired, mostly to try the differences and for learning purposes.</p><pre><code class=\"language-swift\">let result = if sandboxEnabled {\n    try await executable.runSandboxed(\n        arguments: [&quot;-c&quot;, command],\n        workingDirectory: workingDirectory,\n        output: capturedIO,\n        error: capturedIO,\n        sandboxProfile: SeatbeltProfile.workspaceBoundary(for: workspaceRoot)\n    )\n} else {\n    try await Subprocess.run(\n        executable,\n        arguments: [&quot;-c&quot;, command],\n        workingDirectory: workingDirectory,\n        output: capturedIO,\n        error: capturedIO\n    )\n}\n</code></pre><p>That shape was exactly what I wanted. The harness is still the same little harness. The <code>bash</code> tool is still the same <code>bash</code> tool. We are not replacing it with a different concept. We are just giving it a stricter execution path when sandboxing is enabled.</p><blockquote><p>Just FYI, in my final implementation I also included a <code>sandbox</code> subcommand, that helped me run commands directly inside the sandbox without going through the LLM at all. That ended up being really handy for testing the profile itself without having to convince the LLM to do what I needed.</p></blockquote><p>But of course, the interesting part is the profile.</p><p>I’m not gonna lie. I’m not an expert on sandboxing so what I’ve got working probably has flaws, but after some trial and error it seems to work for a few use cases. And that’s all I need for the learning process :)</p><p>For example, the first version was too strict and basically useless. Even <code>pwd</code> and <code>ls</code> were crashing. So this turned into a very practical exercise in seeing what macOS tools actually need to function under Seatbelt.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: List the files in this project and summarize what it is. Use bash.\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;ls -la&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;&quot;,&quot;stderr&quot;:&quot;&quot;,&quot;exit_code&quot;:&quot;signaled(6)&quot;}\n</code></pre><p>The key unlock was importing Apple’s built-in <code>system.sb</code> base profile, and then layering our own file restrictions on top. After that, the profile started becoming actually useful instead.</p><pre><code class=\"language-lisp\">(version 1)\n(deny default)\n(import &quot;system.sb&quot;)\n\n;; Start from a deny-all profile, then re-allow only the minimum needed\n;; for &quot;run bash inside the workspace&quot; to work at all.\n(allow process-exec)\n(allow process-fork)\n(allow signal (target self))\n(allow sysctl-read)\n(allow mach-lookup)\n\n(allow file-read*\n    (subpath &quot;/Applications&quot;)\n    (subpath &quot;/bin&quot;)\n    (subpath &quot;/dev&quot;)\n    (subpath &quot;/System&quot;)\n    (subpath &quot;/usr&quot;)\n    (subpath &quot;/var/folders&quot;)\n    (subpath &quot;/private/var/folders&quot;)\n    (subpath &quot;/var/select&quot;)\n    (subpath &quot;/private/var/select&quot;)\n    (subpath &quot;&lt;workspace&gt;&quot;)\n    (subpath &quot;&lt;sandbox-temp-directory&gt;&quot;))\n\n(allow file-read-metadata\n    (literal &quot;&lt;workspace-parent&gt;&quot;))\n\n(allow file-write*\n    (subpath &quot;/var/folders&quot;)\n    (subpath &quot;/private/var/folders&quot;)\n    (subpath &quot;&lt;workspace&gt;&quot;)\n    (subpath &quot;&lt;sandbox-temp-directory&gt;&quot;))\n\n(deny file-write*\n    (subpath &quot;&lt;workspace&gt;/.git&quot;))\n</code></pre><p>It’s all quite self explanatory but here is my own description of what’s happening:</p><ol><li>We deny everything by default, and we will only enable what we want.</li><li>Import Apple’s base profile, this is what makes pretty much everything work :D</li><li>Allow a few process related rules so we can actually call tools.</li><li>Allow reads on the system locations that <code>bash</code>, <code>ls</code>, <code>git</code>, <code>swift</code>, and friends need in practice, plus the workspace and dedicated sandbox temp directory.</li><li>Allow metadata reads on the parent of the workspace because even something like <code>ls -la</code> peeks at <code>..</code> while listing the current directory.</li><li>Allow writes in the workspace and the dedicated sandbox temp directory, including its <code>var/folders</code> aliases that some commands use under the hood.</li><li>But deny writes to the git folder, this was just to try the ruleset.</li></ol><p>Note that because we’re making a custom subfolder in the temporary directory, we have to update the <code>bash</code> tool instructions to include that path. Otherwise the LLM won’t have any clue where it can write temporary files.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div><p>The nice part is that once this was in place we could actually test the boundary in a very concrete way.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: run ls and pwd\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;pwd &amp;&amp; ls&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;/Users/me/code/swiftagentharness\nAGENTS.md\nPackage.resolved\n...&quot;,&quot;stderr&quot;:&quot;&quot;,&quot;exit_code&quot;:&quot;exited(0)&quot;}\n&lt;Step 2&gt;\nAssistant: /Users/me/code/swiftagentharness\nAGENTS.md\n...\n</code></pre><pre><code class=\"language-session\">&lt;Turn 2&gt;\nYou: do it at the home folder\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;cd ~ &amp;&amp; pwd &amp;&amp; ls&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;/Users/me\n&quot;,&quot;stderr&quot;:&quot;cd: error retrieving current directory: getcwd: cannot access parent directories: Operation not permitted\nls: .: Operation not permitted\n&quot;,&quot;exit_code&quot;:&quot;exited(1)&quot;}\n&lt;Step 2&gt;\nAssistant: Tried in `~`, but permissions prevented listing it.\nOutput: `/Users/me`\nErrors:\n- `getcwd: cannot access parent directories: Operation not permitted`\n- `ls: .: Operation not permitted`\n</code></pre><pre><code class=\"language-session\">&lt;Turn 3&gt;\nYou: write it\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;workspace write test\\n&#39; &gt; scratch.txt &amp;&amp; ls -l scratch.txt &amp;&amp; cat scratch.txt&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;-rw-r--r--@ 1 me staff 21 ... scratch.txt\nworkspace write test\n&quot;,&quot;stderr&quot;:&quot;&quot;,&quot;exit_code&quot;:&quot;exited(0)&quot;}\n&lt;Step 2&gt;\nAssistant: Wrote `scratch.txt` in the workspace successfully.\n</code></pre><pre><code class=\"language-session\">&lt;Turn 4&gt;\nYou: prove that writing into .git failed\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;git dir write test\\n&#39; &gt; .git/scratch-test 2&gt;&amp;1; status=$?; if [ $status -eq 0 ]; then echo WRITE_OK; else echo WRITE_FAILED; fi; exit $status&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;WRITE_FAILED\n&quot;,&quot;stderr&quot;:&quot;/bin/bash: .git/scratch-test: Operation not permitted\n&quot;,&quot;exit_code&quot;:&quot;exited(1)&quot;}\n&lt;Step 2&gt;\nAssistant: Writing into `.git` failed with `Operation not permitted`.\n</code></pre><pre><code class=\"language-session\">&lt;Turn 5&gt;\nYou: prove that writing ../sandbox-escape.txt failed\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;escape test\\n&#39; &gt; ../sandbox-escape.txt 2&gt;&amp;1; status=$?; if [ $status -eq 0 ]; then echo WRITE_OK; else echo WRITE_FAILED; fi; exit $status&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;WRITE_FAILED\n&quot;,&quot;stderr&quot;:&quot;/bin/bash: ../sandbox-escape.txt: Operation not permitted\n&quot;,&quot;exit_code&quot;:&quot;exited(1)&quot;}\n&lt;Step 2&gt;\nAssistant: Writing `../sandbox-escape.txt` failed with `Operation not permitted`.\n</code></pre><pre><code class=\"language-session\">&lt;Turn 0&gt;\nYou: write a random file in a temporary file\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;tmpfile=$(mktemp); head -c 32 /dev/urandom | base64 &gt; \\&quot;$tmpfile\\&quot;; printf &#39;%s&#39; \\&quot;$tmpfile\\&quot;&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;/var/folders/.../swiftagentharness-sandbox/&lt;workspace-hash&gt;/tmp.sjpO8WrRyF&quot;,&quot;stderr&quot;:&quot;&quot;,&quot;exit_code&quot;:&quot;exited(0)&quot;}\n&lt;Step 2&gt;\nAssistant: Created a temporary file with random content at `/var/folders/.../swiftagentharness-sandbox/&lt;workspace-hash&gt;/tmp.sjpO8WrRyF`\n</code></pre><p>It’s actually pretty cool to see that with so little we already have a more secure harness. Now there are some restrictions and safety around what we let the AI do. It still has bash superpowers, but now with a bit more responsibility.</p><h2 id=\"what-this-does-not-solve\">What this does not solve</h2><p>Now, we need to be honest about what this is and what it is not. This does not make the harness “safe” in any absolute sense. It just makes it <em>safer</em>.</p><p>The profile still has to allow a bunch of system paths so normal commands can even run. That means this is not some perfect tiny prison where the process only knows about the repo. It is a constrained execution environment, sure, but it is not perfect isolation.</p><p>Also, this is not solving network access. Some of the more serious tools pair sandboxing with proxy-based network control. We are not doing that here. So this post is not a guide to production-grade AI isolation. It is a guide to learning how native sandboxing on macOS can help, and where it stops helping. Just be careful out there.</p><p>But sandboxing is not a substitute for judgment. A good harness still needs clear instructions, approvals where appropriate, sane defaults, and probably other layers around command execution. If you want the strongest isolation, containers and VMs still exist for a reason.</p><p>And let’s be honest, even with all of that, with enough tools and computing power, LLMs could probably cause trouble on any environment, so this is just about minimizing those risks at the obvious levels.</p><h2 id=\"good-enough-to-learn-something\">Good enough to learn something</h2><p>We’ve transformed some obscure features in popular harnesses and a trust me bro attitude, to an investigation into the implications of letting AI execute and what’s one of the possible mitigations. That’s very valuable.</p><p>Now I understand a bit better what macOS sandboxing actually looks like in practice. I understand the gap between App Sandbox and <code>sandbox-exec</code>. I understand why other harnesses still use Seatbelt. I understand that the policy itself is only half the story, and that the tool prompt also has to describe the environment correctly. And most importantly, I understand that “workspace root” and “security boundary” are not the same thing unless we do the work to make them the same.</p><p>So this is probably not good enough to ship blindly. Not good enough to call secure. But definitely good enough to learn something real.</p><p>And that is good enough for me.</p>",
      "date_published" : "2026-05-05T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/sandboxing-an-ai-harness-on-macos",
      "summary" : "In the <a href=\"https://alejandromp.com/development/blog/do-we-even-need-multiple-tools/\">previous post</a> we gave superpowers to the harness in the form of a <code>bash</code> tool. Even doing it just for the learning experience, it was already very eye-opening. It made this little Swift harness feel much closer to the real thing.",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "Sandboxing an AI Harness on macOS",
      "url" : "https://alejandromp.com/development/blog/sandboxing-an-ai-harness-on-macos"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>After <a href=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/\">teaching the harness to load <code>AGENTS.md</code></a>, my mind went back to thinking about the tools, and wondered, do we even need multiple tools?</p><p>Our tiny Swift harness had the usual little trio: <code>read_file</code>, <code>list_files</code>, and <code>edit_file</code>. Which is fine. It works. It is also exactly the kind of thing that makes agent tooling feel more magical and more special than it actually is.</p><p>Because if you squint a bit, those three tools are not really three capabilities. They are just tiny wrappers around what the shell has done for decades.</p><p>I’ve heard for a while that LLMs are now very good at just using bash, so I thought, maybe that’s the only tool they need? So… let’s remove them all and just use bash. Let’s see what happens.</p><h2 id=\"current-tools\">Current tools</h2><p>Before changing anything, I wanted a baseline. Let’s remind ourselves how the current tool usage looks like.</p><pre><code class=\"language-session\">Swift Agent Harness\nModel: gpt-5.4\nLoaded AGENTS.md (3 lines)\nCtrl+C to quit\n&lt;Turn 1&gt;\nYou: List and read all files in this project and tell me what&#39;s most interesting about them.\n&lt;Step 1&gt;\nAssistant: I can do that, but I need the actual file list first.\n...\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;AGENTS.md&quot;}}\ntool-success: read_file -&gt; 2 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.&quot;}}\ntool-success: list_files -&gt; 7 lines\n&lt;Step 3&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Package.resolved&quot;}}\ntool-success: read_file -&gt; 15 lines\n&lt;Step 4&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Package.swift&quot;}}\ntool-success: read_file -&gt; 22 lines\n...\n&lt;Step 13&gt;\nAssistant: I read all files that were visible from the project root...\n</code></pre><p>The answer itself is not the interesting part, what I care about is the shape of the interaction. The model is not exploring the project in some rich, one off way. It’s as if it was playing a point-and-click adventure with the three verbs I gave it. It doesn’t know the environment in the same way we didn’t know everything that was clickable, so it goes and tries a call at a time and reassesses on every step, just like we rapidly click across the screen until we find an interactable object.</p><p>First <code>read_file</code>, because it already knows <code>AGENTS.md</code> exists. Then <code>list_files</code>, then <code>read_file</code>, then <code>read_file</code> again, then more listing, then more reading. Perfectly reasonable.</p><p>And so the question is, would reducing this down to a single tool that can do anything make this better or worse? Because with these three explicit tools, the model is forced to keep translating its intent into those wrappers over and over. That is not necessarily bad. There is something nice about how explicit it is. You can inspect every step. You can see exactly what the model asked for. You can keep the capabilities narrow.</p><p>But what if the model was able to cook a sequence of bash scripts in a single tool call and get everything it needs all at once? That was enough for me to want the next experiment. Remove the little toolbox. Leave just bash. See if the interaction becomes simpler, or just messier in a different way.</p><h2 id=\"new-bash-tool\">New Bash Tool</h2><p>Let’s start by making the new bash tool without removing the existing ones yet.</p><p>I will use <a href=\"https://github.com/swiftlang/swift-subprocess\">swift-subprocess</a>, because is a nice showcase of modern Swift tooling, but also because it solves a real problem. If you let an LLM call bash, it is very easy for it to produce commands that dump a lot of output. Directory walks, file listings, <code>find</code>, <code>sed</code>, <code>cat</code>, maybe all of them chained together. That means <code>stdout</code> and <code>stderr</code> handling stops being a boring implementation detail very quickly. It is easy to get into awkward buffering problems or deadlocks if you try to wire process spawning by hand.</p><p><code>swift-subprocess</code> is designed exactly for this kind of thing, and its API makes the “run a process and collect output properly” path feel trivial, not like a pile of edge cases we have to rediscover ourselves.</p><p>So the new tool looks like this:</p><pre><code class=\"language-swift\">static func bash() -&gt; Self {\n    ToolDefinition(\n        name: &quot;bash&quot;,\n        description: &quot;Run a bash command with the workspace root as the current directory.&quot;,\n        arguments: [&quot;command&quot;],\n        run: { arguments, workspaceRoot in\n            let command = arguments[&quot;command&quot;] ?? &quot;&quot;\n\n            let result = try await Subprocess.run(\n                .path(&quot;/bin/bash&quot;),\n                arguments: [&quot;-c&quot;, command],\n                workingDirectory: FilePath(workspaceRoot.path),\n                output: .string(limit: 1_048_576),\n                error: .string(limit: 1_048_576)\n            )\n\n            let stdout = result.standardOutput ?? &quot;&quot;\n            let stderr = result.standardError ?? &quot;&quot;\n\n            func jsonString(_ value: String) -&gt; String {\n                guard let data = try? JSONEncoder().encode(value),\n                      let string = String(data: data, encoding: .utf8) else {\n                    return &quot;\\&quot;\\&quot;&quot;\n                }\n                return string\n            }\n\n            return &quot;&quot;&quot;\n            {&quot;stdout&quot;:\\(jsonString(stdout)),&quot;stderr&quot;:\\(jsonString(stderr)),&quot;exit_code&quot;:\\(result.terminationStatus)}\n            &quot;&quot;&quot;\n        }\n    )\n}\n</code></pre><p>And then I just added it to the tools array. Nothing more.</p><pre><code class=\"language-swift\">let tools: [ToolDefinition] = [\n    .bash(),\n    .readFile(),\n    .listFiles(),\n    .editFile(),\n]\n</code></pre><p>This was a very deliberate intermediate step. I did not want to jump straight into “bash only” yet. I wanted to see how the model behaved when bash existed, but the <a href=\"https://alejandromp.com/development/blog/have-you-built-an-agent-harness-already/\">old explicit tools</a> were still there too.</p><p>And the first answer was, not that differently at all.</p><p>When I asked for a quick architecture overview with the previous prompt, the model still preferred the old tools. Even with bash available, it kept thinking in terms of <code>read_file</code> and <code>list_files</code>. Which honestly makes sense. Those tools were still there, still very explicit, still easier to map to the task.</p><p>So I had to push it a bit more and ask explicitly for bash.</p><p>I asked this:</p><pre><code class=\"language-text\">Use bash tool to give me a quick architecture overview of this project, including file sizes or line counts\n</code></pre><p>And that was enough for the LLM to use bash.</p><pre><code class=\"language-session\">&lt;Step 1&gt;\nAssistant: Quick architecture overview based on the current workspace...\n...\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;Top-level structure:\\\\n&#39;; find . -maxdepth 2 \\\\( -path &#39;.\\\\/ .git&#39; -o -path &#39;.\\\\/ .build&#39; \\\\) -prune -o -print | sed &#39;s#^.\\\\/##&#39; | sort; printf &#39;\\\\n\\\\nFile sizes (bytes) and line counts:\\\\n&#39;; find . -type f \\\\( -path &#39;.\\\\/ .git\\\\/*&#39; -o -path &#39;.\\\\/ .build\\\\/*&#39; \\\\) -prune -o -type f -print | sed &#39;s#^.\\\\/##&#39; | sort | while read f; do bytes=$(wc -c &lt; \\&quot;$f\\&quot; | tr -d &#39; &#39;); lines=$(wc -l &lt; \\&quot;$f\\&quot; | tr -d &#39; &#39;); printf &#39;%8s bytes  %6s lines  %s\\\\n&#39; \\&quot;$bytes\\&quot; \\&quot;$lines\\&quot; \\&quot;$f\\&quot;; done&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;Top-level structure:\\n.\\n.DS_Store\\n.gitignore\\nAGENTS.md\\nPackage...\n&lt;Step 2&gt;\nAssistant: Here&#39;s a quick architecture overview of the project, based on the file layout and line counts.\n...\n</code></pre><p>And as expected the LLM was able to make a tool call to get all the information it needed.</p><p>That was useful to see. Adding a more powerful tool does not automatically make the model prefer it. I’m sure we could tweak the system prompt to nudge it that way, but that’s an exercise on LLM research, and we’re here to talk about the harness. So let’s do the next logical step…</p><h2 id=\"one-for-all\">One For All</h2><p>It was time to remove all the other specific tool calls and see what happened. Luckily this part was trivial, just remove them from the tools list and leave <code>bash</code>.</p><pre><code class=\"language-swift\">let tools: [ToolDefinition] = [\n    .bash(),\n]\n</code></pre><p>And the result was immediately different.</p><p>I went back to the original style of prompt, no special instruction this time:</p><pre><code class=\"language-text\">Give me a quick architecture overview of this project, including file sizes or line counts\n</code></pre><p>This time the model had no alternative. If it wanted to inspect the repo, it had to do it through bash. Depending on the run, sometimes it used one bash call and sometimes a couple. But it did not have to follow the old pattern of many tiny file reads anymore. That is what I wanted to see.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Give me a quick architecture overview of this project, including file sizes or line counts\n&lt;Step 1&gt;\n...\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;Top-level:\\\\n&#39;; find . -maxdepth 2 -type f | sed &#39;s#^.\\\\/##&#39; | sort; printf &#39;\\\\n\\\\nTree (depth 3):\\\\n&#39;; find . -maxdepth 3 | sed &#39;s#^.\\\\/##&#39; | sort&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;Top-level:\\n.build\\/.DS_Store\\n.build\\/.lock\\n.build\\/build.db\\n.b...\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;bash&quot;,&quot;arguments&quot;:{&quot;command&quot;:&quot;printf &#39;Line counts (project files only):\\\\n&#39;; for f in AGENTS.md Package.swift Package.resolved README.md run.sh share.sh Sources\\\\/swiftagentharness\\\\/*.swift; do [ -f \\&quot;$f\\&quot; ] &amp;&amp; wc -l \\&quot;$f\\&quot;; done | sort -n; printf &#39;\\\\n\\\\nPackage.swift:\\\\n&#39;; sed -n &#39;1,220p&#39; Package.swift; printf &#39;\\\\n\\\\nREADME.md:\\\\n&#39;; sed -n &#39;1,260p&#39; README.md&quot;}}\ntool-success: bash -&gt; {&quot;stdout&quot;:&quot;Line counts (project files only):\\n       2 AGENTS.md\\n       9 sh...\n&lt;Step 3&gt;\nAssistant: Here&#39;s a quick architecture overview of the project.\n...\n</code></pre><p>But we also need to note what we lose when we remove the specific tools. With <code>read_file</code> and <code>list_files</code>, part of the harness design was encoded in the tools themselves. The model could only ask for fairly narrow things and we had control over it. For example, we could make the list of files not return any hidden files, thus reducing the amount of likely useless data we fed back into the LLM’s context. With bash, that structure is gone. The model has much more freedom, which is exactly why it can gather more in fewer calls, but it also means the quality of the interaction depends much more on how well it shapes the command. As you can see in the exchange above, the LLM got a lot of hidden files that were not necessary and distracted from the goal.</p><p>That tradeoff is exactly what I was hoping to understand with this exercise. Bash really can replace the tiny toolbox. But it does not simply make the harness better. It moves complexity away from the tool harness and into the LLM’s command construction.</p><h2 id=\"performance-impact\">Performance Impact</h2><p>Seeing how the LLM can cook a single tool call using bash to gather all the information in one single step is eye-opening, it makes you realize the performance implications of tools. We’ve already seen how <a href=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/\">the context is the thing that matters to get the most out of AIs</a>, but here we’ve seen how the tools that you give actually can have a tremendous impact too.</p><p>Even in this simple harness we can see how the impact on performance of the AI can be totally different depending on the tools we give it. For that we need to really internalize that every time the LLM needs more information, it must make a tool call, that means an extra step, and extra back and forth between the LLM and our harness. That also means triggering a new inference run. And remember, even if LLM architectures have caches and fancy tricks, every inference run virtually starts from the beginning of the conversation and it autocompletes the next part. So having a lot of back and forth on the same turn just to gather extra data is less than desirable.</p><p><img src=\"https://alejandromp.com/development/blog/do-we-even-need-multiple-tools/tool-roundtrips.svg\" alt=\"Side by side comparison of many small tool calls versus a single bash call\"></p><p>Of course this is simplified, the bash version may still take two calls on some runs, and the multi-tool version may vary too. But the high level shape is the important part. One approach encourages lots of tiny round-trips. The other makes it possible to gather much more context in one go. Even if it’s up to the LLM to decide, the bash tool gives it the option to reduce the roundtrips.</p><p>This is where tradeoffs, analysis, engineering and research all meet to tweak a harness implementation and make it the most performant while making it useful and concrete enough for the LLM to know what to do. And this keeps evolving as LLMs evolve. This is one of the things that might make a harness feel better than another.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div><h2 id=\"so,-do-we?\">So, do we?</h2><p>Can we answer the question posed in the title of this post… as always, it depends. I think there is no real winner here, or at least not as clearly as I thought. I imagine one would have to run deeper and <em>scientific</em> benchmarks to see some sort of trend.</p><p>The multiple specific tools were useful mostly because they encoded structure. They constrained the model, made the interaction easier to inspect, and gave the harness more control over what kind of information could flow back into the context. And that is also exactly their limitation. Bash, on the other hand, got surprisingly close to replacing the whole little toolbox precisely because it removed that structure. Once the model had access to it, it could gather broad context much faster and with fewer round-trips. And that flexibility is also exactly the problem, because you lose control and specificity.</p><p>And that, for me, was the interesting shift in understanding. The question is not only what tools we can give the model. It is where we want the structure to live. In a carefully designed tool protocol, or in the commands the model writes for itself.</p><p>The advantage is that we’re just here to learn how these things work and see past the curtain, and on that I think we won. So for this step of the journey, I will leave it here. Each one can take the conclusion they want. I might just keep all tools available, and add a flag to run it in different modes.</p>",
      "date_published" : "2026-05-01T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/do-we-even-need-multiple-tools",
      "summary" : "After <a href=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/\">teaching the harness to load <code>AGENTS.md</code></a>, my mind went back to thinking about the tools, and wondered, do we even need multiple tools?",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "Do We Even Need Multiple Tools?",
      "url" : "https://alejandromp.com/development/blog/do-we-even-need-multiple-tools"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>In the previous post, <a href=\"https://alejandromp.com/development/blog/have-you-built-an-agent-harness-already/\">Have You Built an Agent Harness Yet?</a>, we built a tiny harness in Swift. We gave it a loop, a prompt, and some tools, and with that it already had most of the features we are used to with modern AI. No magic. Just autocomplete, and a harness deciding what the model gets to see and do.</p><p>But there is another bit of magic we didn’t touch on and we should demystify. The appearance of memory. You know that moment when you open a brand new conversation and the agent already seems to know your project.</p><p>It doesn’t.</p><p>LLMs have no memory between fresh conversations. They just autocomplete from what they have in the current context. So if your AI tool of the week seems to remember your repo conventions, folder structure, or coding rules, what is happening is much simpler.</p><p>The industry has come to agree on the <a href=\"https://agents.md\">AGENTS.md</a>  standard, although some companies think they are special and use other file names. This is where you can put information about the project for the AI to learn.</p><p>But remember, there is no magic in AI. No memory. Just autocomplete. So how does this even work? If you’ve read the first post in this series, you already know the answer. The harness is doing the work.</p><p>The trick is that the harness automatically loads that file when the conversation starts and injects it into the context.</p><p>That’s it. That is the whole trick.</p><p>It is not a secret memory layer. It is not special agent magic. It is just more text in the context.</p><p>Let’s add it to the little harness from last post so you can see how simple the trick really is.</p><h2 id=\"the-agent-only-knows-what-we-send\">The Agent Only Knows What We Send</h2><p>At the end of the previous article, our system prompt was still very simple:</p><pre><code class=\"language-swift\">self.systemPrompt = &quot;&quot;&quot;\n    You are a helpful assistant with access to tools.\n\n    The workspace root is:\n    \\(workspaceRoot.path)\n\n    Tools:\n    \\(toolsPrompt)\n\n    When you want to use the tool, reply with exactly one line in this format and nothing else:\n    TOOL_CALL {&quot;name&quot;:&quot;tool_name&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;some/path&quot;}}\n\n    After receiving a TOOL_RESULT message, continue the task.\n    If no tool is needed, answer normally.\n    &quot;&quot;&quot;\n</code></pre><p>That is enough for the agent to use tools and know where the workspace root is. But it still doesn’t know anything specific about the project itself.</p><p>So if we start a fresh conversation, the agent won’t know anything about our project and will have to go discover it.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: What do you know about this project?\n&lt;Step 1&gt;\nAssistant: I don’t know much yet beyond the project name and whatever is in the package manifest.\nFrom the workspace path, it appears to be a Swift project named **swiftagentharness**.\nI’ve requested the file listing and the `Package.swift` manifest so I can summarize:\n- what kind of Swift package it is\n- its targets/products\n- dependencies\n- likely purpose\nIf you want, I can continue by inspecting the repository structure and give you a concise overview of the project.\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;\\/Users\\/me\\/code\\/swiftagentharness\\/Package.swift&quot;}}\ntool-success: read_file -&gt; 22 lines\n&lt;Step 2&gt;\nAssistant: Based on `Package.swift`, here’s what I know:\n...\n</code></pre><p>That is exactly what <code>AGENTS.md</code> is for.</p><h2 id=\"let’s-load-the-file\">Let’s Load the File</h2><p>The implementation is delightfully boring.</p><p>Before building the system prompt, we try to read an <code>AGENTS.md</code> file from the workspace root:</p><pre><code class=\"language-swift\">let agentsMD = try? String(\n    contentsOf: workspaceRoot.appendingPathComponent(&quot;AGENTS.md&quot;),\n    encoding: .utf8\n)\n\nlet projectInstructions = if let agentsMD {\n    &quot;&quot;&quot;\n    Project instructions in AGENTS.md:\n    \\(agentsMD)\n    &quot;&quot;&quot;\n} else {\n    &quot;&quot;\n}\n</code></pre><p>And once we have that text, we add it to the system prompt:</p><pre><code class=\"language-swift\">self.systemPrompt = &quot;&quot;&quot;\n    You are a helpful assistant with access to tools.\n\n    The workspace root is:\n    \\(workspaceRoot.path)\n\n    \\(projectInstructions)\n\n    Tools:\n    \\(toolsPrompt)\n\n    When you want to use the tool, reply with exactly one line in this format and nothing else:\n    TOOL_CALL {&quot;name&quot;:&quot;tool_name&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;some/path&quot;}}\n\n    After receiving a TOOL_RESULT message, continue the task.\n    If no tool is needed, answer normally.\n    &quot;&quot;&quot;\n</code></pre><p>That’s it.</p><p>Now the “project knowledge” is available to the LLM. Not in some mystical way, but just by being in its context. The harness literally copies the Markdown into the context it sends to the model.</p><p>That’s all this “project conventions” memory is.</p><h2 id=\"making-the-trick-visible\">Making the Trick Visible</h2><p>Because one of the goals of this whole exercise is to break the illusion, I also made the harness print whether it loaded the file:</p><pre><code class=\"language-swift\">let agentsMD = try? String(contentsOf: workspaceRoot.appendingPathComponent(&quot;AGENTS.md&quot;), encoding: .utf8)\nself.agentsMDLineCount = agentsMD.map { $0.components(separatedBy: .newlines).count }\n\n...\n\nprint(&quot;Swift Agent Harness&quot;)\nprint(&quot;Model: \\(client.model)&quot;)\nif let lineCount = agentsMDLineCount {\n    print(&quot;Loaded AGENTS.md (\\(lineCount) lines)&quot;)\n}\nprint(&quot;Ctrl+C to quit&quot;)\n</code></pre><p>If the harness says it loaded 2 lines, that means 2 lines of Markdown are about to shape the model’s behavior. Nothing more. Nothing less.</p><p>For my demo project, the file is tiny:</p><pre><code class=\"language-md\">This is a test AGENTS.md.\nIf user says &quot;Hello Swift Agent Harness&quot;, reply acknowledging you are that Swift agent harness and that you have read this AGENTS.md.\n</code></pre><p>And once the harness injects that into the system prompt, the effect is immediate:</p><pre><code class=\"language-session\">Swift Agent Harness\nModel: gpt-5.4\nLoaded AGENTS.md (2 lines)\nCtrl+C to quit\n&lt;Turn 1&gt;\nYou: Hello Swift Agent Harness\n&lt;Step 1&gt;\nAssistant: Hello! I’m the Swift Agent Harness, and I’ve read the AGENTS.md.\n</code></pre><p>Of course it can answer that way. We literally gave it those instructions in the context.</p><p><img src=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/context-diagram.svg\" alt=\"Diagram of the context sent to the model: the System Prompt box contains Tools, Workspace, and Project Instructions, all fed by the Harness. AGENTS.MD feeds into the Harness. Below the System Prompt sit the User request and AI Response boxes. The User arrow leads into User request.\"></p><h2 id=\"it-is-just-markdown\">It Is Just Markdown</h2><p>This is the part I really want people to internalize.</p><p><code>AGENTS.md</code> is not a magical new memory subsystem for AI. It is not some privileged channel into the model’s <em>brain</em>. It is not a special capability only the big fancy tools have.</p><p>It is just Markdown added into the context.</p><p>Useful Markdown, yes. Important Markdown, maybe. But still just text that your harness decided to prepend before the conversation starts. You could literally start each conversation by pasting that instruction yourself, and it wouldn’t be much different.</p><p>Look what happens if we do that before any of the changes in this post.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: This is a test AGENTS.md. \nIf user says &quot;Hello Swift Agent Harness&quot;, reply acknowledging you are that Swift agent harness and that you have read this AGENTS.md.\n&lt;Step 1&gt;\nAssistant: Understood. How can I help with the AGENTS.md test?\n&lt;Turn 2&gt;\nYou: Hello Swift Agent Harness\n&lt;Step 1&gt;\nAssistant: Hello! I’m the Swift Agent Harness, and I’ve read this AGENTS.md.\n&lt;Turn 3&gt;\n</code></pre><p>Of course, here we’re just trying to learn how things work and demystify them. Production-grade tools can go further. They may walk parent directories, support nested instruction files, merge global and local rules, decide when to refresh or skip them, even summarize huge files to fit the token budget.</p><p>All of that is just more real engineering work (and learning the particularities of the autocomplete machines).</p><p>But the core idea is still exactly this one. Somewhere in the code there is a boring piece of logic reading a file and sticking its contents into the prompt.</p><p>That is the “memory”.</p><p>That is the “project awareness”.</p><p>That is the “magic”.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div><h2 id=\"one-more-layer-off-the-illusion\">One More Layer Off the Illusion</h2><p>I like this example because it teaches a few things.</p><p>The model never knows your project by default. If it knows something, your harness probably put it there.</p><p>The conventions around agent tooling are much more ordinary than they sound. <code>AGENTS.md</code> is not a sacred artifact. It is just a contract between your harness and your repo. If this file exists, load it and send it too.</p><p>And honestly, that is reassuring. Once you see it clearly, the whole thing becomes easier to reason about. Easier to debug. Easier to improve. Easier to distrust when the marketing gets too theatrical.</p><p>It’s just models and context, with a bit of Markdown sprinkled on top.</p>",
      "date_published" : "2026-04-27T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does",
      "summary" : "In the previous post, <a href=\"https://alejandromp.com/development/blog/have-you-built-an-agent-harness-already/\">Have You Built an Agent Harness Yet?</a>, we built a tiny harness in Swift. We gave it a loop, a prompt, and some tools, and with that it already had most of the features we are used to with modern AI. No magic. Just autocomplete, and a harness deciding what the model gets to see and do.",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "AI doesn't remember your project, Markdown does",
      "url" : "https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>For years I have repeated a thing that I still believe. Every programmer should write a promise library once. I think agent harnesses are the 2026 version of that exercise.</p><p>If you use AI coding tools every day, and especially if you have opinions about agent workflows, subagents, MCPs, commands, skills, and <a href=\"https://alejandromp.com/development/blog/development-dogma-is-back-a-i-agents/\">whatever new ritual the week invented</a>, build a tiny one yourself. Once. Not to ship it. Not because you are going to beat the existing ones, although honestly, with so much vibed slop around it would not be that hard. The point is that after you build one, the whole space stops feeling mystical. You stop thinking in terms of what big corp marketing wants and start thinking in terms of simple reality.</p><p>There is a model. There is a loop. There are tools. There is context. That is the heart of it.</p><p>Of course the real products do much more. But it’s still built around a core that is much simpler than people think.</p><h2 id=\"what-is-an-agent-harness\">What is an Agent Harness</h2><p>A harness is just the little program that sits between the model and the outside world. That is it. Yes, a normal program with normal code. I guess nowadays we call it “classic”.</p><p>The model, the AI, does not touch your filesystem. The model does not open files. The model does not run commands. It only autocompletes text.</p><p>It is the harness that decides what messages get sent to the model, what context gets included, how the replies are interpreted, what tools the model has to <em>see</em> the world, and what result gets sent back after something happens locally.</p><p>That also means the conversation is yours.</p><p>This is one of the big things I think people should internalize. The continuity of the chat, the remembered context, the tool results, all of that is managed by the harness. The model endpoint is not secretly keeping your whole little world alive for you, it doesn’t have all your project in memory. The harness keeps appending messages and resending what matters. Sometimes even removing old ones!</p><p>Now, to be fair, modern APIs offer more stateful variants and convenience helpers on the server side. That changes the ergonomics, one would argue that for the worse, but not the core mental model. Somebody still owns the conversation contract, and when you build your own harness that somebody is you.</p><p>So the smallest possible mental model is this:</p><ol><li>You send a message.</li><li>The harness, the program you are running and interacting with, adds a bunch of context that the user doesn’t see, what’s often called the “system prompt”, and sends that to the LLM.</li><li>The model replies with text, by autocompleting from the last message it has received, which includes the entire history.</li><li>Your program decides what that text means.</li><li>If needed, your program does something in the world.</li><li>The result goes back as more text and context.</li></ol><p>That is the whole trick. No magic. No AGI.</p><h2 id=\"let’s-build-a-simple-harness\">Let’s build a simple Harness</h2><p>I can describe what a harness is, but to make sure we internalize it let’s build a simple one, from scratch.</p><p>To start, let’s keep the first step easy and simple. Let’s just make a CLI app that lets you send messages to the AI and shows you the LLM responses. Very simple, but useful to see how this works if you’ve never seen it before, and a necessary step before we get into the proper <em>agentic</em> features.</p><p>Let’s start with a simple CLI Swift package. Nothing fancy. No framework for agents. No giant abstraction tower. Just <code>swift package init --type executable</code> and the bits we actually need. As usual for command line tools in Swift, I used <code>swift-argument-parser</code>. That gives me a proper executable entrypoint with typed arguments.</p><p>At this stage the executable needs only a few things:</p><ol><li>The base URL of the LLM chat-completions endpoint.</li><li>The model name.</li><li>An API key, loaded from the environment.</li></ol><p>That is already enough to talk to an LLM.</p><p>For this post we will assume we have access to some LLM API, which in my project is represented by <code>OpenAICompatibleClient</code>. It’s not the interesting part of the project, but it is useful glue. It takes messages, performs the HTTP call to an OpenAI-compatible endpoint, and gives us back the assistant text. Don’t think it does anything special, it’s literally just an HTTP request-response.</p><p>The <code>SwiftAgentHarness</code> app is just a <code>AsyncParsableCommand</code> that reads the arguments, validates the endpoint, loads the API key from the environment, instantiates the client, creates an <code>Agent</code>, and calls <code>run()</code>:</p><pre><code class=\"language-swift\">@main\nstruct SwiftAgentHarness: AsyncParsableCommand {\n    @Option(help: &quot;LLM chat completions endpoint.&quot;)\n    var baseURL: String\n\n    @Option(help: &quot;Model name to use.&quot;)\n    var model = &quot;gpt-5.4&quot;\n\n    mutating func run() async throws {\n        let environment = ProcessInfo.processInfo.environment\n        guard let apiKey = environment[&quot;LLM_API_KEY&quot;], apiKey.isEmpty == false else {\n            throw HarnessError.missingAPIKey\n        }\n        guard let endpoint = URL(string: baseURL) else {\n            throw HarnessError.invalidArguments(&quot;Invalid base URL: \\(baseURL)&quot;)\n        }\n\n        let client = OpenAICompatibleClient(apiKey: apiKey, baseURL: endpoint, model: model)\n        let agent = Agent(client: client)\n        try await agent.run()\n    }\n}\n</code></pre><p>At this point you can see our agent harness only needs the API client, nothing more. Let’s now make this <code>Agent</code> do something very basic.</p><pre><code class=\"language-swift\">struct Agent {\n    let client: OpenAICompatibleClient\n\n    func run() async throws {\n        print(&quot;Swift Agent Harness&quot;)\n        print(&quot;Model: \\(client.model)&quot;)\n        print(&quot;Ctrl+C to quit&quot;)\n    }\n}\n</code></pre><p>With this in place we can now iterate on the <code>Agent</code> itself.</p><h2 id=\"having-a-conversation\">Having a conversation</h2><p>The first step is to set up what’s needed to have a conversation with the AI. A simple chat, nothing else.</p><p>The key thing to add now is the conversation structure itself. Not just reading one line and making one HTTP request, but actually keeping the state of the exchange in memory.</p><p>First, we need a system prompt. This is the hidden part of the context that the user never sees but that we, as the harness developers, can manipulate. This is quite an important piece for AI to behave as one desires, it’s part of the context, and as you know, context is all that matters (because remember, it is all there is, the only thing the AI sees).</p><pre><code class=\"language-swift\">struct Agent {\n    private let systemPrompt = &quot;You are a helpful assistant.&quot;\n  \n</code></pre><p>Then we need to construct and keep around the conversation array. This is the state that we maintain to keep track of the whole conversation between the user and the AI (and later extra things our harness will do)</p><pre><code class=\"language-swift\">    \n    func run() async throws {\n        print(&quot;Swift Agent Harness&quot;)\n        print(&quot;Model: \\(client.model)&quot;)\n        print(&quot;Ctrl+C to quit&quot;)\n\n        var conversation = [Message(role: &quot;system&quot;, content: systemPrompt)]\n</code></pre><p>Then we need the loop that reads user input from the terminal.</p><pre><code class=\"language-swift\">\n        while true {\n            print(&quot;\\u{001B}[94mYou\\u{001B}[0m: &quot;, terminator: &quot;&quot;)\n            guard let input = readLine(), input.isEmpty == false else {\n                continue\n            }\n</code></pre><p>That gives us the human side of the conversation. But that input is not useful yet until we actually append it to the state that we keep around.</p><pre><code class=\"language-swift\">\n            conversation.append(Message(role: &quot;user&quot;, content: input))\n</code></pre><p>Now comes the important bit. We send the entire conversation, not just the last line typed by the user.</p><pre><code class=\"language-swift\">\n            let response = try await client.send(messages: conversation)\n</code></pre><p>And once we get the response back, we print it and append it too.</p><pre><code class=\"language-swift\">\n            print(&quot;\\u{001B}[93mAssistant\\u{001B}[0m: \\(response)&quot;)\n            conversation.append(Message(role: &quot;assistant&quot;, content: response))\n        }\n    }\n}\n</code></pre><p>And yes, this is the first place where the illusion starts to break. The only reason the AI feels like it remembers what you said two messages ago is because the harness, the app, keeps sending the history back.</p><p>That is already a harness. Not a tool-using one, not a coding agent yet, but a simple one that is just a chat. But definitely a harness.</p><p>And we can give it a spin!</p><pre><code class=\"language-session\">Swift Agent Harness\nModel: gpt-5.4\nCtrl+C to quit\nYou: Hello\nAssistant: Hello! How can I help you today?\nYou: What can you tell me about this project.\nAssistant: I’d be happy to help. Please share the project details—such as a description, code, repository link, files, screenshots, or goals—and I can explain:\n\n- what the project does\n- its architecture and components\n- the technologies used\n- how the code is organized\n- likely strengths, risks, and next steps\n\nIf you want, you can paste the README, folder structure, or source files here.\n</code></pre><p>This is why I wanted to start here. Before the model can read files or edit code or do anything that looks magical, it first has to live inside a very boring loop. Read input. Append message. Call model. Print output. Append response. Repeat.</p><p>This was where AI started, where the ChatGPT revolution stayed for quite some time. You can see how we asked a question about the project, and it had no clue about it. There is no magic, the AI doesn’t know about our project, it has no access to it, so it just replies asking for more context. For a long time, even today, people still use AI this way, copy-pasting information into the context. But that’s not the revolution we expected.</p><p>So then, we gave it tools.</p><h2 id=\"ai-interacting-with-our-world\">AI interacting with our world</h2><p>So far we only have a chat. Useful, yes. But not very exciting. The AI can only talk about whatever is already in the context. It still cannot <em>see</em> our project or interact with the world around it.</p><p>This is the first real step into agentic territory. We need to give the model a tool.</p><p>The important thing here is that there is still no hidden magic API involved. Current models are good enough that you can often teach them a new local convention just by adding it to the context. So for our little harness we do not need any special provider feature. We just extend the system prompt and tell the model that if it wants to use a tool it must reply in a very specific format.</p><p>To keep the first step small, let’s just add one. <code>read_file</code>.</p><pre><code class=\"language-swift\">private let systemPrompt = &quot;&quot;&quot;\nYou are a helpful assistant with access to one tool.\n\nTool:\nName: read_file\nDescription: Read a UTF-8 text file from disk.\nArguments: path\n\nWhen you want to use the tool, reply with exactly one line in this format and nothing else:\nTOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;some/file.txt&quot;}}\n\nAfter receiving a TOOL_RESULT message, continue the task.\nIf no tool is needed, answer normally.\n&quot;&quot;&quot;\n</code></pre><p>This is worth pausing on. The tool is not just a function in Swift. The tool is also part of the prompt. The model needs to know that it exists, what it is for, and what shape of arguments it expects. The better you describe the tool, the better the model can use it.</p><p>Then, after the model replies, we check if the reply starts with <code>TOOL_CALL</code>.</p><pre><code class=\"language-swift\">let response = try await client.send(messages: conversation)\n\nlet trimmed = response.trimmingCharacters(in: .whitespacesAndNewlines)\nif trimmed.hasPrefix(&quot;TOOL_CALL &quot;) {\n    print(&quot;tool-call: \\(trimmed)&quot;)\n\n    let outcome: ToolOutcomeResult\n    do {\n        // Get the tool call text from the LLM\n        let payload = String(trimmed.dropFirst(10))\n        guard let data = payload.data(using: .utf8) else {\n            throw HarnessError.invalidToolInvocation\n        }\n      \n        // Parse the tool call text to the JSON contract we specified in the prompt\n        let invocation: ToolInvocation\n        do {\n            invocation = try JSONDecoder().decode(ToolInvocation.self, from: data)\n        } catch {\n            throw HarnessError.invalidToolPayload(payload)\n        }\n      \n        // Check for the only tool we have\n        guard invocation.name == &quot;read_file&quot; else {\n            throw HarnessError.invalidToolInvocation\n        }\n\n        // Run the &quot;tool&quot;\n        let path = invocation.arguments[&quot;path&quot;] ?? &quot;&quot;\n        let url = URL(fileURLWithPath: path)\n        let result = try String(contentsOf: url, encoding: .utf8)\n        print(&quot;tool-success: read_file -&gt; \\(summarizeToolResult(result))&quot;)\n        outcome = .success(result)\n    } catch {\n        let errorMessage = &quot;Tool invocation failed: \\(error.localizedDescription). Reply again, either with a valid TOOL_CALL line or a normal answer.&quot;\n        print(&quot;tool-error: \\(errorMessage)&quot;)\n        outcome = .failure(errorMessage)\n    }\n\n    conversation.append(Message(role: &quot;assistant&quot;, content: response))\n    conversation.append(Message(role: &quot;user&quot;, content: formatToolResult(outcome)))\n\n    let followUp = try await client.send(messages: conversation)\n    print(&quot;Assistant: \\(followUp)&quot;)\n    conversation.append(Message(role: &quot;assistant&quot;, content: followUp))\n    continue\n}\n</code></pre><p>And just like that now our harness gives more powers to the AI:</p><pre><code class=\"language-session\">You: Read the `README.md` file and summarize it\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\ntool-success: read_file -&gt; 17 lines\nAssistant: `swiftagentharness` is a Swift command-line project built with Swift Package Manager and Apple’s ArgumentParser.\n\nKey points:\n- Requires Swift tools 6.2\n- Requires macOS 26 or later\n- Build with: `swift build`\n- Run with: `swift run`\n- Optionally run via: `./run.sh`\n</code></pre><p>Again, no magic. The model returns text. The harness interprets that text as a tool call. Then the harness decides to execute something locally. The <code>tool-call</code> and <code>tool-success</code> lines are just for the user, and for debugging in the terminal. What goes back to the AI is the <code>TOOL_RESULT</code> message, which includes the actual result together with a tiny <code>SUCCESS</code> or <code>ERROR</code> protocol we invented, just to make it more clear to the AI. With that new context in place, the model can then reply with the summary of the file as we asked.</p><details><summary>The <code>ToolOutcomeResult</code> definition</summary><p>The only extra thing I did allow myself is one small enum to represent the outcome.</p><pre><code class=\"language-swift\">enum ToolOutcomeResult {\n    case success(String)\n    case failure(String)\n}\n</code></pre><p>Doesn’t matter much, but I like having these things properly modeled.</p></details><p>Yes, that really is it. Now the AI has access to, a small part of, our world. The moment you do this, the conversation changes. Before, if you asked about a local file the AI had to ¯\\_(ツ)_/¯ and ask you to paste it. Now it can ask <em>you</em> to run the little contract you gave it.</p><p>And notice what happened here. We did not give the AI direct access to the file system. We did not open a mystical portal into the machine. We only gave it a way to request something in text, and we wrote the normal boring Swift code that fulfills that request.</p><p>And to make it even more clear, I have here a real run from this version of the harness that has so much to learn from:</p><pre><code class=\"language-session\">Swift Agent Harness\nModel: gpt-5.4\nCtrl+C to quit\nYou: tell me about this project\nAssistant: Sure. If you want me to describe a specific project, I need to inspect its files first.\n\nPlease give me:\n- the project path, or\n- the main file to read\n\nYou: readme\nAssistant: Please provide the file path, e.g. `README.md` or `./README.md`, and I’ll read it and summarize the project.\n\nYou: README.md\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\ntool-success: read_file -&gt; 17 lines\nAssistant: This project is `swiftagentharness`, a Swift command-line app. ...\n\nYou: now read the run file\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;run.sh&quot;}}\ntool-success: read_file -&gt; 15 lines\nAssistant: `run.sh` is a convenience launcher for the CLI. ...\n</code></pre><p>When I ask vaguely about the project, the AI still does not know anything about it. That part has not changed. It only knows what is in the conversation. But now knows it has a way to learn. It cannot inspect the whole project by magic, but it knows it can read files if I give it a path.</p><p>But it knows it only has access to reading single files, so note that when I say <code>readme</code>, it pushes back and asks for something more specific. It doesn’t have enough context to make up a proper tool call. That is nice. It means the model is trying to operate within the contract we gave it instead of hallucinating a path.</p><p>Then I give it a proper file name <code>README.md</code>, it uses the read tool, and suddenly that file is now part of the conversation. Thanks to that, I can be a bit more ambiguous and say “the run file” and it understands I probably mean <code>run.sh</code>, because that is now in context too.</p><p>This is one of those moments where the illusion becomes very educational. The AI did not gain general awareness. It just accumulated a bit more text in the conversation, text that came from a tool call your harness fulfilled.</p><h2 id=\"models-being-nice\">Models being nice</h2><p>One funny thing here is that this first version did not stay stable for very long. As is typical with LLMs, things are not consistent and they reply with random responses, is part of their nature. In this case, it’s actually a good thing.</p><p>Soon enough I hit errors like this:</p><pre><code class=\"language-session\">tool-error: Tool invocation failed: Tool invocation payload could not be decoded: {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\nI don’t have the contents of `README.md` yet. Please provide the file contents or ensure the tool result is available, and I’ll summarize it.. Reply again, either with a valid TOOL_CALL line or a normal answer.\n</code></pre><p>At first this looks confusing, because the JSON in there seems totally valid. The problem was that the model, in this case GPT, was trying to be nice. Instead of replying with only the exact <code>TOOL_CALL</code> line I asked for, it sometimes produced an extra little explanatory sentence. Good for the user, not so much for our parsing code.</p><p>And honestly, that is a very model thing to do. Modern models are trained to be helpful, conversational, and a bit eager to explain themselves. They are not trying to break your harness. They are trying to be polite.</p><p>So this is a good moment to improve the code a bit. Not because the architecture changed, but because the real world showed us a new shape of output. This is a whole rabbit hole that makes making a production-ready harness a bit more complex. We would need to tailor the system prompt for every model, improve our parsing, and other engineering techniques that are not worth for this post.</p><p>But at least, let’s make it work for this use case so we can continue learning. Instead of assuming the whole response starts with <code>TOOL_CALL</code>, we now parse the assistant response line by line.</p><pre><code class=\"language-swift\">private func parseAssistantResponse(from response: String) throws -&gt; ParsedAssistantResponse {\n    let lines = response\n        .split(whereSeparator: \\.isNewline)\n        .map { String($0).trimmingCharacters(in: .whitespaces) }\n        .filter { $0.isEmpty == false }\n\n    var userFacingLines = [String]()\n    var invocation: ToolInvocation?\n\n    for line in lines {\n        guard line.hasPrefix(&quot;TOOL_CALL &quot;) else {\n            userFacingLines.append(line)\n            continue\n        }\n\n        let payload = String(line.dropFirst(10))\n        guard let data = payload.data(using: .utf8) else {\n            throw HarnessError.invalidToolInvocation\n        }\n        do {\n            invocation = try JSONDecoder().decode(ToolInvocation.self, from: data)\n        } catch {\n            throw HarnessError.invalidToolPayload(payload)\n        }\n    }\n\n    return ParsedAssistantResponse(\n        userFacingText: userFacingLines.joined(separator: &quot;\\n&quot;),\n        invocation: invocation\n    )\n}\n</code></pre><p>Then the loop can show the assistant text to the user first, and still execute the tool if there is one.</p><pre><code class=\"language-swift\">let response = try await client.send(messages: conversation)\nlet parsedResponse = try parseAssistantResponse(from: response)\n\nif parsedResponse.userFacingText.isEmpty == false {\n    print(&quot;Assistant: \\(parsedResponse.userFacingText)&quot;)\n}\n\nif let invocation = parsedResponse.invocation {\n    // execute tool\n}\n</code></pre><p>This is a bit of the secret sauce of every harness. Not secret magic. Not genius algorithms. Just more cases of text interpretation, because at the end of the day that is still what the harness is doing. AI dumps text, and somebody needs to deal with it to create the illusion.</p><h2 id=\"let-it-explore\">Let it explore</h2><p>At this point the next obvious limitation appears. <code>read_file</code> is useful, but it still depends too much on the human already knowing what file should be read.</p><p>The harness can inspect.</p><p>But it still cannot explore.</p><p>That is why the next tool to add is <code>list_files</code>.</p><p>And this is also the moment where the code starts earning a tiny bit of structure. Having one hardcoded tool inline was nice for the first learning step, but with a second tool it already makes sense to generalize a little.</p><p>So instead of special-casing everything directly in the loop, we will define a small tool type.</p><pre><code class=\"language-swift\">struct ToolDefinition {\n    let name: String\n    let description: String\n    let arguments: [String]\n    let run: ([String: String]) throws -&gt; String\n}\n</code></pre><p>Then in the app entrypoint we can define the available tools explicitly.</p><pre><code class=\"language-swift\">let tools: [ToolDefinition] = [\n    .readFile(),\n    .listFiles(),\n]\nlet agent = Agent(client: client, tools: tools)\n</code></pre><p>And because the tools are now dynamic, the system prompt should be dynamic too. Instead of hardcoding the tool descriptions by hand, the <code>Agent</code> now builds the tool section of the prompt from the actual <code>ToolDefinition</code> values it receives.</p><pre><code class=\"language-swift\">init(client: OpenAICompatibleClient, tools: [ToolDefinition]) {\n    self.client = client\n    self.toolsByName = Dictionary(uniqueKeysWithValues: tools.map { ($0.name, $0) })\n\n    let toolsPrompt = tools\n        .sorted { $0.name &lt; $1.name }\n        .map(\\.promptBlock)\n        .joined(separator: &quot;\\n\\n&quot;)\n\n    self.systemPrompt = &quot;&quot;&quot;\n        You are a helpful assistant with access to tools.\n\n        Tools:\n        \\(toolsPrompt)\n\n        When you want to use the tool, reply with exactly one line in this format and nothing else:\n        TOOL_CALL {&quot;name&quot;:&quot;tool_name&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;some/path&quot;}}\n\n        After receiving a TOOL_RESULT message, continue the task.\n        If no tool is needed, answer normally.\n        &quot;&quot;&quot;\n</code></pre><p>That is an important little detail. It means the prompt does not drift away from reality. If I add a tool in Swift but forget to tell the model about it or if I tell the model about a tool that is not really available, the harness will be lying and the model will freak out. Building the prompt from the tool definitions keeps both sides synchronized.</p><p>With this in place we can extend the <code>ToolDefinition</code> and use it as a way to host the different tools we have.</p><pre><code class=\"language-swift\">extension ToolDefinition {\n    static func readFile() -&gt; Self { ... }\n</code></pre><p>The <code>list_files</code> tool itself is still very simple.</p><pre><code class=\"language-swift\">static func listFiles() -&gt; Self {\n    let fileManager = FileManager.default\n\n    return ToolDefinition(\n        name: &quot;list_files&quot;,\n        description: &quot;List files and directories in a path.&quot;,\n        arguments: [&quot;path&quot;],\n        run: { arguments in\n            let path = arguments[&quot;path&quot;] ?? &quot;.&quot;\n            let url = URL(fileURLWithPath: path)\n            let values = try fileManager.contentsOfDirectory(\n                at: url,\n                includingPropertiesForKeys: [.isDirectoryKey],\n                options: [.skipsHiddenFiles]\n            )\n            let lines = try values\n                .sorted { $0.lastPathComponent &lt; $1.lastPathComponent }\n                .map { entry in\n                    let isDirectory = try entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false\n                    return isDirectory ? entry.lastPathComponent + &quot;/&quot; : entry.lastPathComponent\n                }\n            return lines.joined(separator: &quot;\\n&quot;)\n        }\n    )\n}\n</code></pre><p>Notice what happened here. The architecture did not change. We still have the same loop, the same text protocol, the same <code>TOOL_CALL</code>, the same <code>TOOL_RESULT</code>.</p><p>What changed is just the model’s ability to gather context by itself.</p><p>This is a much bigger jump than it may look. <code>read_file</code> gave the AI a flashlight but <code>list_files</code> gives it a map.</p><pre><code class=\"language-session\">You: List the names of the files in this project.\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.&quot;}}\ntool-success: list_files -&gt; 5 lines\nAssistant: Package.resolved\nPackage.swift\nREADME.md\nSources/\nrun.sh\nYou: summarize the readme\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\ntool-success: read_file -&gt; 17 lines\nAssistant: The README says this is a Swift ...\n</code></pre><p>Nice! Now the user doesn’t have to specify the exact file names and paths because the model can explore directories itself.</p><h2 id=\"ai-needs-longer-turns\">AI needs longer turns</h2><p>The example above was a bit forced to show the happy path. The reality is that we have a problem with our harness.</p><pre><code class=\"language-session\">You: explore this project and tell me what is about\nAssistant: I need to inspect the project files first. Please provide the tool results for the directory listing so I can explore it and summarize what the project is about.\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.\\/&quot;}}\ntool-success: list_files -&gt; 5 lines\nAssistant: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\nYou:\n</code></pre><p>Notice how we asked a more open ended question to the agent, and it correctly recognized that to answer needed to explore the project, which it did by using the <code>list_files</code> tool correctly. Then the harness gave the result of that tool call just like we’ve done before, but hold on. The agent didn’t reply with text, it replied with more tool calls!</p><p>This brings us to the next piece of our learning: <strong>turns</strong> and steps.</p><p>In this context a turn is what includes one user request and the agent response to that request. A step is every individual interaction the agent needs to answer the user’s request.</p><p>When we started with the simple chat model, we had 1 step per 1 turn. User sends a message, the agent replies. When we added tool calling, we changed that a bit. User sends a message, the agent replies with a tool call, harness sends the result and agent replies with the final response. So there was an intermediate step in there, but that was it. And that’s not enough because agents often need multiple tool calls, multiple roundtrips to accomplish the goal the user asked for.</p><p>In this case it knew that it had to explore the project, but once it knew about the list of files, it then wanted to know about a specific file, but our harness is not yet ready for that, so it just stopped and waited for user input.</p><p>The problem is that right now our harness is just following a simple loop, we need to put the tool calling in an inner loop so the agent can have multiple steps in a single turn. We want the agent to be able to keep requesting tool calls until it is satisfied and gives as final answer.</p><p>In other words, a single turn can include multiple steps before the agent can give the final answer to the user.</p><svg viewBox=\"0 0 680 265\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;display:block;margin:2rem auto\">\n  <defs>\n    <marker id=\"harness-t\" markerWidth=\"8\" markerHeight=\"6\" refX=\"7\" refY=\"3\" orient=\"auto\">\n      <polygon points=\"0 0, 8 3, 0 6\" fill=\"#f97eb3\"/>\n    </marker>\n    <marker id=\"harness-s\" markerWidth=\"8\" markerHeight=\"6\" refX=\"7\" refY=\"3\" orient=\"auto\">\n      <polygon points=\"0 0, 8 3, 0 6\" fill=\"#888\"/>\n    </marker>\n    <marker id=\"harness-w\" markerWidth=\"7\" markerHeight=\"5\" refX=\"6\" refY=\"2.5\" orient=\"auto\">\n      <polygon points=\"0 0, 7 2.5, 0 5\" fill=\"#ccc\"/>\n    </marker>\n  </defs>\n  <!-- Turn loop (outer, pink) -->\n  <path d=\"M 155 120 C 280 15, 440 15, 521 120\" fill=\"none\" stroke=\"#f97eb3\" stroke-width=\"2.5\" marker-end=\"url(#harness-t)\"/>\n  <path d=\"M 521 148 C 440 253, 280 253, 155 148\" fill=\"none\" stroke=\"#f97eb3\" stroke-width=\"2.5\" marker-end=\"url(#harness-t)\"/>\n  <text x=\"338\" y=\"10\" text-anchor=\"middle\" fill=\"#f97eb3\" font-size=\"13\" font-family=\"system-ui,sans-serif\" font-weight=\"600\">Turn</text>\n  <!-- Step loop (inner, grey) -->\n  <path d=\"M 399 118 C 432 68, 490 68, 521 118\" fill=\"none\" stroke=\"#888\" stroke-width=\"2\" marker-end=\"url(#harness-s)\"/>\n  <path d=\"M 521 150 C 490 200, 432 200, 399 150\" fill=\"none\" stroke=\"#888\" stroke-width=\"2\" marker-end=\"url(#harness-s)\"/>\n  <text x=\"462\" y=\"62\" text-anchor=\"middle\" fill=\"#888\" font-size=\"12\" font-family=\"system-ui,sans-serif\">Step</text>\n  <!-- User → Harness -->\n  <line x1=\"160\" y1=\"127\" x2=\"268\" y2=\"127\" stroke=\"#ccc\" stroke-width=\"1.5\" marker-end=\"url(#harness-w)\"/>\n  <text x=\"216\" y=\"120\" text-anchor=\"middle\" fill=\"#ccc\" font-size=\"10\" font-family=\"system-ui,sans-serif\">message</text>\n  <!-- Harness → User -->\n  <line x1=\"272\" y1=\"143\" x2=\"164\" y2=\"143\" stroke=\"#ccc\" stroke-width=\"1.5\" marker-end=\"url(#harness-w)\"/>\n  <text x=\"216\" y=\"157\" text-anchor=\"middle\" fill=\"#ccc\" font-size=\"10\" font-family=\"system-ui,sans-serif\">answer</text>\n  <!-- Harness → LLM -->\n  <line x1=\"404\" y1=\"127\" x2=\"512\" y2=\"127\" stroke=\"#ccc\" stroke-width=\"1.5\" marker-end=\"url(#harness-w)\"/>\n  <text x=\"460\" y=\"120\" text-anchor=\"middle\" fill=\"#ccc\" font-size=\"10\" font-family=\"system-ui,sans-serif\">API call</text>\n  <!-- LLM → Harness -->\n  <line x1=\"516\" y1=\"143\" x2=\"408\" y2=\"143\" stroke=\"#ccc\" stroke-width=\"1.5\" marker-end=\"url(#harness-w)\"/>\n  <text x=\"460\" y=\"157\" text-anchor=\"middle\" fill=\"#ccc\" font-size=\"10\" font-family=\"system-ui,sans-serif\">text</text>\n  <!-- Boxes (drawn last so arrows tuck behind edges) -->\n  <rect x=\"40\" y=\"108\" width=\"115\" height=\"54\" rx=\"8\" fill=\"#1f1f1f\" stroke=\"#4a4a4a\" stroke-width=\"1.5\"/>\n  <text x=\"97\" y=\"135\" text-anchor=\"middle\" dominant-baseline=\"middle\" fill=\"white\" font-size=\"15\" font-family=\"system-ui,sans-serif\">User</text>\n  <rect x=\"277\" y=\"108\" width=\"122\" height=\"54\" rx=\"8\" fill=\"#1f1f1f\" stroke=\"#4a4a4a\" stroke-width=\"1.5\"/>\n  <text x=\"338\" y=\"135\" text-anchor=\"middle\" dominant-baseline=\"middle\" fill=\"white\" font-size=\"15\" font-family=\"system-ui,sans-serif\">Harness</text>\n  <rect x=\"521\" y=\"108\" width=\"118\" height=\"54\" rx=\"8\" fill=\"#1f1f1f\" stroke=\"#4a4a4a\" stroke-width=\"1.5\"/>\n  <text x=\"580\" y=\"135\" text-anchor=\"middle\" dominant-baseline=\"middle\" fill=\"white\" font-size=\"15\" font-family=\"system-ui,sans-serif\">LLM</text>\n</svg>\n<p>So the code grows one inner loop.</p><pre><code class=\"language-swift\">while true {\n    turn += 1\n    print(&quot;&lt;Turn \\(turn)&gt;&quot;)\n    ...\n    var step = 0\n\n    while true { // &lt;= new inner loop\n        step += 1\n        print(&quot;&lt;Step \\(step)&gt;&quot;)\n        let response = try await client.send(messages: conversation)\n        let parsedResponse = try parseAssistantResponse(from: response)\n\n        if parsedResponse.userFacingText.isEmpty == false {\n            ...\n        }\n\n        if let invocation = parsedResponse.invocation {\n            ...\n            conversation.append(Message(role: &quot;assistant&quot;, content: response))\n            conversation.append(Message(role: &quot;user&quot;, content: formatToolResult(outcome)))\n            continue\n        }\n\n        conversation.append(Message(role: &quot;assistant&quot;, content: response))\n        break\n    }\n}\n</code></pre><p>That is a really important moment in understanding how agents work.</p><p>Let’s see how this change impacts our previous example.</p><blockquote><p>Note that I’ve added turn and step logs so it’s clearer when there are roundtrips.</p></blockquote><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: explore this project and tell me what is about\n&lt;Step 1&gt;\nAssistant: I need the project files to inspect it, but I don’t have the directory listing yet. Please provide the repository contents or let me know the project path, and I’ll explore it and summarize what it’s about.\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.\\/&quot;}}\ntool-success: list_files -&gt; 5 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\ntool-success: read_file -&gt; 17 lines\n&lt;Step 3&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Package.swift&quot;}}\ntool-success: read_file -&gt; 22 lines\n&lt;Step 4&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Sources&quot;}}\ntool-success: list_files -&gt; swiftagentharness/\n&lt;Step 5&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Sources\\/swiftagentharness&quot;}}\ntool-success: list_files -&gt; 3 lines\n&lt;Step 6&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Sources\\/swiftagentharness\\/App.swift&quot;}}\ntool-success: read_file -&gt; 34 lines\n&lt;Step 7&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Sources\\/swiftagentharness\\/Harness.swift&quot;}}\ntool-success: read_file -&gt; 223 lines\n&lt;Step 8&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Sources\\/swiftagentharness\\/OpenAICompatibleClient.swift&quot;}}\ntool-success: read_file -&gt; 39 lines\n&lt;Step 9&gt;\nAssistant: This project is a small Swift command-line “agent harness” for talking to an OpenAI-compatible chat completions API and letting the model use a couple of local filesystem tools.\nWhat it does\n...\n&lt;Turn 2&gt;\nYou: Thanks!\n&lt;Step 1&gt;\nAssistant: You’re welcome!\n&lt;Turn 3&gt;\nYou:\n</code></pre><p>The model is not magically traversing your project in one giant sweep. It is taking little steps. List files. See the result. Decide what file matters. Read that file. See the result. Then decide if it can answer.</p><p>This is why I wanted to show the failing version first. Once you see the harness stop too early, the need for the inner loop becomes obvious.</p><h2 id=\"giving-it-boundaries\">Giving It Boundaries</h2><p>Now that the agent can list and read files on its own, we need to showcase the dangers of these tools.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Can you explore this project, and then find similar projects in this computer.\n&lt;Step 1&gt;\nAssistant: I can do that, but I need access to the project files first.\nPlease provide the project path, for example:\n- `.`\n- `./my-project`\n- `/home/user/project`\nOnce I have the path, I’ll:\n1. explore the project structure,\n2. identify what kind of project it is,\n3. search nearby/on this computer for similar projects.\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.\\/&quot;}}\ntool-success: list_files -&gt; 5 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Package.swift&quot;}}\ntool-success: read_file -&gt; 22 lines\n\n...\n\n&lt;Step 9&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;..&quot;}}\ntool-success: list_files -&gt; 28 lines\n&lt;Step 10&gt;\n\n...\n\n&lt;Step 23&gt;\nAssistant: I explored the current project and found a few similar ones nearby.\nCurrent project: `swiftagentharness`\n- Type: Swift Package Manager executable\n...\nSimilar projects on this computer\n...\n&lt;Turn 2&gt;\nYou:\n</code></pre><p>wow. I had to redact some things but you get the gist. The agent started listing files outside the directory we’re running it in, the magical <code>..</code> path. Sure, of course I asked for it, but you could imagine the LLM doing it on its own volition.</p><p>Now this is a whole rabbit hole about security that is still an open question, only really solved with full isolation, aka running the agent in its own computer, a sandbox. I’m not gonna pretend to solve this in this exploratory blog post, but we can use this again as an excuse to learn more about the lack of magic.</p><p>The first thing we need is to introduce the concept of a workspace root into the CLI itself.</p><pre><code class=\"language-swift\">@Option(help: &quot;Workspace root for the agent.&quot;)\nvar workspace = FileManager.default.currentDirectoryPath\n\nlet workspaceRoot = try resolveWorkspaceRoot(from: workspace)\nlet agent = Agent(client: client, tools: tools, workspaceRoot: workspaceRoot)\n</code></pre><p>That gives the harness an explicit root directory to work from. <code>Agent.init</code> gains a <code>workspaceRoot: URL</code> parameter, and <code>ToolDefinition.run</code> changes signature to receive it as well:</p><pre><code class=\"language-swift\">struct ToolDefinition {\n    ...\n    let run: ([String: String], URL) throws -&gt; String\n}\n</code></pre><p>That means every tool can call <code>resolvePath</code> to validate paths before touching the filesystem. And now that the root is real in code, we can also tell the model about it in the prompt.</p><pre><code class=\"language-swift\">self.systemPrompt = &quot;&quot;&quot;\n    You are a helpful assistant with access to tools.\n\n    The workspace root is:\n    \\(workspaceRoot.path)\n\n    Tools:\n    \\(toolsPrompt)\n    ...\n    &quot;&quot;&quot;\n</code></pre><p>This is useful. It gives the model more context and helps it avoid asking for nonsense paths. But this is just guidance, it doesn’t do anything to even try to protect you.</p><p>The real protection, albeit simplistic, is in the tool execution layer.</p><pre><code class=\"language-swift\">private static func resolvePath(_ path: String, workspaceRoot: URL) throws -&gt; URL {\n    let candidate = path.hasPrefix(&quot;/&quot;)\n        ? URL(fileURLWithPath: path)\n        : workspaceRoot.appendingPathComponent(path)\n    let resolved = candidate.standardizedFileURL\n    let rootPath = workspaceRoot.standardizedFileURL.path\n    let resolvedPath = resolved.path\n\n    guard resolvedPath == rootPath || resolvedPath.hasPrefix(rootPath + &quot;/&quot;) else {\n        throw HarnessError.workspaceViolation(path)\n    }\n\n    return resolved\n}\n</code></pre><p>That tiny function is resolving the path the agent is asking for and checking if it escapes the workspace.</p><blockquote><p>Not security advice. I’m sure there are a thousand ways of breaking this. It is just an example for this blog post.</p></blockquote><p>And once that is in place, the same kind of interaction looks very different:</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Can you check what I have in my downloads folder\n&lt;Step 1&gt;\nAssistant: I can help, but I only have access to the workspace at:\n`/Users/me/code/swiftagentharness`\n\n&lt;Turn 2&gt;\nYou: Please try, is very important\n&lt;Step 1&gt;\nAssistant: I can try, but with the tools I have I’m only allowed to access paths through the provided file-listing tool.\n\n&lt;Turn 3&gt;\nYou: Just use the tools we have with that path\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;/Users/me/Downloads&quot;}}\ntool-error: Tool invocation failed: Path escapes the workspace root: /Users/me/Downloads. Reply again, either with a valid TOOL_CALL line or a normal answer.\n&lt;Step 2&gt;\nAssistant: I tried, but access was blocked because `/Users/me/Downloads` is outside the allowed workspace.\n</code></pre><p>That is the real lesson.</p><p>The prompt helps. The model can understand the boundary. The model can even explain the boundary back to you. It even respected it at first, but once pushed, it broke away. The prompt is just guidance, it doesn’t offer real protection. Only real classical code can offer real protection, as you can see with the <code>tool-error</code> thrown by the Swift code when the agent tried to escape.</p><p>And yes, even this is still only a lightweight boundary. A real production-grade security story goes much further than path checks. But for understanding how these systems work, this is a perfect example. The magic disappears and what remains is just normal software engineering.</p><h2 id=\"time-to-change-the-world\">Time to change the world</h2><p>Now that there is a bit of safety, it’s time to let our agent change the world. It can read and explore, but we need to let it edit files for it to be useful.</p><p>As we keep doing in this journey, we need to keep the tool simple. We just need the agent to give us the path, and the text that it wants to replace with the updated version. If the old text is empty, we just create the file for it.</p><p>Here is the tool definition:</p><pre><code class=\"language-swift\">static func editFile() -&gt; Self {\n    let fileManager = FileManager.default\n\n    return ToolDefinition(\n        name: &quot;edit_file&quot;,\n        description: &quot;Create a file when old_text is empty, or replace exactly one matching old_text with new_text.&quot;,\n        arguments: [&quot;path&quot;, &quot;old_text&quot;, &quot;new_text&quot;],\n        run: { arguments, workspaceRoot in\n            let path = arguments[&quot;path&quot;] ?? &quot;&quot;\n            let oldText = arguments[&quot;old_text&quot;] ?? &quot;&quot;\n            let newText = arguments[&quot;new_text&quot;] ?? &quot;&quot;\n            let url = try resolvePath(path, workspaceRoot: workspaceRoot)\n\n            if oldText.isEmpty {\n                let directory = url.deletingLastPathComponent()\n                try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)\n                try newText.write(to: url, atomically: true, encoding: .utf8)\n                return &quot;Created \\(path)&quot;\n            }\n\n            let content = try String(contentsOf: url, encoding: .utf8)\n            let matches = content.components(separatedBy: oldText).count - 1\n            guard matches == 1 else {\n                return &quot;Edit failed. Expected exactly one match for old_text, found \\(matches).&quot;\n            }\n\n            let updated = content.replacingOccurrences(of: oldText, with: newText)\n            try updated.write(to: url, atomically: true, encoding: .utf8)\n            return &quot;Edited \\(path)&quot;\n        }\n    )\n}\n</code></pre><p>Nothing fancy.</p><p>And let’s reiterate again the important part. The model is still not editing files directly. It is not dragging a cursor around. The LLM is not an editor. It is not doing anything mystical. It is just proposing a very boring text transformation contract and the harness applies it.</p><p>And now, finally, the harness can do the thing that makes coding agents feel like coding agents. Not just read the world. Change it.</p><pre><code class=\"language-session\">&lt;Turn 1&gt;\nYou: Can you create a new md file to summarize what this project is about.\n&lt;Step 1&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;README.md&quot;}}\ntool-success: read_file -&gt; 17 lines\n&lt;Step 2&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;list_files&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;.&quot;}}\ntool-success: list_files -&gt; 5 lines\n&lt;Step 3&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;read_file&quot;,&quot;arguments&quot;:{&quot;path&quot;:&quot;Package.swift&quot;}}\ntool-success: read_file -&gt; 22 lines\n&lt;Step 4&gt;\n...\n&lt;Step 9&gt;\ntool-call: TOOL_CALL {&quot;name&quot;:&quot;edit_file&quot;,&quot;arguments&quot;:{&quot;new_text&quot;:&quot;# Project Summary\\n\\n...&quot;,&quot;old_text&quot;:&quot;&quot;,&quot;path&quot;:&quot;PROJECT_SUMMARY.md&quot;}}\ntool-success: edit_file -&gt; Created PROJECT_SUMMARY.md\n&lt;Step 10&gt;\nAssistant: Created `PROJECT_SUMMARY.md` in the project root with a summary of the project, its purpose, architecture, tools, and usage.\n&lt;Turn 2&gt;\n</code></pre><p>This is the first moment where the harness stops being just an observer and starts becoming a real tool.</p><div class=\"callout-promo\"><p class=\"callout-promo-label\">Want the finished project?</p><div class=\"callout-promo-body\"><p>The whole point of this post is that you build it yourself, and all the important ideas are already here. But if you want to support my writing, or you just want to save time, I packaged the project for you to download.</p><p><a href=\"https://buymeacoffee.com/alejandromp/e/532022\">Get the harness by supporting me on Buy me a coffee</a></p></div></div><h2 id=\"the-world-is-whatever-the-harness-says-it-is\">The world is whatever the harness says it is</h2><p>There is one last exercise I really recommend if you build one of these.</p><p>Lie.</p><p>I mean it. Go to one of your tools and make it return something false on purpose.</p><p>Make <code>read_file</code> always return a joke. Make <code>list_files</code> invent files that do not exist. A fun one is to rewrite extensions and pretend every Swift file is actually a JavaScript file.</p><p>The model will believe you.</p><p>And yes, of course it will. That is the whole point.</p><p>By now it should be obvious, but I think doing this with your own hands makes it click in a completely different way. The model does not have direct access to your machine. It does not have a secret backdoor into reality. It only has the world your harness describes to it through tool results.</p><p>That means the world, for the model, is whatever the harness says it is.</p><p>Not because the model is stupid. Not because the model is broken. But because that is literally the way this <a href=\"https://alejandromp.com/personal/blog/accepting-pandora-s-box/\">autocomplete machine</a> works.</p><h2 id=\"and-the-illusion-is-broken\">And the Illusion is Broken</h2><p>If you reached this point I hope you now see past the illusion. Now you know how agentic tools and AI harnesses work, you built one yourself!</p><p>No matter what new features you see from marketing, everything boils down to the simple concepts that you learned. The model emits text. The harness interprets that text as a tool call. Your code executes something locally, or pretends to, and then sends more text back.</p><p>No mystical illusion. Just tools and code.</p><p>Models. Loops. Context. Tools. Turns. Engineering.</p><p>Nothing more.</p><div class=\"callout-tip\"><p>In the next post in the series we will learn how the harness helps the AI pretend. Because <a href=\"https://alejandromp.com/development/blog/ai-doesn-t-remember-your-project-markdown-does/\">AI doesn’t remember your project, Markdown does</a>.</p></div>",
      "date_published" : "2026-04-20T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/have-you-built-an-agent-harness-already",
      "summary" : "For years I have repeated a thing that I still believe. Every programmer should write a promise library once. I think agent harnesses are the 2026 version of that exercise.",
      "tags" : [
        "AI Development",
        "Swift",
        "Project"
      ],
      "title" : "Have You Built an Agent Harness Yet?",
      "url" : "https://alejandromp.com/development/blog/have-you-built-an-agent-harness-already"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>I was migrating to a newer version of The Composable Architecture, which meant there was a list of deprecations to clean up. One of the things on that list was adding <code>InferSendableFromCaptures</code> as an upcoming Swift feature flag across all our package targets.</p><p><a href=\"https://github.com/swiftlang/swift-evolution/blob/main/proposals/0418-inferring-sendable-for-methods.md\">SE-0418</a> is genuinely interesting. It makes method references participate more accurately in sendability checking when used as values. One of those proposals that feels obviously correct once you read it.</p><p>I added it while preparing for Swift 6, hoping to catch issues before the TCA 2.0 work. I was ready to fix code. I was not ready for another journey of concurrency discovery. The kind that challenges things I already knew, just to make sure I really knew them.</p><blockquote><p>This is one of those posts I write because <strong><a href=\"https://www.youtube.com/watch?v=XtwsJtdH_q8\">writing is thinking</a></strong>. Even at the risk of being wrong. That is actually better, because I will learn more. So don’t take this as a tutorial or the truth. Use critical thinking as you follow along.</p></blockquote><p>A bunch of warnings appeared, all of the same shape, all of them in places where I was passing a SwiftUI view’s initialiser as a function reference to a navigation modifier.</p><pre><code class=\"language-swift\">.navigationLinkDestination(\n    item: $store.scope(...),\n    destination: ChildView.init(store:)  // ⚠️\n)\n</code></pre><p>The warning said something like “call to main actor-isolated initializer <code>init(store:)</code> in a synchronous nonisolated context”.</p><p>To understand what was happening I had to properly internalise what SE-0418 changes about function references. The warning appears because <code>ChildView.init(store:)</code> is not just “a function that makes a view”. <code>ChildView</code> conforms to <code>View</code>, and that makes the initialiser main actor-isolated. But the <code>destination:</code> parameter expects <code>@escaping (StoreOf<X>) -> some View</code>, with no <code>@MainActor</code>. When I pass the initialiser directly, I am asking the compiler to use a main actor-isolated initialiser as a plain synchronous callback. In Swift 5 mode it lets me do that, but warns me that the isolation does not fit the context I am putting it in.</p><p>That was the first model in my head, at least. It sounded convincing enough…</p><p>Once I saw it that way, the fix felt obvious: switch to a closure.</p><pre><code class=\"language-swift\">.navigationLinkDestination(\n    item: $store.scope(...),\n    destination: { ChildView(store: $0) }  // ✅\n)\n</code></pre><p>No warning.</p><p>We’re done, right? I applied it everywhere, but as I was doing it my inquisitive mind kept asking. But why?</p><p>Why did the closure work? They both call the same initialiser. They both end up constructing the same <code>@MainActor</code> view. Why is one fine and the other not?</p><h2 id=\"what-infersendablefromcaptures-actually-does\">What InferSendableFromCaptures Actually Does</h2><p>SE-0418, the proposal behind <code>InferSendableFromCaptures</code>, adds type inference specifically to function and method references when used as values.</p><p>Before enabling the feature flag, <code>ChildView.init(store:)</code> used as a value slipped through as a non-sendable function value. After enabling it, the reference carries more of what the declaration means. The initialiser is main actor-isolated, so the function reference is treated as main actor-isolated too.</p><p>That is the right thing to do. The initialiser is <code>@MainActor</code>. The reference should say so. But the parameter I am passing it to is typed as <code>@escaping (StoreOf<X>) -> some View</code>, with no <code>@MainActor</code> annotation. So, in my first reading, the compiler was complaining because the function reference now brought main actor isolation with it, and the parameter did not ask for that.</p><p>Closure literals do not work that way. A closure literal has no pre-existing function reference that has to be squeezed into the parameter type. The compiler creates it fresh to satisfy the parameter type, <code>@escaping (StoreOf<X>) -> some View</code>, and then type-checks the body of the closure where it is written. The warning never fires because the compiler is no longer trying to use the isolated initialiser itself as the callback value.</p><p>That seemed to explain the difference between the two syntaxes. But it left me with a deeper question.</p><h2 id=\"the-question-that-bothered-me\">The Question That Bothered Me</h2><p>If the closure literal is created to match type <code>@escaping (StoreOf<X>) -> some View</code>, with no <code>@MainActor</code> annotation, then how can it safely call <code>ChildView(store:)</code> inside? That initialiser is <code>@MainActor</code>. I am inside a closure that is not annotated <code>@MainActor</code>. How is that not a problem?</p><p>In Swift 6 strict mode, calling a <code>@MainActor</code> function from a nonisolated context is an error. Not a warning. An error.</p><p>So I switched to <code>.swiftLanguageMode(.v6)</code>, expecting to see it break.</p><p>It did not.</p><p>The closure compiled cleanly. No warning, no error. And that is when I realised I had been thinking about the wrong thing.</p><h2 id=\"the-closure-is-not-nonisolated\">The Closure Is Not Nonisolated</h2><p>The closure does not have <code>@MainActor</code> in its type, but that does not mean it runs in a nonisolated context. Of course it doesn’t, and I knew that. But I let the compiler warning pull my attention to the wrong place. It is incredible how quickly a weak mental model can crumble when you trust the first explanation that seems to fit.</p><p>I defined it inside <code>body</code>. And <code>body</code> is <code>@MainActor</code>. A closure literal inherits the actor context of the scope it is written in, not just the type annotation of the parameter it satisfies. So the closure is effectively <code>@MainActor</code> at the point where it runs, even if that annotation is not visible in the parameter type.</p><p>Calling <code>ChildView(store:)</code> inside that closure is calling a <code>@MainActor</code> function from a <code>@MainActor</code> context. That is always safe. The compiler knows this. So there is no error in Swift 6. And there was no warning before. Everything was consistent enough to fool me.</p><p>At that point I thought I had it. The closure works because it inherits the main actor context. The function reference warns because it does not get to use that same context in the same way.</p><p>Nice little explanation.</p><p>Or is it?</p><h2 id=\"the-explanation-cracks\">The Explanation Cracks</h2><p>Later I explained the whole thing to a coworker. And while explaining it, I felt that familiar discomfort. The words were coming out, but something felt off.</p><p>Because if the closure works because it inherits the main actor context from the view, then why would that not also apply to the method reference?</p><p>So I made the example smaller and more annoying.</p><pre><code class=\"language-swift\">struct ParentView: View {\n    var body: some View {\n        Text(&quot;parent&quot;)\n    }\n\n    func makeView() -&gt; some View {\n        doSomethingOnTheMainActor()\n\n        let inferredReference = ChildView.init(store:)\n        let typedReference: (StoreOf&lt;Int&gt;) -&gt; ChildView = ChildView.init(store:)\n        let mainActorReference: @MainActor (StoreOf&lt;Int&gt;) -&gt; ChildView = ChildView.init(store:)\n        let mainActorClosure: @MainActor (StoreOf&lt;Int&gt;) -&gt; ChildView = { @MainActor in\n            ChildView(store: $0)\n        }\n\n        _ = (inferredReference, typedReference, mainActorReference, mainActorClosure)\n\n        return ChildView(store: StoreOf())\n    }\n}\n\n@MainActor\nfunc doSomethingOnTheMainActor() {}\n</code></pre><p>The important line is not even the function reference. It is this one:</p><pre><code class=\"language-swift\">doSomethingOnTheMainActor()\n</code></pre><p><code>doSomethingOnTheMainActor()</code> is <code>@MainActor</code>. I am calling it without <code>await</code>. And it compiles.</p><p>That means <code>makeView()</code> is already being treated as main actor-isolated, even though I did not write <code>@MainActor</code> on it. So my previous explanation was incomplete. The surrounding method is not some random nonisolated context. The compiler is already treating it as main actor-isolated.</p><p>Then the results got weirder.</p><p>In Swift 5 mode with <code>InferSendableFromCaptures</code> enabled, the inferred function reference warned. The explicitly typed plain function reference warned. Even the explicitly typed <code>@MainActor</code> function reference warned.</p><p>But the <code>@MainActor</code> closure did not.</p><p>And that is where it started to smell. <code>mainActorReference</code> and <code>mainActorClosure</code> have the exact same type:</p><pre><code class=\"language-swift\">@MainActor (StoreOf&lt;Int&gt;) -&gt; ChildView\n</code></pre><p>But one warned and the other did not.</p><p>That is the part that broke the simple explanation. If the problem were simply “you are in a nonisolated context”, then <code>doSomethingOnTheMainActor()</code> should have required <code>await</code>. It did not. And if the problem were simply “this is main actor-isolated but the parameter does not accept that”, then the closure should have failed too once I realised it was main actor-isolated. In my mind, if one failed, the other should fail. If one worked, the other should work.</p><p>Then I added <code>@MainActor</code> explicitly to <code>makeView()</code>.</p><p>All warnings disappeared.</p><p>Then I removed that explicit annotation again, but switched the package to Swift 6 language mode.</p><p>All warnings disappeared too.</p><p>That was the real clue.</p><h2 id=\"the-transitional-bit\">The Transitional Bit</h2><p>So I no longer think the interesting lesson is “closures inherit actor context and method references do not”. That is too clean. It is also not quite true.</p><p>What I think is happening is that this warning is a Swift 5 migration-mode artifact. Enabling <code>InferSendableFromCaptures</code> in Swift 5 mode makes method references participate in stricter concurrency checking, but that checking does not seem to line up perfectly with the actor isolation inference that Swift 6 applies. The surrounding SwiftUI method is already main actor-isolated. Swift 6 understands the whole thing and accepts it. Swift 5 with the upcoming feature flag sees enough to warn, but not enough to recognise what Swift 6 recognises.</p><p>The closure workaround still makes sense. It avoids the warning and expresses the code in a way the Swift 5 checker is happy with. But it is not proof that the original function reference was unsafe. It is more like a shape of code that avoids a transitional diagnostic.</p><p>And this is the subtle part that I think is easy to miss.</p><p>Upcoming feature flags, at least the ones related to the complex concurrency story, are not always the same experience as fully moving to the language mode where that feature belongs. They are there to surface future issues early, but they can expose interactions with the old mode that do not quite represent the final model.</p><h2 id=\"what-i-took-away\">What I Took Away</h2><p>The fix itself was small. One line changed per call site.</p><p>But understanding why it worked took a real detour through several parts of the concurrency model: how SE-0418 adds inference to function references, how closure literals behave differently, how actor context is inherited rather than just declared, and how Swift 5 migration checking can differ from Swift 6 even when an upcoming feature flag is involved.</p><p>The funny part is that I already knew most of those pieces. I knew closures inherit actor context. I knew SwiftUI views are main actor-isolated. I knew upcoming feature flags are part of a migration story. And yet I still managed to convince myself of an explanation that fit what I was seeing.</p><p>That is the dangerous part. When the first explanation is plausible, it is very easy to stop there. I could have changed all the call sites, written down “closures inherit actor context”, and walked away with a mental model that was close enough to feel right, but wrong enough to mislead me the next time.</p><p>Instead, the discomfort kept bothering me. The result did not feel fully justified. So I kept pushing. I made the example smaller, compared the function reference and closure with the exact same type, tried explicit <code>@MainActor</code>, tried Swift 6 mode, and only then did the model start to hold together.</p><p>The warning was not telling me my code was broken. At first I thought it was telling me I had a straightforward isolation mismatch. That was close. But not quite. The deeper lesson was that the compiler mode matters. In Swift 5 mode with this upcoming feature enabled, I was looking at a transitional warning. In Swift 6 mode, the same code made sense to the compiler. Adding <code>@MainActor</code> explicitly also made it make sense.</p><p>That is the kind of thing you understand properly only when you stop accepting the fix and start asking why it works. Not just until you find an explanation, but until you find one that survives the next question.</p>",
      "date_published" : "2026-04-14T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/a-small-swiftui-warning-and-a-long-journey-to-understand-it",
      "summary" : "I was migrating to a newer version of The Composable Architecture, which meant there was a list of deprecations to clean up. One of the things on that list was adding <code>InferSendableFromCaptures</code> as an upcoming Swift feature flag across all our package targets.",
      "tags" : [
        "Swift",
        "Concurrency",
        "SwiftUI"
      ],
      "title" : "A Small SwiftUI Warning and a Long Journey to Understand It",
      "url" : "https://alejandromp.com/development/blog/a-small-swiftui-warning-and-a-long-journey-to-understand-it"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>There was a period during the Swift Evolution Concurrency discussions that I remember very fondly.</p><p>And I was on the losing side. Or did I?</p><p>I came into the conversation questioning one of the big changes that would later become part of the “approachable concurrency” vision: the move away from the old behavior where non-isolated async functions would naturally hop off the current actor. At the time, I genuinely thought that model was one of the most brilliant parts of Swift Concurrency.</p><p>And I still think there was something beautiful in it.</p><p>Back then, one of my favorite things about Swift Concurrency was that it let developers stop worrying about threads. You didn’t have to constantly ask yourself if something needed to go “to the background”. You just wrote async code and, most of the time, things were fine. Especially in app development, where so many people come from years of GCD, Combine, Rx, and all sorts of “don’t block the main thread” patterns, that felt like a breath of fresh air.</p><p>That old promise, for some reason, mattered to me a lot.</p><p>You can even see that framing in some of my older writing. In <a href=\"https://alejandromp.com/development/blog/limit-swift-concurrency-cooperative-pool/\">Limit Swift Concurrency’s cooperative pool</a> I wrote that “you should not worry about threads at all”. I still love that sentence. It captures something important about why Swift Concurrency felt refreshing in the first place.</p><h2 id=\"the-kind-of-discussions-i-miss\">The Kind of Discussions I Miss</h2><p>The conversation around <a href=\"https://forums.swift.org/t/pitch-inherit-isolation-by-default-for-async-functions/74862\">the original pitch</a> and later <a href=\"https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md\">SE-0461</a> is one I remember with a lot of affection. It was one of those moments that remind you the Swift Forums can be a very special place on the internet.</p><p>You could come in with a strong opinion, push back on a proposal, and instead of getting tribal nonsense, you got thoughtful replies from people who had clearly spent a lot of time thinking about the problem from multiple angles. People like Holly Borla, John McCall, Joe Groff, Mattie, and many others were not just defending a position. They were helping build a mental model.</p><p>That is a very different thing. And not something that happens often in the social interwebs these days.</p><p>Even though I was not sure about the change, it was genuinely enriching to see what the experts were seeing that I was not. That is one of the best things that can happen in a technical discussion. Not “winning”. Not getting your preferred syntax. Just having your picture of the problem expanded.</p><p>And I think that is worth saying explicitly, even now, years later, because we don’t always celebrate that enough. We talk a lot about the final proposal, the final syntax, the final accepted design. But the process of getting there matters too. Sometimes a language community proves its worth not when everybody agrees, but when disagreement is handled with care and clarity.</p><h2 id=\"why-i-pushed-back\">Why I Pushed Back</h2><p>My discomfort was not really about syntax. It was about the original emotional promise of Swift Concurrency.</p><p>I liked that the model encouraged people to stop obsessing over threads. I liked being able to tell developers, especially app developers, to stop trying to manually dispatch everything around. I liked the fact that async code did not automatically mean “you might now be blocking the UI unless you remember to escape somewhere else first”.</p><p>That felt like progress.</p><p>At the time, I said this in the pitch thread:</p><blockquote><p>“The idea of not blocking the main thread is pervasive in app developers.”</p></blockquote><p>And also this:</p><blockquote><p>“the current behaviour unloads a lot of burden once you embrace swift concurrency.”</p></blockquote><p>And to be honest, I still feel a bit of grief around losing some of that simplicity. I am still annoyed that “be careful what you run on the main actor” is now a thing we have to teach more deliberately. I still think there was something elegant in a model where the default behavior helped developers avoid that whole category of thought.</p><p>That part still bothers me a little.</p><h2 id=\"what-changed-my-mind\">What Changed My Mind</h2><p>What convinced me was not that my concern was silly. It was that the other side of the tradeoff was bigger than I had fully appreciated.</p><p>The key argument, as I came to understand it, was that the old behavior made one particular thing feel easy while hiding a much more serious cost, it pushed ordinary async code into situations where data-race safety became harder to reason about, especially once Swift 6’s stricter checking started showing what was actually going on.</p><p>Holly Borla made the core of that case very clearly in the thread. The line that stayed with me was:</p><blockquote><p>“the current default imposes data-race safety errors”</p></blockquote><p>In other words, the old world was nice partly because it let many of us avoid seeing the edges.</p><p>And once those edges started becoming visible, the old default no longer looked like a clever simplification. It looked more like a convenience that was too often in tension with the safety model Swift was trying to build.</p><p>That did not make the transition emotionally pleasant for me. But it did make it intellectually convincing.</p><h2 id=\"and-then…-life-moved-on\">And Then… Life Moved On</h2><p>What is funny is that now the transition has happened and we are just used to the new world.</p><p>And it is fine.</p><p>This is maybe the most humbling part of language evolution. Sometimes a change feels huge when it is being discussed, deeply consequential while it is being pitched, mildly uncomfortable while it is rolling out, and then a year later it is just… the air you breathe.</p><p>That does not mean my concerns were invalid. It just means people adapt. Documentation improves, mental models settle, teams teach the new thing. The ecosystem metabolizes the change.</p><p>The world did not end and the language did not lose its soul. Swift Concurrency is still good, and in many ways it is better.</p><p>And of course, it even made me update <a href=\"https://mastodon.social/@alexito4/112688581210152832\">my lovely context inheritance table</a>, because if Swift Concurrency changes, apparently my diagrams must suffer too.</p><h2 id=\"a-transition-done-right\">A Transition Done Right</h2><p>What I appreciate the most in hindsight is not only the technical direction, but how it was handled.</p><p>The whole “approachable concurrency” effort is a great example of something that was harder to grasp when looked at as isolated pieces. Bit by bit, any one proposal could feel subtle, fussy, or overly specific. But once the direction started to materialize as a coherent vision, you could see the shape of the work more clearly.</p><p>And that, to me, is where Holly and the rest of the team deserve enormous credit.</p><p>Because the hardest part of changes like this is often not the compiler work. It is the human side. It is helping a community reframe its intuitions. It is sequencing changes so they are survivable. It is explaining tradeoffs without talking down to people. It is accepting that some resistance is not ignorance but the result of people caring deeply about the original design goals.</p><p>That is hard work. Maybe the hardest kind.</p><p>Technical systems are difficult, of course. But sociotechnical transitions are something else entirely. You are not just changing a rule. You are changing habits, explanations, blog posts, conference talks, intuitions, code review comments, and the tiny phrases people use to teach one another.</p><p>From that point of view, I think the approachable concurrency effort has been a real success.</p><h2 id=\"but-don’t-forget\">But Don’t Forget</h2><p>Even after having changed my mind, I still think caring about the main actor matters. Just not in the old GCD way of dispatching everything to the background.</p><p>Swift Concurrency is still better than that. It moves the responsibility from the caller to the code that is actually doing the expensive work. That is a meaningful distinction.</p><p>And maybe that is another reason why my fondness for the original design never fully went away.</p><p>The original promise that attracted me to Swift Concurrency is still a good promise, to help developers think less about low-level execution trivia and more about correctness and structure.</p><p>I still want that.</p><p>If anything, I think the lesson is that this promise was not defeated, but refined. The language could not keep that simplicity in exactly the form I first fell in love with. But the goal remains the right one.</p><p>And that is also something I touched on in another old post of mine, <a href=\"https://alejandromp.com/development/blog/why-you-should-care-about-the-future-of-memory-ownership-in-swift/\">Why you should care about the future of Memory Ownership in Swift</a>. A lot of these changes in Swift can look like they are adding complexity when viewed in isolation. But sometimes they are really the cost of making the language’s deeper guarantees more honest and more usable for normal code.</p><h2 id=\"the-real-story\">The Real Story</h2><p>So maybe the real story here is not “I was wrong” or “the experts won”.</p><p>It is that I got to witness a language community do one of the hardest things in software reasonably well: change its mind in public, with care, over time.</p><p>I still remember that period with a lot of warmth. Not because every detail was pleasant, but because it reminded me what a good technical community can look like.</p><p>You come in attached to a mental model.</p><p>You push back.</p><p>People answer seriously.</p><p>You learn what tradeoff you were underestimating.</p><p>The change lands.</p><p>Life goes on.</p><p>And if things go well, you come out of it not defeated, but enlarged.</p><p>Not so bad for the losing side.</p>",
      "date_published" : "2026-04-07T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/swift-concurrency-approachability-transition",
      "summary" : "There was a period during the Swift Evolution Concurrency discussions that I remember very fondly.",
      "tags" : [
        "Swift",
        "Concurrency"
      ],
      "title" : "The Swift Concurrency Transition I Learned to Love",
      "url" : "https://alejandromp.com/development/blog/swift-concurrency-approachability-transition"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Today Mastodon has officially released <a href=\"https://blog.joinmastodon.org/2026/03/a-new-share-button/\">their solution for a Share to Mastodon button</a>. I was already using a third party service on this site, but I decided to give it a try because using officially supported tools seems easier to maintain in the future, especially for something I don’t work on every day.</p><p>The reason something like this must exist is due to the beauty and the problem with Mastodon. It is not a single website. There are thousands of servers. A share button can’t just link to <code>mastodon.social</code> and call it a day, because the user’s account might be in another server. So a redirect tool that lets you pick your instance must exist. That used to be a third-party problem to solve. Not anymore.</p><p>Mastodon now hosts <a href=\"https://share.joinmastodon.org\">share.joinmastodon.org</a>. Open source, no tracking, runs entirely in the browser, remembers your accounts.</p><h2 id=\"the-change\">The Change</h2><p>On the code side, this was a one-liner. The old URL:</p><pre><code>https://mastodonshare.com/?text=&lt;title&gt;&amp;url=&lt;url&gt;&amp;via=alexito4\n</code></pre><p>The new one:</p><pre><code>https://share.joinmastodon.org/?text=&lt;title&gt; &lt;url&gt;\n</code></pre><p>The official service takes a single <code>text</code> parameter with the content you want to pre-fill. Title and URL combined. That’s it.</p><h2 id=\"why-it-matters\">Why It Matters</h2><p>Using a third-party share button means you’re depending on someone else’s uptime, their privacy policy, and their continued interest in maintaining the thing. The <code>via</code> parameter I was passing didn’t even make sense for Mastodon the way it does on Twitter. It was just cargo-culted from the Twitter share URL pattern. And honestly, every time I used it myself, even on Twitter, I end up removing the “via…” anyway.</p><p>The official tool is the right call. It does less, and that’s a good thing.</p><p>If you have a “Share on Mastodon” button on your site using an old third-party service, now is a good time to swap it out. And don’t hesitate to try it with this post, the button is just down below.</p>",
      "date_published" : "2026-03-02T23:00:00Z",
      "id" : "https://alejandromp.com/development/blog/using-the-official-mastodon-share-button",
      "summary" : "Today Mastodon has officially released <a href=\"https://blog.joinmastodon.org/2026/03/a-new-share-button/\">their solution for a Share to Mastodon button</a>. I was already using a third party service on this site, but I decided to give it a try because using officially supported tools seems easier to maintain in the future, especially for something I don’t work on every day.",
      "tags" : [
        "Meta",
        "Web Development",
        "Open Source"
      ],
      "title" : "Using the Official Mastodon Share Button",
      "url" : "https://alejandromp.com/development/blog/using-the-official-mastodon-share-button"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>I have this weird feeling of deja vu.</p><p><img src=\"https://alejandromp.com/development/blog/development-dogma-is-back-a-i-agents/matrix-cat.jpg\" alt=\"Matrix cat\"></p><p>If you know me, you know I think dogma is one of the biggest curses in software engineering. Dogma is what happens when a practice gets treated as universal truth before context, evidence, and tradeoffs are considered.</p><p>Dogma is what made OOP feel like the answer to everything. Dogma is what fueled the whole clean-code-industrial-complex. I lived through that as a junior developer. I was taught from university to my early jobs: follow these rules because they are “best practices.” Then you apply critical thinking and realize many of them do not hold in real projects.</p><p>Not every time. Not for everything.</p><p>Hey, let’s be clear. All these things that get popular actually have good points in them. As much as I’ve pushed back against OOP and dumb rules, some things do make sense. But developers love fashion as much as anyone waiting all year for Paris Fashion Week. It’s just that our fashion makes even less sense. Thankfully I had great mentors that fueled my critical thinking, and I followed people online who were very vocal about it. But the industry didn’t stop at those old programming concepts. Oh no, no! 🤪 They also pushed microservices on everybody until everybody realized they made no sense unless you were one of the few companies that actually needed them.</p><p>But I’m not here to rehash the past. I’m here to tell you we are doing it again.</p><p>Now we are seeing a new version in the age of AI. In an era of exploration, we already see people trying to push their half-baked dogma just to sell you their books (well, online courses nowadays).</p><p>Overcomplicated and useless agentic workflows. Made-up standards that are not proven. <a href=\"https://alejandromp.com/personal/blog/accepting-pandora-s-box/\">Just another thing that this revolution doesn’t need.</a></p><p>And again, some ideas in every wave are genuinely useful. The problem is not the tools, or the core concepts. The problem is turning preferences and unproven opinions into doctrine.</p><h2 id=\"new-tech,-old-tricks\">New Tech, Old Tricks</h2><p>I keep seeing teams copy-pasting agent workflows before proving they improve delivery, reliability, or product outcomes. Without even learning the basics first.</p><p>MCP is a perfect mini-case of this pendulum:</p><ol><li>First, it was treated as inevitable for everything.</li><li>Then came backlash, and teams rediscovered that raw CLIs are often simpler to debug.</li><li>Now it is rising again in a more pragmatic form.</li></ol><p>Hype, backlash, partial recovery. New acronym, same cycle.</p><p>And look, it’s true that MCP solves a real problem in certain scenarios. But 99% of the time it’s not needed. And those who applied critical thinking, who knew the fundamentals of how this tech works, understood the hype wasn’t worth it. Be one of those. Don’t be the sheep that follows the shepherd blindly.</p><p>And if you don’t want to think for yourself and you need a guiding hand, look, even Anthropic, the fan favorite for all the vibe engineers, <a href=\"https://www.anthropic.com/engineering/building-effective-agents\">recommends starting simple</a> and only adding complexity when needed.</p><h2 id=\"earn-your-place\">Earn Your Place</h2><p>Every practice needs to prove its value. If it can’t, get rid of it.</p><p>If a workflow adds setup cost, cognitive load, or debugging pain, it must pay that back with real outcomes in your context. Not in theory. Not in someone else’s codebase.</p><p>Take small functions, one of the most repeated dogmas in our industry. Functions must be small, do one thing. No proof. No study showing smaller functions actually correlate with fewer bugs or better maintainability. Just repetition until it became law.</p><p>What actually happens? A dev breaks logic into five tiny named functions. Another dev comes along, sees a function name that looks like what they need, and reuses it. Months later, someone needs to tweak the behavior. Of course they do, because the reusability was born in imagination, not reality. They change that shared function. Now things break in three places nobody expected. If it had stayed in one place, the blast radius would have been zero.</p><p>Now think about what the AI equivalent of “functions must be small” is. What rules are being spread as gospel right now, with no proof? Things are moving fast enough that by the time we realize a dogma was wrong, half the industry has already built on top of it. This is actually the best moment we’ve had in years to slow down and think before repeating. AIs are smart, but we also have brains.</p><p>Good engineers do not ask, “Is everyone doing this?”</p><p>They ask, “Does this work for my context?”</p><h2 id=\"just-be-a-good-engineer\">Just Be a Good Engineer</h2><p>Use AI. Use agents. Experiment aggressively.</p><p>But look around. Every tool, every platform, every framework now has its own version of the same thing. Agents, subagents, prompts, skills, modes, MCPs, commands… you name it, someone is selling their spin on it. The complexity is artificial. The race to have the most elaborate setup is just the new cargo cult.</p><p>And the irony is that models are actually good enough now that none of it matters that much. If the harness is decent and you know what the AI can and can’t do, just being a good engineer is enough. Asking the right questions. Knowing when to push back. Understanding what you are looking at.</p><p>So the last piece of advice is the oldest one. Focus on the fundamentals, and don’t overfill your tools with crap you don’t need.</p><p>When you stop questioning and start copying, you are not doing engineering anymore. You are doing cosplay.</p><p>Tools are tools, not doctrine.</p>",
      "date_published" : "2026-02-22T23:00:00Z",
      "id" : "https://alejandromp.com/development/blog/development-dogma-is-back-a-i-agents",
      "summary" : "I have this weird feeling of deja vu.",
      "tags" : [
        "AI Development",
        "Methodology",
        "Art of Programming and Paradigms"
      ],
      "title" : "Your Agent Stack Is the New Clean Code",
      "url" : "https://alejandromp.com/development/blog/development-dogma-is-back-a-i-agents"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Today I made an exciting discovery: the existence of <code>AnchoredRelativeFormatStyle</code>. An API that, as of the time of writing this, barely appears in search results other than Apple’s sparse documentation. And to my surprise, it’s exactly what I’ve been looking for over the past few years.</p><h2 id=\"the-problem-with-relativeformatstyle\">The problem with RelativeFormatStyle</h2><p>Some time ago, I wrote about <a href=\"https://alejandromp.com/development/blog/an-exploration-of-formatstyle-as-a-dependency/\">An exploration of FormatStyle as a Dependency</a>. Thanks to my team’s work, we pushed that exploration further and developed an interesting solution (which I should write about!).</p><p>During that work, we defined a few date formatters for our application. One of the most interesting ones was a formatter for relative dates.</p><p>In the old times, with Foundation <strong>Formatter APIs</strong>, to get a formatted string for the relative time between two dates, things like “2 days ago” or “Tomorrow”, one could use:</p><pre><code class=\"language-swift\">localizedString(for date: Date, relativeTo referenceDate: Date) -&gt; String \n</code></pre><p>In the new Foundation <strong>FormatStyle APIs</strong>, the way to do that is using the nice fluent API on a date:</p><pre><code class=\"language-swift\">.formatted(.relative(presentation: .named))\n</code></pre><p>The new API looks elegant for quick code and tutorials. However, it becomes problematic when:</p><ul><li>Writing tests</li><li>Creating development previews</li><li>Building features that need relative times between specific dates</li></ul><p>All these issues arise from the same root problem: the <strong>implicit dependency</strong> on the current <code>Date()</code>. In the Formatter API you could pass the two dates but in the new Style API you just call <code>formatted</code> on a single Date, without a way to pass a second one.</p><p>In our system using <a href=\"https://alejandromp.com/development/blog/on-the-new-pointfree-swift-dependencies/\">swift-dependencies</a>, we had workarounds with limitations. While investigating improvements, I stumbled upon <code>AnchoredRelativeFormatStyle</code> - which solves our exact problem by allowing two dates!</p><h2 id=\"where-anchoredrelativeformatstyle-comes-from?\">Where AnchoredRelativeFormatStyle comes from?</h2><p>As someone who watches nearly every WWDC video and stays current with the Swift ecosystem, I was surprised I’d never heard of this API. A Google search returned just a couple of results with no substantial information. Was I imagining things?</p><p>Thankfully, we live in a world in which Foundation is open-source and easily accessible, so I could just go to GitHub and actually check not only the source code for <a href=\"https://github.com/swiftlang/swift-foundation/blob/main/Sources/FoundationInternationalization/Formatting/Date/Date%2BAnchoredRelativeFormatStyle.swift\">AnchoredRelativeFormatStyle</a> but also its <a href=\"https://github.com/swiftlang/swift-foundation/commits/main/Sources/FoundationInternationalization/Formatting/Date/Date%2BAnchoredRelativeFormatStyle.swift\">history</a>. Thanks to that, I could find the Foundation proposal that described the background of this new API: <a href=\"https://github.com/themomax/swift-foundation/blob/maxobermeier/discrete-format-style-proposal/Proposals/0003-discrete-format-style.md\">The DiscreteFormatStyle Protocol</a>.</p><p>The proposal document is very interesting, but it took me a couple of reads to fully grasp it. I recommend you take a read, but its purpose is to improve the existing <code>FormatStyle</code> infrastructure to enhance its performance when used directly in user interfaces, read: SwiftUI.</p><p>If you’re the one implementing SwiftUI Text and accept a string made from a formatter like the ones mentioned above, you can see how the existing API limits your optimizations. Your system needs to keep the UI up to date with an Input, “the relative target date” that doesn’t change, but internally the output (the string) depends on a moving target, “now”, which you don’t have visibility on because remember, the API is <code>tomorrow.formatted(.relative)</code>. This means you don’t have any alternative other than recomputing that formatted string at a high framerate, even if the output of the format will only change once a second or even once a minute.</p><p>This is why the proposal introduced <code>DiscreteFormatStyle</code>, a new protocol that allows <code>FormatStyle</code>s to expose when an output is expected to be different given the style configuration. This allows UI implementors to optimize the refresh rate by asking the style when it needs a refresh to be scheduled.</p><p>Some existing formatters were able to conform to the protocol without problems, but <code>RelativeFormatStyle</code> couldn’t because its current design has <code>Date()</code> as a hidden dependency, which means the <code>format</code> implementation is not a pure function and thus breaks the assumptions of the infrastructure around the styles. From the infrastructure’s point of view, “tomorrow” is constant so it could optimize things, but the fact that every call to format with the same input can give a different output breaks the assumptions. This is a bit tricky to understand until you internalize how format infrastructure works, with the styles themselves being just value types that are suposed to just be a wrapper around a pure function.</p><h2 id=\"discreteformatstyle,-a-weird-style\">DiscreteFormatStyle, a weird style</h2><p>To solve the above, <code>DiscreteFormatStyle</code>’s design is a bit backwards from what we would expect. A naïve design for this API would be to have a parameter for the “now” in the style itself, something like:</p><pre><code class=\"language-swift\">tomorrow.formatted(.relative(to: now))\n</code></pre><p>That would solve the problem I have with the existing relative style by removing the implicit dependency on <code>Date()</code>. But it wouldn’t solve the problem about performance as, even if now visible to us, the dependency on <code>now</code> that would change all the time wouldn’t be visible to the formatting infrastructure (aka format wouldn’t be a pure function).</p><p>So DiscreteFormatStyle works the other way around. The “anchor”, the data that doesn’t move, is part of the style, and the moving date is instead the instance where format is called on. Something like:</p><pre><code class=\"language-swift\">now.formatted(.relative(to: tomorrow))\n</code></pre><p>But, as stated in the proposal, since this would break expectations from developers, the Foundation team decided to not offer the static method API at all. If you want to use this, you need to construct the FormatStyle yourself, which by the way, as we explored in my previous post, you can do for any <code>FormatStyle</code>, you just need to know the type.</p><pre><code class=\"language-swift\">Date.AnchoredRelativeFormatStyle(anchor: tomorrow).format(now)\n</code></pre><blockquote><p>We explicitly decided against adding such function for Date.AnchoredRelativeFormatStyle as that style always formats its anchor date, and only uses the actual format input as the reference date. Thus any spelling that starts with Date.now.formatted is ultimately misleading or very verbose. - <a href=\"https://github.com/themomax/swift-foundation/blob/maxobermeier/discrete-format-style-proposal/Proposals/0003-discrete-format-style.md#a-static-factory-function-for-dateanchoredrelativeformatstyle\">A static factory function for <code>Date.AnchoredRelativeFormatStyle</code></a>.</p></blockquote><h2 id=\"don’t-tell-them\">Don’t tell them</h2><p>I understand that reasoning, but it also makes this API basically hidden and unknown, which is quite unfortunate. I guess if the main use case is as part of the SwiftUI <code>Text</code> it makes more sense, but for me this API has way more uses than that.</p><p>Since we already have our own bit of sugar on top of FormatStyles that mixes it with swift-dependencies, it’s quite easy for me to improve this API. One could argue if I should do it, but I think for my use cases it’s totally fine.</p><p>If you want to also be naughty, it’s quite easy:</p><pre><code class=\"language-swift\">public struct NaughtyRelativeFormatStyle: FormatStyle {\n    public typealias FormatInput = Date\n    public typealias FormatOutput = String\n    \n    var referenceDate: Date\n    \n    public func format(_ value: Date) -&gt; String {\n        referenceDate.formatted(Date.AnchoredRelativeFormatStyle.init(anchor: value))\n    }\n}\n\nextension FormatStyle where Self == NaughtyRelativeFormatStyle {\n    public static func relative(\n        to referenceDate: Date\n    ) -&gt; Self {\n        NaughtyRelativeFormatStyle.init(\n            referenceDate: referenceDate\n        )\n    }\n}\n\ntomorrow.formatted(.relative(to: now))\n</code></pre><blockquote><p>Of course the code above doesn’t implement all configuration options available on Foundation, but adding them is quite easy. I’ll leave that up to you so you make conscious decisions if you want to be naughty ;)</p></blockquote><h2 id=\"conclusion\">Conclusion</h2><p>This was a very surprising day, I was not expecting to find such a hidden API that actually solved my problems. I still needed to do some work because we still want to use swift-dependencies, but at least I could use a modern API on new OSs and stop relying on the old Formatters. It also made me look again deeply into FormatStyles, which is a very nice API on the usage side but has some fascinating intricacies in its implementation.</p>",
      "date_published" : "2024-10-23T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/anchored-relative-format-style-the-hidden-replacement-for-date-relative-format-style",
      "summary" : "Today I made an exciting discovery: the existence of <code>AnchoredRelativeFormatStyle</code>. An API that, as of the time of writing this, barely appears in search results other than Apple’s sparse documentation. And to my surprise, it’s exactly what I’ve been looking for over the past few years.",
      "tags" : [
        "Swift"
      ],
      "title" : "AnchoredRelativeFormatStyle: The Hidden Replacement for Date RelativeFormatStyle",
      "url" : "https://alejandromp.com/development/blog/anchored-relative-format-style-the-hidden-replacement-for-date-relative-format-style"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Recently, while watching a <a href=\"https://www.youtube.com/@TsodingDaily\">Tsoding stream</a>, I saw something that piqued my curiosity. Alexey demonstrated how one of his applications could render a video just by calling into the <code>ffmpeg</code> binary directly, instead of using it as a source code dependency or even a static or dynamic library linked to his program.</p><p>This caught my attention because, although I sometimes use binaries as dependencies by calling them (a good example is how I call the tailwindcss binary from <a href=\"https://alejandromp.com/development/blog/back-to-the-basics-with-genesis/\">Genesis</a> to generate this website), it had never occurred to me to use them for something as sophisticated as rendering a video. Calling them to do something they can do on their own, like reading some files and generating output? Sure. But calling a binary and somehow passing the data to generate a video seemed like another level entirely. However, the more I thought about it, the more the idea made sense. After all, it’s all just binary data flowing through pipes. So, I decided to spend some time having fun with the idea.</p><h2 id=\"having-fun-with-process-and-pipe\">Having Fun with Process and Pipe</h2><p>The first part wasn’t very difficult; I just had to refresh my knowledge about the incantation of parameters necessary to ask <code>ffmpeg</code> to generate a video from a few frames, something I hadn’t done in years. With a list of parameters at hand, I started writing some Swift code to spawn a <code>Process</code> to run <code>ffmpeg</code>.</p><pre><code class=\"language-swift\">class FFMPEG {\n    let process = Process()\n    let pipe = Pipe()\n    \n    func startRenderingVideo(width: Int, height: Int, framerate: Int) throws {\n        let resolution = &quot;\\(width)x\\(height)&quot;\n\n        process.currentDirectoryURL = // directory to generate the file\n        process.executableURL = // path to ffmpeg in your system\n        process.arguments = [\n            &quot;-loglevel&quot;, &quot;verbose&quot;,\n            &quot;-y&quot;,\n            \n            &quot;-f&quot;, &quot;rawvideo&quot;,\n            &quot;-pix_fmt&quot;, &quot;rgba&quot;,\n            &quot;-s&quot;, resolution,\n            &quot;-r&quot;, String(describing: framerate),\n            &quot;-i&quot;, &quot;-&quot;, \n            \n            &quot;-c:v&quot;, &quot;libx264&quot;,\n            &quot;-vb&quot;, &quot;2500k&quot;,\n            &quot;-pix_fmt&quot;, &quot;yuv420p&quot;,\n            &quot;output.mp4&quot;\n        ] \n        \n        // set a pipe to feed the frames via standard input\n        process.standardInput = pipe \n        \n        try process.run()\n    }\n</code></pre><p>With this code, I was able to spawn <code>ffmpeg</code> and make it wait on standard input for a bunch of frames. Of course, the parameters can be tweaked as needed, but these worked quite well for what I wanted to try.</p><p>The next part was the interesting one: how to pass the raw bytes that defined each frame. For this, we need to look at the parameters I used. We asked for each pixel to be an <code>rgba</code> color and specified the resolution of each frame. So the next step was to create a buffer of bytes with the correct shape.</p><p>Thankfully, on macOS, we have great solutions as part of our tools. In this case, an old one: <code>CoreGraphics</code>. I could have done this part manually to make it work on other platforms, but since I was just having some fun and wanted to generate images with SwiftUI, I went with the easy solution. It would be interesting to use something else that worked on other platforms and make the bytes manually, but that’s an exercise for another day.</p><p>Here’s the bit of code needed to take the bytes from a <code>CGImage</code> and feed them into the <code>Pipe</code>:</p><pre><code class=\"language-swift\">func sendFrame(_ cgImage: CGImage) {\n    let width = cgImage.width\n    let height = cgImage.height\n    let bitsPerComponent = 8\n    let bytesPerPixel = 4 // RGBA = 4 bytes per pixel\n    let bytesPerRow = bytesPerPixel * width\n    let totalBytes = height * bytesPerRow\n\n    var pixelData = Data(count: totalBytes)\n    pixelData.withUnsafeMutableBytes { (ptr) in\n        let context = CGContext(\n            data: ptr.baseAddress,\n            width: width,\n            height: height,\n            bitsPerComponent: bitsPerComponent,\n            bytesPerRow: bytesPerRow,\n            space: CGColorSpaceCreateDeviceRGB(),\n            bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue\n        )!\n\n        context.draw(cgImage, in: CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height)))\n    }\n\n    pipe.fileHandleForWriting.write(pixelData)\n}\n</code></pre><p>A few things to note in the above code:</p><ol><li>The definition needed for the <code>CGContext</code> needs to match what we’ve told <code>ffmpeg</code> to expect: four bytes per pixel, 1 byte per color component, etc.</li><li>I create a buffer with <code>Data</code> where I ask Core Graphics to draw the image. I would be curious if there was a different way of doing this because I imagine the <code>draw</code> actually needs to iterate through the image, but maybe there is a way to dump its bytes directly. Although that would assume the format is the same, so I guess it wouldn’t be very portable.</li><li>I use the <code>Pipe</code>, which is set as the standard input of the <code>ffmpeg</code> process, taking a file handle and writing the data into it. Through the magic of the operating system, this will transfer the bytes from one process to the other, letting <code>ffmpeg</code> render the frame of the video.</li></ol><p>The last bit we’re missing is very easy: just a way to tell <code>ffmpeg</code> that there are no more frames incoming. Otherwise, it will just keep waiting for more data. To do this, we just <code>close</code> the file handle of the pipe. Then it’s just a matter of waiting for <code>ffmpeg</code> to finish the rendering.</p><pre><code class=\"language-swift\">func endRendering() {\n    pipe.fileHandleForWriting.closeFile()\n    process.waitUntilExit()\n}\n</code></pre><p>And with this, we have a very simple class that will render a video from our given frames. Under the hood, it will use <code>ffmpeg</code>, but our usage code doesn’t have to worry about the intricacies of it anymore.</p><pre><code class=\"language-swift\">class FFMPEG {\n    func startRenderingVideo(width: Int, height: Int, framerate: Int) throws\n    func sendFrame(_ cgImage: CGImage)\n    func endRendering()\n}\n</code></pre><h2 id=\"controlled-animation-with-swiftui\">Controlled Animation with SwiftUI</h2><p>To try this out, I wanted to make a small animation with SwiftUI. From time to time, I have the need to create some animation to support an explanation, but I never do it because reaching for motion graphics software is a big step for something so simple. So I thought this would be the perfect time to make a proof of concept with SwiftUI.</p><p>The idea is quite simple: prove that we can get the frames of an animation that changes the contents of some text and another that moves a shape through the screen.</p><p>The SwiftUI part is, of course, very simple. It’s just a stack with some text and shapes:</p><pre><code class=\"language-swift\">VStack {\n    Text(text)\n        .foregroundStyle(Color.white)\n    Circle()\n        .fill(.red)\n        .frame(width: circleSize, height: circleSize)\n        .offset(x: (circlePosition+circleSize/2) - (width/2))\n    Rectangle()\n        .stroke(.green)\n}\n</code></pre><p><img src=\"https://alejandromp.com/development/blog/using-binaries-as-libraries/ffmpegswiftuiframe.jpg\" alt=\"SwiftUI example frame\"></p><p>Getting an image, specifically a <code>CGImage</code>, from this view was also very trivial thanks to SwiftUI’s <a href=\"https://developer.apple.com/documentation/swiftui/imagerenderer\">ImageRenderer</a>.</p><pre><code class=\"language-swift\">let view = ToRender(text: text, circlePosition: circlePosition, width: width, height: height)\nlet renderer = ImageRenderer(content: view)\nlet cgImage = renderer.cgImage!\nffmpeg.sendFrame(cgImage)\n</code></pre><p>The tricky part was figuring out how to get each frame. I would love to use SwiftUI animations and somehow just get a snapshot for each frame, but I’m not sure there is a trivial way of doing it that allows me to leverage SwiftUI animations. So instead of going down that rabbit hole, I opted to drive the animations myself.</p><p>This means, as you can see in the <code>View</code> above, that I can’t just use <code>animation</code> directly in the views. Instead, I need to manipulate their properties, like the circle offset, and then compute that myself on each frame. Luckily, SwiftUI exposes a few core types for animations that are separate from the view layer, so I can use that instead of letting SwiftUI drive the animation.</p><p>For example, we can use <code>KeyframeTimeline</code> to combine <code>LinearKeyframe</code> and even <code>SpringKeyframe</code> to then request the timeline value at a specific time or progress.</p><p>For the text, it’s quite simple for what I need. Knowing at which frame we are, we can just interpolate to know how many characters of the full text we want to display and just prefix the string up to that index.</p><pre><code class=\"language-swift\">let fullText = &quot;Hello World!&quot;\n// Animate text\nlet tEnd = Double(fullText.count-1)\nlet tkf = KeyframeTimeline(initialValue: 0) {\n    LinearKeyframe(tEnd, duration: 1.5)\n    SpringKeyframe(tEnd/2, duration: 0.5)\n    CubicKeyframe(tEnd, duration: 1)\n}\nlet text = String(\n    fullText.prefix(upTo: fullText.index(fullText.startIndex, offsetBy: Int(tkf.value(progress: progress))))\n)\n</code></pre><p>For the circle that we want to move through the screen, we can use the same technique of using a keyframe timeline and asking it to get us the value at a certain time.</p><pre><code class=\"language-swift\">// Move Circle\nlet startPosition = 0.0\nlet endPosition = Double(width) - 100\nlet keyframes = KeyframeTimeline(initialValue: startPosition) {\n    LinearKeyframe(endPosition/2, duration: 1, timingCurve: .linear)\n    LinearKeyframe(endPosition/3, duration: 1, timingCurve: .easeInOut)\n    SpringKeyframe(endPosition, duration: 1)\n}\nlet circlePosition = keyframes.value(progress: progress)\n</code></pre><p>And with this, I can just make a loop that generates all the frames needed and feeds them into <code>ffmpeg</code>.</p><pre><code class=\"language-swift\">let totalSeconds = 3\nlet totalFrames = framerate * totalSeconds\nfor i in 0..&lt;totalFrames {\n    let progress = CGFloat(i) / CGFloat(totalFrames - 1)\n    // ... generate frame and send to ffmpeg\n}\n</code></pre><h2 id=\"what’s-the-point?\">What’s the Point?</h2><p>The point was to have fun and explore the capabilities of calling binaries as if they were libraries, avoiding the integration cost that comes with source code or libraries in other languages. There’s always a trade-off, and of course, source code or library integration exists for good reasons. I haven’t come out of this thinking binary integrations are always the best solution, but now I have more knowledge and practice with them, so it’s yet another tool in my belt. And that was a goal well accomplished.</p><p>On the other hand, I have a proof of concept for creating videos from SwiftUI, which opens the door to using this technique in the future when I want to explain some things without having to do it with a full-featured video editor. Doing it in SwiftUI and code is actually faster for certain things.</p><p>And finally, I hope I’ve piqued your interest as mine was piqued! Have fun with programming!</p>",
      "date_published" : "2024-10-06T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/using-binaries-as-libraries",
      "summary" : "Recently, while watching a <a href=\"https://www.youtube.com/@TsodingDaily\">Tsoding stream</a>, I saw something that piqued my curiosity. Alexey demonstrated how one of his applications could render a video just by calling into the <code>ffmpeg</code> binary directly, instead of using it as a source code dependency or even a static or dynamic library linked to his program.",
      "tags" : [
        "Swift",
        "Art of Programming and Paradigms"
      ],
      "title" : "Using Binaries as Libraries",
      "url" : "https://alejandromp.com/development/blog/using-binaries-as-libraries"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Sometimes in command line tools we want to perform some cleanup final task before the process is killed. To detect this scenario we can hook to unix signals like an interrupt and perform the needed work before existing from the process.</p><blockquote><p>This post was originally written in 2016-05-29 as a follow up to <a href=\"https://alejandromp.com/development/blog/state-swift-scripting/\">The state of Swift scripting</a> and discussed my old library <a href=\"https://github.com/alexito4/Trap\">Trap in GitHub</a>. Since then the ecosystem has grown a lot so it has been updated in 2024 with a more modern approach.</p></blockquote><p>In the server ecosystem this is very well handled with the <a href=\"https://github.com/swift-server/swift-service-lifecycle\">swift-service-lifecycle</a> package, that brings support for graceful shutdown to concurrency tasks. What might surprise you is that the same package exposes a module for handling unix signals, without the <em>service</em> part. I find this ideal for command line utilities that don’t require the full service lifecycle of a server application but still want to handle unix signals.</p><p>To include this module you just need to depend on the library, but instead of including the service module into your target, you just include <code>UnixSignals</code>.</p><pre><code class=\"language-swift\">dependencies: [\n        .package(url: &quot;https://github.com/swift-server/swift-service-lifecycle.git&quot;, exact: &quot;2.6.1&quot;)\n    ],\n    targets: [\n        .executableTarget(\n            name: &quot;...&quot;,\n            dependencies: [\n                .product(name: &quot;UnixSignals&quot;, package: &quot;swift-service-lifecycle&quot;)\n            ]\n        ),\n    ]\n</code></pre><p>With this implace you can now import the <code>UnixSignals</code> module and instantiate a <code>UnixSignalsSequence</code> with the specific signals you want to listen to.</p><pre><code class=\"language-swift\">import UnixSignals\n\nfor await signal in await UnixSignalsSequence(trapping: .sigint) {\n    print(signal)\n}\n</code></pre><p>If you run this on your terminal and try to interrupt it with <code>Ctrl + C</code> you will see <code>^CSIGINT</code> printed in the console but the process won’t stop. Now that you are handling the interruption is up to you to actually exit from the process. Isn’t it nice how respecful unix is?</p><blockquote><p>If you want to kill the process use <code>Ctrl + Z</code>. This will send a different signal and since we’re not handling that one it will behave as default and kill the proess. Alternatively just use Activity Monitor and force quit it.</p></blockquote><p>With this power in our hands we can see an example of how it would work. In the following code we spawn two child tasks, one keeps a running counter that increments after a few miliseconds; the other awaits for an interrupt signal to be triggered.</p><pre><code class=\"language-swift\">await withTaskGroup(of: Void.self) { group in\n    group.addTask {\n        var i = 0\n        while true {\n            print(i)\n            i += 1\n            try? await Task.sleep(for: .milliseconds(500))\n        }\n    }\n    group.addTask {\n        for await signal in await UnixSignalsSequence(trapping: .sigint) {\n            print(signal)\n            print(&quot;Shutting down...&quot;)\n            try? await Task.sleep(for: .seconds(2))\n            exit(EXIT_FAILURE)\n        }\n    }\n}\n</code></pre><p>Running this and using <code>Ctrl + C</code> we can see how the running counter keeps printing for a few extra seconds before the process is closed.</p><pre><code class=\"language-swift\">0\n1\n2\n^CSIGINT\nShutting down...\n3\n4\n5\n6\n7\n</code></pre><p>This illustrates how we can have extra time to perform a gracefull shutdown of whatever process we are running.</p><p>If you need something more complex make sure to check out all the capabilities from <a href=\"https://github.com/swift-server/swift-service-lifecycle\">swift-service-lifecycle</a>.</p>",
      "date_published" : "2024-10-02T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/swift-catch-os-signals-trap",
      "summary" : "Sometimes in command line tools we want to perform some cleanup final task before the process is killed. To detect this scenario we can hook to unix signals like an interrupt and perform the needed work before existing from the process.",
      "tags" : [
        "Swift",
        "Scripting"
      ],
      "title" : "It’s a trap! React to Unix signals in Swift.",
      "url" : "https://alejandromp.com/development/blog/swift-catch-os-signals-trap"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Recently, I finished an almost final draft of my novel and decided it was time to refresh my online presence and website. Instead of starting my next novel, I dove into creating the next version of my site, what I call <code>alejandromp4</code>. And, of course, you know what that means… using a new static site generator! Yes, I have a problem with focusing my hobbies; I’m working on it.</p><h2 id=\"the-status-quo\">The status quo</h2><p>Let me first tell you what was the status quo of the version 3 of my site. It was built using <a href=\"https://github.com/JohnSundell/Publish\">Publish</a>, a static site generator written in Swift. That worked really well as Swift is my favorite language, but more than that, it’s the ecosystem where I spend most of my time. This helps me avoid surprises when I come back to the website after a while, something I disliked about previous incarnations based on Ruby or Node, ecosystems that seem to break every time I had to publish an article.</p><p>But Publish wasn’t all roses. I have some peculiar requirements for my site around taxonomies and multiple sections that needed to work well together. Although Publish offers some customization options, it wasn’t enough. I quickly had to change some things in Publish, which meant discarding the nice defaults that come with it. I also had to fork it and open some internal functions to avoid repeating what was already built. Over time, I grew comfortable with that codebase and my customizations, but it’s not something I want to deal with long-term.</p><p>So, I thought it was a good occasion to look for a replacement.</p><h2 id=\"the-current-sphere\">The current sphere</h2><p>Recently, another contender has appeared: <a href=\"https://github.com/twostraws/Ignite\">Ignite</a>. There are many others, but this one has the backing of a prominent figure in the ecosystem, which has propelled it to popularity. I spent some time with it, and it’s not that dissimilar from Publish—in the end, all static site generators are more or less the same. There are things I really like about Ignite, the documentation and the easy of use is really something to admire. But ultimately I decided it wasn’t for me. In an effort to be easy to use out of the box, it’s too tight for my taste. For example, to build pages, you need to accept using Bootstrap and a DSL, which is nice for a quick start but I needed the flexibility to not only use another CSS system (because I don’t like Bootstrap) but also use existing HTML templates that were already good to go.</p><p>I looked around for a bit and found other interesting alternatives with intriguing declarative systems to build the site in various shapes and forms. This is a trend in modern static site generators—a sort of declarative way of specifying pages and content for the generator. It’s an approach that aims to simplify the configuration process, though it comes with its own set of tradeoffs.</p><p>Ultimately, I came to the conclusion that it was time for me to tackle the problem myself. But I you should probably look at the mentioned projects if you are in need of a static site generator, they are very good and probably more inline what you expect.</p><h2 id=\"why-genesis\">Why Genesis</h2><p>With a few years of experience and having seen how other generators work, I thought it wouldn’t be too hard to make one myself. In fact, the core idea is an old one, as is the name, but I never pursued it because I didn’t want to have something to maintain.</p><p>Now, I was not happy with my current situation, a forked engine with the original project barely maintained. And if I had to switch to another project that, even if promising today, could be abandoned any time, I wouldn’t be in a better position. So the tradeoff of making my own is very different.</p><p>But if I have to build my own site generator, and make sure doesn’t take too much time from writing the actual content for the blog, I need to make a few decisions that will help me now and in the future. So I defined a core rule: simplicity.</p><p>This meant no fancy declarative rule system, no fancy HTML DSL, no building things from scratch, etc. But the best thing came with the realization that doing things that way woudl actually bring me all the benefits that I found lacking in other solutions. It was a win win sitaution!</p><p>So, armed with valor, some open-source code that I could reuse, and a few ideas in my mind, I sat down and started writing requirements to not lose the north star, drawing some diagrams, and finally coding.</p><h2 id=\"the-goals-of-genesis\">The Goals of Genesis</h2><h3 id=\"simplicity\">Simplicity</h3><p>The first overarching goal was to keep things simple—no, really, as simple as possible. I knew that remaking my entire site wouldn’t be trivial; there’s a lot of custom logic and hacks I’ve done to Publish that, although simpler in my own engine, still had to be ported. I also have some peripheral tools for my site (a CLI to work with it and a tool to create posters), but I was confident those could be moved directly.</p><p>I acomplish this by making the API very clear and specific, without fancy declarative engines or DSLs. What you get are just a few small protocols and a bunch of functions that you can call.</p><h3 id=\"flexibility\">Flexibility</h3><p>The other goal was to keep things flexible. Not to make this a fancy library, neither for my future self. It wasn’t flexibility for the sake of flexibility or purity. It was flexibility because I already know that my current requriements have some complexity that requires it.</p><p>Learned from experience, I don’t want to have defaults that become irrelevant as soon as you want to customize things. And if you want to customize things you should be able to reuse as much as the engine as possible without having to fork and make things public. This means that I don’t want to have a single call that does everything, but instead the tools that I can compose as needed.</p><p>Genesis provides APIs to load content and generate pages using simple function calls. It’s procedural on purpose. Just call the functions however you want without being tied to a specific structure or rule system.</p><p>For example there is a <code>clearBuildFolder()</code> function that does what the name indicates. But you don’t have to call it if you don’t want, nobody is forcing you. Of course when publishing your site you probably want to do a clean generation, but while iterating and previewing it? It’s just a waste of time. Or not. You decide. That’s the point.</p><h3 id=\"pages-are-just-strings\">Pages are just Strings</h3><p>What I wanted for sure the new generator to give me was the flexibility to handle pure HTML templates directly so I could take a beautiful template made from a good designer and just use it. But also I knew that eventually I would want to write some HTML in Swift with one of the fancy DSLs that everybody uses. But which one was another story. Beacuse of that I didn’t want to imbue the engine with any DSL, I wanted to engine to just work witht the minimum thing that needed.</p><p>So what’s the only thing that is needed as an input to generate a file that can be served? A <code>String</code>.</p><p>So instead of using a fancy DSL for typed Swift HTML, Genesis simply works with strings. This simplicity opens up a world of possibilities: from using existing HTML templates to experimenting with various Swift HTML DSLs, all without changing the core engine.</p><p>The advantage is that acomplishing this is also very simple, you just need a function that returns a <code>String</code>.</p><pre><code class=\"language-swift\">func render(context: Context) async throws -&gt; String {\n    &quot;&quot;&quot;\n    &amp;lt;html&gt;\n    &amp;lt;body&gt;\n    &amp;lt;p&gt;A page in your site is just a String. Easy.&amp;lt;/p&gt;\n    &amp;lt;/body&gt;\n    &amp;lt;/html&gt;\n    &quot;&quot;&quot;\n}\n</code></pre><p>I know many will be screaming on seeing this, but honestly I’ve written most of this site you are reading in Swift String interpolation and is totally fine. Reusing “components” is just making a separate function that returns a String. It doesn’t get simpler than that.</p><p>And the best thing is that I can easily add fancy DSLs on top. It took me five minutes to add suport for a new DSL library, <a href=\"https://github.com/sliemeobn/elementary\">elementary</a>, and a few more to try another one. Adding support for loading tempalted files is also trivial.</p><h3 id=\"no-web-dependencies\">No web dependencies</h3><p>Something else I didn’t want was to tie the engine to specific web frameworks. For example, I’m not a fan of Bootstrap so that wouldn’t come prebacked. The old version of the site used Bulma but the previous version of <a href=\"https://alejandromp.com/author/\">my author site</a> used TailwindCSS. So the engine doesn’t care what you use, again, it simply expects a <code>String</code>. It’s actually very nice how different requirements coalesce into the same goal.</p><h3 id=\"content-loaders\">Content Loaders</h3><p>Genesis doesn’t automatically load any dynamic content for you. I don’t want it to check specified folders for predefined files. Instead Genesis expects you to define <code>ContentLoader</code>s to load dynamic <code>Content</code>. This allows you to determine what the content is and how it’s loaded, from markdown files, a database, or a remote CMS. The engine doesn’t care about it.</p><p>A simple async function is all you need to implement. Even what “content” is is not defined, you bring your own.</p><pre><code class=\"language-swift\">struct BlogLoader: ContentLoader {\n\tfunc load(context: Context) async throws -&gt; sending [any Content] {\n    let contentDirectory = await context.contentDirectory\n    let contents = try FileManager.default.contentsOfDirectory...\n    return try contents\n        .map { BlogPost(...) }\n}\n</code></pre><h3 id=\"pages\">Pages</h3><p>Besides loading content, the other core concept of the engine is generating pages. A <code>Page</code> is a type that implements the <code>render</code> function explained above. That’s it. This makes it very trivial to generate <em>single</em> pages for your site, things like the <a href=\"https://alejandromp.com/\">Home</a> or the <a href=\"https://alejandromp.com/author/\">Author page</a>.</p><p>But there are pages that need to be rendered multiple times, for each instance ot the loaded content. like each post page. For this the engine has another simple protocol, the <code>PageProvider</code>. And again, it’s a very simple requirement: just return a list of pages.</p><p><code>func source(context: Genesis.Context) async throws -> [any Page]</code></p><h2 id=\"simple-core\">Simple Core</h2><p>These few basic requirements are what defines the core of the engine, which also makes it very easy to understand what’s the process of generation and how you interact with it.</p><ol><li>A <strong>Context</strong> that is passed around and offers a set of functions to manipulate the site.</li><li>All the <strong>Content</strong>, which can be anything, is loaded into the context via <strong>Content Loaders</strong>.</li><li>It renders and writes to disk <strong>Pages</strong> that can be single instances statically given, or</li><li><strong>Page Providers</strong> can instantiate multiple pages from the content loaded in the context.</li></ol><p>And the reality is that that’s all a static site generator is at its core. When you put it this way it’s not that scary anymore. If you even think about it, and I have, half of these protocols are not even necessary from how simple they are. But I found that having them helped me have a better mental model of the core of the engine.</p><h2 id=\"expandable-shell\">Expandable Shell</h2><p>The beauty of this simple architecture is that it let’s me expand it as needed. I already mentioned above that adding a DSL for HTML is very simple, but there is more that can be done.</p><h3 id=\"standard-pages\">Standard Pages</h3><p>To reiterate, the core of the engine doesn’t do anything for you. You might expect RSS, sitemaps, etc to be generated, like it happens in most other engines. But in Genesis that’s up to you, you give it the Pages that you want to generate.</p><p>But of course, RSS and Sitemaps are pages that are quite standarized. So for that I took some code from other engines and with a few tweaks it was working fine for my site. Now this are bundled in the library but is up to you to use them or not.</p><h3 id=\"markdown\">Markdown</h3><p>This is probably the first thing anybody wants from a static site generator, so you might be surprised that, again, Genesis doesn’t have support for Markdown fiels embeded into its core. That’s another consious decision. I don’t want to make the mistake of embeding a specific library that will make things harder for the sites build with the engine, I’ve already suffered that in the past.</p><p>But agian, Genesis doesn’t need this embeded, it can be added from the outside because its core is very small. In my case I’ve decided to use <a href=\"https://github.com/apple/swift-markdown\">Apple’s swift-markdown</a> because I think is a safe bet nowadays, it makes it trivial to parse Markdown fiels intro an AST that then can be transofrmed into a String.</p><p>Crucially, the fact that I have full control over the Markdown library used and of the type that defines a “post” means that it has been trivial to do something that I’ve found impossible in any other site: keep the markdown tree trough all stages of generation so it can be tweaked in specific pages. This small thing has given me so many headaches over the years that I can’t believe how trivial it is in Genesis. Just because you have the contorl and you decide.</p><h2 id=\"conclusion\">Conclusion</h2><p>By going back to the basics, I’ve been able to build a static site generator and port my site to it in a few days. I love how Genesis offers a flexible and powerful static site generator that doesn’t constrain you to specific workflows or technologies. It’s designed to grow with your needs, allowing for simple setups for basic sites and complex configurations for more demanding projects.</p><p>I haven’t optimized for a marketable experience, so while it may not have the polish or out-of-the-box features of some popular generators, Genesis provides the building blocks for creating exactly the site you want, without fighting against pre-defined structures or limitations. It’s a testament to the power of simplicity and the value of understanding your tools from the ground up.</p><p>Of course this is just the begining, I will continue to develop and use Genesis for my own site, I’m excited to see how it evolves. I already have a few ideas to add more batteries while keeping the core simple.</p><p>And finally, this is not a sales pitch. As I said at the start, you probably want to check the existing generators with bigger communnities, but if you like things simple and are not afraid of getting your hands dirty you can try <a href=\"https://github.com/alexito4/Genesis\">Genesis</a>. Or maybe if you want to build your own this post can serve as a guide and word of advice: keep it simple!</p>",
      "date_published" : "2024-09-24T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/back-to-the-basics-with-genesis",
      "summary" : "Recently, I finished an almost final draft of my novel and decided it was time to refresh my online presence and website. Instead of starting my next novel, I dove into creating the next version of my site, what I call <code>alejandromp4</code>. And, of course, you know what that means… using a new static site generator! Yes, I have a problem with focusing my hobbies; I’m working on it.",
      "tags" : [
        "Swift",
        "Project",
        "Release",
        "Meta"
      ],
      "title" : "Back to the basics with Genesis",
      "url" : "https://alejandromp.com/development/blog/back-to-the-basics-with-genesis"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>In my team, one of the things we always make sure of is keeping our dependencies up to date. We rarely add a dependency without a really good reason, and when we do, we assign an <strong>owner</strong> who is responsible for keeping it up to date.</p><p>These owners always keep an eye on new updates and make it a task to go through them as soon as they can. But sometimes, life happens, and one is not able to keep up with it. More often than not, though, the reason why updates are not kept on top of is simply because people forget that it’s part of their responsibilities. And I don’t blame them. Right now, we just keep the list of owners in our documentation site, which is a very passive way of doing things.</p><p>Instead, we’ve now worked on upgrading our nightly notification system! This existing system not only runs a set of unit tests and sends a message with test results and code coverage to our Slack channel, but it also shows a list of outdated dependencies thanks to <a href=\"https://github.com/kiliankoe/swift-outdated\">swift-outdated</a> and some custom code on top that is part of our development tools.</p><p>So before, the message just included a table of dependencies to be updated:</p><blockquote><p>| Package | swift-composable-architecture | 1.12.0 | 1.13.0 | ❗️</p></blockquote><p>But now, we’ve added an <code>@mention</code> to the Slack user responsible for each outdated dependency.</p><p>In my experience, the reason updates often get neglected is simply because it can be easy to forget or overlook tasks that aren’t constantly in front of you. That’s why I think having a <a href=\"https://www.youtube.com/watch?v=XtwsJtdH_q8\">system for notes</a> is so crucial - it helps keep important responsibilities top-of-mind. By providing a daily reminder of the pending updates they are responsible for, our new notification system has helped streamline things for the whole team.</p><p>Doing this is very simple since we already have the infrastructure in place. It was just a matter of having a mapping between dependencies and Slack users in our CLI. Something very low-tech that does the work 👌</p><p>But there was an extra requirement of mine: if there is a dependency we don’t have an owner for, we send an error on the nightly message so we can assign one immediately. This is something not trivial with <code>swift-outdated</code> because that reads from the <code>.resolved</code> file, which has no information about what dependencies are transitive and which ones are directly yours. Since in our opinion, we don’t want to be bothered with transitive dependencies, we don’t assign owners to those.</p><p>To solve this, we read the <code>Package.swift</code> file in advance so we know what the direct dependencies are. This is quite simple, as SPM makes it very easy to get a JSON representation of a package: <code>swift package dump-package</code>.</p><p>So work done.</p><p>But you know me, I can’t avoid getting excited with interesting ideas. So when discussing how to approach this with my team, I wondered… what if we could attach the owner to the dependency itself in the <code>Package.swift</code> file? Well, hold my glass of water.</p><p>Something I love about SPM is that the package definition file is actually a Swift file, which means you can run Swift code in it. Thanks to that, we can add an API to the <code>PackageDescription</code> that does what we want.</p><pre><code class=\"language-swift\">.package(url: &quot;...&quot;, exact: &quot;...&quot;, owner: &quot;...&quot;)\n</code></pre><p>To do this, we can extend the SPM types with our own method. Of course, we need to return what SPM expects.</p><pre><code class=\"language-swift\">extension PackageDescription.Package.Dependency {\n    static func package(\n        url: String,\n        exact version: Version,\n        owner: String\n    ) -&gt; PackageDescription.Package.Dependency {\n        .package(url: url, exact: version)\n    }\n}\n</code></pre><p>So what’s the point of having the new parameter if it’s not used? Well, that we have it in the same source, and we can extract it from it!</p><p>But if you’re thinking SPM dump will include this data, well, of course, it won’t, as SPM doesn’t know anything about it. That doesn’t mean we can’t get it; it just means it’s our work. For things like this, I always rely on <a href=\"https://github.com/pointfreeco/swift-parsing\">swift-parsing</a>, and it makes it quite easy to extract the dependency from the file.</p><p>This is the output of running a quick proof of concept command line:</p><pre><code>▶ ./spm-owners Package.swift\n┏━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┓\n┃   ┃ Package                       ┃ Version  ┃ Owner    ┃\n┃   ┃ &lt;String&gt;                      ┃ &lt;String&gt; ┃ &lt;String&gt; ┃\n┡━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━┩\n│ 0 │ swift-parsing                 │ 0.13.0   │ Martin   │\n│ 1 │ swift-argument-parser         │ 1.5.0    │ Martin   │\n│ 2 │ swift-composable-architecture │ 1.13.0   │ Alex     │\n└───┴───────────────────────────────┴──────────┴──────────┘\n</code></pre><p>I love this! Having the ownership data as part of the <code>Package.swift</code> makes it so things are always in sync and up to date.</p><p>Of course, I’m not using this in our team’s system because it’s not production-ready; it’s just a proof of concept. The big disadvantage is that although <code>Package.swift</code> can run Swift code, you can’t just import a separate package, which means one would have to copy-paste the code above manually. Not a deal-breaker, but not nice enough for me to want to maintain 😂 If one could import in the package file, it would be another story.</p><p>So there you go, an interesting proof of concept that has kept me entertained for a couple of hours. I wish SPM would officially support a way to accomplish this, somehow to be able to add metadata and that it would be kept for other tools. And maybe there is, and I’m not aware?</p><p>You can check out the proof of concept on the <a href=\"https://github.com/alexito4/spm-owners\">spm-owners</a> repo, if you are curious.</p>",
      "date_published" : "2024-08-20T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/swift-package-manager-dependency-owners",
      "summary" : "In my team, one of the things we always make sure of is keeping our dependencies up to date. We rarely add a dependency without a really good reason, and when we do, we assign an <strong>owner</strong> who is responsible for keeping it up to date.",
      "tags" : [
        "Swift",
        "Swift Package Manager"
      ],
      "title" : "Swift Package Manager Dependency Owners",
      "url" : "https://alejandromp.com/development/blog/swift-package-manager-dependency-owners"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Every year SwiftUI improves and gets new modifiers that widen the capabilities of the framework. Sadly, as responsible developers, we can’t use any of that because our users take some time to update and, as opposed to the Android ecosystem where Jetpack Compose is just a library embedded in every app, we need to wait for our users to be at the required minimum OS version to enjoy those fancy new tools.</p><p>Or do we?</p><p>There are some APIs that are easy to backport if we disregard the old versions of the operative system. This may sound awful but is a valid option that is worth considering. If you know you have a very little percentage of users stuck in the old version but you can’t get rid of it because of some policy, you know the impact is very minimal. And if time has already passed and dropping that version is on the horizon, you have more reason to plan for the future and not get stuck in the past. No matter what, we should make sure that users in the old version don’t get a bad experience. It may be not perfect and modern, but they still should be able to use the app properly. It’s a case-by-case decision that you have to take, but if you do, here is how to pull it off.</p><h2 id=\"porting-swiftui-modifiers\">Porting SwiftUI modifiers</h2><p>We will focus on SwiftUI modifiers. Other features can also be backported, but they will need slightly different techniques. The one for modifiers is actually quite simple, but it always depends on the functionality itself.</p><p>I’ve used this technique for a while, and it works very well. For example the <a href=\"https://developer.apple.com/documentation/swiftui/view/scrolldismisseskeyboard(_:)\">scrollDismissesKeyboard(_:)</a> modifier is a good candidate. It’s a new functionality that users in old versions won’t miss because they wouldn’t have access to it before (well, unless your app was UIKit, again, case by case).</p><p>The first step is to copy the modifier function signature in your own <code>View</code> extension:</p><pre><code class=\"language-swift\">extension View {\n    /// docs...\n    @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)\n    public func scrollDismissesKeyboard(_ mode: ScrollDismissesKeyboardMode) -&gt; some View\n}\n</code></pre><p>I also recommend copying over the documentation so the users of this function will get all the info they need. Just as if it was the legitimate function from the framework.</p><p>Conceptually the implementation of the function is very simple: we check in which OS version we are and we call the SwiftUI function when available and fallback to something else when not. This applies to all modifiers you want to port, but there are two things to consider.</p><h2 id=\"fallback-on-old-os\">Fallback on old OS</h2><p>What do you do as a fallback? That’s the first question to consider and it will depend on the feature, its complexity, how much worth it is for you and a multitude of other aspects, technical and non-technical. This is the “case by case” part of this work.</p><p>In this example, the fallback is simply to do nothing. And that works for many cases, really. To implement this, we just need to return self in the old version so our modifier does nothing with the view.</p><pre><code class=\"language-swift\">if #available(iOS 16.0, *) {\n    self\n        .scrollDismissesKeyboard(mode)\n} else {\n    self\n}\n</code></pre><p>That’s what we want, easy, right? Well, remember that for this to work, we need to opt in this function into the ViewBuilder DSL. Check out <a href=\"https://alejandromp.com/blog/viewbuilder-vs-anyview/\">ViewBuilder vs. AnyView</a> for more details about this.</p><p>If your modifier didn’t take any parameters, that would be all you needed, but that’s rarely the case.</p><h2 id=\"backport-parameters\">Backport parameters</h2><p>Most modifiers take some parameters to adjust its functionality. Those types are often new and thus not present in old versions, so we need to port those too.</p><p>The process for that is similar, just copy the existing types into your own file.</p><pre><code class=\"language-swift\">public struct ScrollDismissesKeyboardMode {\n    public static var automatic: ScrollDismissesKeyboardMode { get }\n    public static var immediately: ScrollDismissesKeyboardMode { get }\n    public static var interactively: ScrollDismissesKeyboardMode { get }\n    public static var never: ScrollDismissesKeyboardMode { get }\n}\n</code></pre><p>Now that’s a fine way to start, but I find it significantly easier to work with an <code>enum</code>. That way, you don’t have to worry about internal implementations or storing any data. Furthermore, I also recommend prefixing the name of the type so is distinct from the SwiftUI one. This is not strictly necessary, but I’ve found that makes the compiler way happier as it helps distinguish the functions easily. It’s also a hint to the user of the function that there are some shenanigans going on.</p><p>So let’s convert that type to an enum instead:</p><pre><code class=\"language-swift\">public enum MyScrollDismissesKeyboardMode {\n    case automatic\n    case immediately\n    case interactively\n    case never\n}\n</code></pre><p>This makes it so the usage side can use the same dot syntax and names as with the original function while using our custom types. But we still need to give SwiftUI its own types, so I always add a property to do the conversion.</p><pre><code class=\"language-swift\">// MyScrollDismissesKeyboardMode\n    @available(iOS 16.0, *)\n    var swiftUI: ScrollDismissesKeyboardMode {\n        switch self {\n        case .automatic: return .automatic\n        case .immediately: return .immediately\n        case .interactively: return .interactively\n        case .never: return .never\n        }\n    }\n</code></pre><p>This property is only available in the new OS since when we call it we will be in the correct version.</p><p>Now we can switch the function to use the new type.</p><pre><code class=\"language-swift\">  @ViewBuilder func scrollDismissesKeyboard(\n      _ mode: MyScrollDismissesKeyboardMode\n  ) -&gt; some View {\n      if #available(iOS 16.0, *) {\n          self\n              .scrollDismissesKeyboard(mode.swiftUI)\n      } else {\n          self\n      }\n  }\n</code></pre><p>And that’s the function fully back ported. We take our custom parameter, and if we are in the new version, convert it to the SwiftUI one and call the real modifier. Otherwise, we just return the view unmodified.</p><h2 id=\"using-our-function\">Using our function</h2><p>With this, the usage side would look exactly as with the real code.</p><pre><code class=\"language-swift\">SomeView()\n  .scrollDismissesKeyboard(.immediately)\n</code></pre><p>For that to work you just need to make sure your function is in your module, or if it’s in a separate package (which I always <a href=\"https://alejandromp.com/blog/ios-app-architecture-in-2022\">recommend</a>) you need to import it in your file.</p><p>This works because your code always takes precedence over system frameworks. This means the compiler will pick your custom function and types before SwiftUI’s.</p><h2 id=\"obsoleted\">Obsoleted</h2><p>Another very useful tip is to use an availability annotation to ensure the compiler will remind you to remove this code when your minimum target matches the one that introduced the modifier. For this, we can add an <code>@available</code> with the <code>obsoleted</code> parameter.</p><pre><code class=\"language-swift\">@available(iOS, obsoleted: 16.0, message: &quot;SwiftUI.View.scrollDismissesKeyboard is available on iOS 16.&quot;)\n</code></pre><p>This is very helpful and makes the compiler work for you.</p><h2 id=\"pick-your-battles\">Pick your battles</h2><p>Just like deciding what to do as a fallback based on tradeoffs, choosing which parameters and features to support is important too.</p><p>For example, if we want to port <a href=\"https://developer.apple.com/documentation/swiftui/view/presentationdetents(_:)\">presentationDetents(_:)</a> we will find out is a bit more complex. The custom detent needs some generic work that might not be trivial, so when you see that sort of complexity you can decide to just ignore it. Remember that this code is for you. It’s likely that you are not trying to make a full featured framework for the entire world, so if you don’t need some functionality right now, just skip it. By the time you need it, you will use the native solution already.</p><p>So in this case, you may decide to simplify the detents you offer:</p><pre><code class=\"language-swift\">public enum MyPresentationDetent: Hashable {\n    case medium\n    case large\n    case fraction(_ fraction: CGFloat)\n    case height(_ height: CGFloat)\n    // no custom detent supported\n}\n</code></pre><h2 id=\"more-than-back-porting\">More than back-porting</h2><p>This technique is not only useful to back port functionality to older OSs but also to improve functionality and make it work the way you want. You can use the same technique, ignoring the version checks, and that will give you a way to offer more reusable parameters and options and to have a reusable place to include extra logic, all of that while keeping the same syntax as the system.</p><p>This is beneficial because it allows your code to be easily adjusted when Apple adds new options natively, saving you from having to completely refactor multiple views.</p><h2 id=\"conclusion\">Conclusion</h2><p>This technique is just another tool on your belt. It’s up to you to decide when is appropriate to use it, but I recommend you to think about it. There are plenty of times when we could use modern APIs with little problem while keeping compatibility with older systems.</p><p>Just have it in mind, analyze the tradeoffs and decide.</p>",
      "date_published" : "2023-10-19T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/backport-swiftui-modifiers",
      "summary" : "Every year SwiftUI improves and gets new modifiers that widen the capabilities of the framework. Sadly, as responsible developers, we can’t use any of that because our users take some time to update and, as opposed to the Android ecosystem where Jetpack Compose is just a library embedded in every app, we need to wait for our users to be at the required minimum OS version to enjoy those fancy new tools.",
      "tags" : [
        "Swift",
        "SwiftUI"
      ],
      "title" : "Backport SwiftUI modifiers",
      "url" : "https://alejandromp.com/development/blog/backport-swiftui-modifiers"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>It’s been a while since the last time I thought about traits and mixins in Swift, but the <a href=\"https://alejandromp.com/development/blog/wwdc23-notes/\">WWDC23</a> presentations about macros brought a lot of memories from those times when I really lamented not having that functionality in the language. Thankfully, <a href=\"https://alejandromp.com/tags/swift/\">Swift</a> 5.9 gives us a form or meta-programming that allows us to get some basic support for mixins.</p><p>It’s still not fully generic since macros don’t give you all the type information you need for that yet, but they are functional and usable via ad hoc macros.</p><p>But also I don’t think we have a big need for them anymore, the development landscape has changed a lot since those days.</p><p>Check out this video:</p><iframe width=\"560\" height=\"315\" src=\"https://www.youtube-nocookie.com/embed/znqIjEE0a84\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen></iframe>\n<p>Previous conversations about traits and mixins:</p><ul><li><a href=\"https://alejandromp.com/development/blog/uiviewcontroller-extension-keyboard-avoidance/\">UIViewController extension for keyboard avoidance, and the missing features of Swift | Alexito’s World</a></li><li><a href=\"https://alejandromp.com/development/blog/im-not-the-only-one-that-wants-traits-in-swift/\">I’m not the only one that wants traits in Swift | Alexito’s World</a></li><li><a href=\"https://alejandromp.com/development/blog/jonathan-blow-libraries-discussion-jai/\">Jonathan Blow’s libraries discussion | Alexito’s World</a></li></ul>",
      "date_published" : "2023-06-11T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/swift-5-9-macros-unlock-mixins",
      "summary" : "It’s been a while since the last time I thought about traits and mixins in Swift, but the <a href=\"https://alejandromp.com/development/blog/wwdc23-notes/\">WWDC23</a> presentations about macros brought a lot of memories from those times when I really lamented not having that functionality in the language. Thankfully, <a href=\"https://alejandromp.com/tags/swift/\">Swift</a> 5.9 gives us a form or meta-programming that allows us to get some basic support for mixins.",
      "tags" : [
        "Swift"
      ],
      "title" : "Unlocking Mixins with Swift 5.9 Macros",
      "url" : "https://alejandromp.com/development/blog/swift-5-9-macros-unlock-mixins"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>WWDC23 is over and as previous year I’m sharing my <strong>unedited</strong> notes from all the sessions I’ve watched.</p><p>This year has been quite entertaining as the main focus has been on the new platform, something that hasn’t taken too much brain power of mine. visionOS is very cool, but it still feels like a very narrow and futuristic device. For what I care, the software and the engineering, there are two parts: apps as we know them, and new spatial design. For the first one there is not much difficult things to learn since is all based on all the farmeworks we know and love. For the spatial design it requires bringing 3Dc ontent into apps, which is a different discipline that although I’ve dabbled with, it’s not in my priorities right now. The combination of that means all the visionOS talks have been a breeze to go trough, like watching a scifi show.</p><p>The rest of sessions have been on the usual topics and frameworks. In general I’ve found all of them quite straightforward, without anything new or revolutionary that would make by brain go boom. Most of this is thanks to Swift being open source, which removes the surprise factor of the things I’m interested in by a lot. I basically knew everything coming on that front already. So that leaves SwiftUI as the delightful surprise machine.</p><p>I’m not complaining, is good to have a year a bit more relaxed. Before WWDC started I already decided to take this year a bit calmed, for my own mental health. I could jump in the wagon of content generation but I prefer to enjoy it, treat it as entertainment and learn with it. It’s way more fun!</p><h2 id=\"favorite-sessions\">Favorite sessions</h2><p>After long nights and early mornings, I’ve watched more than 90 sessions. That’s pretty much every single session that is not gaming, ML, web or business related 😂. It’s not that much time when you watch it an higher speed ⏭️ I surprised even myself, but with all the sessions being quite low in new content time flew by, it was very enjoyable!</p><p>From all of those the ones I’ve kept bookmarket are:</p><ul><li><a href=\"https://developer.apple.com/wwdc23/10156\">Explore SwiftUI animation</a></li><li><a href=\"https://developer.apple.com/wwdc23/10160\">Demystify SwiftUI performance</a></li><li><a href=\"https://developer.apple.com/wwdc23/10037\">Explore pie charts and interactivity in Swift Charts</a></li><li><a href=\"https://developer.apple.com/wwdc23/10170\">Beyond the basics of structured concurrency</a></li><li><a href=\"https://developer.apple.com/wwdc23/10248\">Analyze hangs with Instruments</a>, probably the best session with hands-on explanations on how to solve performance issues, exploring all different situations and giving nuggets of knowledge along the way.</li></ul><p>Even if this year the content was not super impactful for me, every single session and presenter is amazing. The quality is over the clouds. They all are interesting to watch and listen, and present the topics in a very entertaining and interesting manner. And I love how Apple, a huge coorporation, let’s every presenter add a bit of their own personality to the videos. It makes them so much approachable. My congratulations to all the ones involved in making this sessions so enjoyable!</p><h2 id=\"raw-notes\">Raw Notes</h2><blockquote><p>This won’t include every detail in all sessions. The notes reflect things that have caught my attention and that I want to remember their existence for later. Details are best obtained by going to the documentation when needed knowledge is up to date.</p></blockquote><h3 id=\"keynote\">Keynote</h3><ul><li>Stickers in third party apps like emojis?</li><li>Journal! gratitude and memories with suggestions from photos, music, trips…<ul><li>suggestions api</li></ul></li><li>health mood tracking with state of mind, assessments</li><li>Craig: “multiple timers, we truly live in an age of wonders.”</li><li>vision health, how much time spent outdoors in sunlight, screen distance</li></ul><h3 id=\"sotu\">SotU</h3><blockquote><p>With WidgetKit, you can surface your content in many places across the system. With App Intents, your app’s functionality is integrated into the system even more seamlessly. With TipKit, you can surface beautiful tips directly in your app. And with AirDrop, you can make it even more convenient for your users to share content with those nearby.</p></blockquote><ul><li>WidgetKit<ul><li>Available on StanBy mode, lockscreen on ipad, and mac desktop. with background removed.</li><li>Identify the background (.containerBackground modifier) and update its padding (remove it, it comes by default now).</li><li>Interactivity with buttons and toggles to trigger App Intents.</li><li>Transitions supported.</li></ul></li><li>TipKit<ul><li>Configure tooltips to be displayed to users with rules. Sync across devices.</li></ul></li><li>Values:<ul><li>Accessibility:<ul><li>new pause animated images flag</li><li>avoid bright flashing lights in videos</li></ul></li><li>Privacy</li></ul></li><li>App Store<ul><li>New swiftui api to make the subscribe screens following apple standards.</li></ul></li><li>Xcode<ul><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23xcodesummary.jpg\" alt=\"\"></li><li>Code generation for asset catalogs</li><li>Macro for swiftui previews (and for uikit and appkit!)</li></ul></li><li>visionOS<ul><li>Windows, Volumes and Spaces</li><li>Existing apps are just using Windows, but if you rebuild you get the proper look and feel with materials.</li><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23visionOSstack.jpg\" alt=\"\"></li><li>ZStacks can have depth.</li><li>Mix SwiftUI with RealityKit.</li><li>Ornaments fix UI elements to the side of the window, great for toolbars and menus.</li><li>Hover effects where the user looks.</li></ul></li></ul><h3 id=\"swiftui\">SwiftUI</h3><p><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23swiftuisummary.jpg\" alt=\"WWDC23 SwiftUI Summary\"></p><h4 id=\"charts\">Charts</h4><ul><li>Pie and Donut charts with new SectorMark</li><li>Chart Selection that automatically handles the gesture recognitizers to allow selection along axis, ranges and angles. Allows to provide a custom gesture whilte still making it easy to select the value based on the gesture location.</li><li>Scrollable with chartScrollableAxis and specify the visible domain. It also supports scroll target behaviours.</li></ul><h4 id=\"inspectors\">Inspectors</h4><p>Views that show further detail of selected content.Is a right sidebar that adapts to a sheet on smaller sizes.</p><h4 id=\"presentation\">Presentation</h4><p>New <code>presentationBackground</code> to change the background of the sheets overlays, the overlay that covers the entire screen.<code>presentationBackgroundInteraction</code> let’s you enable the interaction of the content behind even when a sheet is up.<code>presentationCornerRadius(_:)</code><code>presentationContentInteraction(_:)</code><code>presentationCompactAdaptation(_:)</code>Some of these modifiers also work when Inspectors are presented as sheets.</p><h4 id=\"animations\">Animations</h4><p>Phases:</p><ul><li>loop between phases applying styles on each one.</li><li>starts immediately on appearance</li><li>or pass a trigger to animate when it changes</li></ul><p>Keyframes:</p><ul><li>Run parallel animation tracks, inside each track is a sequence</li><li>Refreshes the view on every frame</li><li>MapKit has support to keyframe the camera</li><li>KeyFrameTimeline allows you to evaluate an animation, and even display it in a chart.</li></ul><p>UnitCurve and Spring contain the algorithms/curves that drive the change of values. You can use them directly to get a value at a specific point in time.</p><p>CustomAnimation protocol to write custom animation curves. Can impleemnt velocty and merge behaviour to mix with other animations that was triggered while another was running.</p><p>Transaction is now public. Is another implicit-data flow construct, a dictionary that is passed trough the view hierarchy on every update and discarded at the end. it’s used for animatable views to know if they should animate on this update. It’s what withAnimation does, and the animation modifier touches. Now you can add your own data to know if during a body update it was triggered programmatically or by the user for example.A new animation modifier with a closure let’s you apply the animation just to specific effects instead of inserting it in the view hierarchy. the other children views are not affected by this animation, unlike the normal animation modifier that injects the animation to all children.</p><ul><li>transfer velocity auomatically, smooth spring by default</li><li>sfsymbol animated</li><li>contentTransition(.numericText(value: totalCaffeine.value))</li></ul><h4 id=\"observable\">Observable</h4><p>macro, so we just need State <img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23swiftuipropertywrappers.jpg\" alt=\"\">te and Environment wrappers now.</p><h4 id=\"scroll-effects\">Scroll effects</h4><p><code>scrollTransition</code> to modify the rows when the enter the visible area.</p><p><code>scrollTargetBehaviour(.paging)</code> for full screen paging deceleration<code>scrollTargetBehaviour(.viewAligned)</code> to decelerate targeting a view, to mark which views are targets use <code>scrollTargetLayout</code> for lazy stacks (important because not all views are created yet, but the stack knows and will tell the scroll) or <code>scrollTarget</code> for the individual views themselves.Can use custom behaviours with <code>ScrollTargetBehaviour</code> conformance.</p><p><code>safeAreaPadding</code> modifier, so the scrollview still extends to the full screen but its views are padded (both your content and its scroll indicators).<code>contentMargin</code> modifier to specify padding only to the content or to the scroll indicators.</p><p><code>containerRelativeFrame</code> to let the view grow based on the container size. And can use parameters to let multiple views share the space, making grid like layouts.</p><p><code>scrollPosition(id:</code> gives a binding to scroll to that view.</p><h4 id=\"visual-effects\">Visual Effects</h4><p>It gets the content and a geometry proxy.</p><p>You can use a coordinate space outside of the view, and compute the distance of a point in the space to the view using the proxy, then use that distance to change the content with effects.</p><h4 id=\"vertical-tabs-in-watchkit\">Vertical tabs in watchkit</h4><h4 id=\"menu-palette\">Menu palette</h4><p>Picker with the palette style</p><h4 id=\"performance\">Performance</h4><p>Make sure IDs are fast to compute.Just pass to views what they need, otherwise they depend on more.ForEach should just have 1 view so it can match the ID with it. It eagerly gets all IDs, but lazy the views. If there are more views it needs to easily get them too which is bad.</p><h4 id=\"focus\">Focus</h4><p>You can specify the interaction for when a view is focused. (edit or activate)Default focus to specify which element should be focused by default.New onKeyPress modifier.Focus values to tell menu commands which view is in focus.Focus sections to let directional focus (like in tvOS) fall into a non-adjacent view that otherwise wouldn’t be elegible for focus.</p><h3 id=\"widgets\">Widgets</h3><ul><li>Animations and transitions when the data of the widget changes</li><li>Interactive<ul><li>Can use buttons and toggles with new inits that trigger an app intent.</li><li>App intent interactions are ensured to trigger a reload immediatly, unlike tmieline changes.</li><li>Invalidate content modifier for specific views that should show interaction update to the user faster, specially when is on mac since it goes over wifi.</li><li>Toggle optimistically updates the state so appear faster</li></ul></li><li>Widgets show in mac via continuity</li><li>Widgets for watch stack have design recommendations, instead of hardcoded layouts like in the original apis<ul><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23watchwidgetslayouts.jpg\" alt=\"\"></li></ul></li></ul><h3 id=\"coredata\">CoreData</h3><ul><li>Composite attributes, alternative to transformables</li><li>Staged schema migrations</li><li>Deferred migrations, to delay the cleanup of schema migrations (colum or table removals) for later to not block the user.</li></ul><h3 id=\"swiftdata\">SwiftData</h3><p><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23swiftdatasummary.jpg\" alt=\"\"></p><ul><li>Built on top of proven core data persistance layer (?what does this mean??)</li><li>Schemas and definitions all in code, with macros. No external schema files.</li><li>@Model gives you:<ul><li>Persistence</li><li>Schema modeling</li><li>Lightweight migration</li><li>Relationship management</li><li>iCloud synchronization</li><li>Spotlight search</li><li>Undo/Redo</li></ul></li><li>@Query to fetch and show results in SwiftUI.</li><li>Model container understands the relationships of the schema so even if you just give it 1 type it will infer the related types it needs to persist.</li><li>Advanced model configuration and schema can combine multiple different schemas with different stores into 1.</li><li>Get the model context trough the environment to operate with the database. It propagates changes to the model container.</li><li>It uses the undo manager from the environment to automatically support undo/redo.</li><li>Auto saves in sync with SwiftUI related events and system events. You can also save manually.</li><li>Fetch trough the context with fetch descriptor and the new predicate macros, which give compiler validated queries.</li><li>Use the context enumerate method to effeminately fetch lists of data, uses automatically batch processing under the hood, and safe mutation guards. It can be customized.</li><li>Document based apps by using a SwiftData model as a packaged file</li><li>But models are still classes…  they haven’t embraced an immutable model. Pretty weird. :(</li></ul><h3 id=\"cloudkit\">CloudKit</h3><p>CKSyncEngine.Between high level core data integration and low level operations.</p><h3 id=\"tipkit\">TipKit</h3><p>Looks very interesting, specially with the rules system that will prompt the tips at the appropiate moment and a consistent UI.</p><h3 id=\"foundation\">Foundation</h3><ul><li>Improvements in gramatical agreement with other arguments and even concepts outside the string.</li><li>New terms of address for pronouns</li><li>Resumable upload url session tasks, and support in SwiftNIO</li></ul><h3 id=\"swift\">Swift</h3><ul><li>if/switch expressions</li><li>improved result builders</li><li>type parameter packs</li><li>macros<ul><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23macroroles.jpg\" alt=\"\"></li><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23swiftbuild-incodeexpansion.jpg\" alt=\"\"></li><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23macroroleprotocols.jpg\" alt=\"\"></li><li><img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23macronamespecifiers.jpg\" alt=\"\"></li></ul></li><li>native swift foundation</li><li>ownership</li><li>c++ interoperatibility</li><li>discarding task group</li></ul><h3 id=\"xcode\">Xcode</h3><ul><li>Improved suggestions order</li><li>New localization strings catalog format<ul><li>Eventually will replace strings and stringdict.</li><li>Allows variant of strings based on platform (click vs tap)</li><li>Can make multiple catalogs to have different string tables</li><li>Xcode automatically extracts localizable strings from code. Use localised initialisers in strings and LocalizedStringResource.</li><li>In SwiftUI use LocalizedStringResource in your views to accept localizable strings</li></ul></li><li>Code gen asset catalog and localisation access</li><li>Documentation preview in editor assistant</li><li>Preview macro and UIKit/appkit support</li><li>Bookmarks</li><li>Source control improvements</li><li>test results new design</li><li>oslog integration in Xcode output, with filters</li><li>Streamlined workflows for app distribution</li></ul><h3 id=\"uikit\">UIKit</h3><p>Custom traits let you bridge with swiftui environmentStatus bar changes color automaticallyPage control with progress and timer.Menu paletteNew text selection style. Comes from free with system text views. Now can also be used in custom text views together with the loupe.New <code>UITextItemTagAttributeName</code> to tag ranges in a text so we can get interactions, ideal for “See More” in text! And it has new APIs to change the default menu behaviour when interacting with them.List and bullets supported in attributed string.</p><h3 id=\"appkit\">AppKit</h3><ul><li>Table column customizations for free</li><li>NSProgress support in NSProgressIndicator</li><li>Popover background can expand to the chevron</li><li>NSBezier path, CGPath interoperability</li><li>CADisplayLink!</li><li>More Sendable everywhere!</li></ul><h3 id=\"app-intents\">App Intents</h3><p>Improvements on app intentsApp shortcuts are now discoverable in Spotlight, design for it.<img src=\"https://alejandromp.com/development/blog/wwdc23-notes/WWDC23appintentsintegrations.jpg\" alt=\"\"></p><h3 id=\"accessibility\">Accessibility</h3><ul><li>isToggle trait</li><li>AccessibilityNotification.{Anouncement}.post</li><li>Anouncement priority to avoid interruptions</li><li>Direct touch options</li><li>Accessibility shape</li></ul><h3 id=\"app-clips\">App Clips</h3><ul><li>new size limit 50mb for digital invocations</li><li>Default app clips urls hosted by the app store</li><li>launching app clips from apps</li></ul><h3 id=\"privacy\">Privacy</h3><ul><li>new calendar write only permission</li><li>New embedded photos picker that is smaller, so users picks what they want without requiring to give gallery permissions.<ul><li>Customizable: Hide accessory UI, buttons, specify frame and padding, get selections in real time.</li><li>Inline style, ideal to combine with hidden accessories.</li><li>Compact style that is just a row.</li></ul></li><li>new macOS window picker to screen recording</li><li>support for OHTTP relay</li><li>Communication safety for apps with Sensitive Content Analysis framework to detect nudity. sensitive content warning to blur images.</li><li>Xcode privacy manifest generates privacy details from third party sdks.<ul><li>Tracking domains help the system block calls to those domains until the user has agreed to be tracked.</li><li>New Instrument that displays requests to potentially tracking domains</li><li>Required reason APIs are apis that have potential to be misused for fingerprinting. apps and sdks must declare a reason to use them.</li></ul></li><li>Signatures for third party sdks to validate the code comes from the correct authors.</li></ul><h3 id=\"core-location\">Core Location</h3><p>New Monitor API based on actors.</p><h3 id=\"other\">Other</h3><ul><li>VisionKit to integrate photo subject lifting into our apps.</li><li>Accessibility audits now can be automated in UI Tests</li><li>SFSymbols components to mix in your custom ones.</li><li>Push Notification Console website<ul><li>Send push to specific device for testing and development</li><li>Delivery log to know what went wrong. Put device in low power mode to not receive them.</li><li>Debug tool to validate tokens.</li></ul></li><li>Improved App Store Connect APIs and a swift server library</li><li>DockKit is pretty cool. I didn’t know iOS had this functionality out of the box!</li><li>Keyboard is now out of process.<ul><li>Remember to use the keyboard autolayout guide in UIKit. In SwiftUI the safe area includes the keyboard automatically. If you are still using the old notifications check out the session <a href=\"https://developer.apple.com/wwdc23/10281\">Keep up with the keyboard</a> because with all the recent keyboard modes it doesn’t behave as expected.</li></ul></li></ul>",
      "date_published" : "2023-06-08T22:00:00Z",
      "id" : "https://alejandromp.com/development/blog/wwdc23-notes",
      "summary" : "WWDC23 is over and as previous year I’m sharing my <strong>unedited</strong> notes from all the sessions I’ve watched.",
      "tags" : [
        "WWDC",
        "Swift",
        "SwiftUI",
        "Xcode",
        "iOS Development",
        "macOS Development"
      ],
      "title" : "WWDC23 notes",
      "url" : "https://alejandromp.com/development/blog/wwdc23-notes"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>One of the many things I love about working with <a href=\"https://alejandromp.com/tags/pointfree-composable-architecture/\">The Composable Architecture</a> is how it encourages the developer to focus on domain modeling. I think that properly designing the models or entities of a domain is one of the biggest contributors into good code, and the lack of it makes everything way harder than it should be. Thankfully <a href=\"https://alejandromp.com/tags/swift/\">Swift</a> is a great language that allows us to express almost everything we need to in a very strict and concise way, another reason the language of your choice matters most than people pretends.</p><p>So the topic I will discuss here is not really specific to the TCA, but more about domain modeling and types. Let’s start by exposing the issue:</p><h2 id=\"modeling-the-problem\">Modeling the problem</h2><p>Let’s imagine we want to model the result of some side effect, like an API call, that can succeed and fail. We can easily do this with Swift’s <code>Result<Success, Failure></code> type, but in this case we <strong>do not care</strong> about the success data at all, only about the failure. In other words, we want to know which specific error occurred, but if it succeeded we don’t want to know anything about the success data.</p><p>Of course if you have done any meaningful coding with TCA this will feel very familiar, and indeed is where I find this more often, with its other variations of I only care about success data and not the error, I don’t care about any of the two, etc… But let’s ignore that for now and see what the issue is and why is so important to model this correctly.</p><h2 id=\"algebra\">Algebra</h2><p>What? Math? Seriously? Yes! It’s actually the easiest way of realizing what you are doing when modeling a solution. Don’t fear, it’s very simple.</p><blockquote><p>If you want to learn more about <a href=\"https://www.pointfree.co/collections/algebraic-data-types\">Algebraic Data Types</a> I really recommend <a href=\"https://www.pointfree.co/subscribe/personal?ref=4Fj20c5I\">subsribing to Pointfree</a> (get one month free with this link)</p></blockquote><p>Let’s first think how many potential scenarios the most basic design gives us:</p><pre><code class=\"language-swift\">Result&lt;Int, Error&gt;\n</code></pre><p>With this type, we have <code>n</code> possibilities for the integer in case of success, and <code>m</code> possibilities for the error in case of failure. (let’s assume <code>Int</code> is what the side effect responds with). These are <code>n + m</code> cases we would have to deal with, too many! Remember, we don’t care about the success data.</p><p>So if we don’t care about the success and only about the error, maybe we can ditch the Result type completely and just use the Error.</p><pre><code class=\"language-swift\">Error\n</code></pre><p>That will give us <code>m</code> possibilities, but we are lacking a way to know if it succeeded. We want to know that it succeeded, but not about its specific data. But Swift already provides with a tool for it, <code>Optional</code>.</p><pre><code class=\"language-swift\">Optional&lt;Error&gt; // Error?\n</code></pre><p>Now we have <code>m</code> from the <code>Error</code> and <code>1</code> from the optional nil case, so <code>m + 1</code>. Exactly the amount of cases we wanted!</p><p>Simple right?</p><p>But let’s say we want to still use the <code>Result</code> type for consistency with the rest of the code. How can we use that type, which requires a <code>Success</code> and still keep the same <code>m + 1</code>? We need a type that only gives us 1 option, and again, we have one. Swift calls it <code>Void</code> or <code>()</code> (the empty tuple), called Unit in other languages. With that knowledge, let’s use <code>Result</code> again.</p><pre><code class=\"language-swift\">Result&lt;Void, Error&gt;\n</code></pre><p>Thanks to <code>Void</code> we can use the <code>Result</code> type and still have <code>1 + m </code> cases to deal with.</p><p>This may seem like a minor thing, after all, you could just use the original data (<code>Int</code>) and just ignore it on your code. But that’s a slippery slope that makes your code harder to reason about. When your future self comes back and reads the code, the types will explain the story of what is happening without having to read the code. The compiler will make you remember the decisions you took when you already forgot them. Such is the power of properly modeling domains with a proper type system.</p><h2 id=\"the-limitations\">The limitations</h2><p>This is all nice but we need to use this response in another type, in the case of the TCA we need to embed this in an Action enum, but it could be any other type you use to carry data around.</p><p>This is how it would look if we cared about the success data:</p><pre><code class=\"language-swift\">struct ApiResponse {\n    let response: Result&lt;Int, Error&gt;\n}\n</code></pre><p>All well so far. But let’s make this type more realistic and make it conform to <code>Equatable</code>, which is something we need most of the time.</p><pre><code class=\"language-swift\">struct ApiResponse: Equatable { // Type &#39;ApiResponse&#39; does not conform to protocol &#39;Equatable&#39;\n</code></pre><p>Boom! 💥 The compiler can’t synthesise the conformance because our <code>Result</code> type is not equatable. <code>Int</code> is obviously fine, the problem is the <code>Error</code>.</p><p>The reason for this is a rabbit whole we don’t have time to get into now, but there are multiple solutions for it. Thankfully PointFree has already provided one for the users of the TCA in the form of <code>TaskResult<Success></code> (<a href=\"https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/taskresult\">docs</a>) so we can ignore this exact problem and move on to the one I want to discuss.</p><pre><code class=\"language-swift\">struct ApiResponse: Equatable {\n    let response: TaskResult&lt;Int&gt;\n}\n</code></pre><p>Now the compiler is happy again. Keep in mind that even if we don’t see the <code>Error</code> type anymore, the failure case is still there <code>case failure(Error)</code> so we still have the data we care about.</p><p>But we are still using the awful <code>Int</code>, let’s switch back to use our lovely <code>Void</code>.</p><pre><code class=\"language-swift\">struct ApiResponse: Equatable { // Type &#39;ApiResponse&#39; does not conform to protocol &#39;Equatable&#39;\n    let response: TaskResult&lt;Void&gt;\n}\n</code></pre><p>💥  Here we are again! Now it’s <code>Void</code> that is not <code>Equatable</code> and if you know a bit about Swift you know why.</p><p><code>Void</code> is a non-nominal type, which in simple terms it means there is not a name you can use to refer to it in some language features. One such feature is protocol conformance, which allows types to adopt a set of behaviors defined by a protocol, in this case equatability. Because Void is not a named type, Swift does not inherently know whether it conforms to any given protocol. Although we might want to make Void conform to Equatable ourselves by defining an extension, we can’t do it because we cannot reference it by name! Therefore, we can’t retroactively make Void conform to any protocol. 😞</p><pre><code class=\"language-swift\">extension Void: Equatable {} // Non-nominal type &#39;Void&#39; cannot be extended\n</code></pre><p>So if we can’t use <code>Void</code>, maybe we can use the alternative of using an <code>Optional</code>?</p><p>Well, that brings us back the problem with <code>Error</code> that we solved with <code>TaskResult</code> so is not a solution.</p><pre><code class=\"language-swift\">struct ApiResponse: Equatable { // Type &#39;ApiResponse&#39; does not conform to protocol &#39;Equatable&#39;\n    let response: Error?\n}\n</code></pre><h2 id=\"equatablevoid\">EquatableVoid</h2><p>My favorite solution to this problem is just to make <code>Void</code> a nominal type. Of course you can’t make the existing one but you can just make your own. Remember how we described Void as the empty tuple <code>()</code>? A tuple is an anonymous product type, but we can use a named product type instead.</p><pre><code class=\"language-swift\">struct EquatableVoid: Equatable {}\n</code></pre><p>Our <code>EquatableVoid</code> it has the same shape a <code>Void</code>, it only has 1 possible value which we can get by calling the initializer <code>EquatableVoid()</code>. Is not as nice as just <code>()</code> but such is the price we need to pay.</p><p>With this new type, we can change our response and finally have what we want:</p><pre><code class=\"language-swift\">struct ApiResponse: Equatable {\n    let response: TaskResult&lt;EquatableVoid&gt;\n}\n</code></pre><p>This gives us <code>1 + m</code>  cases, no more, no less. We have expressed exactly our domain.</p><p>We can use our types like so:</p><pre><code class=\"language-swift\">let ok = ApiResponse(response: TaskResult&lt;EquatableVoid&gt;.success(EquatableVoid()))\nlet ko = ApiResponse(response: TaskResult&lt;EquatableVoid&gt;.failure(Badabum.granBadabum))\n</code></pre><p>And work with them in a switch statement exactly like we wanted:</p><pre><code class=\"language-swift\">switch response {\ncase .success(let void):\n    // not much you can do here, you only have 1 possible value that is always the same\ncase .failure(let error):\n    // but here you have all errors so you can work with them as you please\n}\n</code></pre><h2 id=\"other-unsatisfactory-solutions\">Other unsatisfactory solutions</h2><p>There are, of course, other solutions that have other tradeoffs, so as usual I give you the tools so you can take the decision on your own.</p><p>You could use the optional alternative by using a specific error type that was equatable: <code>SpecificEquatableErrorForYourDomain?</code>. That would work but then you need to reconcile your specific error type with Swift’s untyped errors. With time I’ve learned to embrace Swift’s error system and stop fighting it, so I don’t recommend this. Is a fix for this exact part of the code, but you will have to write patches in other parts to deal with any errors and default cases.</p><p>Another solution would be to use <code>NSError</code>, which is convertible to <code>Error</code> and is equatable. I tend to run away from it. I’m fine receiving it from Objective-C APIs but I want my Swift code to be Swift code. <code>NSError</code> loses the type safety at the usage side. You can’t cast it to a specific type but need to rely on strings and integers. It’s not a tradeoff I’m willing to make.</p><p>And I imagine there are other ways of solving this with different tradeoffs, but I’m not aware of one that gives me what I want in a better way :)</p><h2 id=\"other-usages-of-this-technique\">Other usages of this technique</h2><p>This technique is not something new that I only use for this case, in fact this is the third occurrence in my codebase of this solution.</p><p>In our project, we use the TCA to its fullest and I made a generic loading system that we are very happy with. The closure that lets you customize how to get the data receives a parameter that can be injected from other domains. Think of an ID that you need to fetch a user, for example. But not all screens need parameters, so you would want to define the generic type as <code>Void</code>, but again it needs to be equatable. In this case, I employ another trick that let developers avoid even constructing this type at all.</p><pre><code class=\"language-swift\">public typealias NoParameter = _NoParameter?\npublic struct _NoParameter: Equatable {}\n</code></pre><p>By using <code>NoParameter</code> as the generic type, developers (in fact our library has this default cause supported) can just pass nil when constructing the domain and they don’t have to deal with the parameter in their implementation.</p><blockquote><p>I should talk about our generic loading system because I think is pretty neat. <a href=\"https://mastodon.social/@alexito4\">Ping me</a> if it’s something you would be interested in :)</p></blockquote><p>Another place where this pattern occurred is in the API library I implemented, which uses Swift’s generic type system to make it very simple to define endpoints. In an endpoint, there is a generic parameter that defines the response model you want to decode, but of course, sometimes you don’t really want to decode anything. The same goes for the generic parameter that specifies which parameter to send to the API. For that, again, you would love to use <code>Void</code> but it’s not <code>Decodable</code> or <code>Encodable</code> so it won’t work.</p><pre><code class=\"language-swift\">public struct Ignore: Equatable, Codable {}\n</code></pre><p>Maybe I should think about making a <code>SuperVoid</code>  that covers all the cases :)</p><h2 id=\"conclusion\">Conclusion</h2><p>Domain modeling matters, and having a language with a type system that supports us when designing, is crucial. Swift gets very far, but it still has some limitations with non-nominal types that thankfully can be solved by defining our own types to cover for these cases.</p>",
      "date_published" : "2023-03-08T23:00:00Z",
      "id" : "https://alejandromp.com/development/blog/non-nominal-types-workaround-for-domain-modeling",
      "summary" : "One of the many things I love about working with <a href=\"https://alejandromp.com/tags/pointfree-composable-architecture/\">The Composable Architecture</a> is how it encourages the developer to focus on domain modeling. I think that properly designing the models or entities of a domain is one of the biggest contributors into good code, and the lack of it makes everything way harder than it should be. Thankfully <a href=\"https://alejandromp.com/tags/swift/\">Swift</a> is a great language that allows us to express almost everything we need to in a very strict and concise way, another reason the language of your choice matters most than people pretends.",
      "tags" : [
        "Swift",
        "PointFree Composable Architecture",
        "Software Design and Architecture"
      ],
      "title" : "Non-nominal types workaround for domain modeling",
      "url" : "https://alejandromp.com/development/blog/non-nominal-types-workaround-for-domain-modeling"
    },
    {
      "authors" : [
        {
          "name" : "Alejandro M. P."
        }
      ],
      "content_html" : "<p>Buckle up. This is an adventurer journal while exploring what’s the best way of exposing the <em>new</em> style of formatting APIs in Foundation. We will walk through the paths of the type system, including existential types, generics and type erasure; and discover the magical possibilities of the dependency system.</p><h2 id=\"a-clash-of-styles\">A clash of styles</h2><p>This idea came to mind after finding a clashing in styles of development. When I work on my side projects and I have to do things quickly, mostly for fun, I love the new formatting APIs,  <code>thePast.formatted(.relative(presentation: .numeric, unitsStyle: .abbreviated))</code>. But then when working in a proper product, in a team, with a long-term vision, you need to use proper engineering techniques, which often involves doing dependency injection.</p><p>I use PointFree’s dependency library (as part of the TCA) which I’ve discussed previously (<a href=\"https://alejandromp.com/development/blog/on-the-new-pointfree-swift-dependencies/\">On the new Point-Free swift-dependencies library</a>) but this applies to any other DI technique pretty much. Usually when you want to DI something that lets you format, the best way is to provide a function that takes the input value and gives you an output value, typically a <code>String</code>.</p><pre><code class=\"language-swift\">public struct DateFormatter {\n    public let format: (_ date: Date) -&gt; String\n}\n</code></pre><p>Then the <code>live</code> implementation of this dependency can use a <code>DateFormatter</code> or the new <code>.formatted</code> style. The advantage of having this API as a dependency is that it allows you to <strong>very</strong> easily switch the implementation for testing and previews.</p><pre><code class=\"language-swift\">static var testValue: DateFormatter = XCTUnimplemented(&quot;\\(Self.self).format&quot;)\nstatic var previewValue: DateFormatter = { _ in &quot;Tue, Nov 29&quot; }\nstatic var liveValue = { $0.formatted(...) }\n</code></pre><p>Being able to just have a hard-coded value for previews or testing without having to deal with the formatters themselves is a tremendous benefit that can’t be underestimated.</p><p><strong>But</strong> as nice as this is, I resented the usage side not being as nice and natural as using the Foundation fluent API.</p><pre><code class=\"language-swift\">formatDate(selectedDay.date)\n</code></pre><p>It’s not bad, but I prefer the fluent API in this case. I just enjoy it a lot more when working on my side projects, so I started thinking how I could keep the joyfulness of the code but still adding the seriousness of DI.</p><h2 id=\"formatstyle-instances\">FormatStyle instances</h2><p>To start, I had to learn more about how the newly fluent API worked. I explored it in the past but Apple’s documentation is not super clear in this area, but then I was pointed to <a href=\"https://goshdarnformatstyle.com/\"><strong>Gosh Darn Format Style!</strong></a> which does a great job of explaining how the API works.</p><p>The key is understanding that behind these fancy fluent APIs there is one protocol: <code>FormatStyle</code>. All the dot syntax calls create types conforming to that protocol and fluently mutate them. In other words, you can hold an instance of the style that you want.</p><pre><code class=\"language-swift\">// wo this seems so complex!\n(2.5).formatted(.number.decimalSeparator(strategy: .always).precision(.integerLength(1)))\n\n// but is just really a mutation of structs\nlet numberStyle = FloatingPointFormatStyle&lt;Double&gt;.number\nlet withDecimalSeparator = numberStyle.decimalSeparator(strategy: .always)\nlet withPrecision = withDecimalSeparator.precision(.integerLength(1))\n(2.5).formatted(withPrecision)\n</code></pre><p>This seems obvious, but I think this API has so many combinations and you get so overwhelmed with the auto-completion, that is sometimes hard to realise the simplicity of it. I think the eye-opening moment is when you actually understand that the API surface is not in the extension of the <em>input type</em> but in the fluent extensions of the concrete <code>FormatStyle</code>. The <code>Date</code>, <code>Double</code>, … types are not the ones with the complex API, they just have one method, the <code>.formatted</code> call. It’s in their specific formatter styles that the complexity resides.</p><p>What this also means is that you can easily create your predefined instances of styles in order to reuse them in your application. I think this is key because otherwise the risk of slightly doing different formats in different views is too big.</p><pre><code class=\"language-swift\">extension FormatStyle where Self == FloatingPointFormatStyle&lt;Double&gt; {\n    public static var numberFancy: FloatingPointFormatStyle&lt;Double&gt; {\n        Self.number.decimalSeparator(strategy: .always).precision(.integerLength(1))\n    }\n}\n\n// Use numberFancy instead of the long chain from before\n(2.5).formatted(.numberFancy)\n</code></pre><h2 id=\"dependency-inject-formatstyle\">Dependency inject FormatStyle</h2><p>With this ability in our hands, let’s go back to defining how it would ideally work with dependency injection.</p><p>If we consider I would love to keep the fluent API and have DI the obvious usage looks like this:</p><pre><code class=\"language-swift\">@Dependency(\\.numberFancy) var style\nnumber.formatted(style)\n</code></pre><p>The simple way of accomplishing this with the dependency system just involves declaring the dependency using the <code>FloatingPointFormatStyle</code> type.</p><pre><code class=\"language-swift\">extension DependencyValues {\n    var numberFancy: FloatingPointFormatStyle&lt;Double&gt; {\n        get { self[NumberFancyFormatterStyleKey.self] }\n        set { self[NumberFancyFormatterStyleKey.self] = newValue }\n    }\n}\n\nprivate enum NumberFancyFormatterStyleKey: DependencyKey {\n    static let liveValue: FloatingPointFormatStyle&lt;Double&gt; = .number.decimalSeparator(strategy: .always).precision(.integerLength(1))\n}\n</code></pre><p>If you have used the Dependency system, this should be pretty familiar. We just declare the <code>DependencyKey</code> and provide the property <code>DependencyValues</code> to get the <code>KeyPath</code>. The important part is how we can now declare the <code>FloatingPointFormatStyle</code> we want and that is injected into our code, while it lets us still use the nice fluent syntax.</p><p>And we are done, right? Nope!</p><h2 id=\"hardcoding-an-output\">Hardcoding an output</h2><p>Remember the advantage of using a function: we can <em>easily</em> provide test and preview values without having to deal with the formatters themselves. With the new setup we lost this ability, there is no way, to my knowledge, to mutate a style implementation like <code>FloatingPointFormatStyle</code> and force it to return a custom string.</p><p>The alternative I pursued was to create a custom format style, after all, <code>FormatStyle</code> is just a protocol. It may seem that each input type, like numbers or dates, is tied to a specific FormatStyle type, but that’s not true. It may be confusing because the types are nested and so it feels they are hardcoded, but in reality, the <code>formatted</code> function is generic.</p><pre><code class=\"language-swift\">// From Foundation extension BinaryFloatingPoint\nfunc formatted&lt;S&gt;(_ format: S) -&gt; S.FormatOutput where Self == S.FormatInput, S : FormatStyle\n</code></pre><p>This constrained function lets you pass any <code>FormatStyle</code> with a <code>FormatInput</code> associated type that it’s a <code>BinaryFloatingPoint</code>. This means that it accepts a <code>FloatingPointFormatStyle</code> but also a custom type with the correct associated types.</p><p>Let’s make a format style that always returns the same string:</p><pre><code class=\"language-swift\">struct HardcodedNumberFormatStyle: FormatStyle {\n    typealias FormatInput = Double\n    typealias FormatOutput = String\n    \n    func format(_ value: FormatInput) -&gt; FormatOutput {\n        return &quot;hardcoded&quot;\n    }\n}\n</code></pre><p>And we can use this to format the Double from before:</p><pre><code class=\"language-swift\">number.formatted(HardcodedNumberFormatStyle())\n</code></pre><blockquote><p>To make it look nicer, we should provide an extension to allow dot syntax as shown above.</p></blockquote><p>This is all good, but how do we stuff this in our dependency system?</p><pre><code class=\"language-swift\">private enum NumberFancyFormatterStyleKey: DependencyKey {\n\tstatic let liveValue: FloatingPointFormatStyle&lt;Double&gt; = ...\n  static let testValue = HardcodedNumberFormatStyle()\n</code></pre><p>Of course, this doesn’t compile, the types of <code>liveValue</code> and <code>testValue</code> don’t match. But again, we don’t care about the specific types, just about the input and output types of the format style. So maybe if we use generics…</p><h2 id=\"welcome-to-type-system-land\">Welcome to type system land</h2><p>If we change the type to <code>some FormatStyle</code> the dependency is fine, but also useless. You can’t use the style because it’s not constrained, so the formatted function won’t accept it. The solution would be to specify the associated types like <code>some FormatStyle<Double, String></code> but sadly, the <code>FormatStyle</code> protocol doesn’t offer primary associated types. So we can’t use generics at this level.</p><p>A solution that occurred to me is to create a wrapper type that dispatches to the correct formatter accordingly. But the question is how to decide the dispatch.</p><pre><code class=\"language-swift\">struct WrappedNumberFormatStyle: FormatStyle {\n    func format(_ value: Double) -&gt; String {\n        return FloatingPointFormatStyle or HardcodedNumberFormatStyle\n    }\n}\n</code></pre><p>A typical way I would do this is by passing the correct style at init time. After all, the dependency system makes the decision for us.</p><pre><code class=\"language-swift\">private enum NumberFancyFormatterStyleKey: DependencyKey {\n    static let liveValue: WrappedNumberFormatStyle = .init(FloatingPointFormatStyle&lt;Double&gt;.number ...)\n    static let testValue: WrappedNumberFormatStyle = .init(HardcodedNumberFormatStyle())\n}\n</code></pre><p>But of course, this raises the question of how we store the style generically. Maybe we can try to make the type generic…</p><pre><code class=\"language-swift\">struct WrappedNumberFormatStyle&lt;Wrapped: FormatStyle&gt;: FormatStyle where Wrapped.FormatInput == Double, Wrapped.FormatOutput == String {\n</code></pre><p>But even if the type itself compiles, we have solved nothing. Generics end up polluting the api surface and it means we are back at the same problem. Using a different <code>Wrapped</code> type results in a different <code>WrappedNumberFormatStyle</code>.</p><p>Another possibility is to keep the wrapped property type erased and enforce the correct types in the init, that’s a technique that works well when you want to erase some types.</p><pre><code class=\"language-swift\">struct WrappedNumberFormatStyle: FormatStyle {\n    private let wrapped: any FormatStyle\n    \n    init&lt;T: FormatStyle&gt;(wrapped: T) where T.FormatInput == Self.FormatInput, T.FormatOutput == Self.FormatOutput {\n        self.wrapped = wrapped\n    }\n</code></pre><p>But this doesn’t compile because the existential type itself doesn’t conform to <code>Codable</code>, <code>Equatable</code> and <code>Hashable</code>, all protocol requirements inherited from <code>FormatStyle</code>. This means we would need to type erase all the requirements of those protocols… and at that point I’m starting to lose interest.</p><p>But just for fun, let’s assume that we don’t care about those requirements right now and move on, <a href=\"https://alejandromp.com/development/blog/hole-driven-development-swift-fatalerror\">hole driven development</a> for the win. The next challenge is to make the format call compile.</p><pre><code class=\"language-swift\">func format(_ value: Double) -&gt; String {\n    return wrapped.format(value)\n}\n</code></pre><p>The immediate problem is that since <code>wrapped</code> is an existential, the return type of the format call is not constrained, it returns <code>Any</code>. And since <code>Any</code> is not <code>String</code> it won’t compile. The fix here is easy: since we know the only way to create the <code>wrapped</code> property is going through the <code>init</code> that applies the constraints, we can assume the output type is a <code>String</code>, so a force cast should be safe.</p><pre><code class=\"language-swift\">func format(_ value: Double) -&gt; String {\n    return wrapped.format(value) as! String\n}\n</code></pre><p>But now we encounter another problem: <code>Member 'format' cannot be used on value of type 'any FormatStyle'; consider using a generic constraint instead</code>. This error is a bit weirdly explained, but what is happening is that since <code>wrapped</code> is an existential, even though it has the <code>format</code> method defined, the input type of the format call can’t be known at compile time and thus the requirement of <code>value: Double</code> being equal to the input type of <code>wrapped</code> is not satisfied.</p><p>Here is where I got stuck. I’m able, thanks to the recent additions to opening existential, to create a generic function that opens the wrapped type. But I haven’t figured out a way to relate that with the input type. I was hoping the following worked…</p><pre><code class=\"language-swift\">private func _format&lt;F: FormatStyle&gt;(\n    formatStyle: F,\n    value: F.FormatInput\n) -&gt; F.FormatOutput {\n    formatStyle\n        .format(value)\n}\n\nfunc format(_ value: Double) -&gt; String {\n    return _format(formatStyle: wrapped, value: value)\n}\n\n</code></pre><p>… but it can’t: <code>Type 'any FormatStyle' cannot conform to 'FormatStyle'</code>. Again, the error is a bit misleading because the issue is really with the second parameter. If the <code>_format</code> function just had 1 parameter <code>formatStyle: F</code> the compiler is able to open the existential at runtime and is all fine. But we can’t link the type of the second parameter <code>F.FormatInput</code> to another value and keep the relation with the first one.</p><p>I was confused by this for some time. I even read the proposal that introduced this functionality twice. But in the end, it makes sense. I’m passing a <code>Double</code> as the second parameter, but that has no relation with <code>wrapped</code> whatsoever. I wonder if there is some trick here that I’m not aware of.</p><p>The only way I know of making this compile is to add more force casts. And even though they should be safe since the <code>init</code> constraints the types of the wrapped instance, it feels too much trouble.</p><h2 id=\"less-generic-alternative\">Less generic alternative</h2><p>So I tried to come up with an alternative. The idea of passing the type from the outside is nice, especially because I would have loved to have a generic format style wrapper that could reduce boilerplate; but if that didn’t work, I wanted to try a less generic approach.</p><p>Let’s still keep the dispatch decision on the dependency system, but specify the types of the formatters we need. Keeping the types concrete would solve the problem we have. For this I decided to switch to an enum with associated types since it felt like the quickest solution.</p><pre><code class=\"language-swift\">enum WrappedNumberFormatStyle: FormatStyle {\n    case foundation(FloatingPointFormatStyle&lt;Double&gt;)\n    case hardcoded(String)\n    case unimplemented\n    \n    func format(_ value: Double) -&gt; String {\n        switch self {\n        case .foundation(let floatingPointFormatStyle):\n            return floatingPointFormatStyle.format(value)\n        case .hardcoded(let string):\n            return string\n        case .unimplemented:\n            return XCTestDynamicOverlay.unimplemented()\n        }\n    }\n}\n</code></pre><p>There are no generics in this enum. All types are concrete, so we don’t have any problems with the type system. We exchange a sort of dynamic dispatch with a static and manual dispatch instead. I also took the opportunity to remove the <code>HardcodedNumberFormatStyle</code> type since a <code>String</code> in the <code>hardcoded</code> does the same job, and also add the <code>unimplemented</code> functionality for tests.</p><p>With this, the dependency integration is quite simple:</p><pre><code class=\"language-swift\">private enum NumberFancyFormatterStyleKey: DependencyKey {\n    static let liveValue: WrappedNumberFormatStyle = .foundation(FloatingPointFormatStyle&lt;Double&gt;.number.decimalSeparator(strategy: .always).precision(.integerLength(1)))\n    static let testValue: WrappedNumberFormatStyle = .unimplemented\n    static let previewValue: WrappedNumberFormatStyle = .hardcoded(&quot;hardcoded&quot;)\n}\n</code></pre><p>This works!</p><p>Now, I admit that seeing <code>    case foundation(FloatingPointFormatStyle<Double>)</code> still makes me want to make it generic but we’ve been through that rabbit hole so there is no need to repeat it 😂</p><h2 id=\"locale-for-testing\">Locale for testing</h2><p>One of the big benefits of having the dependency system in place is that we can solve one of the annoying parts of formatters: dealing with the locale. It’s true that with the code above we can control the format style dependency itself, but if we wanted to still use the real one for testing, we would still have to deal with the locale. A very nice thing we can do is use the <code>@Dependency</code> ourselves in the implementation.</p><pre><code class=\"language-swift\">func format(_ value: Double) -&gt; String {\n    @Dependency(\\.locale) var locale\n\n    switch self {\n    case .foundation(let floatingPointFormatStyle):\n        return floatingPointFormatStyle\n            .locale(locale)\n            .format(value)\n</code></pre><p>With this minor change, we now have a formatter that automatically gets the locale, like the default format styles, but with the difference that we can control that dependency.</p><h2 id=\"a-bit-more-magic\">A bit more magic</h2><p>Seeing how we can access the dependencies internally, there is another trick that you can employ. Be ready for some magic 🧙‍♀️ .</p><p>The way that PointFree dependency system decides which value to provide is using a context value, that is itself part of the dependency system 🤯.  This is very powerful because it means you can change the context yourself, or read it and leverage the internal logic of the library.</p><p>Thanks to this, we can actually have a type that magically changes.</p><pre><code class=\"language-swift\">struct MagicNumberFancyFormatStyle: FormatStyle {\n    func format(_ value: Double) -&gt; String {\n        @Dependency(\\.context) var context\n        @Dependency(\\.locale) var locale\n        \n        switch context {\n        case .live:\n            return FloatingPointFormatStyle&lt;Double&gt;.number.decimalSeparator(strategy: .always).precision(.integerLength(1))\n                .locale(locale)\n                .format(value)\n        case .preview:\n            return &quot;hardcoded&quot;\n        case .test:\n            return XCTestDynamicOverlay.unimplemented()\n        }\n    }\n}\n</code></pre><p>In fact, you can use this trick and expose the format style as an extension, without going through <code>DependencyValues</code> at all! It means that <code>(2.5).formatted(.numberFancy)</code> would look exactly normal, like any other Apple API, but under the hood you made your code fully testable and controllable. This is a huge win in my opinion, and I’m really tempted to use this trick directly without having to deal with the @Dependency in the usage side.</p><blockquote><p>Of course this might be <strong>too much magic</strong>. Analyze the tradeoffs and decide by yourself if it’s worth.</p></blockquote><h2 id=\"where-does-the-path-end?\">Where does the path end?</h2><p>Here is where I stopped my exploration. It was fun, and I learned a bunch of things again. We still need to decide if these wrapped types are worth the effort or if we will just keep the function style as the dependency. But for my side project usages I’m very tempted to use the magic solution :)</p><p>As always, thanks for coming with me in this journey, I hope you learned like I did. And if you know of other tricks or solutions that could get me the requirements I want, don’t hesitate to tell me 😜</p>",
      "date_published" : "2023-01-19T23:00:00Z",
      "id" : "https://alejandromp.com/development/blog/an-exploration-of-formatstyle-as-a-dependency",
      "summary" : "Buckle up. This is an adventurer journal while exploring what’s the best way of exposing the <em>new</em> style of formatting APIs in Foundation. We will walk through the paths of the type system, including existential types, generics and type erasure; and discover the magical possibilities of the dependency system.",
      "tags" : [
        "Swift"
      ],
      "title" : "An exploration of FormatStyle as a Dependency",
      "url" : "https://alejandromp.com/development/blog/an-exploration-of-formatstyle-as-a-dependency"
    }
  ],
  "language" : "en",
  "title" : "Development Blog - Alejandro M. P.",
  "version" : "https://jsonfeed.org/version/1.1"
}