# Will Chen - Full Content > I build agentic systems around my own life, run small businesses with agents, and write guides on how to do both. ## About I build systems around language models: agent runtimes (/writing/idyllic-v4), search over a personal corpus (/writing/cortex), persistent goal systems (/writing/idyll), and interfaces that let models operate on structured work (/writing/semantic-objects). Previously, I founded and led Terran One, an R&D lab for CosmWasm developer tools. We built a contract language that compiles to Rust (/writing/cwscript), a contract simulator and debugger (/writing/cw-simulate), a browser implementation of the contract runtime (/writing/cosmwasm-vm-js), a local chain and project scaffolds (/writing/localterra), and a partially generated documentation system (/writing/terran-one-docs). Before that, I led developer relations and ecosystem growth at Terra. My current goals are: - Earn $50,000 to $100,000 a month after tax from businesses that operate without my daily involvement. - Speak six languages conversationally without making language learning my life. - Have expert guidance on any subject available while I'm out walking. On this site I: - document what I build and what it cost - share the tools and techniques that make it work - write guides for individuals and organizations building systems of their own ## Current Work (updated 2026-08-27) I run my life on ai-organs, a personal system an agent operates for me, and I'm building busibody, a fleet of small businesses that agents run the same way. I'm writing both up in enough detail to be useful. ## Projects ### Active - Idyllic Labs: where we build the primitives these systems are made of (https://idylliclabs.com) - Elements of Agentic System Design: Decomposing intelligent behavior into code primitives (https://github.com/idyllic-labs/elements-of-agentic-system-design) ### Personal - busibody: a fleet of small businesses, each run by an agent (Idyllic Labs) - Mechanistic Mindset: a wiki on computational self-engineering (https://mechanisticmindset.com) - ai-organs: the personal system I run my life on; small tools composed by an agent ### Background - Head of DevX @ Terra - Terran One WASM Research Lab (founder) - Compilers, smart contracts, developer tools ## Links - Website: https://wcdc.io - Writing: https://wcdc.io/writing - RSS Feed: https://wcdc.io/feed.xml - GitHub: https://github.com/ouiliame - Twitter: https://x.com/stablechen - LinkedIn: https://www.linkedin.com/in/will-chen-3a71ba16b/ ## Contact - Twitter DM: https://x.com/stablechen - Email: william@idylliclabs.com --- # Writing --- ## Controls for agent workflows URL: https://wcdc.io/writing/building-idyllic-agent-experience Date: 2026-09-10 Description: We designed the builder, execution controls and mobile responses around the different jobs of creating an agent and using one. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *June-August 2025* By the summer of 2025, an Idyllic agent had a prompt, custom procedures and events that could start its work. I wanted someone to build one by describing what it should do, inspect the resulting procedure and then use it without returning to the builder for every interaction. That created two different interface problems. The person building an agent needed to see its instructions and test individual operations. The person using it needed an answer, a useful control or a request for missing information. A chat box beside a program editor could support both, but the application had to make clear which conversation was changing the agent and which was asking the agent to do its job. ## Building and deploying the procedure In June, I worked on the builder's ability to create and edit documents. I added conversion between an XML representation and the document editor, then tests and evaluations for document generation. The model could produce a structured document that a deterministic parser converted into editor blocks. Giving the builder a defined format made its work easier to test than a growing collection of unrelated editing commands. The procedures themselves still needed to be readable. An event-driven agent could start from a message instead of a person pressing Run. In the July Telegram prototype, a Message Received block contained an instruction to reply using the Send Message tool. The event and the available action appeared directly in the document. ![A Message Received trigger contains an instruction to reply using the Send Message tool.](/assets/idyllic-prior-iterations/building-idyllic-agent-experience/agent-experience-telegram-trigger.jpg) July 10, 2025. The implemented Telegram trigger and reply instruction, preserved in my development worklog. Running in response to events made saving more consequential. I did not want an unfinished prompt edit to silently become the version an active agent executed. I designed a separation between the current draft and deployment snapshots. The editor could autosave the draft, while execution would use the latest deployment. The July design also kept local undo history and accounted for the same draft being open in several tabs. ## Function blocks and their controls Julius and I worked through the function-block interface in Figma. A function definition describes a reusable procedure; a function call runs that procedure at a particular point. Those concepts needed different visual treatment. Using the same lightning-bolt metaphor for both made it harder to tell whether a block was defining something or doing something. We also examined where inputs belonged. A search operation needs a query. A campaign operation may need several parameters. If every parameter stayed open, a short procedure became a long form. Hiding them all behind settings made a newly inserted operation harder to complete. We considered showing fields while the person created or edited a block, with a compact form available afterward. ![Agent builder design with Web Search, Summarize and Create Campaign blocks beside a preview pane.](/assets/idyllic-prior-iterations/building-idyllic-agent-experience/agent-experience-builder-design.png) The Builder Era page in our Idyllic 3.0 design file shows function parameters and a preview beside the procedure. Its content and execution states are design examples. The surviving Builder Era design places the procedure next to a preview pane. It gives individual steps room for their parameters and gives execution state a visible place beside them. This is a design frame with placeholder content, including proposed error and active states. The exact revision date of the frame is unverified, but the July design session records the corresponding work on function calls, parameter fields and execution indicators. The controls also had to explain what they affected. A Play button at the agent level could mean running the entire agent, while a Play button inside a procedure could mean testing that procedure. Multiple Save buttons raised a similar question: did one save the parameters, the block contents or the whole agent? We discussed these choices through the actual layouts because the meaning depended on where the control sat relative to the thing it changed. During July, I implemented streaming output inside executed blocks. The runtime and interface had to agree about which run an event belonged to and which block should display the text. A useful preview required that connection. A finished response in the wrong place would obscure the procedure just as effectively as showing no result at all. ## Mobile questions and results The mobile prototype tested the other side of the application. Here, the user was already talking to an agent and needed to answer a question. The July 25 playground showed a presentation-preparation example with an explicit choice to create a checklist. The chosen response appeared in the conversation, followed by a document card. ![Mobile chat playground showing a create-checklist confirmation, the chosen response and a document card.](/assets/idyllic-prior-iterations/building-idyllic-agent-experience/agent-experience-mobile-confirmation.png) July 25, 2025. My mobile interaction playground tests a confirmation component and document card. It demonstrates the interface and message protocol, before the full Day Bear behavior was connected to the engine. A button gave the answer an exact form. The user did not need to type a sentence that the model would then interpret as permission to continue. A document card also gave the result somewhere to open, instead of requiring the entire output to remain a long chat message. The recording demonstrates the mobile components and message protocol in a playground. Connecting the full Day Bear behavior to the engine remained on the work list. That separation let me test the human interaction while the agent implementation continued: the builder described and tested a procedure, and the mobile interface handled the questions and results that procedure would present to its user. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: Documents that run AI procedures](/writing/building-idyllic-executable-documents) [Next: A language for AI applications](/writing/building-idyllic-domain-encoding) --- ## Clarity Bear: From idea to specification URL: https://wcdc.io/writing/building-idyllic-clarity-bear Date: 2026-09-10 Description: Clarity Bear organized a braindump, an adaptive interview and an editable result around the information needed to specify a project. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *October 2024* I wanted an AI interview that could turn a rough idea into a usable specification. In October 2024, I tried a simple version of the process myself: write a braindump, have AI process it, then answer questions until there was enough detail to make a list of work. The result gave me action items in a useful order and at a useful level of detail. I wanted to make that process into an Idyllic app. We called it Clarity Bear. The person using it would begin with an incomplete account of what they wanted to do. The application would identify what was missing, ask about it and assemble the answers into something they could edit. The difficult part was deciding how the interview should progress and when it had enough information to finish. ## Four stages for the interview On October 31, I drew the app as four pages: braindump, processing, interview and review. Each page had a different job. The braindump page was a document editor with space to get the initial idea down. Processing would extract an outline and prepare the interview. The interview would gather missing information, and review would present the resulting specification. ![Four rough screens for braindump, processing, interview and review, connected as an app flow.](/assets/idyllic-prior-iterations/building-idyllic-clarity-bear/clarity-four-pages.png) My four-page Clarity Bear sketch, October 31, 2024. Each screen represents a distinct stage of the proposed interview process. Giving the stages separate screens made the transitions visible. After submitting the braindump, the person would see that it was being processed. During the interview, the conversation would have a particular purpose. At the end, the interface would provide a document to review. A chat history alone did not make those boundaries obvious. ## Choosing the next question The question loop needed more state than the most recent message. My sketch passes the previous question, the latest answer and the earlier questions into an evaluation step. The agent also keeps a scratchpad organized around a rubric. That gives it somewhere to collect what it has learned about the intended outcome and what remains unclear. ![Diagram of an interview loop that evaluates answers against a scratchpad rubric, updates clarity and asks another question or moves to review.](/assets/idyllic-prior-iterations/building-idyllic-clarity-bear/clarity-question-loop.png) My interview-loop sketch, October 31, 2024. A rubric and scratchpad would guide the next question and the transition to review. The next question would come from those gaps. The drawing includes prompts about success criteria, next steps and the smallest step that could be taken. These were ways to make a broad intention more specific. Asking an arbitrary follow-up could keep the conversation going indefinitely; the rubric was meant to give the interview a direction. I also proposed a clarity score. Once the interview crossed a threshold, the interface would offer a way to finish and generate the specification. The score was a control for the proposed flow, with its meaning still to be worked out. We had not established that a percentage could reliably measure whether a project was sufficiently specified. A related Figma design makes that interaction concrete. It combines the conversation with a visible clarity meter and a “Show results” control. The displayed percentage and greeting exchange are sample content. They show where the person would see progress and choose to leave the interview. ![Clarity Agent interface with an example clarity percentage, agent progress, a Show results control and a sample greeting exchange.](/assets/idyllic-prior-iterations/building-idyllic-clarity-bear/clarity-interface.png) Related Clarity Agent interface from the Idyllic Figma archive. The percentage and conversation are sample content; the frame’s exact date is unrecorded. Implementing this meant treating the interview as a program that happened to use a chat interface. Each app run needed its own conversation. After an answer, the program would evaluate the history, update its notes, decide what to ask and change the interface state. The question text and the availability of the review step came from the same process. ## A specification that stays editable The third drawing follows the information through that process. The initial braindump becomes an outline and a set of questions. The interview contributes a history and a scratchpad. Those materials then feed the creation of a task list. ![Information flow from braindump and outline through interview history and rubric to a task list.](/assets/idyllic-prior-iterations/building-idyllic-clarity-bear/clarity-information-flow.png) My Clarity Bear information-flow sketch, October 31, 2024. The proposed task-generation step would use both the original outline and what the interview learned. The output was intended to be an editable specification with tasks that could connect to Linear. I wanted it to remain useful after the interview ended. In the October design, this involved document blocks that the person could edit inside Idyllic. For the first implementation, I planned to hardcode the app and its server routes. That would let me work through the interview before deciding how a general app builder should represent it. The reusable system still had to express a document editor, a processing step, a stateful conversation and a review screen. Clarity Bear gave that system a concrete example to support. When a person answered a question, several things needed to happen together: the agent's record changed, the interview advanced and the controls on screen reflected the new state. The app builder needed a way to describe those connections. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: Interactive results for AI apps](/writing/building-idyllic-interactive-apps) [Next: A visual builder for AI apps](/writing/building-idyllic-ui-and-logic) --- ## A language for AI applications URL: https://wcdc.io/writing/building-idyllic-domain-encoding Date: 2026-09-10 Description: The September and October 2025 work on domain languages, XJSN, and an interface for agents to read and change business objects. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *September-October 2025* In September 2025, I wanted someone designing a delivery route or a pricing rule to work with AI as directly as a programmer could work with a coding assistant. They should be able to inspect the relevant objects, describe a change, and see the result in terms they understood. Building a new application around every idea introduced a large amount of unrelated work: database tables, API calls, screens, and the connections between them. The earlier Idyllic builders had let me assemble prompts, workflows, and tools. I was now concentrating on the representation underneath those interfaces. A route has stops and constraints. A customer has records and relationships. A price-monitoring process has observations and rules for reacting to them. I wanted those concepts to exist as things an agent could read and manipulate directly. ## A language for the domain I called this **domain encoding**. The job was to define the vocabulary of a particular activity, together with the operations and rules that made sense within it. The implementation behind an operation could still make database calls. The person and the agent would work with the domain concept that those calls represented. ![An original Idyllic slide places DSLKit and XJSN Schema between a business icon and code, under the heading Domain Encoding.](/assets/idyllic-prior-iterations/building-idyllic-domain-encoding/domain-encoding.png) Domain encoding, from the proposal discussed on September 8, 2025. Original Idyllic pitch-deck slide. This slide came from the September pitch-deck work. DSLKit and XJSN Schema sit between a business and its representation as code. The comparison on the right is unfinished, but the intended division is visible: Idyllic would supply the language tools, and each application would supply its own concepts. A domain-specific language is a language with a limited job. It does not need to express every program a computer can run. It needs to express the valid structures and operations of the activity it serves. That limit was useful here because I wanted to check an agent's proposed changes before carrying them out. I had been implementing and testing XJSN, short for Extensible JavaScript Notation, as a common notation for those languages. It combined JSON values with function-call syntax. The calls were inert: parsing a document produced a tree of data, and validation checked that tree against the functions and argument types allowed by the domain. A call's appearance as code did not give it permission to execute arbitrary JavaScript. This let me separate two decisions. The notation determined how a model wrote a structured expression. A schema determined what that expression was allowed to mean. Different applications could use the same parser while offering different operations. I describe the parser, schema, and validation loop in the [XJSN article](/writing/xjsn). My working hypothesis was that a constrained vocabulary could make a model more reliable at a particular job. The September language experiments encouraged that direction. They did not establish that an entire business could be encoded automatically, or that a model would understand a domain merely because its output passed a validator. Choosing useful concepts and checking the quality of the work were still separate problems. ## Defining the agent's behavior ![An original Idyllic slide describes intelligence design as developing agents that work within a domain represented as code.](/assets/idyllic-prior-iterations/building-idyllic-domain-encoding/intelligence-design.png) Designing agent behavior within the domain's language. Original Idyllic slide from the September 2025 proposal. The second stage was intelligence design: deciding how an agent should operate within the language. A schema could make a route valid without making it a good route. An agent still needed the relevant context, examples, and a procedure for deciding what to do. In the September plan, this was where workflows and agent behavior belonged. The domain representation gave that behavior something definite to act on. The slide makes a broad claim that the problem becomes a coding-agent problem. What I was trying to carry across was a useful property of coding environments: a model can inspect a structured artifact, change part of it, and receive specific feedback about what is wrong. I wanted a similar interaction for work whose natural objects were routes, customers, or business procedures. ## An interface for the encoded system The third stage in the plan was deployment. Once the domain and the agent's behavior were defined, I wanted to expose them through an editor, a mobile application, or an API. That part of the September deck described the intended product. The integration path was still being worked out. By October, I was exploring a dashboard of widgets as an interface to the same idea. A widget would hold useful state, provide information to the AI, and update when the AI changed it. A board of those widgets could make the configured system visible while also being available through an API. I wanted the interface both to operate the system and to test whether the underlying representation worked. An equipment-support example made the interface questions concrete. Before opening a conversation, a person could select the piece of equipment they needed help with. That selection could supply known information to the assistant instead of making the person retype it into a prompt. We discussed rough wireframes to work through that experience. The source of the equipment data, the administrator's view, and the customer-facing interface still needed separate decisions. The domain language specified what the agent could work with, but it did not decide how a person should encounter those objects. The October work brought those questions together: which information should already be present, which action should be available first, and what change should become visible when the agent acts. Those requirements carried into the next iteration, where I began testing the application model directly in code. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: Controls for agent workflows](/writing/building-idyllic-agent-experience) [Next: A shared runtime for concurrent agents](/writing/building-idyllic-stateful-runtime) --- ## Documents that run AI procedures URL: https://wcdc.io/writing/building-idyllic-executable-documents Date: 2026-09-10 Description: I worked on a document editor that could execute AI instructions and show the results beside the steps that produced them. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *March-May 2025* I wanted to write a research procedure in a document and run it from the same page. The document would contain the instructions and the material they operated on. After a run, I should be able to inspect the results beside the steps that produced them, change a step, and run the procedure again. The appeal came from how much work already fit into a document. A page could hold background notes, references and a sequence of questions. But an ordinary page did not say which paragraphs were instructions, what information each instruction could use, or where a generated result belonged. Those details determined whether the page described a procedure or actually executed one. The Idyllic 2.0 design below combined structured information with action controls and a chat composer. It shows the intended experience: work remains visible on the page while the person asks for an operation on it. This is a product mockup from the surviving design file. Its copy describes the ambition; the frame itself does not establish that the program ran. ![Idyllic document-as-app design with an example invoice table, action chips and a chat composer.](/assets/idyllic-prior-iterations/building-idyllic-executable-documents/executable-document-design.png) Our Idyllic 2.0 mockup combines a sample invoice table with actions and a chat composer. ## Context at each step In March, I separated the document editor from the representation the executor used. The editor gave people familiar blocks and headings. Underneath, the program formed a tree of operations, so a section could contain a sequence of steps and a step could refer to a tool or an object. The editor changed that structure instead of leaving the runtime to guess the meaning of arbitrary page layout. The difficult part was deciding what each step knew. In a research procedure, a search needs the query entered earlier. An analysis step needs the search results. The next step should receive the results that belong to its part of the procedure, rather than an undifferentiated transcript of everything that has happened in the application. I represented that available information as a context scope. When the executor entered a section or an event handler, it established the context for the work inside it. Executed steps contributed their requests and results to the context used by later steps. A phrase inside an instruction could also ask AI to fill in a value from the information available at that point. ![Execution test table showing a search query and document results accumulating in the context available to later steps.](/assets/idyllic-prior-iterations/building-idyllic-executable-documents/executable-document-context-table.png) March 22, 2025. My development visualization of context before and after each operation. The example data tests execution rules; it does not establish a completed live search application. The March visualization made those rules inspectable. Each row shows an operation, the context before it ran, its output and the context afterward. In the search example, the input step adds a query. The search step adds document results. A later operation receives both. These were development tests with example data. I still needed to inspect the actual messages sent to the model to verify that they followed the context rules. ## The execution movie That table helped me work on execution semantics, but it exposed too much machinery for someone using the document. A person waiting for a report needed to know which work had completed and what was happening now. They also needed a way to inspect details when a step produced something unexpected. Julius designed a compact execution display that we called the “movie.” It presented the run as changing rows with status indicators and expandable content. The expandable areas mattered because a search result and a generated document do not need the same fixed layout. Each operation could show its own useful detail while the surrounding display retained a consistent structure. ![Compact execution card with a completed analysis row, an active fetching-sources row and expandable detail.](/assets/idyllic-prior-iterations/building-idyllic-executable-documents/executable-document-movie.png) April 10, 2025. My implementation of Julius Lattke’s execution “movie,” connected to a demo executor while the new execution specification was still being built. I connected a rough implementation of his design to a demo executor on April 10. That distinction mattered during development. The interface let us try the pacing and inspect how operations appeared before the new execution specification was complete. We could discuss a running presentation of the work while I continued implementing the rules underneath it. ## The generated document Creating the final document introduced another dependency. A chat response could say that it had run the prompts, but an artifact card needed an actual document to open. Citations also needed to point into that document. On April 20, I changed `executeDocument` so it created a new document, applied Julius's improved artifact design and connected citations to the generated result. I also chose sequential execution for the document prompts. Earlier outputs could then contribute to later work, which made the order of the document meaningful. The exact rules for how prompts used headings and surrounding context were still being refined. At that point, the chat itself remained local and its persistence still needed work, so creating an artifact did not yet establish the complete storage model for every conversation. By May, I was refining how references appeared as inline pills. An object or document could sit inside an instruction as something the user could recognize, while the executor retained a reference it could operate on. The visible page, the program structure and the generated artifact each had a distinct job. The next implementation problem was keeping those representations consistent as people and agents edited them. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: A task list shared with an agent](/writing/building-idyllic-shared-task-state) [Next: Controls for agent workflows](/writing/building-idyllic-agent-experience) --- ## Interactive results for AI apps URL: https://wcdc.io/writing/building-idyllic-interactive-apps Date: 2026-09-10 Description: Task Bear and Search Agent made checklists, progress and citations part of the interface, with Julius’s designs connected to working agent flows. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *August to October 2024* A generated checklist should let you check things off. It sounds obvious, but it changes what an AI application has to keep track of. The model can produce the words in a task list. The application has to preserve which tasks you completed, display nested items and let you return to the same list later. By late summer 2024, I was separating two ways of interacting with Idyllic. You could have an open-ended conversation, or you could use an application with an interface fitted to a particular task. Both could use AI. I wanted the application to have controls and output types that made sense for the work being done. ## A feed for interactive results Task Bear was one of the designs we used to work through this. Its output was a checklist, and its screen combined several kinds of information: what the agent was doing, what it had produced, and the controls available to the person using it. A September 1 annotation divides those areas explicitly. ![Colored annotations divide a Task Bear screen into agent activity, interactive results and bottom input controls.](/assets/idyllic-prior-iterations/building-idyllic-interactive-apps/interactive-anatomy.png) Anatomy of an Idyllic UI, captured September 1, 2024. The annotation separates the feed, agent activity, interactive output and input controls. The feed contained agent activity and interactive results. A row could report that the agent was processing something, while a card could contain a result with its own interactions. Controls at the bottom could open a sheet for input. This gave us a common layout without requiring every application to ask for the same information or display the same kind of answer. At this stage, I had made screens manually. Dynamically loading different app definitions into a shared layout was still work to do. The design helped specify what that shared layout needed to support: a feed, app state, settings and controls that could summon more detailed interfaces. ## State belonged to the output The checklist made the state question precise. If someone generated one list, used it, then generated another, the first list still needed to remember its completed items. In the September 19 specification, I put checklist state on the individual output item. You would scroll back to that particular result to continue checking it off. ![Nested Task Bear checklist in Figma, with selection guides and Julius Lattke’s cursor visible.](/assets/idyllic-prior-iterations/building-idyllic-interactive-apps/interactive-nested-checklist.png) Collaborative Task Bear design, September 19, 2024, with Julius Lattke’s Figma cursor visible. The specification assigned checklist state to each generated output. That decision affected both the interface and the implementation. A checkbox could no longer be treated as temporary decoration around model output. It belonged to that particular output. The same specification described input controls that would disappear while an input step was active, then return when the person completed or canceled it. Each stage had to define what the person could do and where their changes were stored. Julius and I were also using Storybook as a shared place to work on components. A component could be developed with sample data before it was connected to the running agent. That made it possible to discuss a particular checklist or input control while the execution code was changing underneath it. ## Showing a search in progress Search Agent brought a different set of requirements into the same interface. The idea was to ask questions of personal material and see an answer grounded in relevant sources. A single block of generated text would hide most of that work. The interface needed to show searches in progress, which material had been found, and how that material related to the answer. On October 7, Julius gave me a design that separated those parts. Search themes appeared near the top. Dated source chips made the retrieved material visible. A later section presented the answer with citation markers. The text in this design is placeholder content; it shows the intended arrangement rather than an actual search result. ![Search Agent card shows search themes, dated source chips, a summarizing stage and citation markers around placeholder text.](/assets/idyllic-prior-iterations/building-idyllic-interactive-apps/interactive-search-agent.png) Julius Lattke’s Search Agent design, October 7, 2024. The result text is placeholder content showing the placement of sources and citations. I connected the design to a LangGraph search flow, using Convex to carry updates back to the interface. Vector search became a tool the agent could call, and the agent reported its progress as it worked. By October 8, I had citations working. Streaming and the display of subsearch queries remained active implementation tasks. The Search Agent card needed to communicate an unfolding operation, while a checklist needed to preserve an editable result. Supporting both gave the feed a more specific purpose. It could hold activity that was still changing alongside outputs that people would continue to use after generation had finished. That distinction would become especially useful for an application whose main interaction was a sequence of questions, with a document waiting at the end. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: From Telegram workflows to AI programs](/writing/building-idyllic-workflows) [Next: Clarity Bear: From idea to specification](/writing/building-idyllic-clarity-bear) --- ## Building Idyllic - Prior Iterations URL: https://wcdc.io/writing/building-idyllic-prior-iterations Date: 2026-09-10 Description: The earlier Idyllic designs, with original sketches and prototypes from 2024 and 2025. I started building Idyllic in 2024 to make AI tools that people could adapt to their own work. A useful conversation often depended on context I had supplied and a sequence of questions I had worked out. I wanted to save that work in an application I could keep using and change as I learned what it needed. Julius Lattke worked with me on the product designs. We explored small applications with their own feeds, documents that could run a procedure, and agents that shared editable objects with the person using them. I built prototypes to work through the behavior behind those interfaces. The articles below cover the 2024 and 2025 iterations. They use the original sketches and interface captures to explain the problem each version addressed and the decisions that shaped it. Captions distinguish proposed designs from prototype behavior. ## 2024 [From Telegram workflows to AI programs](/writing/building-idyllic-workflows) begins with an assistant for preparing design proposals. The question was how to connect a model response to the information and steps surrounding it. [Interactive results for AI apps](/writing/building-idyllic-interactive-apps) covers the feed-based applications and Julius's Search Agent. A running workflow needed somewhere to ask for input and show the sources behind its answer. [Clarity Bear: From idea to specification](/writing/building-idyllic-clarity-bear) follows Clarity Bear. I wanted the conversation to collect missing information and leave me with a document I could work from. [A visual builder for AI apps](/writing/building-idyllic-ui-and-logic) covers the visual program builder and the experiments with editable objects inside documents. Buttons and generated results needed behavior that the application could represent explicitly. ## 2025 [A task list shared with an agent](/writing/building-idyllic-shared-task-state) follows the planner designs. Changes made through conversation needed to affect the same tasks I could edit through the interface. [Documents that run AI procedures](/writing/building-idyllic-executable-documents) covers the document editor and Julius's execution movie. Instructions, references and intermediate results had to remain understandable while the program ran. [Controls for agent workflows](/writing/building-idyllic-agent-experience) follows the builder and mobile interfaces. An agent needed controls suited to its task, including confirmations and editable documents. [A language for AI applications](/writing/building-idyllic-domain-encoding) covers the work on representing an application's concepts and operations in a form a model could use. The language and builder had to agree on what those operations meant. [A shared runtime for concurrent agents](/writing/building-idyllic-stateful-runtime) follows the move to TypeScript classes and the Virtual Office prototype. The browser and the agents operated on state belonging to the same running application. --- ## A task list shared with an agent URL: https://wcdc.io/writing/building-idyllic-shared-task-state Date: 2026-09-10 Description: Julius designed the planner, and I connected its interactive task cards to the same data the assistant used. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *January-February 2025* In January 2025, I wanted to plan my day with an assistant and then use the resulting plan as an ordinary task list. If I asked it to add a task, that task should appear in the planner. If I checked a task off myself, the assistant should work from the updated list the next time we spoke. A checklist inside a chat response only solves the first part. It shows what the model suggested at the time it answered. Once I change the actual plan, the old response becomes a second version that I have to reconcile with the application. I wanted the conversation and the planner to refer to the same stored tasks. ## The planner and its task data Julius designed the planner around expandable task cards. A task could contain subtasks, and the cards sat inside named categories. The hierarchy let a large activity remain compact until I needed to work through its details. A checkbox gave me a direct way to change its status without having to describe the change in another message. ![Planner component test showing categorized task cards and an expanded task with nested checkboxes.](/assets/idyllic-prior-iterations/building-idyllic-shared-task-state/shared-state-planner-components.png) January 10, 2025. My Storybook implementation of Julius Lattke’s planner design, before the later database integration. The tasks are component examples. I implemented his components in Storybook first, which let me work on their appearance and interaction without connecting the database at the same time. The screenshot comes from that component test, so its example tasks demonstrate the interface before the later data integration. Once those components worked, I assembled the planner pages and connected them to Convex, the database we were using for live application state. The page structure followed the task hierarchy. The planner opened on a day, a task had its own detail view, and a subtask could have a deeper view. Selecting a date also supplied context to the chat. Otherwise a request about “today's tasks” could refer to a different day from the one I was looking at. I gave each application a small configuration record. The planner registered the components it could display inside chat and a context provider that supplied the selected date. This kept planner-specific behavior together instead of adding another special case to the main chat component whenever the application gained a feature. ![TypeScript application configuration registers planner components and supplies the selected date to chat.](/assets/idyllic-prior-iterations/building-idyllic-shared-task-state/shared-state-app-registry.png) January 11, 2025. The application registry and planner context provider from my implementation. The selected date is shared with the planner’s chat components. ## Returning a component from a tool Julius also proposed using AI in the add-task interaction. I implemented that alongside the planner, then connected interactive components to tool calls. A tool call lets the model ask application code to perform an operation. In this case, the response from that operation could include instructions for displaying a task component as well as the data the model needed for its answer. My first attempt used structured model output to describe the interface, but that approach did not fit the tool-calling flow I had in place. I moved the interface description into the tool result. When a result contained a `genUI` field, the backend attached it to the chat response, and the frontend used the application's registry to display the corresponding component. That arrangement gave the component several possible behaviors. A static result could show data captured at the time of the call. A live component could use an ID to query the stored task, and an interactive one could also send mutations back to the database. The live task card therefore had a way to stay consistent with the planner after the original chat message had finished. The interface needed rules of its own. One request might cause several tool calls, each returning a component for the same task. Showing all of them produced duplicate controls inside a single answer. I added deduplication because the number of backend operations did not determine how many task cards a person needed to see. ## Reusing objects outside the planner By February, I was extending the same idea to objects beyond the planner. A named object could expose operations, and an operation could create another object that remained available to the conversation. The prototype below invokes a summary method and returns a reference to the resulting summary. The method description and object names stay visible, so I can see what the assistant is operating on. ![Object-method prototype showing a named journal object, its summarizeEntries method and a reference to the resulting summary.](/assets/idyllic-prior-iterations/building-idyllic-shared-task-state/shared-state-object-method.png) February 9, 2025. An object-method prototype returns a summary as another named object. The May 2024 dates identify the test collection, not the date of this interface. I described the mechanics separately in [Semantic objects: giving models handles instead of data](/writing/semantic-objects). The planner supplied the complementary interface requirement: a reference also needed a useful view and, where appropriate, controls. A task deserved a checkbox. A collection needed a way to narrow it. A generated summary needed to remain addressable after the message that created it. The planner’s task cards used their Convex query and mutation bindings inside completed chat messages. Finishing the model response did not disable the controls or freeze the data they displayed. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: A visual builder for AI apps](/writing/building-idyllic-ui-and-logic) [Next: Documents that run AI procedures](/writing/building-idyllic-executable-documents) --- ## A shared runtime for concurrent agents URL: https://wcdc.io/writing/building-idyllic-stateful-runtime Date: 2026-09-10 Description: The November and December 2025 framework experiments that put shared state, concurrent agents, and a live interface inside one application model. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *November-December 2025* By November 2025, I wanted to make an AI application that could keep working on a shared task while a person watched, intervened, and returned to it later. A chat endpoint could receive a message and stream a reply. The application also needed somewhere to keep its documents, current work, and progress, together with operations that could change them. I started Idyllic v4 with a basic working chat so I could exercise the whole path from the browser to the model and back. The next step was to put an application model behind that endpoint. I wanted to test its behavior in code while the components were still easy to change, then design the visual builder around the resulting mechanics. One component was a module: a configured resource with its own state and operations. A document collection would provide both a place to keep documents and the methods for working with them. A messaging module would bring the conversation data needed by its functions. The plan was for an agent application to compose those resources without requiring its author to assemble a separate storage system for every capability. ## One object for the session The other question was what should own the state of the application itself. By December, I was working with a TypeScript class. A class describes an object through its properties and methods. That gave me a familiar way to express a running session: properties for its current state, methods for the things a person or another program could ask it to do. Several agents could work inside that session. Each could have its own prompt and context while reading and updating a shared document or task board. I did not need a separate network service for every agent to express that coordination. Ordinary functions, variables, and promises were enough to describe the arrangement in code. ## Compiling the application into a running system ![A development sketch connects a Next.js application and idyllic source directory to bundling, script upload, a routing table, and a dispatcher worker. Red notes mark open implementation questions.](/assets/idyllic-prior-iterations/building-idyllic-stateful-runtime/deployment-sketch.png) Deployment sketch from my Durable Objects research notebook, captured December 26, 2025. Red annotations mark open questions. The December 26 sketch traces how that code might become something a browser could use. The application has an `idyllic/` directory beside its frontend. A deployment command bundles the server code, uploads it, and records where requests should go. The browser connects to the resulting application through Idyllic's routing layer. The red annotations are questions I was still resolving, including where to build the bundle and how the live connection should pass through the system. I was building on Cloudflare Workers and Durable Objects, with one stateful object representing a session. The research prototype helped me distinguish the object I wanted to write from the machinery needed to run it. Important state still had to be stored and restored; an ordinary in-memory property was not enough to make it durable. Idyllic's job was to connect that storage and synchronization behavior to the application's source model. The source transform became central. I wanted the author to write a TypeScript class while Idyllic generated the worker and stateful-object code around it. By December 29, I had a prototype that transformed `system.ts`, connected to a React interface, and supported multiple parallel streams. Changes to synchronized fields on the server appeared in the browser as the work progressed. The more detailed account of that model is in [Idyllic v4](/writing/idyllic-v4). Streaming required more than a changing string. The interface needed to know which output was receiving text, whether an operation was still running, and when it had finished. A research application could have an explorer, a critic, and a synthesizer working through their own outputs while the session retained their common state. This gave me a concrete reason to define fields, actions, and completion behavior together. ## Testing a shared task board ![The Virtual Office prototype shows two tasks in progress, a documentation task in the backlog, and generated work from simulated employees Alice and Bob beside an activity log.](/assets/idyllic-prior-iterations/building-idyllic-stateful-runtime/virtual-office-working.png) December 29, 2025: Virtual Office running demonstration tasks. The task board and each simulated employee's generated work are visible together. Still from my development recording. Late on December 29, I chose a Virtual Office as another test application. It gave the runtime a small shared world to manage. A task board sat beside two simulated employees, Alice and Bob. Each had a current assignment and a panel showing generated work. An activity log recorded changes across the whole office. The first capture shows two tasks in progress: a login form and a landing page. A documentation task remains in the backlog. These were demonstration tasks. I was testing whether several concurrent activities and the state they shared could remain visible through one application. The board also provided a place for human review. A task could move into review, receive feedback, and return to work. That interaction needed to change the same state the agents used. Otherwise, the board would only illustrate a process running elsewhere, and a person's intervention would have no reliable connection to the next operation. ![A later Virtual Office view shows one task done, one in review, and one in progress. Alice and Bob are taking breaks, while the activity log records task feedback and state changes.](/assets/idyllic-prior-iterations/building-idyllic-stateful-runtime/virtual-office-review.png) Later in the same December 29 recording: tasks have moved between work, review, and completion. The energy bars and breaks are behavior of this test application, not measures of model performance. In the later capture, the documentation task is marked done, the landing page is in review, and the login form is back in progress. Both simulated employees are taking a break. Their energy indicators were part of the toy office's behavior, not measurements of model capability. What mattered for the framework was that task status, generated output, and activity could be shown together as the application changed. The same connection also helped with debugging. A coding agent could call the running system through its remote interface while I watched the browser update. Both were clients of the same session. The application did not need a second, separate representation just to make the test visible. These prototypes exercised the stateful runtime and its connection to the interface. The larger module ecosystem, session access, and support for more complex application patterns still needed work. I now had a way to investigate them by writing small applications with specific shared state, actions, and visible results, while keeping the deployment and synchronization code inside the framework. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: A language for AI applications](/writing/building-idyllic-domain-encoding) --- ## A visual builder for AI apps URL: https://wcdc.io/writing/building-idyllic-ui-and-logic Date: 2026-09-10 Description: The November app builder connected screens to executable flows, while inline semantic objects and December context experiments made results reusable. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *November to December 2024* In November 2024, a button in Clarity Bear carried a surprising amount of responsibility. It had to take the contents of a document editor, send them through an AI step, store the result and move the person to the interview page. The next page then needed that result to begin its own conversation. I could write those connections directly in application code. To make them editable in Idyllic, I needed a representation that connected what appeared on screen to the program running behind it. A diagram from November 8 puts the two sides next to each other: UI on the left, logic on the right. ![Side-by-side UI and logic diagram connecting a document editor, button callbacks, AI processing, interview messages and review output.](/assets/idyllic-prior-iterations/building-idyllic-ui-and-logic/composition-ui-logic.png) Idyllic UI-and-logic study, November 8, 2024. The drawing maps interactions on each screen to the operations that produce its next state. In the drawing, a button click starts the outline-generation flow. A message in the interview triggers evaluation of the conversation and generation of another question. The result updates the clarity indicator. The review page receives a generated document. This made the relationship between a user action and an AI operation explicit enough to implement. ## Connecting screens to operations I began translating the hardcoded flows into an abstract syntax tree, or AST: a structured description of the operations for an interpreter to run. The visual editor supplied nodes and connections, and a parser turned those into the executable description. This let me work on the editor and the runtime as separate parts of the same system. During November, I got an interpreter running and connected button interactions, navigation and string-based chat callbacks. A text element could receive streamed text from an AI node. Streaming also required more careful state updates; I fixed a race condition by partitioning updates by key, while streaming callbacks and initial state still needed work. The graph itself was another design problem. An execution sequence tells you what happens next. When connecting AI calls and interface state, you also need to see where each value comes from. On November 20, I planned to move the editor toward data flow and revise how function results bound to state and appeared in the UI. ![Idyllic builder with page, state and logic controls and a graph of entry point, AI call, state update and navigation nodes.](/assets/idyllic-prior-iterations/building-idyllic-ui-and-logic/composition-november-builder.png) The Idyllic editor on November 20, 2024. This capture still shows an action sequence, beside notes proposing a move toward data flow. This capture still shows an action sequence: an entry point, an AI call, a state update and navigation to another page. It records the editor during that revision. I also needed to design expressions and type checking, because the way values moved between nodes would determine how understandable the graph could become. ## Objects with their own interfaces Alongside the app builder, I was exploring what kinds of things the interface could contain. On November 5, we described a “semantic object” as something with data, relationships, methods and a renderer or editor. In this version of the idea, an object carried enough structure for both an AI program and a person to work with it. A color palette was a useful example. Its colors could be represented as data, but the person should see swatches and controls. A musical pattern should have a playable interface. The object's type would tell Idyllic how to display it and which interactions to offer. ![Inline palette, sparkchart and musical pattern with playback controls in an Idyllic prototype.](/assets/idyllic-prior-iterations/building-idyllic-ui-and-logic/composition-semantic-objects.png) Still from the November 5, 2024 semantic-object prototype. Different structured objects appear with their own visual controls. The November prototype placed a palette, a small chart and a musical pattern inline. The still shows the visual objects and their controls. The broader proposal also included objects such as calendars, task lists and schedules, with relationships and operations between them. Those examples described the direction of the system; the visible prototype established the narrower question of rendering different object types together. This required a way to choose and configure components. A related Figma design shows a library containing elements such as a button, chat, progress bar and an agent activity display. The activity preview distinguishes completed, active and pending steps. A component had to expose properties the builder could edit and events the runtime could connect. ![Component library with Agentic Thinking, Button, Chat and Progress Bar choices and a preview of completed, active and pending steps.](/assets/idyllic-prior-iterations/building-idyllic-ui-and-logic/composition-component-library.png) Component-library design from the Idyllic Figma archive. The preview uses placeholder steps; the frame’s exact date is unrecorded. ## Context inside the project By December, I was using a chat application I had built and considering how to combine it with the builder. The December 10 proposal treated a project as a place with its own context, state, interface and logic. A person could work inside it immediately, then customize the parts they needed. Context became a more specific design problem within that workspace. Joining strings into a prompt obscured the operations I wanted to perform on the information itself. I wanted to select material, add it to context, transform it or remember a result for later. On December 21, I was sketching an interface for those operations, with prompt templates kept separate from the contexts they would use. Some of this was becoming ordinary application behavior. Agent mode could show a sequence of thinking steps. By December 25, I had added a project memory mode that searched other chats in the same project and included results in a new query. I still needed to make that retrieval more specific. That left a concrete design boundary: storing information in a project was only the beginning. An operation needed to select the relevant parts, pass them to the model in the right form and put its result somewhere the person could use again. The screens, program representation and context model all had to agree about what that result was. --- [Series index](/writing/building-idyllic-prior-iterations) [Previous: Clarity Bear: From idea to specification](/writing/building-idyllic-clarity-bear) [Next: A task list shared with an agent](/writing/building-idyllic-shared-task-state) --- ## From Telegram workflows to AI programs URL: https://wcdc.io/writing/building-idyllic-workflows Date: 2026-09-10 Description: Telegram feedback loops, Julius’s proposal process and the first experiments with AI programs represented as data. [Building Idyllic - Prior Iterations](/writing/building-idyllic-prior-iterations) *March to July 2024* In March 2024, I made a small workflow that let me journal through Telegram. A message to the bot became an entry on that day's Notion page. I used n8n to connect the services and Redis to pass messages between them. The capture step worked, although I had not yet figured out how to use the accumulating material. This became one of the starting points for Idyllic. I wanted to build AI tools around information that persisted between conversations. A journal entry could become context for a later question. A response could receive feedback. The next invocation should be able to use what happened during the previous one. ## A feedback loop for Telegram On April 11, I drew a feedback system for the n8n workflow. A template and some context produced a prompt. The model generated an answer, which the workflow sent to Telegram. Alongside that answer, the system would store the prompt, the output and the Telegram message ID. ![Handwritten workflow connecting a prompt template and context to a model call, Telegram delivery and a stored invocation.](/assets/idyllic-prior-iterations/building-idyllic-workflows/workflows-april-generation.png) My feedback-system sketch, April 11, 2024. The generated response would retain a link to its prompt and Telegram message. Keeping the message ID mattered because feedback arrives after generation. If I replied that an answer was useful, or explained what it had missed, the system needed to connect that feedback to the invocation that produced it. The second page sketches a refinement loop that would use those records to change the prompt or add examples. The drawing still has questions in it. I was specifying how the loop might work, with the data model chosen to make refinement possible. ![Handwritten generation, user-feedback and prompt-refinement flows, with feedback connected to the original invocation.](/assets/idyllic-prior-iterations/building-idyllic-workflows/workflows-april-feedback.png) The second page of my April 11 sketch. Feedback would be matched to the invocation before being used to refine a prompt. ## An assistant in the proposal process At the same time, Julius and I were considering how an AI assistant could participate in a longer process. Julius was working on the design and user experience; I was particularly interested in the developer tools that would make these systems easier to construct. We needed a process concrete enough to describe, so we used his design proposal workflow. A client request starts with a decision about whether to take it on. Research and a meeting establish what the client needs. A rough proposal identifies deliverables, milestones and estimates. Further discussion revises it until there is an agreement. Work then produces files, progress updates and material for review. An assistant could help at several points without owning the whole process. It could research a request, extract useful information from meeting notes or prepare a proposal from an agreed template. Julius would still decide whether to proceed. Those boundaries gave us a way to distinguish an AI action from a human decision and to specify which information each stage required. I modeled the proposal process as a state machine and imported it into TypeScript on April 14. That represented the stages and transitions; attaching useful agent behavior was the next task. I also started a visual editor with React Flow and tried a chat interface. By April 15, a terminal chat with message history worked, while the dynamic context and task interactions were still being worked out. The proposal exercise also exposed a limit in a strict process diagram. Proposals accumulate information through negotiation. Their meaning changes while their stage may stay the same. My notes considered a looser description of the process that could evolve with the work, while keeping the state machine as a concrete place to begin. ## Programs represented as data By July, I was exploring a different part of the same construction problem: how to describe an AI program independently of the application running it. One experiment generated a form from a request. It passed through a planning step and a structured specification of the elements before reaching HTML. I used a nested schema to constrain what the model could produce. ![A request passes through planning and structured element specifications to produce an empty HTML form.](/assets/idyllic-prior-iterations/building-idyllic-workflows/workflows-july-form.png) My early July 2024 form-generation study. The experiment used a nested schema to constrain the generated interface. The form experiment put an interface at the end of a model pipeline. That raised a practical question: could the pipeline itself become something a person could save, edit and run again? I began representing programs as data, separate from the Python code that executed them. An essay-ranking script gave me a small working example, including parallel processing of the essays. The architecture drawing from this period places the program description between a composer and an execution engine. The engine runs the steps, while a database carries state back to the interface. The wider drawing includes a proposed AI composer; the working experiments at this point were narrower, with programs I had written and tested myself. ![Architecture diagram connecting a user interface, AI composer, program description, execution engine and database.](/assets/idyllic-prior-iterations/building-idyllic-workflows/workflows-july-architecture.png) Idyllic architecture study, late July or early August 2024. The diagram includes a proposed composer alongside the execution system I was testing. A description of a program could eventually support several ways of making it: writing it directly, editing a diagram, or asking AI to help construct it. Before that, the runtime needed to execute it and the interface needed to make its results usable. A form was one possible result. An essay ranking was another. The next designs asked what happens when the result also needs buttons, selection, progress and a place to keep its own state. --- [Series index](/writing/building-idyllic-prior-iterations) [Next: Interactive results for AI apps](/writing/building-idyllic-interactive-apps) --- ## Idyll: a convergence layer for agents URL: https://wcdc.io/writing/idyll Date: 2026-08-20 Description: Running many agents at once, I was the part holding the goal and how the pieces related. An idyll is a folder that holds it instead. ## Motivation I was curious about the most effective way to work with multiple agents. Naïvely, I thought the solution was to spawn a lot of agent sessions in parallel, wrangle them through tmux, make sure all the work aligned with the goal, and jump in whenever one needed help. This is what you see on X when people post crazy monitor setups full of Claude Code and Codex sessions. But this can't possibly scale. It fractures my attention and makes the work cognitively exhausting. Nobody wants to spend all day at a desk micromanaging agents. I've been following the agent space for a while, and I've noticed that new breakthroughs usually solve one of two problems: 1. They reduce how much a human has to prompt by automating it, obviating it, or abstracting it away. 2. They give the human a legible surface for understanding the high-level work, so the human can make decisions at that level instead of operating inside every task. That gave me a hint about a new primitive: something that sits above an agent or set of agent sessions and replaces what I do at the desk. I've also been thinking about how to use agents to pursue goals that extend beyond one session: managing a fundraise, conducting a job search, losing weight. These are long-horizon goals that outlast the timescale of most cloud agents. The same primitive should handle those too. ## Breaking down the human's role The first step was to analyze the role I played in my agents. My agents and I form one effective unit that the external world sees through merged PRs and published content. Agents are the interface through which I get work done. If I wanted to create the layer above them, the best place to start was wherever I was still needed: when I intervened, what refinements I supplied, and what context I held implicitly that the agents could not access. Every morning I would open a session and type some version of what we were doing. The goal and our progress against it lived in my head, so every session received a slightly different copy. If I spent an hour steering one agent through a bug, I would come back to the others having lost part of the larger picture. I was doing more than supplying context. I was deciding: - what mattered at the current level of the project - which work should happen next - which outputs were good enough to keep - which decisions were settled and should stop being revisited - whether the work was moving toward the goal at all The agents were producing candidate work. I was selecting, verifying, and carrying the destination between sessions. So that is the thing to build: the goal, written down outside my head, in a form something else can check work against. ## What an idyll is An idyll is one goal, written down in a folder, with an agent whose only job is that goal. The folder contains the end state I want and the conditions that would tell me I reached it, where things currently stand, what has already been tried and what it yielded, and dated records of what actually happened. The agent reads all of that, works out the difference between the current state and the desired state, and hands tasks down to ordinary agent sessions. Those sessions do not need to understand the whole goal. They only need to know what to do next. The example I keep returning to is weight loss. The folder holds the target and how I would know I reached it. Contact is what I ate, what I weighed, how I slept, and what training I did, all dated. Attempts are the interventions I tried and whether they worked. Every day, the idyll folds the new contact into the state and identifies the next difference that matters. A job search has the same basic shape. So does an essay. Each has an end state, a record of contact with reality, a history of attempts, and a process for computing the gap between where things are and where they should be. I called it an idyll rather than an agent on purpose. “Agent” already means a chat session that runs tools. I wanted a word without that baggage, so the thing could grow into whatever it turns out to be. ## Macrostates, not microstates When working with agents, the most important boundary is between “I care that this gets done” and “I care how this gets done.” There are probably thousands of ways to write code that satisfies a specification. Most of the time I do not care which path an agent takes. I care whether the result has the properties I asked for. Drawing that line creates a hierarchy. Without it, every agent output arrives at the same level of importance. You read implementation choices, intermediate reasoning, tool output, and project-level decisions as if they all deserve equal attention. That flatness is what makes twenty sessions unmanageable, not simply the volume. The terms come from statistical mechanics. A macrostate is the description you care about, such as temperature and pressure. A microstate is one particular arrangement of particles that produces it. Enormously many microstates can satisfy the same macrostate, and the point of the macrostate is that you do not need to inspect them individually. An agent session searches through microstates. The idyll holds the macrostate: the end state I want and the conditions that would tell me I am there. This also explains where taste belongs. Sometimes I genuinely care about the path: the architecture, the tone of the prose, the way an interaction feels. That does not mean I need to supervise every microstate. It means those properties belong in the macrostate. If I care that an essay sounds direct and human, that is part of the destination, not a reason to watch every sentence being written. The better I articulate the macrostate, the more freedom agents can have underneath it. ## Prototyping it in Markdown I wanted to reach for a graph database. Goals, attempts, criteria, and evidence map neatly onto nodes and edges, and I had already been excited about graph databases for months. Being excited about a technology is a bad reason to build on it. Infrastructure installed before the problem is understood will carry the design whether or not the design is good. So I gave myself a rule: the concept has to work as plain Markdown files that a person can read and edit before I write a software system around it. If I cannot prototype it in Markdown, I do not understand it yet. That rule also settled the container question. My first instinct was an object with methods. A goal has a lifecycle. Conditions get added and retired. Evidence arrives and attaches to a condition. That sounds like a type with an API. I had built two versions of exactly that in the weeks before. Both encoded my current theory of what a goal was, and that theory kept changing. Every improvement to the concept became a migration of the object model. Markdown makes the opposite trade. It gives up formal structure at the beginning so the representation can move while the domain is still being learned. A person can change it in an editor. An agent can read the new version directly. The stable structure can be extracted later, after it has appeared repeatedly in real use. ## The model already existed in how I use Claude Code Markdown files still need a container. I hand Claude Code folders every day and it already handles them well, so I looked at what those folders contain. Here is a writing skill I use: ```text .claude/skills/prose/ ├── SKILL.md ├── register.md ├── examples.md └── structure.md ``` `SKILL.md` opens with frontmatter describing what the skill is and when the agent should load it: ```yaml name: prose description: > William's writing standards, distilled from every documented instance of him rejecting AI-register prose. Use this skill whenever you are about to write user-facing prose of any kind. ``` The body explains the skill and points to the other files. An agent opens the directory, reads the file that describes it, and follows the references. There is no manifest or registry between the agent and the material. When I want to change what it knows, I edit a [Markdown](https://commonmark.org/) file. A repository works the same way one level up. `CLAUDE.md` sits at the root, and the rest of the project is available through filenames, headings, links, and ordinary search. I have never needed to write a parser for either. Files, prose, and an agent that can read are flexible enough to absorb changes that would require migrations in a typed object model. An idyll uses the same shape, one folder per goal, with the goal where the instructions normally go. It becomes the layer above [Claude Code](https://claude.com/claude-code) that holds the destination and hands work down. ## What goes in the folder The current format has five entries. Only one section changes shape between domains. ```text IDEAL.md destination: one paragraph describing the world when this is done acceptance criteria: each carrying its measurement procedure domain section: the only variable grammar STATE.md where things stand now, dated and kept separate from IDEAL ATTEMPTS.md what was tried -> what it yielded worked / did not work / unclear, append-only contact/ dated records of what happened, with excerpts and references registers/ standing records for recurring people, policies, incidents, hypotheses, applications, or other domain objects ``` ### `IDEAL.md` `IDEAL.md` states the destination and the conditions that would make it true. Each acceptance criterion carries its own method of measurement. “The essay is good” is not a criterion. “A technical reader can reconstruct the compiler pipeline without opening the codebase” is closer, because someone can actually test it. The domain section is allowed to vary. A policy goal may contain rules and causes. A research goal may contain hypotheses and falsifiers. A job search may contain market hypotheses and pipeline stages. A learning goal may contain a curriculum and mastery tests. That variation stays in the text. There is no application-level catalogue of goal types. ### `STATE.md` `STATE.md` describes where things stand now. It is dated, factual, and glanceable. Keeping it separate from `IDEAL.md` matters more than it appears. If the current state can leak into the desired state, the goal quietly becomes whatever has already been achieved. The two files create a hard boundary between the map and the current position. ### `ATTEMPTS.md` `ATTEMPTS.md` is append-only. Each entry says what was tried and what it yielded. The result can be `worked`, `did not work`, or `unclear`. “Unclear” matters because missing evidence is different from failure. It tells the idyll to collect information before repeating or rejecting the attempt. ### `contact/` Contact is what actually happened: a weight measurement, a meal, a reply from an investor, a rejected application, a user interview, a changed artifact, a new test result. Every record is dated and includes the relevant excerpt or measurement. Without absolute dates, the idyll cannot calculate a rate, a streak, staleness, or distance to a deadline. ### `registers/` Registers hold recurring entities that accumulate history: one file per company in a job search, one record per policy or incident, one page per hypothesis, one record per investor. The code only knows that the folder and files exist. Everything that differs between losing weight, raising money, and revising an essay lives in the prose. ## What convergence means An idyll is not just a memory folder. Its job is to make a large number of agent runs converge. Each run creates a candidate change to the world: a code edit, a message, a revised argument, a new experiment, a different diet. The idyll has to decide what to keep, what to reject, what is now settled, and what uncertainty remains. The operators I keep arriving at are: - **Diverge:** generate meaningfully different approaches when the current one is not enough. - **Verify:** check an output against the written conditions while the work can still be changed. - **Select:** choose the best candidate at the right level of granularity. A paragraph can be kept without keeping the entire draft. - **Lock:** record settled decisions so later agents do not reopen them without new evidence. - **Distill:** turn repeated results into a reusable rule, preference, or higher-level instruction. Merge is a form of selection at a finer grain. Instead of choosing one complete output, the idyll can keep the strongest parts of several. Three things separate convergence from wandering: 1. Verification happens inside the round, not after a long chain of work has already accumulated. 2. Settled decisions are locked, so the system does not repeatedly explore the same branch. 3. Attempts and contact remain available, so the system does not repeat work without knowing what happened before. ## Why I removed the progress score My first implementation had a distance number and a convergence verdict. They are the obvious things to put on a dashboard. They also invite the model to fabricate precision. Ask for progress from zero to one hundred and it will usually produce a number, even when the criteria share no common scale and half the evidence is missing. The guess looks exactly like a measurement. The better procedure is less compact: 1. Take one condition from the macrostate. 2. Find the evidence relevant to it. 3. Check the condition using its written measurement procedure. 4. Cite the evidence. 5. Return `satisfied`, `not satisfied`, or `not verifiable`. `Not verifiable` is a real answer. It means the next task may be to obtain a measurement rather than to do more work. This also clarifies the role of summaries and indexes. Assume the agent can read every session, revision, [Git](https://git-scm.com/) commit, message, and subagent tool call at no cost. What is still missing? The summary can be reconstructed and the index can be derived, so neither is missing. The macrostate stays missing until a person articulates it, and verification stays missing because reading a record is not the same as checking it against conditions. Summaries, indexes, and scores may still be useful when context is expensive. They are caches. I should add them after observing the expensive read they save, not build them into the concept in advance. ## How to tell whether it is converging I do not think convergence reduces to one number. Three trends have to agree. ### The state moves toward the criteria More of the written conditions become satisfied, or the gap on a measurable condition gets smaller. ### The criteria stop moving Early in a project, criteria should sharpen as the goal becomes better understood. Over time, that churn should decline. If the destination changes after every attempt, apparent progress may only be the system rewriting the goal to match what it happened to do. ### The error signal narrows The next unresolved question should get smaller and cheaper: ```text How would we measure this? -> Which artifact contains the evidence? -> Does section three explain the runtime boundary? -> Add one sentence distinguishing validation from execution. ``` An attempt bought something if it made the next question more specific. A hundred attempts that return the same error signal are activity, not convergence. So an idyll is converging when the state approaches criteria that are stabilizing, while each round produces a narrower next question. This is still a theory, not a result I want to overclaim. The structure is clear enough to test; whether it holds across months-long goals has to come from running it against months-long goals. ## Where the project is now The idyll currently runs as a skill file plus a small verifier that can inspect any folder containing an `IDEAL.md`. The larger engine and interface are on a branch. I am still holding the project to the Markdown rule. Every implementation wants to add stages, indexes, scores, or new types. I keep cutting those back unless repeated use gives me a reason to keep them. I also ran a lab that was supposed to test convergence. It exposed a bad experiment: the custodian was grading its own output, and no real work happened between passes, so the system converged by construction. It could test whether the folder was internally consistent. It could not test whether the folder caused better work in the world. The next test is not another simulation. It is to run real goals for months: a job search, a body-composition goal, a fundraise, a substantial piece of writing. The structure should come out of what those goals repeatedly require instead of being proposed to them in advance. The standard for adding more software remains the same. When the Markdown stops being enough in the same place more than once, that is evidence for the next abstraction. --- ## Project Cyborg: $27k of compute pointed at my own life URL: https://wcdc.io/writing/project-cyborg Date: 2026-06-11 Description: I had $27,000 of expiring Azure credits and no idea what to point them at. A startup I had left behind me came with $150,000 of Azure credits. They were down to $27,000 by the spring, with an expiry date on them, and I could not think of a single thing to do with them that I actually wanted. The problem is stranger than it sounds. I write about wanting leverage constantly. I had fifteen years of my own writing sitting in a folder, indexed and embedded. I had agents that could run in parallel without supervision. And I sat there with a working superpower and no idea what to point it at. Below is what I did about it, and what forty days of pointing compute at my own life actually returned. ## The constraint was never compute The first thing I had to give up was the assumption that the credits were the scarce thing. Compute stopped being a limiting resource some time ago. If I want a hundred thousand model calls I can write a script this afternoon and have them by morning. I could not say what those calls should be for. Everything I actually wanted turned out to need a system around the calls rather than more of them, and a system is the part that costs attention. Attention is the resource in genuinely short supply, and it is the one thing more calls cannot buy. Ask a vague question ten thousand times and you get ten thousand vague answers, which is a reading problem stacked on top of whatever problem you started with. The credits could produce text far faster than I could ever read it. So the question stopped being what to build with the credits and became something narrower: **what kind of system converts compute into value reliably enough that adding more compute adds more value?** I ended up calling that compute market fit. A system has it when the only thing standing between it and more output is more compute. Most systems do not have it. You drive compute into them and they produce noise, or they produce good work that nobody has the attention to consume, and either way the marginal dollar buys nothing. Finding one of those systems was the actual experiment. Everything else was apparatus. ## Deploy it in chunks small enough to abandon $27,000 is not an amount to deploy cleverly. Cleverness needs a plan, a plan needs a hypothesis, and I had no hypothesis worth $27,000. It is fifty-four separate experiments at $500 each. That number is chosen for one property: $500 is enough compute to get a real answer from a real corpus, and small enough that I will abandon the approach without arguing for it. Run one a day, write down what happened, and let the next day's design come from the last day's result. Forty days of that, one experiment a day, each one written up. The commitment was to the cadence rather than to any particular idea, because I did not have a particular idea and pretending otherwise would have produced a plan I defended instead of a search I ran. Three things were ruled out from the start, and ruling them out is most of what made the rest tractable. - **No product.** Turning credits into a thing to sell converts an open question into a roadmap, and I had no evidence yet about which direction was worth committing to. - **No revenue.** Compute goes into capability, never into a business model. A credit spent proving that a structure works is worth more than the same credit spent generating billable output, because the structure survives the credits. - **No forward observation.** Nothing waits for new data. Every program reads history. That last one is the one that made the whole thing possible, and it deserves its own section. ## Compute over history rather than over the future Most systems that watch you are forward-looking. They observe from the moment you install them and get more useful as they accumulate. Which means the day you turn one on, it knows nothing, and the interesting results are months away. I already had the accumulation. Fifteen years of journals, chat logs, voice transcripts and notes, dense and already embedded. So the experiment could run backwards: compute over history rather than over forward observations, which turns a cold start into a warm one and makes the corpus the asset rather than the schedule. That inverts what compute is for. Instead of watching and waiting, the system samples what already exists, many times, from many angles. I had been doing a version of this by hand for a while, running three hundred subagents to pre-generate syntheses of things I might need the next day and then sampling from them. Project Cyborg is that instinct with an accounting layer attached. ## The accounting comes before the research The ledger was built before any research program, because an experiment that cannot say what it spent is not an experiment. Every model call goes through one wrapper, which appends one line to a file: ```ts interface LedgerEntry { ts: string; // ISO timestamp at completion runId: string; program: string; model: string; inputTokens: number; cachedInputTokens: number; outputTokens: number; costUsd: number; ms: number; ok: boolean; error?: string; } ``` Nothing else is allowed to be the source of truth. A database exists for querying and gets rebuilt from the file, so a disagreement between them is settled by throwing the database away. Failed calls land there too, since a run that errored still cost tokens. Three append-only files hold everything the engine knows about itself, and their line counts are the shape of the experiment: ``` engine/data/ledger.jsonl 6,211 one line per model call, with its cost engine/data/runs.jsonl 12,282 lifecycle: run_start, run_end engine/data/frontier.jsonl 413 one line per open question ``` The lifecycle file carries 6,211 `run_end` events against 6,061 `run_start` events. The gap is the useful part: a hundred and fifty runs ended without a recorded start, because I restarted the engine five times mid-flight. A file that recorded only completions would have shown a clean number and hidden every interruption. The runner enforces a hard stop well below the available credits, computed from estimates rather than billed figures, because in the first engine those two diverged by a large factor. The watchdog sits where being wrong by that factor still cannot spend real money. ## Repetition across independent samples is the cheapest signal detector Ten programs ran. The one that mattered most is the simplest. **ensemble** asks the same question many times, each from a different sample of the corpus, independently, and then counts. An idea appearing in one run is a model producing plausible text. An idea appearing in most of them is a property of the material. Only someone with compute to burn can do this. A single expensive answer and forty cheap independent ones cost about the same, and only the second tells you whether the answer is in the corpus or in the model. Independent repetition converts surplus compute directly into confidence, which is as close to compute market fit as anything I found. The other programs are variations on where to point it: connections across documents I never stated in one place, mechanisms for frictions that keep recurring, ideas that must come back with a test and a kill criterion, and a synthesis pass every forty runs or so over everything produced since the last one. Models are routed rather than pooled. The strongest model gets the fewest slots and the questions needing a mechanism; the cheapest gets the most slots and the questions needing volume: ```ts export interface Program { name: string; model: string; weight: number; // scheduler weight charBudget: number; // packet size, ~4 chars per token maxOutputTokens: number; weights?: Record; // packet collection mix buildSystem: () => string; buildPrompt: (packet: Packet, seed: number, extra?: string) => string; } ``` The corpus is a read-only local snapshot, and the sampler that draws from it is seeded and reproducible. An experiment that mutates the material it studies cannot be re-run, and this material is the record I actually live in. The sampler weights are where I got it wrong, and the comment I left when I fixed it is the finding: ```ts // Weights are per-COLLECTION, so per-file draw odds = weight / file count. logs (79 files) // and profile (~100) were being oversampled ~19x/file vs claude (5,225 files) at the old // 0.10/0.05 weights — thousands of redundant re-reads of the same few dense files. ``` A weight that reads as a preference between collections is a preference between files once you divide by how many files each collection holds. Nothing reports it. Every draw is legal, every packet looks varied, and the only symptom is the same material arriving under different seeds. ## What it returned The output is a file of attractors: claims that kept re-emerging across independent samples, each with the number of waves it survived and a pointer to its strongest evidence. ``` 1) Append-only trace/artifact substrate is the missing primitive 12 waves stable 2) The product wedge is a source map for intelligence 5 waves strengthening 3) The right unit is a persistent executable artifact, not chat 11 waves stable 4) The stall is failure to freeze one minimal runtime contract 9 waves stable 5) Reality contact belongs in a compiler or gate, not a philosophy 12 waves stable 6) The trust membrane is a receipt and checkpoint layer 8 waves stable ``` Two of those changed what I did next. **The fourth one is about me, and I did not ask for it.** Nine independent waves converged on the same diagnosis: the repeated stall is not a lack of architecture, it is reaching the right abstraction and then refusing to pin one boring end-to-end contract and one proving case. I have been told versions of that by people. Hearing it from nine independent samples of my own writing is different, because the evidence is entirely mine and the reading has no stake in being kind. **The first and third together decided the next year of building.** An append-only substrate where runs, documents, decisions and learnings are all first-class artifacts with lineage, and a persistent executable object rather than a chat, are the two things that show up everywhere I have built since. So $27,000 of compute bought me a small number of claims about my own work, each one having survived being sampled from many angles, with the wave counts attached so I can tell how hard each is to dislodge. It bought no product, no revenue and no body of essays. ## The engine could not learn between waves For most of its life the thing was open-loop, and the ledger could not see it. Synthesis generated exactly three questions for the next wave and kept a short list of unresolved ones. Those questions went into a markdown file, and nothing read them. Every indicator said the system was healthy. Runs completed, artifacts got written, syntheses produced good questions about those artifacts. The failure was visible only in the content of successive waves: the same five convergences kept re-emerging, so marginal insight per dollar was falling while spend held flat. The question that named it, put to my own system, is the one worth stealing: **are we making an agent or just making random calls?** An [open-loop controller](https://en.wikipedia.org/wiki/Open-loop_controller) acts without measuring what its action achieved, which describes a heater with no thermostat and described this engine exactly. Every wave started at a fresh random sample, because the sampler had no access to anything the synthesizer had learned. The fix is one harvester scraping questions out of the synthesis prose into a line-delimited file: ```json {"ts":"2026-06-11T19:13:28.725Z", "q":"What is the minimal process primitive that unifies organs, documents, triggers, and agent runs?", "source":"2026-06-11181420-synthesis-00193.md"} ``` The `source` field is what makes it a loop rather than a queue, because every question walks back to the artifact that raised it. Two programs consume it, and each returns a verdict rather than prose: ``` frontier-chase answered | needs-data | wrong-question deepen promote | revise | kill ``` Three verdicts, not two. A binary forces every question into settled or unsettled, and the third option in each row carries the information the pair throws away. A `needs-data` verdict has to design the experiment that would settle the question, so the cheap answer of declaring more evidence necessary costs something to give. It seeded with 134 questions pulled from 26 synthesis files and now holds 413. The mechanism transfers: **a feedback loop needs a machine-readable channel between its halves, and prose is not one.** The synthesis output was always full of good questions embedded in paragraphs, which is a format only a human can act on, and a human acting on it was exactly what the experiment could not afford. ## What I would tell someone with a block of expiring credits Compute buys one person very little on its own. The binding constraint is attention, and no quantity of calls relieves it. Compute buys the ability to run a structure many times, which makes the only question worth asking which structures survive that treatment. These did, in the order they mattered: - an accounting layer you trust, built before any research program - a corpus the engine cannot write to, and a sampler reproducible from a seed - history rather than forward observation, so the system starts warm - independent repetition rather than single answers, since repetition is what separates a property of the material from a plausible sentence - a machine-readable path from what the system learned back to what it does next - trajectory capture, which the ledger still does not provide and which belongs above every research program on this list The last one is the gap I named on the first night and never closed. The ledger records that a call happened and what it cost, and nothing about how the run got there, so a good run and a lucky run are indistinguishable in the record and nothing downstream can learn what made a program work. The first engine had most of that list, ran well, and could not accumulate. The distance between a system that produces and a system that improves was one file and a consumer for it, which is the cheapest item here and the one everything else was waiting on. --- ## Cortex: searching everything I've written in one second URL: https://wcdc.io/writing/cortex Date: 2026-03-20 Description: I had 100,000 chunks of journals, model conversations, transcripts, and notes. Making them searchable in a second shaped the retrieval pipeline, index, and storage model. Cortex searches everything I have written: journals going back to 2012, every conversation I have had with a model since 2024, meeting transcripts, voice notes, and book digests. I can ask a question in ordinary language and get the ten passages most likely to answer it. I first tried [qmd](https://github.com/tobi/qmd), Tobi Lutke's local search engine for Markdown corpora. It worked, indexed the whole corpus, and took an afternoon to set up. It also took 15 to 40 seconds to answer a hybrid query and installed about two gigabytes of local embedding models. After a few days, I had stopped using it. That gave me the requirement for Cortex: return useful results in about a second. Search has to fit inside a train of thought. At one second, I will try five versions of a vague question and follow whatever looks promising. At 15 seconds, each query becomes an errand, so I only search when I already expect a useful result. Once I set that budget, the architecture followed from a practical question: what can Cortex compute before I ask anything? ## Keyword and vector search cover each other's misses Keyword search is fast and precise. [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) ranks a passage by the query terms it contains, giving more weight to rare terms. It works especially well for names, quotations, and distinctive phrases. A search for `IdyllicValue` should find that exact string. It misses paraphrases. If I search for "forcing function," BM25 will overlook an entry where I wrote "a constraint that makes me do the thing." Vector search handles that case. Cortex embeds every chunk as a vector and embeds the query with the same model. Nearby vectors tend to express similar ideas, even when they share few words. The tradeoff is precision: semantic search often returns material related to the topic without answering the question, and unfamiliar proper nouns may disappear entirely. I run both searches in parallel and combine their rankings with [reciprocal rank fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf). BM25 scores and vector distances have different meanings and scales, so I discard the raw scores. RRF only uses each result's position in each list: ```ts const k = 60; for (let i = 0; i < bm25Results.length; i++) { const rrf = bm25Weight / (k + i + 1); // add rrf to this chunk's fused score } for (let i = 0; i < vectorResults.length; i++) { const rrf = vectorWeight / (k + i + 1); // add rrf to this chunk's fused score } ``` A chunk that ranks well in both lists rises above one that only one method likes. The constant flattens the top of the curve, keeping a single first-place result from overwhelming consistent evidence from both searches. Fusion takes less than a millisecond. RRF improves recall, though it still cannot tell whether a passage answers the query. BM25 counts word overlap. Vector search compares two independently computed representations. Neither reads the query and passage together. ## I retrieve broadly, then rerank fifty candidates A reranking model reads the query beside each candidate and scores their relevance as a pair. Its judgments are much better than either first-pass search. Running it across 100,000 chunks would be slow and expensive, so Cortex gives it the fifty candidates retained by BM25, vector search, and RRF. That produces a three-stage query path: 1. Run BM25 and vector search in parallel. 2. Fuse their candidate lists with RRF. 3. Send the top fifty to [Cohere's reranker](https://cohere.com/rerank) and return the best ten. The command exposes each stage so I can inspect and tune it: ```text $ organs cortex search --help Options: -m, --mode vector (default), hyde, keyword, hybrid -n, --limit Max results (default: "5") -c, --collection Filter to a single collection -p, --path Filter results to path prefix -v, --verbose Show timing breakdown --pool Candidate pool size before reranking (default: "50") ``` `--mode` lets me compare keyword, vector, and hybrid results for the same query. `--pool` controls how much work reaches the reranker. A larger pool gives it more chances to recover a good passage and costs more per query. `--verbose` prints the timing of every stage because a total alone does not tell me what to fix. ## A flat vector scan fits this corpus better than HNSW I initially assumed Cortex needed HNSW because it is the standard index for vector search. [HNSW](https://en.wikipedia.org/wiki/Hierarchical_navigable_small_world) builds a graph over the vectors and walks that graph toward likely neighbors. It is valuable when a full comparison against millions of vectors would be too expensive. Cortex has roughly 100,000 chunks on one laptop. I also filter searches often: one collection, one directory, or the entire corpus. HNSW makes those filters awkward because its graph was built across the full dataset. Filtering removes nodes that the graph expects to traverse. I chose an exact flat scan using SIMD distance computation. Filters become ordinary SQL conditions, and smaller scopes make the scan faster: ```sql SELECT chunk_id, distance FROM vec_chunks WHERE embedding MATCH ? AND k = ? ORDER BY distance ``` Collection and path constraints are additional `AND` clauses. I get exact ranking, simple filters, and no recall parameter to calibrate against a ground-truth dataset I do not have. The scan grows linearly with the corpus. When Cortex held 300 to 400 chunks, it took about 1.5 milliseconds while embedding a query took 100 to 200 milliseconds. The corpus has since grown by three orders of magnitude, and the scan remains below the slowest query stages. If that changes, I can add an approximate index in response to a measured bottleneck. ## Markdown is the source of truth I had used the Cortex name before for a portable wiki. Each `.cortex` file was a SQLite database containing entries with double-bracket links. A command-line tool edited it, a small web server displayed it, and semantic search eventually indexed its chunks. I made six of these databases from book digests; the largest contained 73 entries and about 400 chunks. Putting the writing inside SQLite made the format responsible for everything around it. I needed an editor, a viewer, synchronization, and diffs. Other tools on the machine could no longer treat the entries as ordinary files. The current Cortex keeps the corpus as Markdown, subtitle, and text files. SQLite stores a disposable search index derived from those files: ```sql CREATE TABLE documents ( collection TEXT NOT NULL, path TEXT NOT NULL, content TEXT NOT NULL, hash TEXT NOT NULL, indexed_at TEXT NOT NULL, UNIQUE(collection, path) ); CREATE TABLE chunks ( doc_id INTEGER REFERENCES documents(id) ON DELETE CASCADE, chunk_index INTEGER NOT NULL, text TEXT NOT NULL ); CREATE VIRTUAL TABLE vec_chunks USING vec0( chunk_id INTEGER PRIMARY KEY, embedding float[1536] distance_metric=cosine ); CREATE VIRTUAL TABLE documents_fts USING fts5( title, content, collection, content='documents' ); ``` The hash makes indexing incremental. Cortex skips an unchanged file and recomputes the chunks and embeddings for a changed one. I can delete the database and rebuild it from the corpus at any time. A derived index can fall behind its sources, so Cortex reports that state explicitly. `status` tells me which files still need indexing or embedding. This keeps stale data visible without turning the index into another source of truth. ## The timings changed what I worked on I timed each query stage separately on the full corpus. These figures show where the current system spends its time; the corpus has roughly doubled since the first implementation. | Stage | Warm | Cold | |---|---:|---:| | BM25 over the keyword index | 22–107ms | 8465ms | | Embed the query | 286–792ms | 873ms | | Scan the vectors | 475–761ms | 955ms | | Fuse the two lists | \<1ms | \<1ms | | Rerank fifty candidates | 351–362ms | 448ms | | **End to end** | **1.2–1.7s** | **10.7s** | Warm queries take 1.2 to 1.7 seconds. The query embedding and vector scan account for most of the variation. The hosted reranker is unusually stable at about 350 milliseconds, and RRF is effectively free. The first query after the system has been idle is completely different. BM25 jumps from tens of milliseconds to 8.5 seconds while the other stages change much less. I have not proved the cause, though the pattern looks like the operating system reading the index from disk into its page cache. The next query returns to the warm range. Separating cold and warm measurements matters here. Their average would describe neither experience and could send me toward the wrong component. The warm path tells me where repeated searches spend time. The cold path isolates a startup problem in the keyword index. The measurements also justified replacing qmd. Its local embeddings protected text from leaving my machine, while Cortex already sent candidates to Cohere. That privacy boundary had already been crossed. Hosted query embeddings removed the two-gigabyte local model and cut a 15-to-40-second query to the range where I would use it. The hosted design leaves a clear bill: one embedding call and one reranking call per search, plus embeddings for every new or changed chunk. I accepted that cost because the local alternative had already shown me its cost in time, disk, memory, and abandoned searches. ## Retrieval semantics belong in the schema version An early version used the wrong vector distance metric. The embedding model produces normalized vectors, so Cortex should compare them with cosine distance. The database accepted another metric without an error or warning. Results still looked plausible; they were simply worse. Correcting the metric required re-embedding the corpus. Bugs in stored representations get more expensive as the index grows, even when the code change is one line. I added a schema version so Cortex can identify an incompatible index and rebuild it deliberately. The vector extension also constrained the runtime. SQLite has to permit extension loading, and the database driver has to expose it. The usual native SQLite package, [better-sqlite3](https://github.com/WiseLibs/better-sqlite3), did not build under [Bun](https://bun.sh/), while the SQLite bundled with macOS was compiled without extension loading. [Homebrew SQLite](https://brew.sh/) paired with [Bun's SQLite driver](https://bun.sh/docs/api/sqlite) satisfied both requirements. That compatibility chain decided the runtime setup. ## Each source type owns its preprocessing Cortex began with Markdown, then gained `.srt` transcripts and plain-text meeting notes. Subtitle files contain sequence numbers and timestamps that waste embedding tokens and distort chunk boundaries. I wanted each new source type to add one implementation without adding format checks throughout the indexer. The indexer selects a chunk strategy by file extension: ```ts interface ChunkStrategy { preprocess(text: string): string; chunk(text: string): Chunk[]; extractTitle(text: string, path: string): string; } ``` The Markdown strategy splits around headings. The subtitle strategy removes its structural noise before using the plain-text chunker: ```ts preprocess(raw) { return raw .replace(/^\d+\s*$/gm, "") .replace(/^\d{2}:\d{2}:\d{2}[.,]\d{3}\s*-->.*$/gm, "") .replace(/\n{3,}/g, "\n\n") .trim(); } ``` Unknown extensions use the plain-text strategy. The generic chunker never needs to know what a timestamp or Markdown heading looks like; that knowledge stays with the relevant format. ## Indexing carries the work that search cannot afford Cortex decides chunk boundaries, computes embeddings, and generates summaries before any query arrives. It indexes the summaries beside the raw text, giving semantic search a distilled description of a long conversation or transcript. At query time, the system only embeds the question, retrieves candidates, fuses them, and reranks fifty passages. Three commands maintain that precomputed state: ```bash $ organs cortex index # hash files and chunk what changed $ organs cortex embed # embed new chunks, ten in parallel $ organs cortex status # report work still outstanding ``` Precomputation also defines what the search index cannot answer. Vector similarity has no concept of recency, so a passage from 2020 can outrank one from yesterday. I use a separate command that reads files by date for questions such as "what happened this week?" Recency and semantic similarity are different orderings, and I keep them separate. The final system is a directory of ordinary files, a derived SQLite database, and two hosted model calls. BM25 preserves exact language. Vector search catches paraphrases. RRF combines their rankings. The reranker spends its attention on fifty candidates. A flat scan keeps filtered search simple and exact at the current corpus size. I arrived at that design by measuring the version I had. The existing tool proved that the corpus was searchable and that 15 to 40 seconds was too slow. Cortex moves almost everything it can to indexing time so the remaining query path is short enough to use while I am still thinking. --- ## ai-organs: building a personal system out of small programs URL: https://wcdc.io/writing/ai-organs Date: 2026-03-19 Description: I split habits, health, finance, writing, and memory into separate tools with their own state, then use a coding agent and the shell to compose them. ai-organs is the personal system I run on my own machine. It includes separate tools for habits, health, finance, Korean study, writing, memory, goals, relationships, and search. Each tool owns its data and exposes a small command-line interface. A coding agent calls them together when I ask a question that crosses domains. I arrived at this structure after building several versions around context. I collected notes, Markdown files, embeddings, and databases, then loaded the relevant material into a model. The answers were useful at first. They degraded as soon as the material went stale. That failure changed what I was building. A folder of personal context can only describe what was true when I last updated it. I wanted components that could ingest new data, check their own state, and tell me when something had broken. I called those components organs. ## An organ owns a domain and maintains it My earlier systems treated the model as the only active part. Everything else was material for it to read. That made me responsible for keeping every fact, summary, and index current. The system consumed the same attention I had built it to save. In ai-organs, each component owns a domain and the work required to maintain it. The habits organ records checks, renders weekly views, and detects problems with its database. The finance organ pulls transactions and tracks the freshness of its data. The writing organ has no database, though it still owns a stable operation: evaluate prose against a set of standards and return a verdict. I made maintenance part of the interface: ```ts export interface OrganLifecycle { name: string; setup(): Promise; doctor(): Promise; fix(checkName: string): Promise; } ``` Twelve organs implement this lifecycle. `setup` prepares the organ on a new machine. `doctor` reports what is missing, stale, or misconfigured. `fix` handles repairs that can be automated. If I cannot write a meaningful `doctor`, the component has no way to distinguish healthy state from silent decay. This definition also separates an organ from a context file. Two directories may contain identical data, while only one has a process responsible for keeping that data accurate. The difference is operational ownership. ## I use organs for practices that lose to willpower I needed an admission rule because almost anything can become a personal automation. "What takes time?" produced a list of minor chores. Most saved a few minutes from work I was already doing. I now ask which useful practices repeatedly disappear when my attention gets thin. Financial review, habit tracking, prose review, and spaced study all have proven value for me. Each one works when I do it consistently, and each one tends to collapse during a bad month. Those are good candidates for organs. A daily financial review may take ten minutes, yet automating it does not save ten minutes because I was often skipping it. The useful change is frequency: the review goes from occasional to daily. I am scaling a practice that my available willpower could not sustain. This test keeps ai-organs focused on work I already understand. I do not automate a speculative routine and hope it becomes valuable. I automate the maintenance around a practice that has already paid for itself. ## Developer tools gave me the initial inventory Introspection gave me a short, arbitrary list of organs. My development environment gave me a better one because it already contained small programs selected through years of actual use. Developer tools perform general information operations. A build system turns one representation into another. Version control makes history addressable. A linter evaluates work against a standard and returns errors that another process can act on. Those operations apply well outside software. That analogy produced the current package inventory: ```text packages/ habits/ health/ finance/ korean/ social/ cortex/ memory/ macrostates/ log/ voice/ imagine/ google/ writing-tools/ digest-book/ ai-provider/ cli/ data/ ``` The directory contains sixteen organs and three infrastructure packages: the model provider, the CLI bindings, and the data resolver. Each organ has a domain, a command surface, and an output that another program or agent can inspect. A chat window does not meet that bar. A prose linter does, even though it stores nothing. ## MCP gave the agent tools; the shell made them composable I first considered exposing every organ through one [MCP](https://modelcontextprotocol.io/) server. MCP lets a model choose a function, call it, and receive the result in its conversation. That works well for isolated operations. Cross-domain questions reveal the cost. Suppose I ask for my average strain across the last thirty workouts, grouped by the weeks when I actually meditated. The agent needs two datasets and a join. Through MCP, it calls one tool and reads thirty workout records into the conversation. It calls another and reads a list of habit dates. It then performs the join inside its context. The conversation becomes the storage layer for every intermediate result. Large outputs consume tokens even when the model only needs to pass them to the next operation. Reusing a result means keeping it in context or reading it again. MCP supplies function calls, while composition still has to happen inside the model. The shell already has a place for intermediate data: ```bash organs health strain -n 30 > /tmp/strain.json organs habits show "Meditate" --json \ | jq -r 'select(.checked).date' > /tmp/med.txt join /tmp/strain.json /tmp/med.txt ``` The two organs write files, and `join` combines them. The model can inspect the final result without reading every number along the way. Pipes pass output directly between programs; files and variables preserve results for later use. This is why ai-organs is a CLI instead of a large tool server. I already had a filesystem, processes, pipes, variables, permissions, and remote execution. Recreating those features inside a model protocol would have given me a weaker version of the operating system on my laptop. ## Functions, organs, and systems have different jobs I use three levels of composition: - A **function** performs one operation, such as checking a habit, reading today's strain, or appending a log entry. - An **organ** groups functions around one domain and owns that domain's state. - A **system** reads across several organs to answer a larger question. The morning briefing is a system. It reads recovery from health, this week's adherence from habits, recent spending from finance, and current goals from macrostates. Each organ returns a narrow fact. Their combination can tell me that a week is coming apart. I define systems as prompts containing organ calls: ```md Read this morning's state and tell me what it means, not what it says. organs health strain organs habits day organs finance tx -n 20 organs macrostates tree Name the one thing that changed since yesterday. If nothing changed, say so. ``` Writing a system takes a paragraph and a few commands. The organs remain independent, while the agent decides how to combine their outputs for the current task. | Biology | Unix | ai-organs | |---|---|---| | cell function | a command such as `grep` | a function such as `habits check` | | organ | a program such as `git` | an organ such as `organs habits` | | organ system | a pipeline of programs | a prompt such as the morning briefing | | organism | a shell session | an agent session | | nervous system | the shell | the agent | The model sits above the organs because it owns no domain state. It reads their outputs, chooses what to call next, and composes results. The shell handles the mechanical movement of data; the model handles decisions whose sequence cannot be fixed in advance. ## I stopped designing a personal operating system I had spent two years describing this project as a personal operating system. One architecture document specified a kernel context, a user context, a process messaging interface, and scoped databases for each process. My laptop already provided those facilities. The coding agent could coordinate processes. The shell could connect them. The filesystem could store shared artifacts. SQLite could give each component a local database. iCloud could synchronize ordinary files. I reduced the architecture to three assignments: - [Claude Code](https://claude.com/product/claude-code) coordinates the work. - The `organs` CLI exposes each component's operations. - [iCloud](https://www.icloud.com/) synchronizes the files. That decision also killed two of my own packages. One was a wiki format that stored entries inside a custom database. Once writing entered that database, every editor, viewer, sync tool, and diff needed an integration. Markdown files already worked with all of them. The habits command surface shows how little custom substrate I needed: ```bash organs habits check "Meditate" organs habits check "Exercise" -n "Ran 5km" organs habits check "Exercise" -d 2026-03-17 organs habits uncheck "Exercise" organs habits day organs habits week organs habits grid organs habits stats organs habits add "New Habit" -d "description" organs habits archive "Old Habit" ``` Two commands write entries, several read them at different resolutions, and two manage the habit list. The surrounding infrastructure is generic: a coding agent, a cloud drive, a shell, and SQLite. ## Each organ owns its state and one resolver owns its location The organs use different storage because their domains need different things: | Organ | Domain | State | |---|---|---| | `habits` | behavior tracking | SQLite | | `health` | body data | the [WHOOP](https://developer.whoop.com/) API | | `finance` | money | the [Mercury](https://mercury.com/) API | | `korean` | learning | Markdown lessons | | `write` | prose quality | stateless | | `memory` | world model | [Zep](https://www.getzep.com/), a hosted temporal graph | | `social` | relationships | SQLite and one Markdown file per person | | `macrostates` | goals | Markdown folders encoding status | I only enforce two boundaries. An organ cannot read or write another organ's private state. It also cannot hardcode the location of its own data. One resolver chooses the root for every package: ```ts function resolveDataRoot(): string { if (process.env.AI_ORGANS_DATA) return process.env.AI_ORGANS_DATA; if (existsSync(ICLOUD_BASE)) return join(ICLOUD_BASE, DATA_DIR_NAME); const xdg = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"); return join(xdg, DATA_DIR_NAME); } ``` The explicit override wins. A machine with my iCloud folder uses it. Other machines fall back to the platform data directory. No organ contains an absolute path. I learned the value of this boundary when one package bypassed the resolver and opened a database at its hardcoded iCloud path during command registration. On a Linux machine without that path, every `organs` command crashed at startup. A broken task-list database prevented an unrelated habit check from running. I fixed the path and delayed resource creation until the relevant command executes. Now a missing store takes down one operation at call time instead of the entire CLI at startup. State ownership also requires a single writer. ai-organs runs on my laptop and a Linux box, while only the laptop writes to synchronized SQLite files. SQLite cannot merge concurrent histories. If both machines write, a habit check or log entry can disappear without a conflict I can resolve. ## Libraries contain domain logic; the CLI contains wiring The organs began as independent packages. I wanted them to remain usable from TypeScript while still sharing one command convention. Each package exports its data types, readers, renderers, lifecycle, and command registration: ```ts export { HabitDB } from "./db"; export type { Habit, Entry } from "./db"; export { renderDay, renderWeek, renderMonth, renderStats } from "./views"; export { habitsLifecycle } from "./lifecycle"; export { registerHabitsCommands } from "./commands"; ``` The CLI package owns flags, output formatting, and command registration. Other code can call `HabitDB` or a renderer directly without pretending to be a terminal process. The wiring layer is also the only place where one organ can break the common command surface. I keep registration lazy so importing an organ does not open files, connect to APIs, or construct services: ```ts export function registerHabitsCommands(cmd: Command, providedDb?: HabitDB) { const runWithDb = (fn: (db: HabitDB) => T): T => { const db = providedDb ?? new HabitDB(); try { return fn(db); } finally { if (!providedDb) db.close(); } }; } ``` Registration describes the command. The handler opens the database when the command runs and closes it afterward. A machine without the habits database can still use every unrelated organ. ## I designed the human input around bad weeks Some state can only come from me. A wearable knows my strain, while it cannot know that I abandoned a goal or forgot to record a workout. The habits schema makes that ambiguity visible: ```sql CREATE TABLE entries ( habit_id INTEGER NOT NULL REFERENCES habits(id), date TEXT NOT NULL, checked INTEGER NOT NULL DEFAULT 1, notes TEXT, UNIQUE(habit_id, date) ); ``` Completing a habit writes a row. Skipping it and forgetting to record it both leave no row. More schema cannot recover information that never entered the system. I therefore limit the system to two required moments of attention each day: a morning check and an evening check. Everything between them runs automatically or gets captured as I mention it to the agent. Two check-ins give me less information than continuous logging. They also survive weeks when continuous logging disappears entirely. The shared log is one timestamped Markdown file per day. The agent appends entries as I work, so I do not have to open a form. I keep the log as plain text because I read and diff it directly; query performance matters less there than visibility. The daily cycle gives maintenance a deadline. A continuously running personal system can become vaguely stale without a clear failure point. A morning and evening cycle tells both me and the agent when each check is due. ## The interface can change without moving the state A shell is a good composition layer and a poor dashboard. I wanted to see the day, habits, strain, balance, and recent log entries without asking the agent to assemble them every time. I built a text interface with a navigation rail, a main view, an activity feed, glance metrics, and a command bar. Plain text entered in the bar goes directly into the daily log. The most common interaction is also the cheapest one. The dashboard reads across several organs through a snapshot builder. It receives read access only. Every write still goes through the organ that owns the affected state, preserving the same boundary as the CLI. Three different interfaces have now sat above the command surface. I could replace each one without migrating the underlying data. That is the practical payoff of state ownership. The interfaces also exposed a limit in the architecture. When I stopped opening a dashboard, the interactions it had encouraged disappeared with it. Stable storage does not create attention. The surface still has to earn a place in my day. ## ai-organs is the boundary between maintenance and composition The system now has a simple division of work. Each organ maintains one domain, owns its state, and exposes commands. The operating system stores and moves intermediate results. The coding agent reads across organs and decides how to compose them for the question I am asking. That boundary keeps the system small. Adding a domain means building one package and its maintenance lifecycle. Adding a cross-domain view means writing a prompt or snapshot that calls existing organs. Replacing an interface leaves the state untouched. ai-organs runs on Markdown, SQLite, APIs, command-line programs, and a coding agent. The individual technologies are ordinary. The useful part is that I no longer need one model context, one database, or one application to contain my entire life. I need small components that stay accurate on their own and a reliable way to combine them. --- ## Elements of Agentic System Design: a map of the code around a model URL: https://wcdc.io/writing/harness-engineering Date: 2026-03-13 Description: Breaking down intelligent behavior as code patterns around context management. [Elements of Agentic System Design](https://github.com/idyllic-labs/elements-of-agentic-system-design) maps ten behaviors people attribute to AI agents to the code that produces them. I wrote it while building an agent runtime because I needed to decide which capabilities the runtime should own and which ones applications should supply. ## Motivation That boundary kept moving. Memory could live in the runtime or in application storage. A planner could be a built-in primitive or another prompt. Tool execution could belong to the model harness, a plugin system, or the application itself. A feature list did not help because every feature could plausibly sit on either side. I started writing a book to force the decision. Each chapter needed working examples, and each example needed real code. If I could not explain a behavior without assigning it to a component, I had not found the boundary yet. I kept the book independent from [Idyllic](https://idylliclabs.com). The examples could use my runtime, but the explanations had to begin with language models and ordinary software. Otherwise I would have documented one framework instead of producing a map I could use to design it. ## Breaking down "intelligence" I started with a narrow description of a language model: it takes text and returns text. It does not retain your last message, know who you are, or affect anything outside its response. The surrounding program creates those behaviors. - An agent remembers your name because the program loads your name into the prompt. - It searches the web because the program parses its output, matches a tool name, runs a function, and sends the result back in another prompt. - It works overnight because a scheduler starts it and reconstructs its context from stored data. The model still reads text and writes text in every case. Storage, loops, schedulers, parsers, policies, and function calls turn those responses into a system. Most references organize that system by technique: retrieval, function calling, planning, caching, and multi-agent coordination. Those categories match libraries and papers. They do not match the problem a developer brings to a debugger. A developer says, "The agent forgot what I told it," or, "It keeps wandering off." So I worked backward from each observed behavior to the code that could have produced it. | "It seems to..." | The program actually... | | --- | --- | | Remember what I said | includes conversation history in the next prompt | | Have long-term memory | stores data and retrieves selected records into context | | Do things in the world | parses structured output and dispatches a function | | Think step by step | runs several model calls and passes state between them | | Plan before acting | asks for a plan, then executes each step | | Check its own work | runs a separate verification call and retries on failure | | Have multiple experts | routes work among prompts with different instructions | | Work while I sleep | starts from a timer or external event | | Learn from experience | stores outcomes and retrieves them during later work | When an agent forgets a fact, I can inspect the context assembled for that call and the storage and retrieval code that supplied it. I do not have to search a catalogue of techniques and guess which chapter contains the symptom. ## The ten elements The behavior-to-code pass produced ten elements: | # | Element | What it describes | Where I look in the code | | --- | --- | --- | --- | | 1 | Context | information available during one model call | token budget and context construction | | 2 | Memory | stored information retrieved into later contexts | storage and retrieval | | 3 | Agency | conversion of model output into effects | parser, policy, and execution boundary | | 4 | Reasoning | chains, loops, and branches across model calls | call structure and computation between calls | | 5 | Coordination | communication and sequencing between reasoning processes | execution flow and data flow | | 6 | Artifacts | shared persistent state | typed objects, operations, and lifecycle | | 7 | Autonomy | triggers and ownership of the main loop | schedulers, event handlers, and context reconstruction | | 8 | Evaluation | measurement of success | quality signals and scoring functions | | 9 | Feedback | signals that steer current work | signal sources and injection points | | 10 | Learning | feedback stored for future work | extraction, storage, and update pipeline | Each element had to point to a code address. If I could not say where a developer would implement or debug it, I left it out or folded it into another element. Consider a research agent that keeps reading material nobody requested. The complaint can come from four places: ``` Context The task description permits a broad search. Reasoning The loop never checks whether the current question still serves the task. Agency The search tool has no scope parameter, so the model cannot request a narrow search. Evaluation Nothing checks whether the agent used the documents it retrieved. ``` Those diagnoses lead to changes in different files. The map does not choose one without evidence. It gives me the four places to inspect before I spend a week rewriting the prompt. ## Agency Tool calling almost became an element because every model SDK presents it as a core capability. It only covers one path from model output to an application effect. A tool-calling API standardizes three parts: | Part | What the application does | | --- | --- | | Format | asks the model for structured output instead of parsing prose | | Vocabulary | gives the model a closed set of names and schemas | | Router | translates the selected name and arguments into a function call | The model only produces a string. `delete_all_records` remains inert until application code parses it, finds a registered function, checks the policy, and runs it. | Stage | Example | | --- | --- | | Model output | `{"tool": "delete_all_records"}` | | Application boundary | parse, look up, authorize, dispatch | | Effect | delete rows, write a file, or send an email | Application code creates agency in the middle row. The registry and policy also determine which strings can become effects. If the dispatch table has no deletion function, the model cannot delete anything through that path. That gives me three debugging questions when an agent takes a bad action: 1. Did the model select a bad action from the instructions and context it received? 2. Did the execution layer authorize an action it should have blocked? 3. Did I expose a capability or policy that made the outcome possible? The same classification covers plugin protocols and skills. A plugin protocol extends the functions available to the router. A skill loads procedural instructions into context. Both matter, but neither needs a new element because the ten elements already identify the code involved. ## Editing the map My first outline borrowed levels from science: philosophy, physics, chemistry, engineering, and applications. I removed that structure once the examples showed that it separated principles from the code that used them. I introduced each principle where it affected a design choice instead. The element list also changed as I built examples. I applied the code-address test to every edit: | Edit | Reason | | --- | --- | | Renamed proactivity to autonomy | Proactivity describes how the behavior feels. Autonomy identifies triggers and ownership of the main loop. | | Folded grounding into context | Grounding changes which facts the program places in the prompt. | | Folded planning into reasoning | Planning uses one particular arrangement of model calls. | | Renamed semantic objects to artifacts | The broader name covers any shared typed state used for coordination. | | Separated feedback from learning | Feedback changes current work. Learning stores a change for later work. | Two names collapsed when they led to the same code. One name split when it led to two different implementations. I generated more examples whenever a boundary remained unclear and watched for the same code appearing across them. ## Externalization Three elements move information out of one model call so another call can use it: ``` Memory stores context for one agent to retrieve later Artifacts store shared state that several agents can change Learning stores feedback that changes future behavior ``` They use the same broad operation and serve different consumers. Memory reconstructs what one agent should know. Artifacts coordinate work around a shared object. Learning changes how later work runs. Evaluation, feedback, and learning form another dependency: ``` Evaluation measures the result. Feedback uses that measurement to change the current task. Learning stores the change for future tasks. ``` A system can measure a bad result and continue unchanged. It can also repair the current task and repeat the same mistake tomorrow. Keeping the three elements separate tells me which connection the implementation lacks. ## Writing the book I gave every chapter the same four sections so a reader could apply each element in the same order: ```markdown ### Introduction ### Demystification ### Design Considerations ### The Reframe ``` The introduction names the behavior. Demystification traces it to code. Design Considerations covers the implementation choices. The Reframe converts a complaint about the agent into a change a developer can make. For context, the reframes look like this: ```markdown Before: "Why does the AI keep forgetting things?" After: "I need to load the relevant history into context on each call." Before: "The model is hallucinating." After: "I left out the facts needed to answer, so the model filled the gaps." Before: "The AI's personality is inconsistent." After: "The system prompt or reconstructed context changed between calls." ``` I wrote each reframe so the reader would know which code to open. ## Diagrams My first diagrams repeated the prose. I replaced them with diagrams that locate the component responsible for a behavior. The tool-calling diagram gives the parser and policy boundary more space than the model and tool because that code decides whether text causes an effect. The continuity diagram shows what the program stores between calls and what it reconstructs for the next one. The identity diagram holds the conversation history constant while it changes the model, then holds the model constant while it changes the history. The diagrams let me point at the component that caused the observed behavior. ## Context reconstruction I used conversation continuity to test the map. On every turn, the application assembles a new context from stored messages and other data. The model receives that context as text. It does not carry the previous call inside itself. This changes how I debug several common failures: - If an agent loses a fact, I check whether storage retained it and whether retrieval placed it in the next prompt. - If an agent contradicts an earlier answer, I compare the two contexts. - If its personality changes, I compare its instructions, retrieved history, and model configuration. I can also test where continuity comes from: ``` Keep the history and replace the model. The character mostly remains. Keep the model and replace the history. The character changes. ``` The application produces continuity by preserving state and reconstructing context. I can inspect both operations in the logs and compare the results across runs. ## Shipping the map The map targets people who build frameworks, languages, SDKs, and agent platforms. They decide where memory lives, who owns the execution loop, how tools cross the policy boundary, and which state agents share. [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents), [12-Factor Agents](https://github.com/humanlayer/12-factor-agents), and [agentic design patterns](https://www.deeplearning.ai/the-batch/how-agents-can-improve-llm-performance/) teach construction patterns. Elements gives a developer a way to take an existing system apart: identify which elements it implements, find them in the repository, and see what is missing. I ship the framework as a [Claude Code](https://claude.com/claude-code) skill: ```markdown --- name: intelligence-designer description: Analyze and design agentic AI systems using the Elements of Agentic System Design framework. Use when asked to analyze an agent architecture, understand how an agentic system works, or design a new agent system. argument-hint: allowed-tools: Read, Grep, Glob, WebFetch --- ``` The skill receives four read-only tools. It can inspect a repository and apply the map, but it cannot change the code. That keeps analysis separate from implementation and lets a developer review the diagnosis before acting on it. ## Harness engineering I initially called the field agentic system design. A [LangChain post](https://blog.langchain.com/) led me to the term harness engineering, which HumanLayer and others were already using for the code around a model. I adopted the common term because it named the same work and saved readers from translating another private vocabulary. The ten elements still describe the decisions inside a harness. A model, a system prompt, and a tool list do not provide memory across sessions, autonomous execution, shared state, evaluation, or learning. Storage, schedulers, execution policies, context builders, and feedback pipelines provide them. I now use the elements to settle the boundary that started the project. When an agent appears to remember, plan, act, coordinate, or learn, I trace the behavior to one of those mechanisms, find the file that owns it, and decide whether it belongs in the runtime or in the application. --- ## OpenClaw: what changes when an agent stays running URL: https://wcdc.io/writing/openclaw-vs-cc Date: 2026-01-31 Description: How OpenClaw's always-running daemon produces memory, autonomy, and tool use, traced through its source. ![OpenClaw header](/assets/openclaw-header.png) [OpenClaw](https://github.com/openclaw/openclaw), previously called Moltbot and Clawdbot, runs an AI agent as a daemon on your computer. You talk to it through WhatsApp, Telegram, Discord, iMessage, Slack, or another connected channel. It can use the shell, browse the web, edit files, send messages, and start work on a schedule. [Peter Steinberger](https://x.com/steipete) created it. [Federico Viticci's review on MacStories](https://www.macstories.net/stories/clawdbot-showed-me-what-the-future-of-personal-ai-assistants-looks-like/) in January 2026 brought it a wide audience. Most of those capabilities already exist in [Claude Code](https://claude.com/claude-code). I wanted to understand why OpenClaw still felt like a different product to the people using it. The source code points to one architectural difference: OpenClaw stays running after the task ends. That changes who owns the work between model calls. The daemon keeps the files, transcripts, schedules, triggers, and message connections alive. A model call becomes one step inside a longer process instead of the whole process. I used the [ten elements of agentic system design](https://github.com/idyllic-labs/elements-of-agentic-system-design) to trace how OpenClaw builds that process and where the design remains thin. ## The difference | | Claude Code | OpenClaw | | --- | --- | --- | | Lifecycle | starts for a task, then exits | runs continuously as a daemon | | Persistence | organized around a session | keeps files and transcripts across restarts | | Interface | terminal | messaging apps and terminal | | Triggers | user starts the session | messages, cron jobs, webhooks, and heartbeats | | Installation | command-line program | LaunchAgent on macOS or systemd service on Linux | Suppose I think of an automation while walking: remind me to go to the gym at 2 p.m., check again at 5 p.m., and bother me if I still have not gone. Claude Code can write that program, but I still have to open a terminal, explain the task, create the script, install the schedule, decide where to store state, and inspect it when it fails. With OpenClaw, I can send the instruction from my phone. The running process already owns a scheduler, persistent workspace, conversation history, and return channel. The agent can turn the request into a scheduled job and message me later through the same conversation. That smaller setup cost changes how people use it. An automation no longer needs to justify a small software project. Users can accumulate reminders, monitors, summaries, and recurring checks until the daemon functions like a personal server with a conversational interface. ## The daemon The daemon gives OpenClaw one control plane for work that would otherwise end up scattered across cron scripts, [Zapier](https://zapier.com), [n8n](https://n8n.io), [Make](https://www.make.com), and one-off programs. Its workspace exposes the state as ordinary files under `~/.openclaw/`, so a user can inspect and edit the same material the agent uses. The process also reconnects each new model call to earlier work. When a message arrives, OpenClaw: 1. loads its system prompt, identity files, history, and relevant memory; 2. calls the model; 3. runs any requested tools and returns their results to the model; 4. sends the final response back through the originating channel; 5. appends the exchange to the session transcript. The model still handles one context window at a time. The daemon creates continuity by rebuilding that window from persistent state. ## Context OpenClaw assembles the system prompt from files in the workspace: ```text ~/.openclaw/workspace/ ├── SOUL.md personality and tone ├── USER.md information about the user ├── AGENTS.md operating instructions ├── TOOLS.md tool guidance ├── HEARTBEAT.md periodic checks ├── MEMORY.md curated long-term notes └── memory/ ├── 2026-01-28.md daily log ├── 2026-01-29.md └── ... ``` The prompt builder loads these files as separate context fragments. If it finds `SOUL.md`, it tells the model to follow that persona and tone. `USER.md` supplies personal facts, while `AGENTS.md` and `TOOLS.md` tell the agent how to work. Here's the code that loads context files (`src/agents/system-prompt.ts`): ```typescript const contextFiles = params.contextFiles ?? []; if (contextFiles.length > 0) { const hasSoulFile = contextFiles.some((file) => { const normalizedPath = file.path.trim().replace(/\\/g, "/"); const baseName = normalizedPath.split("/").pop() ?? normalizedPath; return baseName.toLowerCase() === "soul.md"; }); lines.push("# Project Context", "", "The following project context files have been loaded:"); if (hasSoulFile) { lines.push( "If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.", ); } lines.push(""); for (const file of contextFiles) { lines.push(`## ${file.path}`, "", file.content, ""); } } ``` A user can open and edit the files that define the agent's identity. A different `SOUL.md` produces different behavior because the next model call receives different instructions. The personality does not live inside the daemon or the model. It comes from files the daemon loads on every run. OpenClaw also writes important context to disk before a conversation reaches its limit. By default, the memory flush runs 4,000 tokens before compaction. The model receives an instruction to save useful details, and later calls can retrieve those details after the original messages have left the window. The check (`src/auto-reply/reply/memory-flush.ts`): ```typescript export function shouldRunMemoryFlush(params: { entry?: Pick; contextWindowTokens: number; reserveTokensFloor: number; softThresholdTokens: number; }): boolean { const totalTokens = params.entry?.totalTokens; if (!totalTokens || totalTokens <= 0) return false; const contextWindow = Math.max(1, Math.floor(params.contextWindowTokens)); const reserveTokens = Math.max(0, Math.floor(params.reserveTokensFloor)); const softThreshold = Math.max(0, Math.floor(params.softThresholdTokens)); const threshold = Math.max(0, contextWindow - reserveTokens - softThreshold); if (threshold <= 0) return false; if (totalTokens < threshold) return false; const compactionCount = params.entry?.compactionCount ?? 0; const lastFlushAt = params.entry?.memoryFlushCompactionCount; if (typeof lastFlushAt === "number" && lastFlushAt === compactionCount) { return false; } return true; } ``` ## Memory The filesystem remains the source of truth. OpenClaw stores curated facts in `MEMORY.md`, appends daily notes under `memory/`, and writes complete session transcripts as JSONL: ```text ~/.openclaw/agents//sessions/.jsonl ``` A SQLite index with [`sqlite-vec`](https://github.com/asg017/sqlite-vec) makes the Markdown files searchable. OpenClaw combines vector similarity with [BM25](https://en.wikipedia.org/wiki/Okapi_BM25) keyword scores, then returns the highest-ranked passages to the context builder. A saved fact does not automatically enter the next context. Retrieval still has to select it. When the agent appears to forget something, I would inspect two steps: whether OpenClaw stored the fact, and whether the next run retrieved it. ## Agency OpenClaw gives the agent several ways to affect the computer around it: - shell commands with PTY support; - browser control through a dedicated Chromium instance; - messaging through connected channels; - HTML, CSS, and JavaScript served through [Canvas](https://docs.openclaw.ai/platforms); - device capabilities from connected phones and laptops; - cron jobs, webhooks, and sub-agent sessions. A model response cannot perform any of those actions by itself. OpenClaw parses the requested tool call, checks the available tools and policy, and dispatches application code. That execution boundary determines what the agent can do. OpenClaw controls the boundary at three levels: 1. The sandbox decides where a tool runs. The main session runs on the host by default, while group chats can run inside [Docker](https://www.docker.com). 2. Per-agent allow and deny lists decide which tools a session can call. 3. Elevated approvals provide an escape hatch for commands that need more access. The registry defines capability, and the policy defines permission. Removing a destructive tool closes that path even if the model requests it. Allowing the tool on the host gives a prompt the same practical reach as the user account running OpenClaw. ## The loop OpenClaw uses a conventional agent loop. It assembles context, calls the model, executes a requested tool, adds the result to context, and calls the model again. The loop ends when the model returns a final response. The core runner also handles queueing, authentication, model selection, context limits, and failover. It does not impose a separate planning stage or a generate-then-evaluate pipeline. The model chooses the next action from the current context, observes the result, and chooses again. For larger tasks, a parent session can start a child with `sessions_spawn`. The child receives its own context and transcript, completes the assignment, and reports the result to the parent. `sessions_send` lets sessions exchange messages directly. OpenClaw stops the tree at one level. A child session cannot spawn another child: ```typescript if (typeof requesterSessionKey === "string" && isSubagentSessionKey(requesterSessionKey)) { return jsonResult({ status: "forbidden", error: "sessions_spawn is not allowed from sub-agent sessions", }); } ``` That limit prevents recursive agent growth. Agents can still coordinate through shared workspace files, which also serve as persistent artifacts after a session ends. ## Autonomy The daemon can begin work without an active conversation. OpenClaw exposes four mechanisms for that. **Cron** runs a task at a precise time. A job can reuse the main session or start in an isolated session: ```json { "id": "daily-standup", "schedule": "0 9 * * 1-5", "task": "Check my calendar and Slack for today's priorities", "session": "isolated" } ``` **Webhooks** let GitHub, Zapier, home automation, and other external systems start a run. **Heartbeat** wakes the main session every 30 minutes by default. The agent reads `HEARTBEAT.md`, checks the current context, and returns `HEARTBEAT_OK` without sending a message when nothing needs attention. Cron executes a known task at a known time. Heartbeat asks the agent to inspect the situation and decide whether to act. **[Lobster](https://github.com/openclaw/openclaw/tree/main/extensions/lobster)** handles workflows that need deterministic steps, approval gates, and resumable state. It stores those pipelines as `.lobster` files instead of asking the model to improvise the entire sequence on every run. These triggers produce the practical difference from a terminal session. A user can leave, close the laptop, or stop thinking about the task while the process retains responsibility for starting it again. ## Evaluation OpenClaw invests much less in evaluation, feedback, and learning than it does in context, memory, agency, and autonomy. The built-in evaluation layer tracks tokens and cost and includes [Vitest](https://vitest.dev) suites for the software itself. It does not provide an LLM judge, prompt experiments, or automatic quality metrics. For a personal assistant, the user sees the output and supplies most of the evaluation. Feedback follows the same pattern. A user corrects the agent in conversation, and the agent can write the preference into `MEMORY.md`. A later memory search may load that preference into context. OpenClaw does not maintain a central response-quality dataset or optimize against one. The agent can also edit `SOUL.md`, `AGENTS.md`, and the other files that shape later prompts. That gives it a form of persistent self-modification. It changes instructions and memory on disk; it does not tune model weights or automatically prove that the change improved future work. OpenClaw can preserve a correction and reuse it. The user still has to judge the result, and the system has no strong built-in loop for measuring whether the stored correction helped. ## Permissions Persistence and broad tool access increase the cost of a bad instruction. A short-lived coding session ends when I close it. OpenClaw keeps listening, retains credentials and context, and can act later through schedules or incoming messages. The main risks follow directly from those capabilities: - host shell access exposes local files and processes; - content from the web, email, or chat can carry prompt injections; - local configuration files may contain credentials; - stored instructions can trigger harmful behavior later; - messaging and webhooks give the agent channels for sending data out. [Vectra applies](https://www.vectra.ai/blog/clawdbot-to-moltbot-to-openclaw-when-automation-becomes-a-digital-backdoor) [Simon Willison's](https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/) "lethal trifecta" to this combination: private data, untrusted input, and external communication. OpenClaw contains sandbox, tool-policy, and approval controls, but the main session runs on the host by default because local access supplies much of the product's value. Every new tool or trigger widens the paths from text to effects. A secure installation therefore depends on concrete configuration: which sessions run in containers, which tools each channel can use, which actions require approval, and which credentials the process can read. ## Deployment OpenClaw can run locally as a LaunchAgent on macOS or a systemd service on Linux. That gives it direct access to the user's machine and keeps the workspace under the user's control. Cloudflare also publishes the [moltworker](https://github.com/cloudflare/moltworker) reference deployment. It combines Workers, the Sandbox SDK, R2 storage, Browser Rendering, and AI Gateway. The Workers paid plan starts around $5 per month, with model and service usage charged separately. The deployment choice changes the security boundary. A local daemon can reach the user's files and applications. A hosted version trades some of that reach for a container and network boundary that the operator can configure separately. ## The ten elements The full decomposition shows where OpenClaw concentrates its engineering: | Element | OpenClaw implementation | | --- | --- | | Context | identity files, session history, memory retrieval | | Memory | Markdown files, JSONL transcripts, SQLite vector and keyword search | | Agency | shell, browser, messaging, devices, cron, and webhooks | | Reasoning | iterative model and tool loop | | Coordination | parent and child sessions with one delegation level | | Artifacts | workspace files, Canvas output, and transcripts | | Autonomy | cron, webhooks, heartbeat, and Lobster workflows | | Evaluation | token and cost tracking, plus software tests | | Feedback | corrections written into memory files | | Learning | later prompts changed through memory and identity-file edits | Context, memory, agency, and autonomy explain most of the excitement. OpenClaw keeps a model connected to the same state, tools, triggers, and return channels for weeks instead of reconstructing that setup as a new project every time. Evaluation and learning remain mostly manual. Permissions require careful configuration because the daemon combines long-lived context with access to private data and external systems. OpenClaw uses familiar models, tools, files, and schedulers. Keeping them together inside one running process removes enough setup work that small automations become worth creating. That is why it feels different from opening Claude Code, even when Claude Code could perform the same individual action. --- ## State Space Explosion URL: https://wcdc.io/writing/state-space-explosion Date: 2026-01-31 Description: You open one problem with AI, and thirty minutes later there are five plausible directions and no way to pick between them. There's a feeling I've had trouble naming. You open Claude to solve a specific problem. Thirty minutes later, you're looking at five different approaches, each with three variants, each variant raising two new questions. The original problem is buried somewhere. Your mind is racing. It feels like suffocation, a dire need to collapse the superposition. I've started calling this **state space explosion**: the combinatorial unease that builds when AI expands possibilities faster than you can prune them. ## The Phenomenon 1. You start with a forcing function: curiosity about a specific problem, a question you want answered 2. AI responds with branching possibilities ("Here are five approaches...") 3. Each branch spawns sub-branches ("For approach 2, you could either...") 4. Your working memory fills with options you can't evaluate 5. The original forcing function gets buried under the load 6. You're now "exploring" without anchor The issue is structural: AI generates options faster than human working memory can evaluate them. Your brain holds roughly 7 items in working memory. Evaluating 20 options against each other is O(n²), meaning 190 pairwise comparisons on hardware limited to 7 slots. ## The Corrupted EV Sensor Normally you run an implicit expected value calculation on possible actions: some weighted function of reward, probability, effort, and time. You don't consciously compute this. You just *feel* which paths seem promising. AI interferes by generating options whose value you can't estimate. You can see twenty options but you have no experiential data on any of them. The paths are visible but unweighted. When enough options are unpriced, your decision process degrades. You're looking at a list with no way to rank it. ## The Depth-First Trap AI tends to go depth-first when exploring solutions. It picks one approach and dives deep before you've had a chance to survey the landscape. This makes state space explosion worse. The trap: you typically want breadth-first pruning before going deep. Survey the options, eliminate the obvious non-starters, then commit to exploring one path. But AI's default interview style pulls you into the weeds of option #1 before you've even seen options #2 through #5. This triggers scarcity processing: "I must search all paths or miss the best one." Wrong algorithm for an abundance problem. When options were scarce, exhaustive search was correct. But AI creates abundance. There are always more approaches, more variants, more frameworks. The reframe: you're not searching for the optimal path. You're looking for *a path that works*. There are many. If the first one fails, there are others. ## The Cybernetic Frame I like looking at this from a cybernetic third-person view: human as information agent, AI as compute amplifier. What's happening information-theoretically? The failure mode of "using AI incorrectly" is usually invisible. You don't know what you don't know. But state space size gives you a visible heuristic. If your possibility space is exploding and you can't collapse it, something is wrong with the interaction pattern. This isn't about what the AI knows. It's about what *you* know. The state space exists in your head, not the model's. The model can generate infinite branches. Your working memory cannot hold them. ## What's Helped Me ### Backwards Chaining Before engaging with AI-generated options, establish what you actually want, even provisionally. The goal doesn't have to be final, but it has to be clear enough to evaluate candidates against. Then backwards chain: What leads to that goal? What leads to that? Find the nearest node to your current position that's on the chain. Ignore everything else. The goal functions as a query. Queries enable filtering. Without a query, you just accumulate options. ### Breadth-First Pruning Resist the depth-first pull. Before diving into any single approach, force a survey: "What are all the major approaches? Don't elaborate, just list them." Then prune. Eliminate the obvious non-starters. *Then* go deep on one. Quick heuristics for pruning: cluster the options, filter by gut sense of which feel central to the problem, pick the top three and do a quick sanity check on each. A biased filter that moves is better than a perfect filter that's still loading. ### Graduating to Management This is a confession: I've been slow to adopt multi-agent thinking. Stuck in the midwit trap where I feel like I need to micromanage the AI, verify every step, maintain control over the microstates. But this doesn't scale. A manager doesn't care about microstates. A manager samples the macrostate, performs verification at checkpoints, and trusts the execution layer to handle details. We select managers who know how to do the job themselves because they might need to enter kernel mode to debug. But they don't live in kernel mode. The pattern: let AI explore the state space while you verify. Generation can be noisy if verification is cheap. AI can generate twenty approaches, most of them mediocre, as long as you have a way to identify the good ones. This is hard if you're technically sophisticated. You *know* how to do the thing, so delegating feels like loss of control. But when exploration is cheap, the leverage is in specifying what "good" looks like, not in doing the exploration yourself. ## Parsing the Unease When I feel that suffocating dread working with AI, I label it: state space explosion. Treating it as a structural problem rather than a personal failing changes how I respond. Then I ask: - **Do I have a query?** If not, define at least a provisional goal. - **Am I being pulled depth-first?** If so, force a breadth-first survey before committing. - **Am I micromanaging?** If so, step back to the manager seat. Specify verification criteria. Let the AI generate candidates. The computational frame helps because it re-expresses "I'm overwhelmed" as "O(n²) comparisons on 7-slot hardware." Working memory capacity is fixed. The algorithm isn't. --- *State space explosion is the combinatorial unease that builds when AI expands possibilities faster than you can prune them. The failure mode is usually invisible, but state space size makes it visible. The response isn't to evaluate faster. It's to install queries that filter, force breadth-first pruning before depth, and graduate from micromanager to manager.* --- ## Intelligence design: treating architecture as the source of behavior URL: https://wcdc.io/writing/intelligence-design Date: 2026-01-30 Description: I had been treating five projects as five different questions. They were one question: how to design the software around a language model. Intelligence design is the name I use for designing the software around a language model: the program that assembles its context, carries state between calls, exposes operations, and decides whether a result is good enough to keep. The term came out of several projects that had started with different questions. Semantic objects asked what an agent should be able to manipulate. A visual grammar tried to show how information moved through a model workflow. XJSN gave agents a restricted language for expressing programs. Coding agents showed what changed when a model could work against durable files and external tests. In parallel, I was describing human behavior in terms of state, costs, signals, and transitions. Each project moved some part of intelligent behavior out of the model and into an explicit mechanism. A model call has no memory of the previous call, no durable state, no authority outside the text it returns, and no independent way to know whether its work succeeded. The surrounding program supplies those properties. The hard part was recognizing that this program was the thing I had been designing all along. The prompt was one input to it. ## Start with a stateless call, not a personality A language model receives a sequence of tokens and samples a continuation. Everything that makes the interaction feel continuous is constructed outside that operation. If an agent remembers a name from last week, the name was stored somewhere and a retrieval policy selected it for the current call. If it sends an email, application code recognized an operation in the output, checked that the operation was allowed, and passed it to an email service. If it works overnight, a scheduler started the run and reconstructed enough state for the next call to continue. Treating the model as the whole agent hides those mechanisms behind one personality. Treating it as a stateless component gives each behavior an address in the program and a place where it can be inspected or changed. The model still performs work that ordinary code cannot perform economically. It interprets ambiguous language, makes local judgments, proposes plans, and generates artifacts. But the architecture decides what it is judging, which options it may choose, and whether the result becomes real. This changed how I thought about prompts. A prompt is rarely one authored string. It is the final serialization of instructions, retrieved material, current state, tool descriptions, prior results, and an output contract. Context engineering names the work more accurately because the important decision is what the call gets to see. A carefully phrased instruction cannot recover a missing document, repair contradictory state, or remove an operation the model should never have been offered. Those are system decisions, and they have to be made before the model samples anything. ## Frameworks gave me answers before I had the questions My first attempts at LLM applications were built the way most were: write a prompt, run examples, adjust a phrase, and repeat. The method was adequate for finding a prompt that worked often enough in one situation. It gave me very little language for explaining why it worked or predicting what would survive when the situation changed. The common fixes were all local. Add a role. Add examples. Ask for step-by-step reasoning. Lower the temperature. Change the order of the instructions. Each could alter the output, but none supplied a stable unit of design. The next task started the search again. Frameworks promised to move the work up one level. LangChain supplied chains, agents, tools, and memory. LangGraph made the workflow a graph. DSPy treated prompt selection as an optimization problem. I built with each because I wanted the missing abstractions to already exist. Each abstraction was useful in its intended case, but it combined decisions I needed to vary independently. A `Memory` object usually included at least three policies: what to store, how to retrieve it, and how retrieved material enters the next call. Changing one often meant replacing the abstraction. A graph provided nodes and edges, but it did not answer what deserved a node, which information crossed an edge, or who owned the state between them. An optimizer could improve a metric only after I had found a metric that represented the failures I cared about. The API made one author's answers convenient without establishing that the underlying boundary belonged in every system. That distinction matters because an arbitrary boundary becomes expensive only after the system grows around it. If retrieval and context injection are one component, adding a second context strategy requires a fork. If control flow and data flow share one kind of edge, a diagram can show that two nodes are connected without saying what moves between them. If an agent owns both decision-making and state mutation, it becomes difficult to replay the decision without repeating the mutation. I stopped asking which framework had the right objects and started asking what each object would have to mean. That led to semantic objects. ## Give the agent objects, not a bag of verbs The standard tool-centric question is, "What can the agent do?" The answer becomes a list of functions: `read_file`, `write_file`, `search`, `run_tests`, `send_email`. I changed the question to, "What can the agent manipulate?" A code assistant manipulates a `Codebase` and a `TestSuite`. A writing system manipulates a `Document`, its sections, and its annotations. A research system manipulates sources, claims, and a report. The operations belong to those objects rather than floating in one registry. ```typescript // Tool-centric const agent = new Agent({ prompt: "You are a code assistant...", tools: [readFile, writeFile, search, runTests] }) // Object-centric const agent = new Agent({ prompt: "You are a code assistant...", objects: [codebase, testSuite] }) ``` The second version does not remove tools. It changes their scope. The `codebase` exposes `.getModule()`, `.getFunction()`, and `.updateImplementation()`. The `testSuite` exposes `.run()` and `.getFailures()`. File reads and command execution still happen underneath, but they are implementation details of operations named in the domain. The interface also defines the level at which the agent can act. With `read_file` and `write_file`, the agent manipulates paths and bytes. With a `Document`, it can manipulate a section while preserving the rest of the document. With a `Codebase`, an update operation can parse the replacement, run the type checker, and reject a change that breaks callers before committing it. The object also constrains what exists from the agent's point of view. If the only write operation is `document.replaceSection(id, content)`, deleting an unrelated database is outside the language of the interface. A prompt can tell an agent not to call a dangerous function. An object can decline to expose the function at all. Text remains the medium crossing the model boundary, but it stops being the system's only representation. A section reference can be serialized into context, selected by the model, resolved by the runtime, and applied to the actual document object. The model holds a handle; the data and invariants stay behind it. The design immediately produced another problem. Where do these objects live? An object reconstructed from a tool result on every call is still stateless. Agents need an environment that owns object identity, storage, relationships, and lifecycle. The environment is what lets a result from one operation become an object another operation can use without pasting the entire value back through the model. Semantic objects therefore changed two boundaries at once. They moved tools onto the things they affect, and they moved persistence out of the transcript into a workspace the runtime controls. ## Tool calling hides the state transition Tool calling is usually presented as one primitive. The model emits a function name and arguments, the application executes it, and the result is appended to the conversation. There are two operations inside that sequence. The first is a decision: given the current state, the model chooses to search, calculate, update, or ask. The second is a state transition: the runtime decides where the result goes and what later steps are allowed to see. A search result makes the difference concrete. The same search can be handled in several ways: - paste all results into the next model call - store them as a research artifact and pass only a handle - extract citations and discard the rest - update an index without returning anything to the model - ask another component to rank them before continuing Each version begins with the same decision to search and produces a different information flow through the rest of the system. Collapsing both into "tool calling" makes the system hard to inspect. When a run fails, the query may have been poor, the search may have returned poor results, the runtime may have injected too much of them, or the result may have been written into the wrong artifact. Those failures belong to different parts of the architecture and need different fixes. It also makes composition expensive. A tool whose return value always becomes chat history cannot be reused inside a workflow that needs the result stored, filtered, or routed elsewhere. The transport convention has become part of the operation without being named in its interface. Separating decision from state transition gave me a cleaner model. The model proposes an operation. The runtime interprets that operation against the current objects and applies an explicit transition. The result may become context for another call, but that is a separate decision rather than the automatic meaning of a tool response. ## Drawing the system exposed what the code had left implicit I tried to represent these systems visually because logs showed events in time without showing why information moved where it did. I wanted to see the context assembled for each call, which artifacts were read or changed, where a model made a decision, and where deterministic code took over. The diagrams were useful immediately. A redundant retrieval appeared as two arrows carrying the same material. A loop that looked reasonable in code showed that the model was rereading an unchanged context. A box called "agent" expanded into retrieval, one model call, a parser, and a write to state. The visual grammar also forced premature decisions. Every box type asserted that several operations belonged together. Every edge type asserted what counted as flow. Some concepts that were easy to draw were awkward to execute, while ordinary code paths produced diagrams full of exceptions. I kept visualization as a way to inspect a design and stopped treating the diagram as the executable definition. A diagram can make causal structure visible. It cannot enforce a state boundary or type-check an artifact unless those concepts already exist in code. That was the point at which code became the final representation rather than one implementation of a higher-level picture. The remaining question was how much code an agent should be allowed to generate. ## Use familiar syntax without granting arbitrary semantics Coding is expressive because a short program can hold a sequence of decisions and deterministic operations. Tool calling loses that advantage when every step requires another model turn. Arbitrary generated code preserves the expressivity and creates a much larger verification problem. XJSN was my attempt to keep the useful middle. The source looks like JavaScript function calls: ```javascript Pipeline([ Filter({ status: "active" }), Map({ extract: ["id", "name", "email"] }), GroupBy({ field: "department" }), Aggregate({ count: Count(), averageAge: Average("age") }) ]) ``` Models already generate this syntax fluently because JavaScript is common in their training data. XJSN does not execute the result as JavaScript. It parses the text into an abstract syntax tree, validates that tree against a grammar, and sends the accepted nodes to a custom interpreter. ```typescript const grammar = { Pipeline: { children: ["Filter", "Map", "GroupBy", "Aggregate"] }, Filter: { args: { status: "string" } }, Map: { args: { extract: "string[]" } } } const output = await agent.generate("Transform this data") const ast = parse(output) const errors = validate(ast, grammar) if (errors.length) return retryWith(errors) return interpret(ast, data) ``` The grammar is the security and composition boundary. It decides which node types exist, which arguments they accept, and which operations may contain others. The model can emit `DeleteEverything()`, but the parser has no valid node to construct from it and the interpreter never sees it. This is different from sandboxing arbitrary code. A sandbox limits what a program can damage after the system accepts it as a program. A domain language decides which programs belong to the product in the first place. The two mechanisms can be used together, but they protect different boundaries. The AST also separates the notation from the execution model. JavaScript-shaped syntax helps generation. The runtime is free to make `Filter` a database query, a stream operation, or a call into a remote service. Familiar syntax does not require JavaScript semantics. The design becomes more valuable when the grammar belongs to a domain. A compliance team can define audits, evidence requests, escalation rules, and retention operations. A publishing system can define sections, citations, review passes, and approvals. Domain experts extend the system by defining valid objects and transitions rather than by writing longer instructions about how a general agent should behave. I did not finish XJSN as a general platform. The useful result was the boundary it exposed: an agent needs a language expressive enough to state a plan, while the runtime needs a grammar narrow enough to validate and interpret that plan. ## Coding agents supplied the working example Coding agents made the architecture concrete because software development already contains most of the primitives an agent system needs. A repository gives artifacts stable names. Files provide persistent state. Git records changes and makes them reversible. The shell supplies a composable effect surface. Compilers, linters, and tests evaluate work without asking the model whether it succeeded. Error messages return structured evidence to the next attempt. The model is important inside that environment, but the environment explains the continuity. A coding agent reads the repository, selects relevant files, writes a patch, runs a command, receives the failure, and tries again. The model call changes each time because the harness changes its context. Draft-run-revise loops work because the second attempt is conditioned on evidence produced by executing the first. Repeating the same prompt would sample another answer. Running the code turns the first answer into information the next call can use. The verifier matters as much as the loop. A model reviewing its own patch may catch a mistake, but it is still another model judgment. A type error or failed test identifies a property outside the model's opinion. The harness can attach that evidence to the next call and stop only when the external condition passes. This explains why coding agents were more capable than many domain agents built from the same models. The coding environment already had addressable objects, durable artifacts, powerful operations, and cheap verification. A generic customer-support or research agent usually received a prompt, a flat tool list, and a transcript. The missing capability was often in the environment rather than the model. Other domains need equivalents of the filesystem, type checker, test suite, and patch. A writing agent needs sections it can address and editorial checks it can run. A research agent needs claims tied to sources and a ledger of what has been searched. A financial agent needs typed transactions, permissions, and reconciliation. Once those exist, the model has a world it can act inside rather than a collection of functions it can ask the application to call. ## Name the design decisions that repeat While building these systems, I was also writing the [Mechanistic Mindset](/writing/mechanistic-mindset) wiki, which describes behavior through computational mechanisms. Instead of treating procrastination as a character judgment, the useful questions concern activation cost, expected reward, uncertainty, and the state transition required to begin. That practice made the connection difficult to ignore. If intelligent behavior can be described in terms of inputs, state, operations, and feedback, then designing an agent is a software-design problem before it is a personality-design problem. I used that frame while writing [Elements of Agentic System Design](https://github.com/idyllic-labs/elements-of-agentic-system-design). The goal was to identify the decisions present across coding agents, research systems, personal automations, and multi-agent workflows without organizing them around one framework's API. Ten elements covered the systems I had built: | Element | The decision it names | Conventional software analogue | | --- | --- | --- | | **Context** | What information is assembled for a call | inputs and representation | | **Memory** | What persists across calls | data structures and storage | | **Agency** | Which effects the system can produce | I/O and side effects | | **Reasoning** | How model calls are arranged | control flow and algorithms | | **Coordination** | How components exchange work | protocols and distributed systems | | **Artifacts** | Which outputs persist and evolve | data models and versioning | | **Autonomy** | What triggers computation | events and scheduling | | **Evaluation** | How success is measured | tests and observability | | **Feedback** | Which signals return to the process | errors, traces, and monitoring | | **Learning** | How later behavior changes | updated policies, data, or parameters | The list is useful because each element points to code. "The agent forgot" becomes a question about what was stored, what retrieval selected, and what fit into context. "The agent took the wrong action" becomes a question about the decision policy, the available operations, and the permission boundary. "The agent never improves" becomes a question about which evaluation result is written back into future behavior. The elements also expose defaults. Every system has a memory design even when the answer is "keep the transcript until the window fills." Every system has an evaluation design even when the answer is "accept whatever the model returned." Leaving an element unnamed does not remove it; it lets an accidental implementation decide it. This was the point at which I started calling the work intelligence design. The term names the whole program that produces intelligent behavior, not a new technique layered on top of prompt engineering. ## Reliability comes from deciding what may remain probabilistic A model will sometimes misunderstand an instruction, choose a poor plan, or produce an invalid artifact. The architecture cannot remove sampling from the model, but it can decide which properties are allowed to depend on it. Structure is the easy part to move out. Structured outputs can require fields, types, and valid enum values. A parser can reject an invalid XJSN program. An object method can refuse a state transition that violates its preconditions. None of those establishes that the content is correct. Semantic reliability needs a verifier matched to the claim. - Code can be compiled and tested. - A citation can be checked against the source it names. - A transaction can be checked against permissions and balances. - A document can be checked for required sections and unsupported claims. - A plan can be simulated against a bounded model of the environment. The model may participate in verification when judgment is unavoidable, but the distinction should remain visible. A model critic is another probabilistic operation. A compiler error is deterministic evidence. They belong in different parts of the acceptance rule. The loop also needs state. A real retry carries the failed artifact, the verifier's result, and the attempts already made. Calling the model again with the original request is a reroll. Feeding back the failure turns the sequence into directed search. The termination path is part of the same design. After repeated failure, the system can narrow the operation, preserve a partial artifact, request a decision, or stop. "Keep trying" is not autonomy; it is a missing stopping condition. This is how a system can become dependable around a component that remains fallible. Contracts, checks, and fallbacks do not make the model reliable in isolation. They prevent particular model failures from becoming accepted system behavior. ## Productization means choosing a user mode Coding agents usually run with a wide interface to the machine. They can traverse the filesystem, execute commands, use credentials, install packages, and change whatever their process can reach. I think of this as kernel mode. It is effective because the agent can construct a new operation whenever the existing tools are insufficient. The same freedom is difficult to ship as a product boundary. A user needs to know which information the system can read, which effects it can cause, and what can be reversed. Restricting the prompt does not answer those questions because the underlying operations remain available. The product version needs a user mode: a smaller world whose objects and operations correspond to the domain. Semantic objects provide one half. A reporting product can expose a `Report`, `Dataset`, and `CitationStore` without exposing the server's filesystem. Each object owns its state and can validate operations before committing them. The agent remains capable inside the world the product defines. Constrained languages provide the other half. Instead of generating arbitrary Python to transform data, the agent generates a program from the transformations the product supports. Instead of issuing one tool call, reading the result, and deciding again, it can state a short validated program that the runtime executes deterministically. The combination changes the role of permissions. Permission is no longer a warning attached to a general capability. It becomes a property of an operation on a particular object. `report.publish()` can require approval while `report.addAnnotation()` does not. An audit grammar can admit `Escalate` and exclude deletion entirely. This is also where domain-specific systems gain an advantage over a general coding agent. They give up arbitrary computation and receive stronger guarantees, clearer interfaces, better inspection, and operations named at the level their users care about. Every object type and grammar rule removes some programs from the product's expressible set. A domain model built too early will reproduce the same problem I had with agent frameworks: local intuitions hardened into APIs. The way out is to derive the interface from working operations, keep the underlying artifacts accessible, and make extensions add explicit types rather than prompt conventions. ## Build the reusable layer below the application The recurring mechanisms sit below categories such as coding agent, writing agent, or research agent. Those applications need different domain objects, but they reuse context construction, artifact versioning, permission checks, retry state, evaluation, and scheduling. That suggests a horizontal architecture: - context builders select and serialize information for one call - artifact stores preserve identity and history - object interfaces expose bounded domain operations - interpreters execute validated plans - evaluators return evidence for acceptance or revision - schedulers resume work from durable state A vertical application supplies the domain vocabulary and assembles these pieces. A research product defines sources, claims, and reports. A coding product defines repositories, patches, and tests. The runtime does not need to pretend those objects are the same in order to reuse the machinery beneath them. This is why I prefer theory over framework-specific recipes. Frameworks are implementations of state ownership, control flow, representation, and failure recovery. Distributed-systems design, programming-language theory, and state machines make those choices visible. Learning only the framework teaches its current answers. The architecture should make it possible to replace an answer without replacing the system. Context retrieval can change without changing artifact identity. A new evaluator can be inserted without rewriting the operation it checks. An object can move from local files to a database without changing the language the agent uses to manipulate it. Those separations are what let improvements in one application become reusable infrastructure for the next. ## Where the intelligence lives The term intelligence design is useful only if it changes what gets built. When a system fails, I no longer begin by rewriting the prompt. I ask which information the model received, which state it believed it was changing, which operations were available, what evidence the runtime collected, and why the loop accepted the result. The answer usually points to a component with an interface and a test. Model capability still sets important limits. A stronger model can make better local judgments, follow a grammar more consistently, and recover from more complicated feedback. But those gains enter through a call whose context, authority, and acceptance conditions are designed elsewhere. The system's behavior has always been determined by code. Intelligence design is the practice of treating that code as the primary artifact: the context machine around the call, the object system behind the tools, the state transitions inside the loop, and the verifier that decides when the work is done. --- ## Prose is free now URL: https://wcdc.io/writing/substantives Date: 2026-01-30 Description: Fluent text is free now, so the only thing worth paying for is what a reader could not have produced themselves. AI can generate fluent, grammatical, well-structured text on any topic. The marginal cost of generating 2,000 words of coherent AI-generated text is negligible. As a16z puts it: the marginal cost of creation is approaching zero. This changes which parts of content creation are scarce and valuable: {/* SUBSTANTIVE: reframe table */} | Era | Scarce | Abundant | Value lives in | |---------|-------------------------|-------------------------|---------------------------| | Pre-AI | Prose | Ideas, facts, artifacts | The writing itself | | Post-AI | Ideas, facts, artifacts | Prose | What the prose carries | Prose is the medium, and the value lies in specific elements the reader can extract and use. I call those elements **substantives**. --- ## Substantives {/* SUBSTANTIVE: definition */} A substantive is a discrete, reusable unit of content that carries standalone value for the reader. Examples include definitions, checklists, code samples, and diagrams that can be reused or referenced without the surrounding explanation. {/* SUBSTANTIVE: the extraction test */} **The extraction test:** Assume a reader skims your piece in 30 seconds and extracts 3-5 things to save or share. Identify what they would extract. - A checklist: extractable, substantive - A screenshot: extractable, substantive - A paragraph of explanation: usually not, tissue - A definition: extractable, substantive - A transition sentence: not extractable, tissue Pieces built only from connective prose are harder to reuse or reference later. --- ## Why This Matters Now Readers quickly filter out low-value content. The backlash is real: "AI slop" entered the lexicon, and mentions increased ninefold in 2025. Platforms are responding: YouTube stopped paying for AI slop, and Pinterest added filters to hide AI content. Readers ignore writing that shows these patterns: - Generic claims without specific evidence - Fluent prose without anchoring details - Structure without substance Many AI-generated listicles, self-described comprehensive guides that say nothing, and posts with interchangeable advice contain no specific, reusable elements. --- ## The Taxonomy {/* SUBSTANTIVE: taxonomy table */} Substantives can be grouped by what the reader gains from them: | Category | Reader gets | Examples | |-----------------|------------------------|---------------------------------------------------------------| | **Utility** | Something to use | Code snippet, checklist, template, prompt, procedure | | **Knowledge** | Something to know | Definition, taxonomy, framework, distinction | | **Proof** | Reason to believe | Screenshot, benchmark, citation, timestamp, specific name | | **Perspective** | New way of seeing | Reframe, analogy, decomposition, contrast | | **Connection** | Orientation in space | External link, comparison table, prerequisite pointer | | **Experience** | Felt understanding | Worked example, failure story, interactive demo | | **Shortcut** | Compressed wisdom | Heuristic, pattern, anti-pattern, threshold, trade-off | | **Vocabulary** | Words to think with | Coined term, precise definition, distinction pair | Not every piece needs all eight. Most pieces have 2-3 primary value types. --- ## The Workflow {/* SUBSTANTIVE: workflow contrast */} One common workflow: 1. Define the main message 2. Outline the structure 3. Draft and refine the prose 4. Add supporting evidence Substantive-first: 1. Define what the reader should have when they finish 2. Identify substantives that provide that value 3. Order them into a sequence 4. Add prose that connects them Identify and design the substantives first, then write the prose that presents and connects them. --- ## Checklist Before Publishing {/* SUBSTANTIVE: checklist */} Before publishing, scroll through looking only at visual breaks and extracted elements: - Is there a substantive every 300-500 words? - Could someone extract 5+ useful things? - Is there at least one visual (screenshot, diagram, table)? - Is there at least one structural (checklist, framework, definition)? - Are there inline substantives (links, specific names, numbers)? If the piece contains mostly transitions and explanation with few concrete elements, revise it by adding more substantives or removing nonessential prose. --- ## How Substantives Differ from Arguments {/* SUBSTANTIVE: contrast table */} Traditional essays rely on arguments: claims supported by reasoning that the reader is expected to follow. Substantive-first content is built around artifacts, like checklists or definitions, that a reader can use without reading the rest of the piece. | Argument | Artifact | |-------------------------|-------------------------------| | "X is true because Y" | Screenshot showing X | | "You should do X" | Checklist for doing X | | "X works better than Y" | Benchmark comparing X and Y | | "X means Y" | Definition: X = Y | Arguments depend on the reader accepting your reasoning. Artifacts like checklists or definitions provide value directly. --- ## The Reusable Artifact Test {/* SUBSTANTIVE: criteria list */} To make a document reusable as a reference, ensure that it is: 1. **Extractable** - Elements can be pulled out and used independently 2. **Stable** - Content doesn't rely on context that will change 3. **Addressable** - Specific sections can be pointed to 4. **Machine-readable** - AI can parse and reference it Treat the document as a reusable artifact when automated tools can reliably parse its structure, you can copy sections into other documents without rewriting, and you can link directly to specific sections. --- {/* SUBSTANTIVE: core reframe, repeated */} The shift: start with what the reader should HAVE, not what you want to SAY. Design the substantives first, then write the prose, and test the result by checking what can be extracted. --- ## Reference: Exhaustive Substantive Types ### Utility — Reader Can Use This | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Code snippet** | Executable capability | Any technical content where reader might implement | | **Template** | Fill-in-the-blank starting point | Process or document the reader will create themselves | | **Checklist** | Verification tool, nothing forgotten | Any multi-step process with failure modes | | **Procedure** | Step-by-step execution path | "How to" content of any kind | | **Prompt text** | Ready-to-use AI invocation | AI-related content, workflow content | | **Configuration** | Working setup they can copy | Technical content with setup requirements | | **Query/command** | Executable one-liner | Technical content, data content | | **Calculation/formula** | Computable relationship | Quantitative decisions, estimation | | **Decision tree** | Navigation through choices | Complex decisions with branches | ### Knowledge — Reader Now Knows This | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Definition** | Precise meaning of term | Any time you use a term that could be ambiguous | | **Taxonomy** | Classification system | Domains with multiple types, options, or categories | | **Framework** | Multi-part mental model | Complex domains requiring structured thinking | | **Fact** | Verified true statement | Foundational claims, surprising truths | | **Distinction** | Difference between two things | Commonly confused concepts | | **Relationship** | How X connects to Y | Systems, dependencies, causes | | **Counter-example** | Case where intuition fails | Correcting common misconceptions | ### Proof — Reader Is Convinced | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Screenshot** | Visual evidence something exists | Any claim about a system, UI, output | | **Benchmark** | Measured performance data | Claims about speed, quality, comparison | | **Log output** | Machine-generated evidence | Claims about system behavior | | **Repo link** | Inspectable source of truth | Any claim about code you've written | | **Citation** | Authority backing | Claims reader might doubt, contested topics | | **Testimonial/quote** | Another person's attestation | Claims about user experience, reception | | **Replication** | Someone else got same result | Scientific or technical claims | | **Timestamp** | When something happened | Narrative claims, incident reports | | **Specific name** | Who/what specifically | Any claim that could be vague | ### Perspective — Reader Sees Differently | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Reframe** | "Not X, but Y" shift | Correcting common framing errors | | **Analogy** | Understanding via familiar domain | Complex concepts, cross-domain transfer | | **Lens/model** | Way of analyzing things | Recurring situations reader will face | | **Contrast** | Before/after, old/new | Change, improvement, evolution | | **Decomposition** | Breaking whole into parts | Complex wholes that seem monolithic | | **Synthesis** | Combining parts into whole | Scattered elements that form pattern | | **Scale shift** | Zooming in or out | When reader is stuck at wrong altitude | ### Connection — Reader Is Oriented | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **External link** | Path to deeper/related content | Any mention of external work | | **Comparison table** | Relative positioning of options | Decisions between alternatives | | **Influence chain** | Where ideas came from | Novel concepts, intellectual positioning | | **Contrast with alternative** | How this differs from X | Competitive positioning | | **Prerequisite pointer** | What to learn first | Content with dependencies | | **Next step pointer** | Where to go after this | Content that's part of larger journey | | **Ecosystem map** | How this fits in larger landscape | Tools, frameworks, communities | ### Experience — Reader Felt This | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Interactive demo** | Learning by doing | Concepts that must be experienced to understand | | **Worked example** | Watching the process | Skills, procedures, problem-solving | | **Narrative incident** | Vicarious experience | Lessons that come from living through something | | **Failure story** | Learning from pain (yours) | Warnings, cautionary knowledge | | **Dialogue excerpt** | Witnessing an exchange | Interpersonal dynamics, debates | | **Sensory detail** | Grounding in physical reality | Abstract content that needs anchoring | | **Exercise** | Active practice | Skills that require repetition | ### Shortcut — Reader Has Compressed Wisdom | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Heuristic** | Rule of thumb | Repeated decisions, common situations | | **Pattern** | Recurring structure with name | Situations reader will encounter multiple times | | **Anti-pattern** | What to avoid and why | Common failure modes | | **Threshold** | When X becomes Y | Decisions that depend on quantity | | **Priority order** | What to do first | Resource-constrained situations | | **Trade-off** | What you give up for what | Decisions with real costs | | **Warning sign** | Indicator of problem | Diagnostic situations | ### Vocabulary — Reader Can Think With This | Substantive | What Reader Gets | When To Use | |-------------|-----------------|-------------| | **Coined term** | New word for previously unnamed thing | When existing vocabulary fails | | **Precise definition** | Sharpened meaning of existing term | When term is used sloppily | | **Distinction pair** | Two words that mark important difference | When one word conflates two things | | **Acronym/abbreviation** | Compressed reference | Concepts that will be referenced repeatedly | | **Catchphrase** | Memorable crystallization | Core insight you want reader to retain and repeat | --- ## Building a compiler pipeline for a 300-page AI book URL: https://wcdc.io/writing/how-i-broke-down-writing-with-llms Date: 2026-01-21 Description: One-shot generation distorted my ideas, and chapter outlines still omitted the distinctions I cared about. I built move-annotated outlines and a style linter so I could verify structure before prose and voice after it. At two in the morning, I was rereading hundreds of pages of technically coherent prose that sounded like nobody. Every section contained the same phrases and rhythms: "it's important to note," "let's delve into," "this approach offers several advantages." The facts were mostly right. The emphasis and voice were wrong. I was using AI to write *Elements of Agentic System Design*, a 300-page technical book about building AI applications. My source material included interviews, notes, examples, and years of project work. The problem was turning that material into a book without letting the model invent the argument between the notes. I eventually split the work into a pipeline. A move-annotated outline specified what each paragraph needed to do. Generation expanded those moves into prose. A style linter found recurring AI patterns. Separate revision passes repaired structure and voice at different stages. ## One-shot generation lost my emphasis My first attempt began with a long interview prompt. An agent asked questions about state, orchestration, tools, and context construction. I answered for hours and produced about fifty pages of organized transcript. I gave the transcript to Claude and asked for the book. It generated plausible chapters, but their emphasis bore little relation to mine. Topics I had mentioned briefly became major sections. Ideas I had spent an hour explaining were compressed into a paragraph. The transcript preserved my words without specifying how those words should become an argument. Claude had to decide which ideas were central, how they related, and where they belonged. Those decisions were exactly the work I needed to retain. I discarded the draft and added an intermediate step. ## Chapter outlines improved coverage but remained too vague In the second attempt, I worked chapter by chapter. Each chapter received an outline listing the topics, sequence, examples, and approximate length. Claude generated one section at a time. This version had the right broad shape. It also contained two persistent problems. The first was style. Stock phrases, symmetrical lists, generic transitions, and empty conclusions appeared throughout the manuscript. The second was emphasis inside each section. An outline item such as "explain context construction" did not record the specific claim, mechanism, distinction, or example I wanted. Claude filled that missing structure with a reasonable interpretation of the topic. Reasonable was insufficient when the point of the book was my interpretation. The outline told the model what a section was about. It did not tell the model what each paragraph needed to accomplish. I separated those two failures. Style needed a verification pass after prose existed. Missing argument structure needed a richer representation before generation began. ## I turned my edits into a style linter I first tried a larger style prompt. It banned phrases such as "delve" and "it's worth noting," supplied good and bad examples, and described the voice I wanted. Claude followed the rules briefly and then returned to its defaults across a long chapter. I stopped asking the generation pass to handle content and style simultaneously. The first pass could focus on explaining the specified material. A second pass would find and repair known style defects. I collected my repeated editing comments from the manuscript: "generic," "nobody says this," "empty transition," "same sentence shape again." I grouped them into concrete patterns a tool could detect: - banned phrases and stock transitions - paragraphs beginning with abstract throat-clearing - repeated contrast templates - excessive parallel lists - conclusions that restated the section without adding information - claims framed as universal lessons when the source described one project The linter marked passages matching those rules. A rewrite pass received the flagged sentence, its surrounding paragraph, and the violated rule. It revised only the affected passage, then the linter ran again. This did not define good prose. It removed defects I already knew how to identify. More importantly, it made those defects visible across hundreds of pages without requiring me to find every instance manually. ## I specified the function of each paragraph The linter improved the voice, while chapters still required heavy substantive editing. I could clean a paragraph perfectly and still have the wrong paragraph in the argument. I needed an outline that preserved rhetorical function as well as topic. I used the idea of rhetorical moves from [John Swales](https://en.wikipedia.org/wiki/John_Swales): a passage can make a claim, explain a mechanism, give an example, establish a contrast, ask a question, or resolve one. I assigned each planned paragraph a primary move: - **CLAIM** states the position. - **MECHANISM** explains how or why it is true. - **EXAMPLE** makes the mechanism concrete. - **CONTRAST** distinguishes the claim from a nearby alternative. - **QUESTION** identifies a tension the section must resolve. - **ANSWER** resolves that tension. - **CONSEQUENCE** explains what changes for the reader or system. The outline became more specific: ```yaml Context Construction: p1: CLAIM: Context is the model's complete input for one call KEY: Everything needed for that decision must reach the call p2: MECHANISM: Why the system must reconstruct context DETAIL: The model has no persistent memory between calls p3: CONSEQUENCE: What this requires from the application PRACTICAL: Load state and relevant history before every inference ``` I could now inspect the argument without reading generated prose. A claim with no mechanism lacked support. A mechanism with no example risked remaining abstract. A question with no answer left the section open. Repeated claims often indicated that two paragraphs were doing the same job. The move notation also constrained generation. Claude received one paragraph specification at a time and could focus on expanding that move instead of inventing the section's structure while writing it. ## The manuscript became a staged build The final pipeline separated decisions that I had previously asked one generation call to make: ```text source material ↓ chapter structure ↓ paragraph moves ↓ prose generation ↓ style linting and local rewrites ↓ substantive review ``` Each stage preserved a different part of the book. The source material contained the ideas and examples. The chapter structure assigned those ideas to an argument. The move outline specified the work of each paragraph. Generation chose sentences. The linter enforced recurring voice constraints. My final review judged whether the explanation actually landed. Errors became easier to locate. Missing coverage belonged in the chapter structure. A weak explanation belonged in a mechanism move. An irrelevant paragraph belonged in the move outline. A stock phrase belonged in the style pass. I no longer regenerated an entire chapter to fix one local problem. ## The intermediate representation made verification possible Traditional outlines preserve headings and topic order. That was insufficient for this project because topic coverage was not the only thing I needed to verify. I needed to verify the argument carried my claims, distinctions, mechanisms, and examples. The move outline recorded those elements directly. It became the source I reviewed most carefully because mistakes there propagated into every later stage. Prose remained easier to regenerate than the reasoning it expressed. The linter served the same role after generation. It turned my recurring style objections into explicit checks and attached each failure to a small passage. Neither representation guaranteed a good book. Together they reduced two large, vague editing problems to inspectable units. I still wrote the argument. I selected the material, decided the chapter sequence, specified the moves, and judged the final explanation. AI expanded those decisions into prose and helped repair known defects. That division finally made a 300-page manuscript manageable. I stopped asking a model to infer the book from a pile of source material. I gave it an argument detailed enough to inspect before generation and a set of checks precise enough to run afterward. --- ## I stopped designing agents and started designing systems URL: https://wcdc.io/writing/what-is-an-agent Date: 2026-01-19 Description: After two years building AI products, I found that 'agent' grouped together systems with different state, control flow, and failure modes. I replaced the label with ten engineering decisions I could inspect directly. After two years of building AI products, I stopped using "agent" as a design primitive. The word covered too many different systems: a chatbot that calls one tool, a loop that works until a task is complete, a scheduled workflow, a group of specialized model calls, or an application that remembers a user across months. Those systems do not share one architecture. They differ in how they construct context, store state, call models, execute actions, recover from failure, and decide when to stop. Calling all of them agents hid the decisions I needed to make. I began describing the implementation directly. That led to a framework of ten elements I now use to design intelligent systems: context, memory, agency, reasoning, coordination, artifacts, autonomy, evaluation, feedback, and learning. ## The model is one component of the system A model API receives input and returns output. Continuity across calls comes from the application around it. I split that application into four parts: 1. **Model.** Produces an output from the context supplied for one call. 2. **Context.** Contains the instructions, history, retrieved data, and tool results available during that call. 3. **Storage.** Persists state across calls and sessions. 4. **Control flow.** Decides when to call the model, which tools to execute, what to add to context, and when to stop. The user experiences their combination as one continuous assistant. I can switch between model providers while preserving the same history, tools, and state, and the application still feels like the same system. The identity lives in the architecture more than in one model invocation. This changed where I looked for improvements. Better models help, but I control the other components. I can improve which history enters context, how stored facts are retrieved, which actions are allowed, how failures surface, and what evidence the next call receives. ## Every run alternates between context and control flow At the center of these systems, I found two recurring operations: - construct the context for the next model call - decide what happens after the call returns A basic tool loop makes both visible: ```python while not done: context = build_context(state, history, task) response = model.call(context) if response.wants_tool: result = execute_tool(response.tool_call) history.append(result) else: done = True return response.text ``` Memory changes `build_context`. Tools add branches to the control flow. Planning creates a plan in one call and feeds its steps into later calls. Multiple model roles use different context builders and pass outputs between them. Thinking in these terms made framework abstractions easier to evaluate. I no longer asked whether a framework supported agents. I asked whether I could inspect and control its context construction, state transitions, tool execution, and termination behavior. ## I use multiple calls when the system needs new evidence Large context windows do not remove the need for loops. A single call can only use information available when that call begins. I add another call when the system must leave the model and return with something new: - A search or file read supplies information that was previously unavailable. - Executing code verifies whether it works. - Asking the user resolves an ambiguity. - Writing a file or sending a message creates an effect outside the model. - Evaluating a draft produces evidence for a revision. The call boundary is where the system observes or changes the world. The next context incorporates the result. Different tasks need different loop shapes: ```text tool loop call → tool → observation → call reflection draft → critique → revision planning plan → execute step → inspect → replan if needed search generate branches → evaluate → retain the best hierarchical outer task loop → delegated inner loops ``` I choose the smallest loop that supplies the evidence the task requires. More calls increase cost, latency, and failure points. A planning or search loop needs to earn that complexity through better results on the actual task. ## "Multi-agent" usually means function decomposition I also stopped treating specialized model calls as digital coworkers. A researcher, planner, and writer usually correspond to three functions with different contexts and output contracts. For example: ```text research(query) → evidence plan(task, evidence) → outline write(outline, evidence) → draft ``` The useful design decisions are the inputs, outputs, state ownership, and failure handling at each boundary. Giving the calls names and personalities does not supply any of those. The anthropomorphic framing can hide missing state. A "researcher agent" remembers nothing unless the application stores and reconstructs its findings. A "debate" is a sequence of model calls that includes earlier outputs in later contexts. If one stage misunderstands the task, later stages inherit that error through the interface. I use multiple model components when context isolation, parallel work, or different evaluation criteria make the decomposition useful. I still implement them as functions over explicit data. That keeps the architecture inspectable and lets me test each boundary without simulating a social relationship. ## The engineering looks like ordinary system design Once I removed the agent abstraction, familiar engineering problems became visible: - Where does state live, and which component owns it? - How do concurrent operations coordinate? - What happens when a model call or tool fails? - Which actions require authorization? - How do I observe a run and reproduce a failure? - Which outputs become durable artifacts? - How does user feedback change later behavior? An LLM makes some components probabilistic, but it does not remove the need for explicit state, interfaces, retries, permissions, logs, and evaluation. These concerns shape reliability more than the label applied to the model loop. I needed a vocabulary that kept those decisions separate. That became the ten-element framework. ## The ten elements describe the decisions I make The elements are not ten modules that every product must implement separately. They are ten categories of decisions that appeared across the systems I built. | Element | The decision it captures | |---|---| | **Context** | What reaches each model call, in what order and format | | **Memory** | What persists, where it lives, and how a later call retrieves it | | **Agency** | Which external actions the system can execute and under what permissions | | **Reasoning** | The topology of model calls: single call, tool loop, reflection, planning, or search | | **Coordination** | How components exchange results and share or isolate state | | **Artifacts** | How documents, code, plans, and other durable outputs are represented and versioned | | **Autonomy** | What initiates a run: user request, schedule, event, or environmental condition | | **Evaluation** | How I measure whether a run or system produced a good result | | **Feedback** | Which user and system signals return after deployment | | **Learning** | How those signals produce changes to prompts, retrieval, tools, or control flow | The categories prevent one design choice from masquerading as another. Adding a vector database addresses memory, while it says nothing about how the next call uses the retrieved results. Adding tools expands agency, while it does not define when the system should invoke them. Scheduling a run adds autonomy, while it does not make the reasoning loop more capable. That separation also exposes defaults. If I do not design memory, the system may only retain the current conversation. If I do not define evaluation, iteration falls back to whether an output feels good. If I do not define autonomy, the user initiates every run. ## Context and memory are connected but separate I separate context from memory because persisted data is not automatically useful to a model. A database can contain years of history while one call receives the wrong ten records. Memory design covers storage, indexing, retention, and retrieval. Context design covers selection, formatting, ordering, and token budget for one inference. Retrieval connects them. This distinction helped me diagnose systems that appeared to have poor memory. The data was present. The context builder was choosing irrelevant records, omitting necessary state, or presenting the results in a form the model could not use. ## Evaluation, feedback, and learning form one improvement loop I also keep the last three elements separate. Evaluation defines quality for a task. Feedback collects evidence from real use, such as corrections, ratings, abandoned runs, or tool failures. Learning turns those signals into a change to the system. ```text run → evaluate → collect feedback → change system → run again ``` The model weights may remain fixed throughout this loop. The system still learns when I improve its prompt, retrieval policy, tool schema, permissions, or control flow in response to evidence. Without evaluation, feedback has no stable interpretation. Without feedback, evaluation stays confined to a test set. Without a process that changes the system, both become dashboards. ## I use the framework to describe the architecture before choosing tools For a new system, I write down one decision in each category. The result is short but concrete: ```text Context current task, relevant project files, latest tool results Memory project repository and run database Agency read files, run tests, propose patches; writes require approval Reasoning tool loop with one repair attempt after a failed test Coordination one model loop; test runner remains a deterministic component Artifacts Git patch, test output, and run trace Autonomy user starts each run Evaluation tests pass and requested behavior is present Feedback user accepts, edits, or rejects the patch Learning update instructions and examples from repeated failures ``` This description tells me more than calling the product a coding agent. It identifies the state model, permissions, loop shape, outputs, success criteria, and improvement path. I can then choose libraries and infrastructure that fit those decisions. I still use "agent" as shorthand in product conversations. I no longer let the word determine the architecture. When I build, I specify the system: what each model call sees, what persists, what can act, how calls compose, and how I know the result worked. --- ## ObjectEnv: a workspace instead of a list of tools URL: https://wcdc.io/writing/objectenv Date: 2026-01-16 Description: A workspace of persistent objects instead of a list of stateless tools, and the read method that refused to return an entry. ObjectEnv gives an agent a workspace instead of a toolbox. Objects live as JSON in [SQLite](https://sqlite.org/), each one addressed by name and carrying its own methods, and the agent works by invoking those methods rather than by calling stateless functions and re-reading the results. The hard part was deciding what an object must carry so that a reader which has never seen it can use it correctly on the first attempt, and then noticing that the most useful thing to put in such an interface is an operation the agent is not allowed to perform. The design below runs from the problem with tools through to the one part of it I would build again. ## A tool has no memory, and a tool protocol has no variables A tool is a function call. The model asks for something, a result comes back as text, and the model reads that text and decides what to do next. Each call is complete in itself, so anything the model wants to keep it keeps in the conversation and re-reads on the next turn. For a short job the missing memory is invisible. For anything that runs a while it becomes the whole problem, because the model carries its working set inside the transcript and pays for all of it on every call. MCP has calls but no pipe-level composability: no variable a tool can write and a later tool can read by name. ``` shell grep ... > hits a name to write to sort hits | uniq -c a later command reads that name tools search(...) -> "..." result comes back as text summarize("...") the text has to be carried in the transcript ``` A shell pipeline composes because a command can write to a name and a later command can read that name. A tool protocol has the commands and no names, so the only place to put an intermediate result is the transcript. The design goal follows directly: give the agent stateful objects it can reference, pass between calls and manipulate through methods. Combining state with the operations that change it follows the actor model more closely than separating a stateless reasoner from a generic memory service. Splitting an agent into a stateless reasoner and a memory service gives you two things that must be kept in agreement. Putting the state on the object with the operations that change it gives you one. ## An agent loop can call, and it cannot return A second problem appears when an agent has to be called from a program rather than chatted with. Its behavior is buried in a framework-specific message log, streaming is coupled to the chat transport, required behaviors are offered as optional tool calls, and the loop has no clean way to pause for a program-defined branch. The third one is the one the whole design turns on. Offering a tool is a request, and a request the model can decline is not a constraint. Everything a caller wants to guarantee has to be arranged so that declining is impossible rather than merely unlikely. The fourth points at something more structural: a tool call continues the agent loop, so the agent has no ordinary way to return control and a value to its caller. A function that finishes can do one of two things: call something else, or return, meaning hand a value back to its caller and disappear. An agent loop only has the first, because every tool result is fed back in and another turn runs. The usual approximation is a `finish` tool watched for from outside, which works the way a `goto` works, meaning fine until you want two of them nested. [Lisp](https://en.wikipedia.org/wiki/Call-with-current-continuation) calls the general machinery for this a continuation, and an agent runtime needs only the ordinary half of it, which is a way to end the loop with a value instead of with another message. ## A runtime, or a file with a script beside it Two designs answered the requirement. The first was a virtual runtime of persistent [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)-like objects backed by SQLite and exposed through one tool surface. The second was a file carrying its own data and code, one format per kind of artifact. I chose the runtime because one global environment gives every object the same storage and invocation path, while file-specific objects require custom schemas and readers for every kind. One environment with a global scope has one storage layer and one invocation path, so adding a kind of object costs a class definition. A file-per-artifact design multiplies both by the number of kinds, and each kind then needs its own schema, its own reader and its own tools. The remaining objection was whether the runtime bought anything a file plus a script did not. The object could exist only while the script was running and still persist its data in the file. Two months later that objection collected its answer. The build was scoped for speed rather than architecture. Moving from a hosted sync engine and server-side [WebAssembly](https://webassembly.org/) to local SQLite and [TypeScript](https://www.typescriptlang.org/) removed deployment from the iteration loop. The earlier attempt at the same idea ran on a hosted sync engine with object schemas defined in it, which put a deployment between every change and every observation. A local file and a local process removed both, and the idea survived contact for the first time. ## The interface has to describe itself, because nothing human will read it The object type has to answer a question an ordinary class never has to. What must an object carry so that something which has never seen it before can use it correctly on the first attempt? A class in a normal program does not carry that and does not need to. The names do most of the work, the signature does the rest, and the documentation is a courtesy for whoever maintains it later. Here the caller is a model that arrives with no prior exposure and does not get a second attempt for free. ```ts export interface ObjectClass { name: string; description: string; defaultState: S; methods: Record>; } export interface MethodDef { description: string; // what this method does and when to use it example?: string; // example usage fn: (state: S, ...args: any[]) => any; } ``` A lock, whole, is small enough to read as one thing: ```ts const Keypad: ObjectClass = { name: "Keypad", description: "A digital keypad lock. Enter the correct code to unlock. " + "Wrong attempts are tracked.", defaultState: { code: "1234", entered: "", maxLength: 4, attempts: 0, maxAttempts: 5, locked: true, hint: "4 digits required", }, methods: { look: { /* ... */ }, press: { /* ... */ } }, }; ``` Everything an agent needs to operate it is in the value. Nothing is in a prompt, and nothing is in a comment. Only `description` is required, and the example is optional because it earns its place only where the call shape is not obvious from the name. A worklog's `log` method carries `invoke("daily-log", "log", ["Implemented feature X", ["dev", "feature"]])`, which shows the positional array and the nested tag list in one line, and an agent that has never seen a worklog can construct a valid call from it. Putting `description` on the type rather than in a doc comment is the decision. A comment is advisory and a field is not, so a class that does not describe itself does not compile, and there is no path by which an undocumented method reaches an agent. Objects live as [JSON](https://www.json.org/) in SQLite and are hydrated with their class only when a method is invoked, which makes the object virtual. It exists during the call and is a row the rest of the time. The whole surface is three verbs: ```ts const env = createEnv("./my-env.db"); env.create("Counter", "page-views"); env.invoke("daily-log", "log", ["Finished implementing feature X"]); env.inspect("daily-log"); // -> methods, each with its description ``` `inspect` is the one that matters, because it is how an agent finds out what an object can do without anybody having put that in a prompt. ## Constrain the interface rather than instruct the agent The requirement appears with the first object big enough to matter. A worklog holding two years of entries is too big to read, and the obvious interface hands the agent a `getAll` and lets it discover that by running out of context. The instinct is to fix it with instruction: tell the agent to check the size first or process long logs in chunks. That remains probabilistic and has to be repeated in context, so the size bound belongs in the interface instead. An instruction is a probability, and it decays with distance from where it was given. An agent that has forgotten looks exactly like one that was never told, which makes the failure impossible to diagnose from the outside. So the worklog has `peek`, marked in the source under the comment `Constraint methods - help agent reason about scale`: ```ts peek: { description: "Get metadata about the worklog without loading entries. " + "Use this FIRST to understand scale before deciding how to process.", example: 'invoke("daily-log", "peek")', fn: (state) => ({ count: state.entries.length, earliest: /* oldest timestamp */, latest: /* newest timestamp */, tags: /* every tag in use, sorted */, }), } ``` A count, a time range, and the tag vocabulary. No entry ever comes back. The instruction to call it first sits in the description because the description is what the agent reads, and it holds without being remembered, because the return type makes any other order pointless. Nothing gets an entry out of `peek`, so an agent that wants entries has to go somewhere else and pick a slice. Underneath it sit readers that take slices rather than everything. `range` between two timestamps, whose own description says it is for chunked processing of large logs, plus `lastN`, `lastHours` and `today`. None does anything a `getAll` could not do, and each one makes a shortcut unavailable. The general form is that a degree of freedom you remove cannot be used wrongly, and a degree of freedom you ask politely about will be used wrongly at some rate you cannot measure. ## The journal indexing run The test was two years of journal data that needed indexing and summarising. Rather than writing the processing logic, I created the object types, populated the environment with the data, and asked the agent to work out a strategy. It chose to chunk by time period, summarise each chunk, and then synthesise the summaries into a whole, which is map-reduce, and nobody wrote the loop. The narrow reading is the one worth keeping. The affordances already pointed at chunking, since one method says to call it first to understand scale and another says it exists for chunked processing of large logs. The agent supplied the axis to cut on and the decision to synthesise rather than concatenate, neither of which anybody had specified. Which is the result the design was actually testing. Constraining an interface changes behaviour more reliably than instructing it does, because making the contents unavailable until the agent has asked how big they are is not something it can forget. ## A search with no way back is a worse algorithm Branching was added after watching agents work, rather than from anticipating that they would need it. An agent exploring a problem hits dead ends, and with no way back it does one of three things: burns tokens reversing its own moves by hand, loses the state and starts over, or stays in a bad position because getting out costs more than continuing. The mechanism is one table. Every state-changing call gets a row carrying the state before it and the state after: ```ts export interface MutationRecord { id: number; branch: string; object_id: string; object_name: string; method: string; args: string; // JSON before_state: string; // JSON after_state: string; // JSON created_at: number; } ``` Once that table exists, the rest is a query against it. A checkpoint is a marked row, an undo reverts the last n mutations by writing `before_state` back, and a branch is the `branch` column, so forking the timeline at a row lets two strategies run from the same position: ```ts export interface BranchRecord { name: string; parent_branch: string | null; fork_point_id: number | null; // the mutation this branch forked at created_at: number; } ``` Three features and one implementation. The maze solver demonstrates it. The agent gets `look`, `move`, `checkpoint` and `restore`, and the checkpoint method's description tells it to save before exploring a path that might be a dead end. In the run recorded at the time it escaped in twelve moves after exploring six dead ends, using six checkpoints and four restores. The loop in that maze is decide, observe, update, decide again, which is search in the ordinary computer science sense. Every search has a way of backing up: recursion unwinds the stack, and an iterative version keeps an explicit one. An agent in a loop has no stack to unwind, so until you hand it checkpoints it is running a search with the back-up step deleted, which is a different and much worse algorithm. ## State as the coordination mechanism The murder mystery is the demo that shows why a workspace beats a message bus. Three suspects, each an object, and the state is where the whole game lives: ```ts interface SuspectState { alibi: string; // what they claim isGuilty: boolean; // hidden truth secretMotive: string; actualWhereabouts: string; trustLevel: number; // 0-100 timesQuestioned: number; gossip: Array<{ about: string; info: string; requiresTrust: number }>; reactions: Record; // keyed by evidence id } ``` Nothing there is a message. Showing evidence to a suspect changes that suspect, questioning them changes what they will say next, and pressing too hard costs trust that gates the gossip you wanted. The investigation is a walk through state, so the solution emerges from the order the agent chose rather than from a path anybody laid down. Two agents working the same case need none of the machinery a message protocol would require, because they are reading and writing the same suspects. Humans collaborate the same way, working on shared documents and boards rather than only talking, and the shared artifact is what makes the talking optional. ## What the pattern kept when the runtime went The later rebuild changed the substrate under one principle: Linux already supplies the persistence, naming, composition and process model, so the system should compose those facilities rather than recreate them. Under that principle the agent is the shell and the filesystem is the store, and the three things ObjectEnv supplies as a runtime are already sitting there. Files persist, are addressable by path, and can be passed around by name. [Git](https://git-scm.com/) has branches and checkpoints. Git also has the mutation log, for the same reason. Which is the answer to the objection I had raised against my own design on the first day, about what a runtime buys over a file with a script beside it. It buys the invocation path and the storage layer, both of which an operating system already provides. The filesystem has no equivalent for the bounded methods, which are a design pattern rather than a package. A `peek` that structurally cannot return an entry is something the application still has to define. The transferable parts: - Put state on the thing that computes with it, so there is one object to keep correct rather than two systems to keep in agreement. - Give the workspace names, because composition needs somewhere to put an intermediate result that is not the transcript. - Ask what the interface must carry for a caller that has never seen it and gets one attempt, then make that a required field rather than a comment. - Remove the shortcut instead of asking the caller not to take it, because an instruction decays with distance and a missing method does not. - Design the return type so the intended order is the only order that gets anywhere. - Give an exploring agent a way to back up before you give it anything else, since a search without one is a different algorithm. - Log before and after state once, and take checkpoints, undo and branching as queries over the same table. - Check whether the operating system already implements the runtime you are about to write, and keep the part it does not. The bounded methods remain useful. I would build them again on top of the filesystem instead of maintaining a runtime of my own. --- ## Idyllic v4: compiling TypeScript classes into stateful agents URL: https://wcdc.io/writing/idyllic-v4 Date: 2025-12-18 Description: I wanted agent applications to own typed state and expose real domain methods. That led me to a class model, a small wire protocol, a source transform, and Durable Objects. In [Idyllic](https://idylliclabs.com) v4, I define an agent application as a [TypeScript](https://www.typescriptlang.org/) class. One instance represents one live session. Properties hold its state, decorated methods define the operations a client can call, and a compiler turns the class into a deployable Cloudflare Durable Object. I chose this model because prompt-and-tool frameworks make persistent state feel external to the program. They give the model instructions and callable functions, then leave the application to name keys, serialize values, reconstruct objects, and keep the store consistent. They also reduce every interaction to a generic message even when the application has clear operations such as `move`, `resign`, or `generateReport`. I wanted the source code to describe the application directly: state as properties, operations as methods, and deployment machinery generated around both. ## The class owns state and compute Most agent frameworks define an agent as a prompt plus tools. Memory usually becomes a message array backed by an external store. Even a simple phase variable needs a key and serialization code: ```ts await store.set(`session:${id}:phase`, JSON.stringify("evaluating")); const phase = JSON.parse( await store.get(`session:${id}:phase`) ?? '""' ); ``` Inside a stateful object, the same operation is an assignment: ```ts this.phase = "evaluating"; ``` JSON encoding itself is cheap. The recurring cost is inventing keys, keeping their shapes synchronized with the code, and rebuilding typed values after every read. State that belongs to a running application should live on the object performing the work. That gave me the base abstraction: an agent application is a program with installed modules. The program owns session state and control flow. Each module adds a configured capability, including the data it needs to operate. ## One instance represents the whole session Once the application became an object, I had to decide what one instance represented. A separate object for every agent creates several owners for state that belongs to one session. Three agents working on the same document would each need a copy or a protocol for synchronizing their views. I use one instance for the entire live system: ```text one instance per agent one instance per session agent A ── state ┌──────────────────────┐ agent B ── state │ shared session state │ agent C ── state │ │ coordination │ agents are functions│ protocol └──────────────────────┘ ``` The session object owns the board, document, accumulated findings, and other shared state. Specialized agents run as functions inside it. They can still have separate prompts, context, and data, but they do not need a conversation protocol to agree on the state of the work. The client interacts with one `AgenticSystem`. Whether the implementation uses one model call or ten agents in parallel stays behind that boundary. ## Actions use the verbs of the application Generic `run` and `onMessage` methods work naturally for chat. They become awkward as soon as the application has operations that users already know how to name. Chess made this obvious. A message interface encodes the move inside text and parses it on the server: ```ts agent.onMessage("I'd like to play e4"); ``` The domain already has a better interface: ```ts agent.move("e4"); agent.resign(); agent.offerDraw(); ``` Methods give the frontend real signatures, make invalid calls visible to TypeScript, and let each application expose something more specific than `sendMessage`. I mark remotely callable methods with `@action()`. Public methods remain ordinary helpers unless I explicitly add the decorator. This keeps a local refactor from silently changing the network API. ## Fields define the synchronized client state I use the same rule for values displayed by the interface. A synchronized value is a decorated property: ```ts export default class SimpleSystem extends AgenticSystem { @field query = ""; @field count = 0; @action() async increment(amount?: number) { this.count += amount ?? 1; } } ``` `@field` exposes state to connected clients. `@action()` exposes a method they may call. Assigning to `this.count` updates the clients, so application code does not contain a separate broadcast operation. Model output needs a second field type because streaming text has a lifecycle. A normal value changes atomically; a stream receives chunks and eventually completes: ```ts @field problem = ""; @field hypo1 = stream(""); @field hypo2 = stream(""); @field hypo3 = stream(""); ``` A stream supports `append`, `complete`, and `reset`. Three model calls can write to three fields in parallel with `Promise.all`. The paths separate their output, so I do not need to multiplex several generations through one application-level channel. The wire messages remain small: ```json { "type": "stream:append", "path": "hypo1", "chunk": "The key insight..." } { "type": "stream:complete", "path": "hypo1", "value": "The key insight is..." } { "type": "action", "action": "generate", "args": [] } ``` The framework owns transport and synchronization. Application code decides when to append history, create an artifact, checkpoint state, or complete a stream. ## History entries remain extensible Conversation history cannot be a closed list of text messages if modules can introduce plans, artifacts, charts, or structured tool results. Encoding every new entry as text creates a second informal protocol inside the first. In Idyllic, a module can define a history entry type together with two conversions: how the interface renders it and how the model sees it. A chart can remain structured in the application while producing ordinary model messages when it enters inference context. This boundary keeps the framework from deciding the shape of every application built on it. Idyllic moves history entries and synchronizes them. The application and its modules own their meaning. ## The source class compiles into a different program The class above is the code I want to write. Cloudflare requires a Durable Object export, storage bindings, request routing, and lifecycle hooks. Those are different programs connected by a source transform: ```text authoring model TypeScript class with fields and actions deployment model Durable Object export with storage and routing compiler transforms the first into the second ``` Separating them let me design the source API around application code and the generated output around the platform. The compiler is the layer that makes both descriptions true. I originally approached Idyllic as a custom language for AI applications. TypeScript already supplied the parts I would have had to rebuild: types, editors, imports, packages, control flow, and familiar object composition. Idyllic therefore restricts and transforms a subset of TypeScript instead of inventing new syntax. The source must continue to read and behave like an ordinary class. Code that does not use an Idyllic construct keeps normal TypeScript semantics. The transform only gives additional behavior to explicit fields and actions. I also keep the deployment target out of authored code. The compiler generates the routing worker that locates the correct session instance. Local development runs the same transform through [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/), keeping local and deployed semantics aligned. This removed Wrangler from the application-facing workflow. Hiding it behind another command would still expose its configuration and errors. Using Miniflare directly gave Idyllic one runtime path that I could control. ## The protocol sets the limit of the transform I designed the wire protocol before finalizing the compiler rules. The protocol determines which state transitions the runtime can represent. The runtime then determines which source constructs the compiler can support honestly. ```text protocol → runtime behavior → source constructs ``` Because the protocol has a field-update event, the compiler can turn assignment to a decorated field into a state change and broadcast. Because it has stream append and completion events, `stream` can expose those operations directly. Because it has an action call, a decorated method can become a typed remote procedure. Anything outside that protocol remains local TypeScript. This gives the transform a clear boundary and prevents convenient source syntax from promising behavior the runtime cannot deliver without hidden round trips. The ordering also made persistence easier to reason about. A field update has one runtime meaning whether it originated from a local action, a model callback, or an external event. The storage and broadcast behavior attach to that transition instead of being reimplemented at every call site. ## Modules bring the state behind their operations Tools expose functions and leave their storage to the application. Idyllic modules package the operations with the data model and configuration they require. A Telegram module, for example, can provision message and contact tables, retain conversation state, install its prompts, and export operations such as `sendMessage` and `getConversationHistory`: ```text install Telegram module brings messages table contacts table conversation state conversation prompts exports sendMessage getConversationHistory ``` Installation is a provisioning step. The application receives a configured stateful resource instead of a function whose memory must be assembled elsewhere. This is the module boundary I care about: a module owns the state required to make its operations meaningful. Stateless capability can remain a tool. Stateful capability arrives with its tables, migrations, prompts, and lifecycle. The result resembles a small cloud environment scoped to one agent application. Installed modules provide resources with operations defined over them, while the session program composes those resources into behavior. ## Durable Objects match the resource model The transport did not decide the platform. Server-sent events, WebSockets, and hosted realtime services can all carry the protocol. I needed an addressable live session located beside persistent storage. [Durable Objects](https://developers.cloudflare.com/durable-objects/) provide that resource directly: one named JavaScript instance with attached storage. The infrastructure now matches the source abstraction. One class instance represents one session in the program, and one Durable Object represents that session when deployed. Ordinary serverless functions could run individual actions, but an external event or background operation also needs to update the session and deliver changes to connected clients. I use request-response execution for the application logic and keep a long-lived connection for delivery. That division fits an edge isolate. A general container would support a wider range of workloads at the cost of a heavier deployment model. Idyllic only needs the execution and storage behavior required by its class model. The deployment layer uses [Cloudflare Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/) with Durable Objects underneath. Idyllic owns code upload, transformation, routing, and the developer-facing lifecycle. Cloudflare owns the machines and stateful runtime. Fast delivery is infrastructure, not the product's reason to exist. "Ten agents streaming" only describes traffic. The application model matters because those agents are updating typed state through named operations inside one shared session. ## The same boundary improved debugging Named actions make a live session inspectable through RPC. A coding agent can connect to a running instance, read its fields, call the same actions as the browser, and observe the resulting state transitions. The interface updates while the coding agent operates the application, so I can watch a debugging session happen through the product itself. I did not need a separate control protocol; debugging reuses the domain methods and synchronized fields already required by the application. This is a useful test of the abstraction. A second kind of caller can drive the same object without converting every operation back into text messages. ## The class carries the application boundary Idyllic v4 moves the important boundaries into the source type system. Persistent state is a property instead of a key in an external store. Client operations are methods instead of instructions hidden in messages. Remote access is explicit through decorators. Streaming output is a typed field with a defined lifecycle. The compiler preserves that authoring model while generating the storage, routing, synchronization, and deployment code required by Durable Objects. Modules package stateful resources together with their operations. The final model is compact: one class represents a live agentic system; its fields hold shared state; its actions expose the verbs of the application; installed modules bring their own storage and operations. TypeScript describes what differs between applications. The generated runtime carries everything else. --- ## Mechanistic Mindset: a wiki three agents maintain URL: https://wcdc.io/writing/mechanistic-mindset Date: 2025-11-19 Description: A wiki maintained by agents, which means every quality rule has to become something an agent can be checked against. The [Mechanistic Mindset](https://mechanisticmindset.com) wiki describes my behaviour in computational language: activation costs, scripts, state, caches and thresholds instead of laziness, discipline or willpower. It lives in a folder of linked markdown files and is maintained by three agents. The design problem was maintenance. New material cannot simply become another page. It has to be reconciled with the existing definitions, terminology and links. That requires turning editorial qualities such as consistency, caution and connectedness into instructions an agent can follow or properties a script can measure. ## Choose the container that matches the state of the thinking The origin was a page called MY OWN LEXICON, a glossary of terms for how my behaviour works, each with the definition I meant rather than the dictionary one. The list kept growing and the entries kept referring to each other, so what it wanted was not a longer glossary but a set of pages that point at one another and accumulate. The material did not fit a blog. Posts imply publication order and a moment of completion. These ideas continued to change, and their relationships mattered more than when I wrote them. A wiki makes revision normal, lets each article stand alone and represents those relationships directly through links. There is a second reason to want the graph, and it aims at the machine rather than the reader. A model works over whatever you put in front of it, so the useful question is which structure makes assembling that material cheap. A graph of standalone pages with typed relations between them lets a retrieval step start at a concept and walk outward, which a chronological archive of posts cannot do. The wiki is a memory substrate for compute before it is a publication. ## The material was written down and not addressable, which decides who maintains it Years of source material already existed in chat transcripts, but none of it was addressable. Each idea was buried inside a longer exchange, and converting those exchanges into a coherent wiki required a second pass over everything. I did not want to perform that pass manually. The publishing system therefore had to be operable by an agent. That requirement ruled out tools whose structure lived behind an application-specific interface. ## A vault is a folder of markdown, and that description is the one an agent can operate I initially dismissed [Obsidian](https://obsidian.md/) as an uninteresting choice. That changed when I described an Obsidian vault by its storage model instead of its interface: it is a folder of markdown files. Wiki links are page names in double brackets, and the graph can be reconstructed by reading the files and following those brackets. Obsidian does not have to be running for the structure to exist. Three things come free from that description: - **the graph edges already exist,** because wiki links are edges - **the graph is queryable,** because a script can walk the vault and build the adjacency itself - **maintenance becomes a file-editing problem,** which is the one thing coding agents are unambiguously good at The useful description of a tool is the one that exposes the parts an agent can operate. For Obsidian, those parts are files, links and directories. ## An exemplar specifies a register that a description can only name I wrote a briefing describing what the wiki was for and how its articles should read, then fed the agent a handful of seminal articles and brain dumps I already had. It started producing pages, and the pages were right. Those first articles became the models for every article after them. Each new article is written with related existing pages in context, so the register of the early pages propagates through the vault. Editing the initial examples carefully mattered because later pages repeatedly sampled them. Exemplars beat descriptions for a reason that says something about what you are talking to. - **Describe the register** and each adjective still covers a wide range of writing. A request for prose that is precise, unhurried and sceptical leaves the model to construct an example of that voice before it can produce a sentence. It usually chooses a generic version. - **Provide three articles** and the target becomes concrete. A language model continues text, so an example supplies the sentence shapes, pacing and degree of certainty directly. A style guide still matters for explicit prohibitions, but examples carry the register. The first articles were part of the system specification. ## The wiki's content is a translation rule, so the rule lives in the agent that writes The wiki has one operation running through every article, which is the conversion of a moral description of behaviour into a mechanical one. Lazy becomes a script that failed to load. Lacking discipline becomes an activation cost above the threshold that was available. Because this translation applies to every article, it belongs in the writer's definition rather than in an individual prompt. The definition carries it as two lists: ```markdown **USE:** - Computational metaphors: state, cost, script, RAM, algorithm, cache, threshold, activation energy, default scripts - System descriptions: "the work_launch_script didn't load" not "you were lazy" - Operational definitions: frameworks described by what they let you DO **NEVER USE:** - Moralistic language: willpower, discipline, laziness, procrastination (except when explicitly translating FROM them) - Scientific claims: "studies show," "research proves" - Motivational/preachy tone - Vague abstractions without operational grounding ``` The parenthesis in the first banned item is the load-bearing part. Banning the moral vocabulary outright would make the wiki unable to state the thing it is translating from, and the exception keeps the source language available inside the translation while keeping it out of the conclusion. A second constraint prevents computational language from turning into a claim about neurology. The wiki documents a practice; computation is a lens, not a literal biological mechanism. The reviewer checks every article against that boundary. The maintainer's brief also defines its authority by listing what it may not do: ```markdown **What you DON'T do:** - Auto-generate content unprompted - Decide what to document (Will leads) - Try to make this "scientific" or prove things rigorously - Create quantifiable predictive models ``` These restrictions prevent the maintainer from treating structural gaps as permission to invent material. It may organize and reconcile my thinking, but it may not decide what I believe or what deserves an article. ## A separate agent exists because an instruction does not survive a long generation The pipeline needed to run without article-by-article supervision. I first used one agent for research, writing and review. In long sessions, its early instructions had less influence on later output, so I kept reintroducing the same constraints. So the work divided into roles, and each role is a file with a frontmatter block naming the model it runs on: ```markdown --- name: wiki-article-writer description: Use this agent when you need to write or draft articles for the Mechanistic Mindset wiki in Obsidian format. This includes: creating new conceptual articles from brain dumps, expanding stub pages into full articles... or translating moralistic language into mechanistic computational metaphors. model: sonnet --- ``` The description is dispatch criteria rather than a summary because the orchestrator uses it to select an agent. The work is divided across three roles: - **Context finder.** Finds pages related to the new material, summarizes their treatment of it and identifies conflicts before drafting begins. - **Writer.** Produces or updates articles using the source material and the related pages. - **Reviewer.** Checks a finished draft for certainty, terminology, register and graph integration. The reviewer has a more specific job than general quality control. Certainty calibration comes first, and each failure is paired with an acceptable replacement: ```markdown ### 1. Certainty Calibration (HIGHEST PRIORITY) **RED FLAGS - Flag immediately:** - "The brain literally implements X" - "Research proves/shows that..." - "This is not metaphor, this is actual mechanism" - Deterministic predictions ("this will cause Y") - Universal statements ("everyone experiences X") **GOOD PATTERNS - Encourage:** - "Appears to use X-like processes" - "Observed in N=1 experience that..." - Probabilistic language ("tends to", "often", "in this case") - Explicit caveats ("This worked for Will, test it yourself") **ACTION:** For every claim, ask: "Is this presented as useful heuristic or scientific truth?" ``` The replacements matter because a prohibition alone gives the reviewer no way to repair a sentence. Build order and run order differ: - **built** writer, context finder, reviewer - **runs** context finder, writer, reviewer ## Edge density has to be measured or it is a wish An agent editing one article cannot assess connectivity across the vault. Instructions to "link related pages" therefore need collection-wide checks. So the properties I wanted became scripts the agents run, and each one names a pathology rather than a metric: | Script | What it reports | The pathology it names | | --- | --- | --- | | `check_links.py` | broken links, orphaned pages, hub and authority pages | a page nothing points at | | `connectivity_analysis.py` | centrality, clusters, bridge pages | a cluster attached to the rest by one article | | `consistency_checker.py` | dead-end pages, missing sections, terminology usage | the same concept under two names | The connectivity script is [NetworkX](https://networkx.org/) pointed at the vault, and its method list is the vocabulary the pathologies are named in: ```python def build_graph(self) # articles as nodes, wikilinks as edges def pagerank(self) # which articles the graph treats as central def betweenness_centrality(self) # bridge pages: remove one, split the vault def degree_centrality(self) def get_mst(self) def get_communities(self) # clusters that formed without being planned def get_reading_order(self, start) # a path through the graph for a new reader ``` Naming the pathology is what makes the number actionable. An orphaned page is a page the reader will never reach, a bridge page is a single point of failure in the graph's connectivity, and inconsistent terminology means the retrieval step will miss half of what it should have found. The graph also produces a reading order derived from the actual links rather than a manually maintained table of contents. The briefing tells agents when to run each script, so link checking follows linking work automatically. The same reasoning produced a fixed ontology, and it sits inside the context finder's own brief so that it gets consulted on every pass rather than looked up: ```markdown 5. **Ontology Awareness**: Always consider where concepts fit in the wiki's structure: - Philosophy & Foundations - Tools & Infrastructure - Methodology - Case Studies - Core Frameworks (Computational Lenses) - Applications - Principles - Teaching & Transmission - Techniques - Supporting Concepts ``` The closed list makes placement consistent. If an article fits none of the ten categories, the mismatch exposes a problem with the ontology instead of encouraging the agent to create an ad hoc category. ## Ingestion is reconciliation, which is why raw material stays outside the graph The wiki got a web reader with retrieval search over it, which is what [mechanisticmindset.com](https://mechanisticmindset.com) serves. The property that mattered more is that dropping a chat transcript into the vault updates the existing articles rather than adding one to the end. Ingestion reuses the context finder. Before anything is updated, it returns four things for a transcript: - every page that discusses the concept, uses related terminology, or would be affected by a change to it - how each of those pages currently treats the concept, and in what language - the existing cross-links and their directions - the inconsistencies between them The conflicts turn ingestion from appending into reconciliation: - **a list of affected pages** tells the writer where to add text - **a list of the conflicts** between how those pages already describe the thing tells it what has to change Resolving those conflicts performs the second pass that made manual transcription impractical. One rule protects the boundary. Incoming transcripts and brain dumps land in a `raw/` directory that the wiki never links to and the graph scripts never scan. Raw material is a source to be read, and an article is something written from it, and mixing them would put unreconciled text inside the structure whose whole value is that everything in it has been reconciled. The exclusion is encoded as a default argument: ```python def __init__(self, vault_path: str, exclude_dirs: List[str] = None): self.exclude_dirs = exclude_dirs or ['raw', '.git', '.obsidian', 'scripts'] ``` The default keeps `raw/` outside the graph unless a caller explicitly overrides it. The context finder always asks which other pages are affected when a concept changes. That propagation step is the difference between maintaining a knowledge system and filing new documents. ## Why the work remains split into three passes The three agents contain instructions that could fit in one prompt, but the single-pass version produces worse articles. Two mechanisms may explain the difference: - **Instruction competition.** A prompt carrying three jobs makes their constraints compete, and review instructions apply to text that does not exist when generation begins. - **A concrete review object.** A separate reviewer receives a finished draft instead of an intention to write one, so every comment can refer to actual language. Larger context windows may reduce instruction competition, but they do not remove the value of reviewing a concrete draft. The pipeline therefore keeps three passes and gives each one an inspectable artifact: the context finder produces an impact map, the writer produces a draft and the reviewer produces specific repairs. The resulting wiki is maintained through boundaries rather than autonomy alone. [Markdown](https://commonmark.org/) exposes the state, exemplars specify the register, role prompts constrain authority, scripts measure graph-wide properties and the `raw/` boundary separates evidence from reconciled knowledge. Together they let agents change the vault without quietly changing what the vault means. --- ## XJSN: a notation for models to write programs as data URL: https://wcdc.io/writing/xjsn Date: 2025-09-08 Description: JSON with one added value type, an inert function call, so a model writes the code shapes it is good at and a validator can still reject the result. XJSN is JSON with one additional value type: an inert function call. A call can appear anywhere a JSON value can appear. It looks like code, but it is never evaluated as JavaScript. The parser turns it into data, and a validator checks the resulting tree against a registry of allowed functions. ```javascript { "user": checkUser("alice"), "actions": [ sendEmail("welcome"), createProfile() ] } ``` The design came from a practical problem. I needed an intermediate form that a model could generate reliably, a program could parse, a person could inspect, and a validator could reject before anything executed. [JSON](https://www.json.org/) satisfied three of those requirements. It struggled with the first. The solution was not to give the model a full programming language. It was to use the part of programming-language syntax that models are unusually good at producing, then remove everything that makes code dangerous or difficult to validate. ## The problem is constrained generation Suppose a user describes a workflow in ordinary language. A model translates that description into an intermediate representation, and a runtime eventually carries it out. That intermediate form has four readers: - The model has to generate it. - The parser has to reconstruct it. - A person may need to inspect or edit it. - The validator has to decide whether it is safe and meaningful. Most serialization formats are designed around only the second reader. They optimize for programs exchanging data. A model-generated program adds a different constraint: the notation must be easy to produce correctly under deep nesting. The central problem is therefore not parsing arbitrary model output. A parser can be written for almost any notation. The problem is defining a small set of structures the model may generate, giving the model a reliable way to express them, and returning errors specific enough for the model to repair its own output. That makes generation, validation, and error reporting parts of one protocol. ## Why plain JSON becomes awkward JSON is the obvious starting point. It is familiar, language-independent, easy to parse, and supported by schema tools. It also has a deliberately small type system: objects, arrays, strings, numbers, booleans, and null. There is no native way to say that a value is a function call, a condition, or a reference. Those constructs can be encoded with tagged objects: ```json { "$type": "conditional", "$condition": { "$type": "function_call", "$name": "user_has_permission", "$args": [ { "$type": "variable", "$ref": "current_user" } ] } } ``` The representation is valid JSON, but it makes both generation and reading harder. The model has to preserve several levels of brackets, repeat structural keys such as `$type`, and remember which fields belong to each tagged variant. A person has to read through the encoding before reaching the operation it represents. Validation also becomes indirect. The shape of an object depends on the value of `$type`, so the schema becomes a union over every possible construct. That is workable in principle, but errors tend to point at a failed union rather than the actual mistake: a missing argument, an unknown function, or a value of the wrong type. The notation had moved the complexity of the language into the least readable part of the document. ## Models are better at code-shaped nesting Deep nesting is not equally difficult in every notation. Models produce nested code much more reliably than equally nested tagged JSON. That is not surprising. Source code is abundant in training data, and nesting is fundamental to it. Models have seen function calls inside function calls, arrays of expressions, object literals containing calls, and long chains of structured arguments. This suggested a different strategy: 1. Start with a shape the model already knows how to generate. 2. Keep only the syntax needed to describe a structured program. 3. Remove variable declarations, assignment, closures, branching, loops, and arbitrary execution. The goal was to use the model's coding priors without accepting general-purpose code. That put XJSN between two familiar languages: - It is a subset of [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) syntax, which gives the model familiar function-call notation. - It is a superset of JSON values, which keeps every parsed result representable as data. The result looks like the declarative part of a JavaScript codebase, but its semantics are closer to a typed data format. ## A function call is the only new primitive JSON needs only one extension to represent most of the structures I cared about: a call that occupies a value position. ```javascript { "workflow": checkPermission(currentUser), "actions": [ sendNotification(), returnResponse("success") ] } ``` The parser never executes `checkPermission`. It produces an AST node: ```json { "$type": "call", "$fn": "checkPermission", "$args": [ { "$type": "reference", "$ref": "currentUser" } ] } ``` The verbose representation still exists internally. The model no longer has to write it. This is the main trade XJSN makes. Complexity moves from generated text into the parser, where it can be implemented once and tested. Calls compose naturally because every argument is itself an XJSN value: ```javascript { "simple": greet("World"), "namespaced": user.create("Alice"), "nested": processData( getData("source"), "transform" ) } ``` The notation does not need separate syntax for pipelines, action nodes, or tagged operations. A domain can express them through the functions it defines. That makes the function call similar to an s-expression. `f(a, b)` and `(f a b)` represent the same tree with different punctuation. XJSN uses the first because models already associate it with ordinary code. ## What XJSN deliberately removes Looking like JavaScript creates a risk: readers may assume it behaves like JavaScript. The safest response is a narrow grammar, not a warning in the documentation. XJSN has no syntax for: - Variable declarations - Assignment - Closures or function definitions - Property mutation - Loops - General branching - Imports - Class or object construction - Arbitrary operators - Access to a JavaScript runtime A call name must resolve to a function declared in the active schema. Its arguments must match the declared signature. Nothing in the document can introduce new behavior. This boundary changes the security model. The runtime does not evaluate a source string or expose a global environment. It receives a parsed tree containing literals, collections, references, and calls selected from a registry. The document describes behavior using an allowed vocabulary. It does not define behavior from first principles. That distinction is what makes the notation useful as an AI-generated intermediate representation rather than another way to ask a model for code. ## Why not [XML](https://developer.mozilla.org/en-US/docs/Web/XML) or a richer configuration language? Before settling on inert calls, I explored a richer external syntax with JSON underneath it. Markup was attractive for three reasons: - It handles nesting and mixed content well. - Models have seen enormous amounts of it. - XML has mature validation systems such as XSD and RELAX NG. The validation ecosystem was especially interesting. A validator that returns only true or false can stop bad output. A validator that reports a precise path, expected type, and invalid value can drive a repair loop: return the errors to the model and ask it to correct the document. But markup solved the parser and validation problem by adding a second representation. The model would write one form, the runtime would consume another, and the system would maintain a translation between them. Richer configuration languages such as Dhall, [EDN](https://github.com/edn-format/edn) with tagged literals, or a [Racket](https://racket-lang.org/)-like DSL offered more expressive syntax. That expressiveness mainly benefits a human author. Models do not need shorthand because they get tired of typing, and they do not need infix syntax to scan a formula quickly. The question that simplified the design was: who is the primary author? If the model writes the document and a person only occasionally reads or edits it, then the notation should optimize first for reliable generation and precise checking. Function calls and JSON values already met that need. More syntax would expand the parser and validator without adding much expressive power. ## The parser converts familiarity into structure Because XJSN is a superset of JSON, a standard JSON parser is not enough. The implementation uses a lexer and parser built with [Chevrotain](https://chevrotain.io/). The parser recognizes ordinary JSON values plus three important additions: - Function calls - Namespaced function names such as `user.create` - References to values supplied by the surrounding runtime Its output is an AST made only of data. A simplified type model looks like this: ```ts type XJSNValue = | null | boolean | number | string | XJSNValue[] | { [key: string]: XJSNValue } | XJSNReference | XJSNCall; type XJSNCall = { kind: "call"; functionName: string; arguments: XJSNValue[]; }; ``` The parser's job is intentionally limited. It answers what the text says, not whether the program is valid for a particular domain. Domain meaning belongs to the schema and validator. This separation allows the syntax to remain fixed while different products define different vocabularies. ## The schema is the actual language XJSN supplies the grammar for calls. A schema supplies the operations those calls may invoke. ```ts const schema = new XJSNSchemaBuilder() .name("Todo DSL") .addFunction("task.create", { name: "create", namespace: "task", description: "Create a new task", arguments: [ { name: "title", type: struct.string() }, { name: "priority", type: struct.string() } ] }) .build(); ``` The schema defines: - The functions available to the model - Their namespaces and names - Their descriptions - Positional or named arguments - The type of each argument - Return types - Any domain-specific constraints Two domains can therefore use identical XJSN syntax while exposing completely different languages. A workflow domain might define: ```javascript { "user": checkUser("alice"), "actions": [ sendEmail("welcome"), createProfile() ] } ``` A game domain might define: ```javascript { "spell": castSpell("fireball"), "effects": [ dealDamage(50), consumeMana(25) ] } ``` The parser sees the same structures in both documents. The schemas assign them different meanings. This makes XJSN a substrate for small declarative languages rather than one large language with every possible operation built in. ## Generate the prompt and validator from one source The schema serves both sides of the model loop. On the generation side, it produces instructions describing the functions the model may use, their purpose, and their argument types. On the validation side, it checks the parsed document against exactly those same definitions. ```ts const result = XJSN.validate(text, schema); // { // valid: false, // errors: [...], // warnings: [...] // } ``` This avoids a common failure mode in generated DSLs: the prompt and the checker slowly diverge. A new function should not require separate edits to prose instructions, [TypeScript](https://www.typescriptlang.org/) types, validation code, and repair logic. With one schema, adding a function updates both what the model is told and what the validator accepts. That closes the generation loop: ```text schema -> prompt -> model output -> parse -> validate ^ | |-------------- repair errors <-------| ``` If validation fails, the system can return a structured error to the model: ```text actions[1]: sendEmail expected 2 arguments but received 1 actions[1].argument[0]: expected EmailAddress, received string "welcome" ``` The error identifies the call, location, and expected signature. The model can repair one part of the document without regenerating the whole structure blindly. Error quality is therefore part of the language interface. A validation failure is an input to the next generation attempt. ## Raise the vocabulary instead of enriching the syntax Once calls are the primitive, most of the design work moves from syntax into function selection. A low-level schema could expose functions such as: ```javascript setColor("blue") setPadding(16) setFontSize(24) ``` A higher-level schema could expose: ```javascript applyHeroStyle("technical", "high-contrast") ``` The second function contains more domain judgment. It reduces the number of decisions the model has to coordinate and gives the runtime a stable place to improve the implementation. [Tailwind](https://tailwindcss.com/) demonstrates a related principle. A constrained vocabulary of reusable style decisions is easier for a model to handle than arbitrary CSS spread across a large program. The useful unit is not always the lowest-level property. It is often a composition the domain has already decided is valid. Every invariant moved into a function definition is one less relationship the model must rediscover in generated text. This does not mean every function should be large. A useful domain vocabulary needs layers: - Small primitives for operations that compose safely - Higher-order functions for common decisions - Namespaces that make the available concepts easy to navigate - Types that prevent invalid combinations The notation stays small while the schema becomes more capable. ## Validation should report domain errors, not syntax accidents Tagged JSON tends to fail at the encoding layer. A missing `$type`, malformed wrapper object, or incorrect nesting prevents the validator from reaching the operation the author intended. In XJSN, the parser owns the encoding. Once parsing succeeds, validation can speak in domain terms: - Unknown function `task.complete` - Missing required argument `taskId` - `priority` must be one of `low`, `normal`, or `high` - `sendEmail` cannot be used in this workflow phase - Return type `User` cannot be placed in a field expecting `Boolean` These errors are useful to both people and models because they describe the attempted program, not the representation used to encode it. The validator can also distinguish errors from warnings. An invalid argument type blocks execution. A deprecated function name or an unusually expensive operation may produce a warning that the model can choose to address. This is where the function registry becomes more than a list of callable names. It can carry effects, capabilities, cost information, deprecation status, and contextual rules. The richer that metadata becomes, the less policy has to be embedded in prompts. ## Execution happens after validation XJSN separates parsing, validation, and interpretation. ```text text -> AST -> validated AST -> interpreter -> registered functions ``` The parser never executes calls. The validator never needs access to the implementation of a function. The interpreter accepts only a tree that has already been checked against the active schema. At runtime, a call node resolves through the registry rather than through JavaScript name lookup: ```ts const fn = registry.resolve(call.functionName); const args = call.arguments.map(argument => interpret(argument)); return fn.invoke(args); ``` The actual implementation may be a local function, an API request, a workflow action, or a constructor for another internal representation. XJSN does not require all calls to execute immediately. A domain can interpret them as plans, UI nodes, game effects, or database queries. This keeps syntax and execution decoupled. The same document can be validated, visualized, transformed, or simulated before a runtime performs any effect. ## Where XJSN fits XJSN belongs to the family of data-oriented language systems rather than general-purpose programming languages. EDN extends data notation with additional literal forms. [Clojure](https://clojure.org/).spec describes the shape of valid data. Racket provides tools for building languages whose programs can be manipulated as data. [Lisp](https://en.wikipedia.org/wiki/S-expression) demonstrates how little syntax is required when calls and lists share one representation. XJSN takes a similar idea and chooses a surface that models already generate well: JSON values plus JavaScript-style calls. It also overlaps with projectional editing. Systems such as JetBrains [MPS](https://www.jetbrains.com/mps/) let users edit program structure directly instead of editing text that is later parsed. XJSN keeps the text interface, but the parser immediately recovers a structural document that tools can inspect and manipulate. The practical difference is adoption cost. A team can define a small schema and use ordinary text generation instead of committing to a specialized IDE or a full language workbench. The parser is not the defensible part of such a system. The value accumulates in domain schemas, high-quality validation, repair loops, editors, visualizers, and runtimes that know how to use the resulting tree. ## Design principles XJSN ended up with a small surface because most of the important choices belong elsewhere. The principles are: 1. Optimize the notation for the system that writes it most often. 2. Reuse syntax the model already generates reliably. 3. Add the smallest primitive missing from JSON. 4. Parse code-shaped text into data; never evaluate it as source code. 5. Put domain expressiveness in a typed function registry. 6. Generate the prompt and validator from the same schema. 7. Return validation errors precise enough to drive repair. 8. Prefer higher-level domain operations over a more expressive general language. 9. Keep parsing, validation, and execution as separate stages. 10. Make every executable operation resolve through an explicit registry. The central idea is straightforward: if a model is best at producing code-shaped structures, give it a code-shaped notation. Then remove declarations, control flow, mutation, and arbitrary execution until what remains is data the system can completely understand. XJSN looks like code because that makes it easier for the model to write. It refuses to behave like code because that makes it possible for the system to trust. --- ## IdyllicValue: deciding what one AI step passes to the next URL: https://wcdc.io/writing/idyllicvalue Date: 2025-04-18 Description: The executor passed strings while the product worked on documents, and one word in my notes was naming four objects with different lifetimes. While building [Idyllic](https://idylliclabs.com), I needed to decide what one step in an AI workflow should pass to the next. The executor passed strings. I was adding workflows where people and models would edit the same document across several steps, so the value between those steps needed to preserve the document, expose addressable parts of it, and remain editable in the interface. I named the interchange type `IdyllicValue`. It represents the document moving through a workflow. Defining it required separating the document from three other objects my notes also called "context": the step environment, the model request, and the execution state. ## The executor was passing strings while the product was working on documents The executor actually returned this: ```typescript { input: string, output: string } ``` If a step needed structure, I asked the model to produce text in the intended shape. The next step then received that text and was expected to understand the convention. Nothing in the runtime enforced that the shape was valid or that two steps interpreted it the same way. The examples still ran because a model can emit JSON and another model can read it. The structure existed only as an agreement between prompts. The executor neither validated it nor preserved it as a runtime object. That fails when a value needs identity outside one call. A person edits a paragraph and a later step annotates it. The runtime needs a stable address for the paragraph and a way to preserve the rest of the document. A string carries neither. My first implementation task was therefore to make the executor pass an actual `IdyllicValue`. Every later operation would depend on that runtime guarantee. ## I had to stop calling four different objects context My notes described how "context" moved between steps. The word referred to four objects with different lifetimes: ``` the environment a step runs in rebuilt per step the document resolved for this step rebuilt per step the request assembled for the model rebuilt per call the state of the running process persists across all of them ``` The environment contains the capabilities and bindings available to the current step. The resolved document is the material the step is working on. The model request contains the selected document content plus instructions, provider options, temperature, and tools. Execution state persists across the workflow and records what has happened. I banned the word `context` from the specification and replaced each use with the object it meant: ``` environment resolvedDocument llmRequest executionState ``` `llmRequest` includes provider, temperature, tools, and output format as well as prompt text. `executionState` persists across steps. `environment` supplies the current step's capabilities. `resolvedDocument` is the value the step receives and changes. `IdyllicValue` therefore represents the resolved document. The runtime constructs requests from it and records changes to it in execution state. ## One document type was more useful than a type for every domain I had previously designed semantic objects as typed domain values: a `JournalCollection`, a `BlogPost`, a `HealthRecord`, each with its own fields and methods. Those types make the domain legible to the model. A journal collection can expose search by date. A health record can expose measurements. A blog post can expose sections and citations. Each type also requires a schema, methods, a renderer, and serialization before the first workflow can use it. Idyllic needed to accept a document immediately and add behavior as the workflow developed. I collapsed the interchange layer to one document type and moved domain-specific behavior into functions attached to it: ``` before JournalCollection, BlogPost, HealthRecord, ... one type per domain after Document, with functions defined on it one interchange type ``` Domain-specific behavior moved into functions on the document. It no longer determined which type the executor could pass. A generic `Document` does not know that one block is a blood-pressure measurement or another is a blog citation. Domain types can add that information above the interchange layer. The executor still passes the same document type between steps. ## My first specification was a list of adjectives I then listed eight properties for `IdyllicValue`: structured, connected, cited, human-readable, intelligible to a model, operational, composable, metadata-rich, and editable. For each property, I wrote the operation it enabled and a small demonstration of that operation: | Property | Proposed demonstration | What the exercise exposed | | --- | --- | --- | | Structured | traverse the value with a query syntax | an operation whose address model was still undefined | | Composable | transform between representations | too broad to demonstrate in one small build | | Metadata-rich | add a metadata field | the property was trivial and bought no new behavior | | Editable | show the value in an editor | the editor already provided this | "Metadata-rich" implied a JSON field without naming a feature that used it. "Composable" left open whether values concatenate, merge by block identity, or pass through a function. "Editable" belonged to the product surface and was already provided by the editor. Traversal produced a concrete operation: ``` idyllicValue.keys() ``` I was imagining something between [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/) and [jQuery](https://jquery.com/) for documents. A step could select a subset of a document without placing the entire thing in the model's context. The operation was to select an addressable part of the document. I postponed the method name because the representation had not yet established whether those addresses were keys, blocks, paths, or selectors. ## I fixed the interface before choosing the representation A tree made hierarchical traversal easy and cross-cutting selections awkward. A graph represented arbitrary relationships and made document order expensive. JSON exposed an implementation format to people editing a document. Using the editor's internal model would couple the protocol to the current editor. The executor would pass an `IdyllicValue` interface between steps. The first implementation could remain a string internally. I would add operations to the interface only when a working use required them, then replace the representation when the string could no longer implement those operations cleanly. The runtime could depend on the interface while the data structure remained cheap to change. Annotation would add block identities when it needed them. Selection could add paths when it needed them. No operation yet required arbitrary graph edges. The interface was an early commitment about how steps exchanged values. The representation remained provisional until the operations made a stronger commitment necessary. ## Asking what the model should do produced the first useful primitive I replaced the property list with operations: searching, filtering, selecting, quoting, transforming, and annotating. Search needs an index or a scan. Selection needs addresses. Transformation needs a rule for preserving identity. Annotation needs a target and a removable layer over the source. I built annotation first because it supported a complete interaction with little machinery. Idyllic's editor used [BlockNote](https://www.blocknotejs.org/), which already represented a document as addressable blocks. A model could return an annotation attached to a block ID. The interface could render that annotation beside the relevant paragraph, and a person could inspect, reject, edit, or delete it without changing the original text. The Korean quiz grader provided the test. The quiz lived in the document. A grading step read the answers and attached feedback to their blocks. The feedback appeared beside the material the model had evaluated. The annotation demonstration was the first one to force stable block addresses, a separate annotation layer, and operations for adding and removing annotations. Those became concrete requirements for `IdyllicValue`; the adjectives in my earlier list had produced no equivalent implementation decisions. Annotations also keep human and model edits distinguishable. The person owns the source document. Model output sits in a separate layer with an address and provenance. ## The editor model and the interchange value stay separate BlockNote supplied the addressable blocks needed by annotation. I kept its document type out of the interchange protocol. The editor model changes for editor reasons: rendering, selection, cursor behavior, collaborative editing, and plugin compatibility. The interchange value changes for workflow reasons: serialization, model access, operations, identity, and persistence. Sharing one type would let a UI implementation decision become part of the execution protocol. An adapter translates `IdyllicValue` operations into BlockNote structures. Replacing the editor requires a new adapter while workflow steps continue to use the same interface. A server-side step can operate on the same interchange value without loading a browser editor. ## Making documents operational creates an injection boundary Operational documents create a prompt-injection boundary. Anyone who can edit the document can place instructions where a later model will read them. If the runtime mixes document text with system instructions, a paragraph can redirect the operation meant to analyze it. Idyllic therefore treats document content as data unless a workflow explicitly promotes part of it to instructions. Functions carry permissions, and annotations carry provenance so later steps can distinguish source content from generated commentary. ## The resulting implementation The executor passes a real `IdyllicValue` between steps. That value is the document. The environment, model request, and execution state have separate types and lifetimes. All workflows use the same document interface. Domain functions add behavior. An adapter connects the interface to BlockNote, and the first implementation can remain a string behind that interface until an operation requires more structure. Annotation is the first such operation. It requires stable block addresses, a separate layer for model output, and provenance. Those requirements now determine the representation. This gives me a direct implementation order: pass the value through the executor, add one operation, and change the representation only when that operation requires it. --- ## Idyllic prompt language: executable prompts that still read like prose URL: https://wcdc.io/writing/prompt-language Date: 2025-03-16 Description: I wanted prompts to branch, call tools, and manage context predictably without turning them into source code. I built a document AST with paragraph statements, mentions, decisions, scopes, and runtime interpolation. I built the [Idyllic](https://idylliclabs.com) prompt language because long prompts were difficult to reuse and reason about. A prompt could mention tools, data, and several steps, but the text did not define which parts were calls, which context each call received, or how execution should branch. I wanted enough structure to make those choices explicit. I also wanted the result to remain readable by people who already knew how to write instructions in prose. Turning the prompt into a conventional programming language would have defeated that purpose. The result is an executable document. Each paragraph is one statement. Inline markers identify objects and modifiers. Structured blocks handle decisions and events. The document parses into an AST that a tree-walking interpreter executes. ## A paragraph is the unit of execution I began with the smallest rule that gave the runtime a stable boundary: one paragraph expresses one instruction. Paragraphs already separate ideas in ordinary writing. Using them as statements meant writers did not need semicolons or one command per line. The interpreter could execute one complete intent at a time and record its input, output, and changes to context. Within a paragraph, I added two inline constructs: - `@mention` names an object, function, or data source. - `#directive` modifies how the instruction should run. They fit into sentences without reorganizing the prose around code syntax: ```text Compare @sleep with @productivity for last month #briefly. ``` The mentions resolve to typed objects and operations. The directive changes execution metadata. The remaining words stay ordinary instructions for the model. I rejected inline conditionals and expression syntax because they changed the document's center of gravity. Once a paragraph contains nested boolean expressions and braces, the prose becomes decoration around a program. Idyllic uses separate structured blocks only when the execution model requires them. ## Decisions let the model choose a branch Prompts still need branching. Many branches depend on judgment instead of a boolean value: whether a document set is too large, whether a message is urgent, or whether the available evidence is sufficient. I represented those branches as decision cases: ```text decision { case: there are too many documents { Summarize each group before comparing them. } case: the documents fit in context { Compare them directly. } } ``` The runtime sends the case descriptions and current context to the model through a constrained tool call. The model selects one case, and the interpreter executes that body. This differs from an `if` statement in an important way. The runtime does not evaluate `there are too many documents` as an expression. The phrase describes a judgment the model must make from the current situation. The tradeoff is explicit. A parser can verify that the block has cases and valid bodies. It cannot prove that a case is true or that two cases cover every possibility. The model supplies that semantic judgment. ## Calls receive context explicitly Early versions allowed tool calls to see whatever data happened to be present in the session. The same call could behave differently depending on which earlier paragraph had loaded an object. I made dependencies explicit. A function only receives the objects passed to it or loaded into its current scope. These forms therefore resolve to the same operation: ```text @Analyze my @productivity and @sleep from last month. @load("sleep") @load("productivity") @Analyze my productivity and sleep from last month. ``` In the first form, inline mentions provide the dependencies. In the second, prior statements load them into the current scope. The `@Analyze` call receives the resolved sleep and productivity objects in both cases. Event blocks create their own scopes: ```text watch @Telegram.NewMessage { @load the latest messages from @ConversationHistory Draft a reply using the new message and recent conversation. @Telegram.SendMessage the reply. Update @ConversationHistory. } ``` Entering the block adds the triggering event to the scope. Each paragraph can read the event and the values produced by earlier paragraphs in that block. Values outside the scope remain unavailable unless the document loads them. This made context management visible in the document. A reader can see where data entered, which operations used it, and when it left scope. ## The AST is the canonical program Plain text was insufficient once the editor needed to distinguish mentions, calls, decisions, scopes, and prose. I made the AST the stored program and treated the document as its editable view. The node set stayed small: ```text section body prompt statement function-call statement decision watch statement variable expression dynamic expression ``` Each paragraph becomes a statement node. Inline mentions become references or function calls. Decision and watch blocks own nested bodies. Plain prose remains a prompt statement. A recursive tree-walking interpreter executes the nodes in document order. That implementation made control flow easy to inspect and produced an execution trace from the same traversal. Each trace entry points back to the node that ran. The editor also works against the tree. Inserting a mention creates a mention node with an object reference; renaming visible text does not silently change the underlying identity. Moving a paragraph moves one statement node. Structured editing prevents a malformed brace or misspelled directive from becoming a runtime surprise. Models can edit the same representation through structured operations. They add or replace nodes instead of regenerating the entire prompt as text, which preserves parts of the document the model did not intend to change. ## Dynamic expressions resolve against the current scope Some values cannot be fixed when the document is written. They depend on data produced earlier in the same run. Idyllic uses `{...}` for a value the model should derive immediately before executing its containing statement. ```text watch @time(everyday 9am) { Load the tokens in @CoinsILike. Check their current prices on @Coinbase. @Telegram.SendMessage {the five best-performing tokens}. Update @ConversationHistory. } ``` When the interpreter reaches the send call, it resolves the dynamic expression against the current scope. At that point the scope contains the token list and price results. The model returns the value required by the call, and the interpreter passes that value to `SendMessage`. The same syntax can choose an amount based on context: ```text Load {enough recent messages to understand the conversation} from @ConversationHistory. ``` The expression describes a value instead of an instruction sequence. Its containing function still defines the operation: load messages. The model only supplies the argument. This kept dynamic judgment local. The runtime controls which function runs and when. The model fills the value that cannot be determined until execution reaches that point. ## Structured and prose statements can coexist I did not require every paragraph to compile into a fully specified function call. That would force writers to resolve every detail before running a draft. The AST stores two kinds of executable statements: - A structured statement names the function, arguments, and dependencies. The interpreter follows that representation directly. - A prompt statement contains prose. An agent interprets the instruction using the current scope. Writers can begin with prose and convert important paragraphs into structured calls later. The editor uses an agent to propose that conversion because mapping an instruction to the right tool and arguments requires semantic judgment. Once the writer accepts the structured form, future runs use the same operation. This makes predictability local. A stable integration call can be fully structured. An exploratory analysis can remain prose. Both live in the same document and share the same scopes and execution trace. The probabilistic step stays visible. If a prose paragraph is interpreted differently on two runs, the trace shows that it was an agent-interpreted node. A structured call does not quietly fall back to model interpretation. ## The language keeps the runtime small The Idyllic runtime only needs a few responsibilities: 1. Walk the AST in document order. 2. Maintain explicit context scopes. 3. Resolve typed mentions and function calls. 4. Ask the model to select decision cases or fill dynamic values. 5. Interpret the paragraphs that remain prose. 6. Record each node's execution in a trace. The model handles judgments expressed in natural language. The interpreter handles ordering, scope, calls, and state. The document shows which side owns each decision. That boundary was the point of the language. I did not need prompts to become deterministic programs. I needed repeated runs to stop making accidental choices about tool identity, context, order, and control flow. Idyllic adds those choices where the writer wants them and leaves the rest as prose. A prompt can begin as an ordinary document, become more structured as its workflow stabilizes, and remain readable throughout. --- ## Semantic objects: giving models handles instead of data URL: https://wcdc.io/writing/semantic-objects Date: 2025-01-25 Description: Claude kept dropping items when I asked it to scan large todo and journal collections. I built typed handles that let the model inspect, filter, and operate on data without loading it into the prompt. I started working on semantic objects after Claude failed a simple task in a todo application. I gave it every task and asked for everything due that day. The answer looked reasonable and omitted several items. The prompt already contained the data. The model understood the request. It still could not reliably perform an exhaustive scan because generation is sampled one token at a time. Asking again could produce a different incomplete subset. I wanted the model to express the selection and let deterministic code execute it. "All todos due today" is a small request regardless of whether the collection contains ten rows or ten thousand. The rows should remain outside the prompt until the model asks for specific contents. I called the resulting reference a semantic object: a typed handle to data that stays where it already lives. ## The model chooses the operation; code processes the collection Pasting a collection into context gives the model two jobs. It has to decide what the user means and apply that decision to every item. Models are good at the first job and unreliable at the second when completeness matters. I split them: 1. The model selects an operation such as "filter by today's date." 2. Application code runs that operation over the full collection. 3. The runtime returns a scalar, a small result, or another handle. This also reduces schema errors. My tool calls had become deeply nested because one request needed to describe the collection, filter, operation, and output shape at once. Models filled those schemas incorrectly once they reached more than a couple of levels. A handle moves the collection and its structure out of the call. The model only supplies the object ID, method, and arguments. A conventional query language solved part of this problem. I began with [GraphQL](https://graphql.org/) because it already expressed deterministic selection and composition. An augmented [SQL](https://en.wikipedia.org/wiki/SQL) could have done the same. I did not need another query syntax yet. Queries still return rows. Returning thousands of filtered rows to the model recreates the context problem one step later. I needed operations to return references as well as values. ## A semantic object describes data the model has not read A semantic object contains enough metadata for the model to decide whether and how to use it: ```text SemanticObject id kind description fields[] name type description methods[] name signature description backing data reference ``` The object does not contain a pasted copy of the collection. Its backing reference points to the application data. The model sees the object's name, type, description, fields, and available methods. Descriptions matter because the model cannot inspect an implementation body. For a method such as `getDateRangeView`, the signature explains the argument shape and the description explains its behavior. Together they are the interface the model programs against. I kept the runtime API small: | Operation | Purpose | |---|---| | `listObjects` | list the handles currently available | | `inspectObject` | return one object's description, fields, and methods | | `lookupTypeInfo` | explain a `kind` shared by many objects | | `invokeMethod` | call a method on an object | | `readObject` | materialize contents when the model actually needs them | Most calls only move metadata. `readObject` is the explicit escape hatch that loads content. `invokeMethod` can return a small value or another semantic object. ## I used `kind` as the type identifier I considered `id`, `ref`, `URI`, and `kind` for the field that identifies an object's type. `id` already names a particular object. `URI` implies a location, while the value identifies a type. `ref` says little about what it references. I chose `kind` because JSON APIs commonly use it for a resource category and it remains distinct from the instance ID. This choice matters more for a model-facing API than for an ordinary library. The model learned each word from many unrelated APIs. Reusing an overloaded term such as `type` adds ambiguity before the model sees any documentation. A familiar, narrower word makes the schema easier to complete correctly. Kinds are namespaced so two applications can define `JournalEntry` without colliding. An object can report a kind such as `so.idyllic.JournalCollection`, and `lookupTypeInfo` returns the shared definition. ## The reference has to survive the prompt boundary The easiest way to fake this design is to resolve an `@` mention into text before sending the prompt. The interface would look correct while the entire collection still entered context. I wrote the prototype around one non-negotiable test: the mention must remain a handle when the request reaches the model. The rest of the acceptance criteria made that observable: - I could add a semantic object through the existing `@`-mention interface. - The prompt contained the object ID and relevant metadata without its full contents. - The model could inspect the object's fields and methods. - The model could invoke one method and chain another call from its result. - Several objects could appear in the same request. - Content only entered context through an explicit read or a method returning content. I tested those capabilities incrementally: an object with metadata only, a filtered view, one method call, a chain of calls, and several objects. Each stage had a visible failure condition. I could inspect the actual prompt and verify that no hidden expansion had occurred. ## Methods can return new handles The working prototype used about 700 journal entries from a corpus of roughly 2,500. Claude began by inspecting the collection: ```text Tool call: inspectObject { "objId": "journal-collection" } Tool result: Journal Collection (so.idyllic.JournalCollection) A collection of journal entries Fields: - title - content - date Methods: - getDayCount() -> number - getEntriesForDay(date: YYYY-MM-DD) -> JournalEntry[] - getTotalEntryCount() -> number - getDateRangeView(startDate, endDate) -> JournalCollection ``` The important method was `getDateRangeView`. It filtered the backing collection and returned another handle: ```text Tool call: invokeMethod { "objId": "journal-collection", "methodName": "getDateRangeView", "args": { "startDate": "2024-04-01", "endDate": "2024-04-30" } } Tool result: New semantic object: 4f6b305b-3f7c-472a-9ac6-bc63b3045c8c Journal entries from 2024-04-01 to 2024-04-30 Tool call: invokeMethod { "objId": "4f6b305b-3f7c-472a-9ac6-bc63b3045c8c", "methodName": "getDayCount", "args": {} } Tool result: 28 ``` Claude narrowed the collection and counted the matching days without reading the entries. The filtered view behaved like the original collection because both implemented the same kind and methods. Returning handles is what makes operations composable. A method that returns contents ends the lazy chain and spends context. A method that returns another semantic object lets the model narrow, join, or transform again before deciding what to read. ## The prototype exposed errors in the metadata The first successful run also found inconsistencies in the design. The implementation called the inspection operation `analyzeObject` while the draft API called it `inspectObject`. The object returned `so.idyllic.JournalCollection` while the proposed naming scheme included an extra `prototype#` segment. A filtered April view inherited a parent description that said April through June. The method calls still worked, but the metadata was part of the model-facing API and had to be correct. I standardized the operation name, simplified the kind format, and regenerated descriptions for derived objects from their actual filters. That last bug was especially useful. A stale description can mislead the model even when the underlying object contains the right data. Semantic objects defer the data, so their metadata carries more responsibility than ordinary labels. ## Semantic objects make retrieval programmable Conventional retrieval chooses passages with a similarity search configured before the model begins reasoning. It returns the selected text to the prompt. A semantic object lets the model choose operations during the task. It can inspect a collection, take a date range, narrow it again, count the result, and only then read specific entries. Deterministic code performs each collection operation, while the model decides which operation answers the user's question. I kept the object machinery minimal after the prototype. A semantic object does not need a class hierarchy, decorators, or an internal event system. It needs an ID, useful metadata, a backing data reference, and a set of operations. Specialized classes only help when an application has behavior that requires them. The result is a simple division of work. The model reasons about names, types, and operations. The runtime performs exhaustive scans and retains large intermediate collections. Data enters context only when the model requests it. That is the problem semantic objects solved for me: Claude could work with a collection much larger than its prompt without pretending it had read every row. --- ## #AGIYOURSELF: from daily automations to a prompt codebase URL: https://wcdc.io/writing/agiyourself Date: 2024-07-18 Description: I tried to build one automation a day for ninety days. The unit was too expensive and brittle, so I rebuilt the challenge around versioned prompt files, reusable macros, rationales, and evals. #AGIYOURSELF began as a challenge to build one AI automation every day for ninety days. I wanted to discover the common infrastructure behind personal AI systems by shipping concrete examples instead of designing a framework in advance. The first season failed because an automation was too large and brittle to produce daily. Each one took hours, depended on a specific tool and trigger, and became wrong when the surrounding process changed. The second season changed the daily unit from an automation to a prompt. That change made the cadence sustainable. A prompt took minutes to write, could be revised without rebuilding its execution path, and stored the reasoning that made an automation useful. I then built a codebase around prompt files, reusable components, rationales, and evaluations. ## The first automation was only a capture pipe I began with the simplest useful path I could build: ```text Telegram message → n8n workflow → Redis → daily Notion page ``` I could send a thought to a Telegram bot and append it to that day's record. The workflow did not classify or summarize the message. I did not know yet which later operations would matter, so I kept the captured data as chronological text. That pipe outlasted most of the automations built around it. It produced a personal corpus before I knew exactly how I would use one. Months later, those journal entries became the real examples I used to evaluate prompts. The implementation moved between storage systems as the project grew. The durable decision was simpler than any database choice: capture first, preserve the original text, and add structure only when a concrete operation needs it. ## Unattended automations failed expensively On the second day, I built a workflow to process email. It had no effective error path and retried failed steps inside a loop: ```text 100 emails about 6,000 model calls $80 in the first two hours $400 before the active run was stopped ``` Turning the workflow off prevented new runs. It did not kill the run already in flight. I had treated an editor toggle as an emergency stop. After that incident, every unattended workflow needed three explicit limits: - a maximum number of iterations or items - a measured budget for model calls and tokens - a kill mechanism that reaches an active run I also tested loops with cheap models and small input sets before connecting them to paid APIs. An error handler that only records a failure is insufficient if the surrounding loop continues to spend money. This incident exposed a broader cost of the daily unit. A prompt can produce a bad answer and stop. An automation combines model behavior with triggers, retries, state, external services, and money. Shipping one safely requires much more than writing the model instruction. ## One automation a day did not compound About a month into the challenge, the cadence slipped. Each new automation solved a separate task and reused little from the previous day. An automation captured too many assumptions at once: - which application held the data - the shape of that application's API - the event that should trigger the workflow - the steps I currently believed were useful - the failure and retry behavior When one assumption changed, the automation often required a rebuild. It continued running perfectly in the meantime, including when the underlying task no longer mattered. The retrospective contained the clearest comparison: ```text 90 automations in 90 days too hard to sustain no repeatable process for creating them fragile system 90 automations < 1 automation with 90 refinements ``` The challenge also lacked a way for its outputs to improve the production process. I could not use one n8n graph to generate or revise the next graph reliably. Visual workflows were fast to start and became difficult to manage as branches, implicit state, and retries accumulated. The problem was the artifact I had chosen to produce. I needed something cheap enough to create speculatively, small enough to revise, and structured enough for the system to manipulate. ## I kept human judgment outside the scheduled workflow Many personal tasks vary at the point where judgment matters. I may want a journal summary today and a comparison with last month tomorrow. Scheduling either operation permanently requires me to decide in advance which interpretation will remain useful. A prompt leaves that decision with me. I choose when to run it and what context to provide. Code handles the repeatable mechanics: loading data, rendering the template, calling the model, and storing the result. Some prompts later become unattended components. I promote one when its trigger and expected output have become predictable through repeated manual use. The prompt text can stay the same; scheduling is a separate layer. This let me discover a workflow before automating it. I could run a prompt manually, revise it several times, and see where it failed without first building a listener, queue, retry policy, and deployment path. ## The second season produced prompts and improvements to the prompt system I restarted #AGIYOURSELF with prompts as the daily unit. Three kinds of work counted: - write a new prompt - improve an existing prompt - improve the process or tooling used to create prompts The third category created the compounding effect missing from the first season. A better macro, evaluator, or prompt loader improved every later artifact. Refining an existing prompt also preserved the value of earlier work instead of adding another disconnected workflow. A prompt took minutes to create, so I did not need proof that it deserved an evening of engineering. Most experiments could fail cheaply. Useful prompts accumulated revisions instead of being replaced by another automation with slightly different assumptions. ## Prompts became files with their own history I moved prompt text out of [Python](https://www.python.org/) functions and into a dedicated repository. This separated the text sent to the model from the code that executes it: ```text promptbase/ prompt composition extract-info.j2 analyze-convo.j2 library.lib.j2 journal/ compare.j2 extract-ideas.j2 agiyourself/ execution cli.py promptfile.py chat.py journal.py ``` The files under `promptbase` contain wording, interpolation, control flow, and reusable prompt components. The Python package loads context, renders templates, calls models, and writes outputs. Prompt text does not live in the execution code. Each prompt uses frontmatter and a [Jinja2](https://jinja.palletsprojects.com/) body: ```jinja --- name: extract-info tags: - utility --- Extract the following information from the input: {% for key in keys %} - {{ key.name }}: {{ key.description }} {% endfor %} Input: {{ INPUT() }} ``` Making the prompt a file gave it a Git history, focused diffs, review, and reverts. I could change the wording without reading the Python around it, and an evaluator could identify the exact prompt version that produced an output. ## Jinja macros made prompt improvements reusable Plain template substitution would still leave repeated prompting patterns scattered across files. I chose Jinja because it supports imports, macros, control flow, and template inheritance. Shared instructions became functions in `library.lib.j2`: ```jinja {% macro role_prompt(role, expertise, task) -%} Assume the role of a {{ role }} with expertise in {{ expertise }}. Your task is to {{ task }}. Approach the task using the knowledge and methods of that role. {%- endmacro %} ``` Prompts call the macro instead of copying its text. Improving the shared instruction changes every prompt that imports it. The same pattern works for structured output, explicit reasoning steps, and common context blocks. This was the reuse I had expected from the automation challenge. The reusable parts were usually prompt structures and execution utilities, not whole workflows. ## I stored the reason for each prompt beside its wording The prompt text records what to send the model. It often does not preserve why I created it, when I expected to use it, or what a good result looked like. I stored that information beside the template: ```text prompts/summarize-journal-entry/ prompt.jinja2 rationale.md ``` The rationale records: - the situation that triggered the prompt - the result I wanted - relevant background and constraints - examples used to evaluate it - changes made after earlier runs This context became more valuable than the original wording. I could usually rewrite a prompt quickly. Reconstructing the situation that made it useful was much harder after several weeks. The rationale also made improvement easier. When an output looked wrong, I could compare it with the intended use instead of editing toward whichever result sounded better in the moment. ## Prompt evaluation needs scores and real personal data Software tests usually return pass or fail. Prompt outputs rarely have one exact correct answer. Two journal summaries can both be valid while one preserves more important details or follows the requested structure better. I evaluated prompts on several dimensions and returned a score for each: ```text test summarize(entry) == expected → pass or fail evaluation score(summarize(entry), dimensions=[coverage, specificity, structure]) → 0.0 to 1.0 per dimension ``` Scores let me compare prompt versions and see whether an edit improved one dimension while damaging another. I used my own journal as the evaluation set. Public benchmarks could measure generic summarization, while these prompts were supposed to work on my entries, habits, omissions, and writing style. Several months of captured data gave me examples drawn from the distribution the system would actually encounter. I preserved the human-authored journal as the source. Model output could be stored as separate annotations, summaries, or metadata. Replacing the original entry with generated prose would contaminate later evaluations with the style and assumptions of earlier model runs. The capture pipe from the first days of the project therefore became part of the prompt-development system. It supplied real inputs, while Git supplied prompt versions and rationales supplied the intended behavior. ## The prompt codebase was the infrastructure I had been looking for The first season tried to discover common infrastructure by accumulating complete automations. The artifacts shared too little and cost too much to refine. The second season moved the reusable intelligence into smaller pieces. Prompt files stored instructions. Macros stored common patterns. Rationales stored the situations and constraints behind them. Evaluations measured changes against real personal data. The execution engine handled rendering and model calls. This structure still supported automation. A prompt that proved useful in repeated manual runs could be connected to a trigger later. By then I had evidence about its inputs, outputs, and failure modes. #AGIYOURSELF began as ninety attempts to automate my life. It became a way to build and evaluate the model-facing components before committing them to unattended workflows. Changing the daily unit made that possible: prompts were cheap enough to explore and structured enough to improve each other. --- ## CWScript: a contract language that compiles to Rust URL: https://wcdc.io/writing/cwscript Date: 2024-01-15 Description: How I decided what the language should understand, what Rust should stay responsible for, and how to translate between the two. CWScript is a language I designed for [CosmWasm](https://cosmwasm.com/) smart contracts. The source reads in terms of contracts, state, messages, permissions, transfers, and events, then compiles into a normal [Rust](https://www.rust-lang.org/) crate. The hard part was not inventing cleaner syntax. It was deciding what the language should understand, what Rust should still be responsible for, and how to translate between the two without turning CWScript into thin shorthand for Rust. This is how I approached those decisions, from the grammar through to the generated crate. ## Start with the right abstraction A CosmWasm contract is a Rust crate compiled to [WebAssembly](https://webassembly.org/). Rust is a strong implementation language for that job, but its primitives are not the primitives of a contract. A token transfer is conceptually simple: validate a recipient, debit one balance, credit another, and emit an event. In Rust, the same operation also involves storage handles, serialization, closures around map updates, error types, and the context objects passed in by CosmWasm. Those details are necessary at the platform boundary. They are not the contract's business logic. That distinction motivated CWScript. I wanted the source language to operate at the level at which developers reason about contracts, while still producing ordinary CosmWasm code underneath. The design method was subtraction. Start with the expressive power of Rust, then keep only the operations that make sense inside a smart contract. A new construct had to do one of two things: name a real CosmWasm concept, or make a useful restriction enforceable. The second part matters. A restricted language is only worthwhile if the restriction buys something back: clearer behavior, safer state access, stronger validation, or code that is easier to audit. CWScript also had to respect CosmWasm rather than hide it behind a new runtime. The generated output would use the same messages, storage libraries, module structure, and entry points as a hand-written contract. Developers could inspect the Rust, test it with the existing toolchain, and use the rest of the ecosystem normally. That made CWScript less like a replacement for Rust and more like a contract-level frontend for it. ## Why build a language instead of a macro? The cheaper route was a Rust procedural macro. Most of CWScript's surface could have been embedded in Rust, and prior work such as [ink!](https://use.ink/) showed how far that approach could go. Macros have a major advantage: interoperability is automatic. The macro can use Rust's parser, type system, libraries, compiler, and editor tooling. There is far less infrastructure to build. But a macro can add syntax without truly taking syntax away. Even if it validates everything inside one annotated module, ordinary Rust remains available around the boundary. That makes it difficult to state that a certain operation is impossible rather than merely discouraged. CWScript was built around subtraction. If queries should never mutate state, or if state may only be changed through approved transitions, those rules need to apply to the whole source program. A separate language gives the compiler control over the complete set of valid operations. I considered several other starting points: | Approach | What it offered | Main tradeoff | | --- | --- | --- | | Rust procedural macros | Rust interoperability and much less compiler work | The Rust abstraction remains reachable and can leak through | | [Lisp or Racket](https://racket-lang.org/) | Source code that already resembles an AST | The syntax is unfamiliar to much of the intended audience | | [LLVM IR](https://llvm.org/docs/LangRef.html) | A mature, proven intermediate representation | It operates far below the level of contracts and state | | [Langium](https://langium.org/) | A generated parser, semantic model, [language server](https://microsoft.github.io/language-server-protocol/), and editor tooling | The compiler has to follow Langium's document and service architecture | | [Z3](https://github.com/Z3Prover/z3) or symbolic execution | A path toward formal verification | Verification still depends on first defining precise language semantics | The question was not which technology was most powerful. It was which layer should own the restrictions. Once I decided that CWScript itself had to own them, a compiler became the natural shape of the project. ## Define the prototype with existing contracts A new language should not begin by proving that it can express a clever new feature. It should first prove that it can express ordinary programs in its domain. My initial target was to translate four existing CosmWasm contracts: [CW20](https://github.com/CosmWasm/cw-plus/tree/main/packages/cw20), [CW721](https://github.com/CosmWasm/cw-nfts), [Terraswap](https://github.com/terraswap/terraswap), and [Mirror](https://github.com/Mirror-Protocol/mirror-contracts). Alongside them, the prototype needed a formal grammar, documentation, syntax highlighting, and the beginning of a language server. That benchmark served two purposes. First, it kept the language tied to real contracts. A syntax decision that looked elegant in a counter example might become awkward in a token contract with allowances, submessages, replies, and multiple storage maps. Second, it exposed the recurring translations that should shape the compiler. If several contracts lower the same source operation into the same Rust pattern, that pattern probably belongs in the intermediate representation or runtime model. The compiler architecture evolved into this pipeline: ``` source -> AST -> validation -> validated AST -> codegen IR -> Rust crate | -> diagnostics ``` Each stage has a distinct job: - The parser decides whether the source is valid CWScript and produces the AST. - Validation resolves names, checks types, and enforces contract-specific restrictions. - The code-generation IR converts general syntax into explicit CosmWasm operations. - The backend renders those operations as a Rust crate. Keeping those responsibilities separate became one of the most important architectural decisions in the project. ## Designing a surface that is familiar but unmistakable CWScript had to satisfy two competing goals. It needed to look familiar enough that a [TypeScript](https://www.typescriptlang.org/) or Rust developer could learn it quickly. But it also needed to look different enough that nobody would assume Rust or TypeScript semantics where CWScript behaved differently. [AssemblyScript](https://www.assemblyscript.org/) illustrates the risk of looking too familiar: the code resembles TypeScript closely enough that developers bring TypeScript assumptions with them. The differences become traps instead of visible design choices. I kept CWScript closer to TypeScript than Rust, but concentrated its distinctiveness in a few small markers: - `$` marks ambient contract context, such as `$state`, `$info`, and `$env`. - `#` marks messages and entry-point names. - `!` marks fallible or effectful forms. - `@` introduces annotations. The syntax is recognizable at a glance without requiring a separate grammar for every contract concept. A developer learns four markers, then reads mostly familiar expressions, blocks, functions, and types. These markers are lexical, not decorative. The lexer produces different tokens for a local name, a `$` context name, and a `#` message name. The parser and later compiler passes therefore know the distinction before name resolution begins. That makes the syntax carry semantic information at very little cost. ## Entry points are typed functions CosmWasm contracts expose instantiate, execute, and query entry points. Within those entry points, message variants are usually dispatched to handler functions. CWScript could have modeled every handler as a completely separate declaration. Instead, I treated handlers as functions with a contract-specific type. The grammar reflects that relationship: ``` fnDefn: (doc)? (exported = EXPORT)? FN (name) (fallible = BANG)? (typeParams)? (params) (ARROW returnTy)? (body) execDefn: (doc)? EXEC (name) (fallible = BANG)? (params) (ARROW returnTy)? (body) queryDefn: (doc)? QUERY (name) (fallible = BANG)? (params) (ARROW returnTy)? (body) ``` `exec` and `query` are not merely labels. They determine the function's available context and legal effects, much as `async` changes the type and behavior of a JavaScript function. An execute handler can receive mutable storage and sender information. A query receives read-only storage and no sender. A fallible handler uses the `!` marker, allowing the compiler to generate the appropriate Rust result type and error propagation. Here is a transfer handler: ``` exec #transfer(recipient: String, amount: U128) { if amount == 0 { fail! InvalidZeroAmount(); } let rcpt_addr = Addr.validate!(recipient); $state.balances[$info.sender] -= amount; $state.balances[rcpt_addr] += amount; emit Transfer($info.sender, rcpt_addr, amount); } ``` The source contains the complete business operation. It validates the amount and recipient, changes two balances, and emits an event. Storage loading, serialization, map-update closures, response construction, and Rust error plumbing belong to the compiler. This is the central trade CWScript makes: the source becomes more specific to contracts, while the generated code becomes more explicit about the platform. ## Treat cross-contract calls as first-class operations Calls between contracts are another place where the conceptual operation is much smaller than its Rust representation. A CosmWasm submessage may need a message payload, target contract, funds, gas limit, reply policy, numeric reply ID, and a handler that decodes the response. In Rust, those concerns are spread across several constructors and a separate reply entry point. CWScript puts the call in one statement: ``` @gas_limit(5000000) @reply.on_success(post_instantiate) instantiate! #TerraswapPair( asset_infos, $state.config.token_code_id, asset_decimals ) { code_id: $state.config.pair_code_id, admin: $env.contract.address, label: "pair" } ``` The statement says what is being instantiated and with which values. The annotations carry operational policy: cap the gas and call `post_instantiate` after a successful reply. This separation keeps the main operation readable without pretending the metadata does not exist. It also gives the compiler structured information from which it can generate message constructors, reply IDs, dispatch code, and handler registration. The same principle applies to `emit`, `exec`, and `fail`: operations that are library calls in Rust become first-class forms when the compiler needs to reason about them. ## Separate omitted arguments from nullable values Rust's `Option` often represents two different facts: - The caller may omit an argument. - The argument is present, but its value may be absent. When both are true, Rust uses `Option>`. The type is correct, but the source no longer makes the distinction easy to read. CWScript puts the two facts in different positions: ``` hello?: str // the argument may be omitted hello: str? // the value may be null bye?: str? // the argument may be omitted and its value may be null ``` The parameter name describes call-site behavior. The type describes the value. This distinction has consequences for the type system. A nullable value cannot be used as its underlying type until the program discharges the null case. The language therefore needs explicit rules for narrowing, defaulting, and propagation. For example: ``` #instantiate(count: U32?, owner: Addr?) { if count? { // count is U32 inside this block } $state.count = count ?? 0; $state.owner = owner ?? $info.sender; } ``` The design question is not only which operator looks best. The compiler has to define how each construct changes the type of a binding, whether the right side of `??` is evaluated lazily, and how optional values cross the Rust boundary. This is a good example of why syntax cannot be designed in isolation. A two-character operator implies rules in the parser, type checker, intermediate representation, and Rust generator. ## State syntax determines the semantic model State is the center of a smart contract language. The surface notation has to balance three goals: - Direct reads and writes should be concise. - The compiler must know which expressions touch persistent storage. - More complex updates must remain atomic and auditable. The simplest syntax treats a map like an ordinary collection: ``` $state.balances[owner] -= amount; ``` That is easy to read, but its Rust translation is not an ordinary index assignment. It may require loading a value from storage, applying checked arithmetic, handling a missing key, and saving the result. For updates with more logic, a closure can make the storage transaction explicit: ``` $state.allowances[[$info.sender, spender_addr]].update( |allow| { // validate and return the new allowance } ); ``` These forms do not have to be competing spellings. They can be two levels of one model: - Compound assignment is syntax sugar for a standard read-modify-write operation. - `update` exposes the operation when validation, deletion, or custom error handling is needed. The compiler can normalize both into the same internal state transition before generating Rust. This normalization point is important. Surface syntax should optimize for the author. The IR should optimize for precise meaning. The Rust backend should optimize for correct and regular output. It also creates a place to enforce stronger state rules. A contract could declare permitted transitions beside a state field, then validation could reject handler code that attempts any other mutation. For example, a counter might allow changes only by one and only when the sender is the owner. The useful guarantee is not that developers usually update state safely. It is that every state write in the program passes through a small set of operations the compiler understands. ## The grammar is the inventory of the language Because CWScript is defined partly by what it removes, its grammar is more than a parser specification. It is the complete inventory of operations a contract may express. The statement rule contains seventeen alternatives: ``` stmt: importStmt | exportStmt | defn | letStmt | constStmt | assignStmt | memberAssignStmt | indexAssignStmt | ifStmt | tryCatchElseStmt | forStmt | execStmt | instantiateStmt | emitStmt | failStmt | returnStmt | exprStmt; ``` Several entries would be ordinary calls in Rust. Giving them dedicated productions lets the parser restrict where they appear and gives the AST an exact node type for each one. For example, `emit` can be restricted to an execute context. `instantiate!` can require a reply policy when its return value is used. `fail!` can mark a control-flow edge as terminating. A general function-call node would force later passes to rediscover all of that from names and conventions. There is a cost. A richer AST means more node types, visitors, formatting rules, diagnostics, and code-generation cases. The grammar should therefore make a construct first-class only when the compiler needs first-class knowledge of it. That rule prevents a domain-specific language from becoming a collection of arbitrary syntax preferences. ## Choosing the parser and AST architecture I explored [Lark](https://github.com/lark-parser/lark), [ANTLR](https://www.antlr.org/), [Chevrotain](https://chevrotain.io/), and Langium while building the front end. The choice was not mainly about parsing speed. Each tool implied a different relationship between the grammar, AST, semantic model, and editor. ANTLR provides a mature parser ecosystem and a clear separation between the grammar and a hand-built AST. That gives the compiler full control over the tree, but every grammar change has to be reflected in AST construction and visitors. Chevrotain puts the grammar in TypeScript and offers tight control over parsing. Translating the grammar into a second framework also acts as a useful test: ambiguities that one parser resolves implicitly often become visible when another requires the choice to be explicit. Langium takes a more integrated approach. The grammar defines a semantic model, cross-references, and enough structure to generate a language server and VS Code extension. The AST is not merely a parse result; it participates in a document lifecycle with linking, validation, and diagnostics. The architectural lesson was to avoid maintaining two versions of the same tree. If the framework's semantic model already represents the program the compiler needs, deriving a second "official" AST creates synchronization work without adding information. A single canonical model lets the parser, language server, validator, and compiler agree on node identity and source locations. It also improves diagnostics because every later pass can report errors against the same document objects the editor already knows. ## Validation is where the language becomes more than syntax A parser can tell that this is a correctly shaped assignment: ``` $state.count += 1; ``` It cannot tell whether `count` exists, what type it has, whether the current handler may modify it, or which Rust storage primitive represents it. Those questions require semantic analysis. The first building block is a symbol table: a map from each name in the source to the declaration it refers to. Contracts introduce namespaces for state fields, messages, errors, events, functions, imports, and inherited members. Function bodies add parameters and locals. `$state.count` should resolve to a state declaration, while a bare `count` may resolve to a local binding. Once names resolve, validation can enforce contract-specific rules: - A query cannot access sender information or mutable storage. - An execute handler may emit events and submessages; a pure function may not. - A state update must match the field's declared type and transition rules. - A message name must refer to a declared or imported message type. - A fallible call must be propagated, handled, or used inside a fallible handler. - An omitted argument and a nullable value must be checked independently. This is also where the strongest version of CWScript becomes possible. If state may only be changed through operations represented in the validated AST, then permissions and invariants can be attached to those operations. For example, imagine a state declaration that says `count` may only change by one and only when `$info.sender == owner`. The validator can check every assignment that resolves to that state field. The rule becomes a property of the language, not a comment authors are expected to follow. The type system does not need to begin with formal proof. It needs to begin with enough information to resolve names, distinguish persistent state from locals, classify effects, and lower each expression unambiguously. Stronger analysis can build on that foundation. ## Work backward from valid Rust Designing the intermediate representation in the abstract produced too many plausible options: a stack machine, a Lisp-like IR, monadic operations, or a runtime with emitted instructions. The more useful method was to start with the Rust. For each small CWScript program, write the canonical Rust output by hand and make sure it compiles. Then compare several translations and extract the operations that recur. Those operations become the code-generation IR. The loop looks like this: 1. Choose the smallest contract that introduces one new behavior. 2. Write its desired Rust output. 3. Compile and test that Rust. 4. Identify the mapping from source constructs to Rust constructs. 5. Add the smallest IR operation that captures the mapping. 6. Generate the Rust and lock it in as a fixture. This approach answers design questions with evidence. If three state assignments all lower to load, transform, and save, the IR probably needs a state-update operation. If execute and query handlers share most of their output, they should probably share one handler representation with different capabilities. It also prevents the IR from becoming a second general-purpose language. CWScript's IR does not need to represent every possible computation. It needs to represent the small set of decisions required to generate correct CosmWasm Rust. ## The IR should move toward the domain, not the machine Traditional compiler IRs lower source code toward machine primitives. LLVM IR is intentionally close to a platform-independent assembly language. CWScript targets Rust, so lowering all the way to machine-like operations would throw away the information the backend needs most. The useful direction is the opposite: convert general syntax into explicit contract operations. Consider: ``` $state.balances[$info.sender] -= amount; ``` The AST sees an index expression and a compound assignment. The code-generation IR can see something more specific: ``` StateMapUpdate { map: balances, key: InfoSender, operation: CheckedSub(amount) } ``` That representation tells the backend that it needs a storage map, a sender-derived key, checked arithmetic, error propagation, and a save. It also gives validators and other tools a meaningful operation to inspect. The same idea applies to cross-contract calls, replies, emitted events, and query responses. The IR should preserve domain meaning until the last responsible moment. ## Choose canonical Rust for generation, not imitation One CWScript construct can often be rendered as several equally valid Rust programs. The backend needs a consistent answer. There are two possible goals: 1. Generate Rust that resembles what a human would write by hand. 2. Generate a regular form that is easy to emit, verify, and test. I chose the second. Generated code is an implementation artifact. The source language should carry the readability. Regular Rust makes the backend smaller and produces stable fixtures that are easy to compare. This affects module layout as well. Instead of reproducing whatever hierarchy a human might choose, the generator can flatten contract types into a predictable namespace and track their fully qualified Rust paths internally. Message types, error types, state handles, and handler functions all have deterministic names. The backend then becomes a set of explicit mappings rather than a formatter trying to imitate human taste. ## Generating the CosmWasm crate The crate generator divides naturally into two parts: 1. Generate the contract interface from declarations. 2. Generate handler bodies from validated operations. Declarations provide enough information to build much of the crate mechanically: - A `state` block becomes [`cw_storage_plus`](https://docs.rs/cw-storage-plus) items and maps. - Error declarations become variants of a [`thiserror`](https://docs.rs/thiserror) enum, along with the host `StdError` conversion. - `exec` handlers become variants of `ExecuteMsg`. - `query` handlers become variants of `QueryMsg` and typed response wrappers. - Handler declarations become implementation functions and dispatch arms. - Contract annotations become entry-point and schema metadata. The source keyword also determines the context type passed to the generated handler: ```rust pub struct ExecuteCtx<'a> { pub deps: DepsMut<'a>, pub env: Env, pub info: MessageInfo, } pub struct QueryCtx<'a> { pub deps: Deps<'a>, pub env: Env, } ``` This is a clean example of using Rust's type system as the final enforcement layer. The CWScript validator can reject a write in a query and produce a source-level diagnostic. If an invalid write somehow reaches the backend, Rust still refuses it because `QueryCtx` contains `Deps`, not `DepsMut`. The source language and target language enforce the same rule at different layers. ## Lowering state updates to Rust The basic state translation is load, modify, save. CWScript: ``` exec #increment() { $state.count += 1; } ``` Canonical Rust: ```rust pub fn exec_increment_impl(ctx: ExecuteCtx) -> Result { let mut count: u32 = COUNT.load(ctx.deps.storage)?; count += 1; COUNT.save(ctx.deps.storage, &count)?; Ok(Response::new()) } ``` The source expresses a state transition. The generated code makes persistence and fallibility explicit. This translation raises several decisions that belong in the validated model or IR: - Is the storage value guaranteed to exist, or should the backend use `may_load`? - Is arithmetic checked, saturating, or wrapping? - Does a missing map entry imply a default value? - Can multiple reads and writes be combined into one `Map::update` closure? - When should a value be cached locally across several operations? - Which errors are converted into `ContractError`? These are not formatting choices. They define the semantics of the source language. The safest way to settle them is to choose one canonical rule for each source operation, encode it in the IR, and test the generated Rust against representative contracts. ## Types must cross the boundary explicitly Type generation looks mechanical until modules and foreign libraries enter the picture. A source type may refer to: - A built-in scalar such as `U128` - A contract-local struct or enum - A type imported from another CWScript module - A Rust type exposed through the standard library or FFI - A message type generated from a handler declaration - A storage wrapper that exists only in generated Rust The compiler needs a resolved type model before code generation begins. Each source type should carry its identity and Rust path, not just its source text. That allows the backend to answer practical questions consistently: - Should `U128` become [`cosmwasm_std::Uint128`](https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Uint128.html) or a generated alias? - Which types need [`#[cw_serde]`](https://docs.rs/cosmwasm-schema/latest/cosmwasm_schema/attr.cw_serde.html)? - Where do generic arguments need explicit Rust paths? - Which imported names must be re-exported? - How are optional and omitted values represented in message structs? This is one reason to keep type resolution out of string templates. By the time the backend renders Rust, the choice should already have been made. ## Modules and the standard library force the design to become real A single generated crate can avoid many difficult language questions. Modules cannot. Imports, exports, inheritance, interfaces, and foreign functions require stable identities across files. The grammar can recognize an import or `extends` clause, but the semantic model has to resolve it to a specific declaration and the backend has to decide where that declaration lives in Rust. CWScript's contract syntax anticipated composition: ``` export contract TerraswapToken extends Cw20Base { // contract-specific state and handlers } ``` Supporting this cleanly requires answers to several questions: - Does inheritance copy handlers, delegate to them, or compose generated modules? - Can a derived contract replace state expected by a base contract? - How are message enums extended without breaking schema compatibility? - What is the fully qualified identity of an imported contract or type? - Which pieces become Rust modules, traits, or ordinary functions? The standard library is an especially useful forcing case. If the library is written in Rust but called from CWScript, every function crossing the boundary needs a source name, CWScript type, Rust path, effect classification, and error behavior. Building a small standard library early is therefore more valuable than designing a general FFI in the abstract. A few real functions force the calling convention, type mapping, module resolution, and documentation format to become concrete. ## The larger opportunity: a representation of CosmWasm The language was one part of a broader idea. CosmWasm contracts already share a high-level structure, but most tools interact with either Rust source or compiled WebAssembly. If that structure is represented explicitly, CWScript does not have to be the only frontend. The same validated model could support: - Alternative contract languages - Generated client SDKs - Documentation and schema tools - Static analysis and security checks - Visual contract explorers - Migration and compatibility tooling - Testing utilities that operate on contract semantics rather than source text The compiler pipeline then becomes more than source-to-source translation. It becomes a stable representation of what a CosmWasm contract is. This is why the AST and IR boundaries matter so much. A Rust-specific AST limits every downstream tool to Rust. A machine-level IR discards the contract concepts those tools need. The useful representation sits between them: resolved, typed, and contract-aware, but independent of how one backend renders it. ## What the design process taught me The hardest decisions in a domain-specific language are not about punctuation. They are about where meaning lives. Does `exec` merely generate a function name, or does it define an effectful function type? Is `$state.balances[key] -= amount` ordinary assignment syntax, or a persistent state transition with checked arithmetic? Is a submessage a library call, or a first-class operation the compiler can validate and inspect? CWScript became clearer whenever I answered those questions in terms of semantics first and rendering second. The practical design principles are straightforward: - Start with real contracts, not isolated syntax examples. - Define the restriction each first-class construct makes enforceable. - Keep one canonical semantic model across the parser, editor, and compiler. - Resolve names, types, and effects before generating strings. - Derive the IR by working backward from valid, tested Rust. - Preserve contract meaning in the IR instead of lowering too early. - Generate regular Rust rather than trying to imitate human style. - Build modules and a small standard library early, because boundaries expose vague design. The central idea behind CWScript is simple: a smart contract language should let the author write the contract while the compiler writes the platform integration. Making that work requires more than concise syntax. The grammar, type system, validation rules, intermediate representation, and Rust backend all have to agree on what each contract operation means. Once they do, the compiler can remove a large amount of incidental complexity without hiding the behavior that matters. --- ## Cognitive Blocks: composing one agent from many URL: https://wcdc.io/writing/cognitive-blocks Date: 2023-11-20 Description: AI work that runs longer than one conversation needs its own primitives. I defined specialized agents, typed the connections between them, and tested the design on a pipeline that drafts a book. I designed a small set of primitives for AI work that takes longer than one conversation. Cognitive Blocks let me define specialized agents, connect them into a process, and treat the whole process as another block. I tested the design by building a pipeline that could plan and draft a short book. ## The work no longer fit in one conversation Most language-model interfaces in 2023 assumed that a task began and ended in one conversation. That was enough for work I could do in a sitting. It broke down when I wanted a model to read a large body of source material, take notes, plan an eighty-page book, draft it in sections, evaluate the result, and revise it. I could wire those calls together in ordinary code. I tried that, and the code quickly filled with details that had little to do with the work itself: prompt construction, execution order, retries, state, and routing between steps. Every new workflow required another custom program. I wanted a smaller vocabulary for describing the work. It needed to answer three questions: - What operations make up the process? - How do those operations interact? - How can I combine a process into a larger process without introducing a second model of execution? I called the resulting framework Cognitive Blocks. ## I needed an abstraction between code and configuration [LangChain](https://www.langchain.com/) exposed enough machinery to build almost any chain, but using it still meant constructing the chain step by step. At the other end, products such as Custom GPTs offered a prompt, tools, and a knowledge base behind a fixed interface. They were easy to configure because they exposed very little composition. I wanted the middle: enough structure to describe a real process, without spelling out every transition in application code. That led me toward a declarative notation. Instead of constructing a tool as an object: ```ts const searchTool = new DynamicTool({ name: "web-search-tool", description: "Tool for getting the latest information from the web", func: async (searchQuery: string, runManager) => { /* ... */ }, }); ``` I wanted to declare the capability: ``` tool web_search_tool "Tool for getting the latest information from the web" { } ``` The runtime could then decide how to load and call it. The declaration would describe the arrangement; the runtime would handle execution. Before I could design that notation, I had to decide what it could name. A declarative language is only useful when its basic concepts stay consistent across different workflows. Syntax came later. I started with the vocabulary. ## I defined blocks as operations My first sketch used familiar job titles: planner, executor, reviewer, and interface agent. Those names were easy to understand, but too broad to compose. An "executor" might make one model call, write a chapter, query an API, or run an entire workflow. The name said almost nothing about its input, output, or place in the process. I replaced jobs with operations. Each block would accept an input, perform one kind of work, and return an output. The vocabulary eventually included: | Block | Operation | | --- | --- | | data transform | converts input data into another form | | evaluation / judge | checks output against acceptance criteria | | synthesis | combines context into a more complex result | | task planner | decomposes a request into executable steps | | supervisor | monitors execution and changes the flow | | data service | reads from or writes to an external system | | knowledge model | stores and retrieves knowledge | | context | supplies relevant data to another operation | | decision | selects among available options | | analysis | examines input from a defined perspective | | annotation | adds structured information to an input | | logging | records what happened during execution | | event listener | responds to a matching event | | event emitter | publishes an event to other blocks | These were designations rather than a closed type system. A block could span two roles, and I could add a role when the existing vocabulary stopped being useful. The important constraint was operational: I needed to know what went in, what came out, and what the block was responsible for. That granularity made composition practical. I could connect a planner to a set of transforms, send their outputs to a synthesis block, and put a judge after the result. The same vocabulary worked across very different workflows. ## I typed the connections too A list of block types described the available pieces, but it still did not describe a process. Two connected blocks might form a pipeline, a feedback loop, a supervisor-worker relationship, or an event subscription. If every edge meant "connected," the runtime could not schedule or validate the graph. I therefore treated relationships as part of the language. The framework needed to distinguish at least: - passing an output to the next block - sending a result back for revision - supervising another block's execution - emitting and listening for events - supplying context without controlling execution This changed the graph from a diagram into an executable description. A runtime could inspect a relation and know whether to pass data, wait for an event, repeat a step, or record a dependency. It also made malformed workflows detectable before they ran. ## I put capabilities behind JSON-RPC I wanted blocks to be installable. A general planner might run in one process, while a search tool or document writer ran somewhere else. The orchestration should not care which language implemented them. I put a [JSON-RPC](https://www.jsonrpc.org/specification) boundary between the runtime and each capability. A long-running process could expose its methods and descriptions over a common protocol. The runtime could discover those methods, call them, and inspect their traffic without importing their implementation. That boundary gave me a few useful properties at once: - blocks could run in different processes and languages - the runtime could discover capabilities when it started - installing a block did not require changing the caller - every interaction could be logged and inspected in the same form The protocol also kept the framework focused. Cognitive Blocks described what a capability did and how it connected to other capabilities. It did not prescribe the code inside each one. ## I made every process a block The central composite was a `ProcessFlow`. It contained blocks, their relationships, and an execution entry point. It accepted an input and returned an output through the same interface as any other block. That decision let me nest workflows without adding special cases. A flow that gathered sources, extracted notes, and summarized them could appear as one research block inside a book-writing flow. I could then place the entire book-writing flow inside a larger publishing process. It also kept roles independent of scale. A judge could be one model call or a `ProcessFlow` containing several evaluators and a decision step. Its internal size did not change how the rest of the graph used it. ## I tested the model on a book-writing pipeline I chose an eighty-page book because it forced the framework past the limits of one prompt. The process had to read source material, preserve useful details, plan the argument at several levels, draft the text, and evaluate it. I first modeled the work as a person would do it: 1. Decide the motivation, audience, tone, and length. 2. Read the source material and take notes. 3. Build a table of contents. 4. Expand it into sections, chapters, paragraphs, and supporting points. 5. Draft the text. 6. Edit the draft against the plan. The sequence mattered because each stage reduced the number of decisions left to the next one. By the time a writing block ran, it should already know what the passage needed to say, why it belonged there, which sources supported it, and how it connected to the surrounding text. ## I preserved why I kept each note The reading stage produced structured notes rather than summaries. I used a schema like this: ``` note source document name text passage or quotation remarks notes what to remember summary what the passage says context details needed to understand it reason why it matters to the project tags generated or supplied by the user embedding vector used for retrieval ``` The `reason` field carried the judgment made during reading. Search could recover the source, passage, and related topics later. It could not reconstruct why I had saved that passage for this particular book. Without that field, the pipeline preserved information and discarded intent. Those notes fed the outline. The outline then absorbed the structural decisions before drafting began. This gave me intermediate artifacts I could inspect and revise early, while changes were still cheap. It also made the drafting blocks simpler because they received narrow assignments instead of open-ended prompts. ## Sentence-level parallelism made the prose worse My first `ParagraphWriter` planned a paragraph and sent each sentence to a separate `SentenceWriter` in parallel. The calls completed quickly. The result read like a pile of sentences. Each writer knew its assigned fact, but it could not see how the previous sentence had framed the point or what the next sentence needed. That missing local context caused repeated introductions, abrupt transitions, and inconsistent emphasis. The architecture had divided the work below the level where the prose stayed coherent. I moved the main generation unit back to the paragraph. A paragraph writer could see the chapter plan, its own purpose, the preceding text, and the point that followed. When I still needed sentence-level control, I passed context according to the sentence's position: | Position | Context | | --- | --- | | introduction | the paragraph's purpose and plan | | body | the surrounding sentences and supporting material | | ending | the paragraph's purpose and what the body established | I also represented sentence roles explicitly—introduction, body, or ending, along with a rhetorical role such as explanation or argument. That gave an evaluator something concrete to check and made revisions more targeted. The prototype changed my rule for decomposition: split work only while each block can still see the context it needs. More calls do not automatically create more useful parallelism. ## I gave synthesis its own plan Long contexts created a second problem. If the source material did not fit in one prompt, I could divide it among several blocks. Their outputs still had to become one report rather than a stack of independent summaries. I modeled synthesis as another process: ``` analyze request -> decompose work -> synthesize result | plan -> write pieces -> assemble ``` The synthesis block first planned the final structure. It then assigned each part with the relevant source material and assembled the results according to that plan. The same pattern could recurse when one part was still too large. Treating synthesis as real work fixed a common failure in map-reduce writing pipelines. Concatenation preserves every fragment's local answer and provides no global argument. A planned synthesis gives each fragment a job in the whole. ## I represented instructions as trees The runtime also needed to build and revise instructions. Plain prompt templates worked when the program only filled in variables: ``` "Write a {{tone}} summary of {{document}} in {{n}} paragraphs." ``` They became brittle when a planner needed to insert a condition, reorder two steps, remove a section, or attach context to one part of the instruction. At that point the program was editing characters and hoping the result still made sense. I started representing instructions as trees, much like an abstract syntax tree in a compiler. A node could represent a goal, constraint, input, step, or output format. The runtime could move or replace a node while preserving the rest of the instruction's structure. Once prompts had structure, a planner could produce them as output and another block could revise one goal or constraint without rebuilding the whole string. ## What I kept from the prototype Cognitive Blocks began with a practical problem: I wanted to automate work that could not fit in one conversation, and direct chains became harder to understand as they grew. The framework gave me a way to describe that work at the level I actually reasoned about it: operations, relationships, and nested processes. The book prototype tested the model where it was most likely to fail. It showed that planning could move decisions upstream, structured notes could preserve intent, and nested flows could handle work larger than one prompt. It also showed that decomposition has a limit. Once I split a paragraph into isolated sentence calls, the system lost the context that made the result coherent. That became the most useful design constraint in the project: a block should be small enough to compose and large enough to do its work with the context it needs. --- ## Semantic blocks: a pidgin between English and code URL: https://wcdc.io/writing/semantic-blocks Date: 2023-11-18 Description: A notation sitting between English and a programming language, and the question of what downstream would ever read one. Semantic blocks are a vocabulary I sketched for writing on a page: English with a small set of added constructs, for the parts of a thought that plain English leaves loose. I described the result as a pidgin between English and a programming language. The hard part was not choosing constructs. A construct earns its place by changing what happens after it is written, and nothing downstream of a page of English reads one, so the sketch had no way to separate a construct doing work from a construct that only looked like it was doing work. The same question came back three more times in the same week, aimed at a ledger schema, at a type system, and at a dependency I was about to take on. The type system is where it gets a usable answer, because a type system already has something downstream that reads what you wrote. ## The page is a wider channel than speech, and English is fitted to speech English got its grammar from speech: a linear channel that rewards fast encoding, immediate decoding, redundancy and tolerance for ambiguity. Writing uses a wider medium in which a reader can take in more context at once, revisit earlier clauses and inspect structure directly. The two channels have almost nothing in common: | | speech | the page | | --- | --- | --- | | delivery | one word at a time, in real time | a whole paragraph at once | | direction | forward only | glance back, skim forward at will | | where structure is held | the listener's working memory | in front of your eyes | | decoding | one pass, at the speed of production | as many passes as you want | | so the grammar is | loose, fuzzy and redundant, to survive being generated live | unconstrained by any of it | A grammar shaped by the left column is being used to write the right one. Speech has to be redundant enough to survive a listener who lost a clause and cannot ask for it again. The page removes that constraint and English spends none of the slack. So the design goal is a notation that uses the width the page offers: constructs for the parts of a thought that a linear channel forced you to leave implicit, and that a reader taking in a paragraph at once could take in too. Naming even one of those constructs requires naming what it is for, and English as a whole is too large a thing to aim at. ## Augmenting a language and replacing it are different design problems The first candidate is to stop using English. A formal notation carries three properties speech could never have given: it is unambiguous, it nests, and a machine can read it without guessing. It also gives up the one property I wanted most, which is carrying a meaning nobody has finished defining yet. Most of what I want to write down is at that stage, so replacing English loses more than it buys. So the notation has to augment English rather than substitute for it. I called the result a pidgin between English and a programming language: a small borrowed vocabulary for ideas natural language leaves loose. Pidgin is the accurate word, and it names three design constraints: - it borrows from both languages - it is simpler than either - it exists for a job rather than for a culture Those rule out the two nearby things I did not want, a formal language wearing English syntax and English with annotations bolted on. Augmentation costs the enforcement. A formal notation gets its strength from a reader that rejects what it cannot parse. A pidgin has no compiler, so a semantic block is only as strong as whatever consumes it, and a page of English is consumed by a person who will forgive anything. None of the constructs got named, because the design goal had nowhere to land. A notation for the page has to say what reads a block before it can hold a single construct. ## A representation earns its structure by changing what happens If the complaint is about English specifically, it should not show up in representations with no English in them. It shows up in two. Accounting software works in codified low-level objects such as ledger entries, transactions and accounts. People reason in broader concepts such as “business expense,” which covers a loose collection of entities, rules and operations. ``` what the software stores ledger entries, transactions, accounts what a person says "business expense" ``` The design goal there is an interface whose nouns are the concepts a person reasons in, and the sketch takes the consequence seriously: a concept-oriented API would expose operations that are themselves loosely defined, the way two people hand work to each other. A loosely defined operation cannot be checked against a signature, so something has to interpret each call, and that interpreter is a component the design does not have. The idea got a name, an API for Human Concepts, and stopped there. Both representations fail the same way. **A representation should be load-bearing.** A structure that does not constrain, produce, or otherwise affect what actually happens is decoration, paid for in attention and returning nothing. ## Erased types are the same defect with a runtime already attached [TypeScript](https://www.typescriptlang.org/) is the case where the structure exists, is enforced, and is deliberately disconnected from runtime behavior. Its types guide the checker and editor but do not influence the code that runs. Compilation deletes them: ```typescript function f(x: Uint8): Uint8 // what you wrote and what the checker read function f(x) // what runs ``` Which makes the transaction lopsided. You pay a strict and verbose type checker and fix everything it complains about, you get editor support, and the compiled [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) carries none of the guarantee. The erasure is deliberate. Dropping the types is what lets TypeScript compile to JavaScript any runtime already accepts, and that compatibility is most of why anyone adopted it. The design goal is to buy the guarantee back without giving up the syntax people already write, which is the trade [Mojo](https://www.modular.com/mojo) makes against [Python](https://www.python.org/): familiar surface, low-level machinery underneath. Koala is the TypeScript superset that does it, stated as three changes: - **Types are enforced at runtime** and introspectable before compilation, so logic can depend on them. - **Decorators** work as they do in Python, rewriting at the AST level rather than annotating. - **Pattern matching** happens over type expressions, with consequences in the logic rather than in the checker. What that buys is the sized types and sum types erasure rules out: ```typescript function addTwoNumbers(a: Uint8, b: Uint8): Uint8 { return a.checkedAdd(b); } ``` `Uint8` has a width, so `checkedAdd` has something to check against. Erase the type and the method has nothing to overflow. ```typescript enum Result { Ok { res: T; }, Err { err: E; } } function divideBy(a: Uint8, b: Uint8): Result { if (b === 0) { return Result.Err { err: "cannot divide by zero" }; } else { return Result.Ok { ok: a / b }; } } ``` `Result` is a choice between two shapes rather than a union the compiler forgets, so a caller cannot read `err` off a value carrying `res`. Three consequences follow from the one decision, and they are the reason the design is a language rather than a library. The runtime has to carry type information the compiler currently discards, so the output stops being ordinary JavaScript. Introspection before compilation makes the compiler an API a program can call. The implementation must then choose between a native runtime, a TypeScript target that recreates the checks, or a lower-level target such as [Rust](https://www.rust-lang.org/). All three are one choice priced three ways: keep the guarantee and give up the free ride on every JavaScript runtime, or keep the ride and go back to a checker whose findings evaporate. The systems-language branch went at it directly, through [LLVM](https://llvm.org/) bindings under the name chickenscript. ## Removing the tool is the test of whether the tool was load-bearing The last case is a dependency rather than a notation, and it is cheap enough to settle before writing code. The candidate was a framework for building editor extensions around a custom [React](https://react.dev/) renderer. A renderer decides what a description of an interface actually turns into, so a custom one would let you describe an editor panel in React and have it come out as something the editor understood. The design goal is to answer the question on paper, because a proof of concept commits you to the answer while you are still deciding it. Written out, it settles itself: ``` React gives JSX, reactivity, state Pays off for markup (HTML, PDF), interfaces (React Native), tree data (an AST) My strongest case web views Already served by React-DOM ``` WebViews were the strongest use case, and React DOM already served them. A custom renderer added no capability the design required. The rule generalises: **if removing the tool costs nothing, the tool was never load-bearing.** The renderer was an interesting angle rather than a requirement, and asking in writing made the reversal cheap. ## Structure exists only when something reads it The hard decision in all four is the same, and it is never about the syntax: what reads the structure. - A semantic block is read by nobody, so it can only ever be a convention between a writer and himself. - A ledger row is read by the accounting software, which is why its structure is real and also why it is the wrong structure. - A TypeScript type is read by the checker and then discarded, so the guarantee stops at the compiler boundary. - A Koala type is read at runtime, which is the entire change and the entire cost. Turning the load-bearing test on the documents themselves sorts them the same way. The medium argument constrains what any notation for the page has to carry, before a line of code exists. The blocks constrain nothing, because nothing downstream of them exists to be constrained. A design document sits on the reading side of the gap between feeling that code is understood and writing enough code to test that understanding. It can feel complete while constraining nothing downstream. The principles that came out of the week: - Aim a notation at a representation small enough to name, never at a whole language. - Ask what reads the structure before deciding what the structure is. - A construct that changes nothing downstream is a convention, and a convention is free to break. - Buying back a guarantee costs whatever property made the original adoptable, so name that property before spending it. - Remove the dependency on paper and see what breaks. The question underneath, what unit carries meaning between a person and a machine, came back twice more with better answers. Semantic objects gave the unit an interpreter, so a model could hold a name for something and call methods on it. A notation for a model to write in gave it a parser, so the structure a model produced could be checked before anything ran. Each of those answers is the 2023 sketch with a reader attached. --- ## cosmwasm-vm-js: running CosmWasm contracts in the browser URL: https://wcdc.io/writing/cosmwasm-vm-js Date: 2022-12-06 Description: I reimplemented the CosmWasm host interface in TypeScript so contracts could run without a chain, expose their storage behavior to tools, and accept frontends written in languages other than Rust. `cosmwasm-vm-js` is a TypeScript implementation of the host interface that runs [CosmWasm](https://cosmwasm.com/) contracts. It can load the same WebAssembly contract deployed to a chain and execute it in a browser or Node.js process. I built it because every contract tool I wanted to make depended on the Rust VM and its surrounding toolchain. A browser-based simulator, debugger, or playground needed a smaller boundary. Once I wrote that boundary down, it turned out to be about fifteen host functions, a handful of required exports, and a memory convention. That also answered a second question I was working on: what would another contract language have to produce? Rust dominated CosmWasm development because its standard library implemented the interface. WebAssembly itself did not require Rust. A second host and two experimental compilers let me test both sides of that claim. ## CosmWasm contracts run against a small host interface [WebAssembly](https://webassembly.org/) can compute and read or write its own linear memory. It cannot access files, the network, a clock, or chain state unless the host gives it a function for doing so. Those functions are imports. In the JavaScript VM, I provide them when instantiating the module: ```ts const imports = { env: { db_read: this.db_read.bind(this), db_write: this.db_write.bind(this), db_remove: this.db_remove.bind(this), addr_validate: this.addr_validate.bind(this), secp256k1_verify: this.secp256k1_verify.bind(this), query_chain: this.query_chain.bind(this), debug: this.debug.bind(this), abort: this.abort.bind(this), // fifteen in total }, }; ``` A contract can only reach operations in that object. If the host does not supply filesystem access, the contract has no filesystem operation to call. This makes the import list the effective boundary between the contract and the chain. The CosmWasm imports cover a narrow set of capabilities: | Imports | Purpose | Availability | |---|---|---| | `db_read`, `db_write`, `db_remove` | contract storage | always | | `db_scan`, `db_next` | storage iteration | `iterator` feature | | `addr_validate`, `addr_canonicalize`, `addr_humanize` | address validation and conversion | always | | `secp256k1_verify`, `secp256k1_recover_pubkey` | secp256k1 signatures | always | | `ed25519_verify`, `ed25519_batch_verify` | Ed25519 signatures | always | | `query_chain` | queries against surrounding chain state | always | | `debug` | diagnostic output | always | | `abort` | failed assertions | `abort` feature | Contracts expose functions in the other direction. The host needs `allocate` and `deallocate` to move data across the memory boundary. It also expects `instantiate` and an interface-version marker. Application entry points such as `execute`, `query`, `migrate`, `reply`, and `sudo` depend on what the contract supports. ## The contract owns its memory JavaScript cannot hand a string directly to a WebAssembly function. The contract owns its linear memory, so the host asks the contract to allocate a region and then writes the encoded bytes into it: ```ts public allocate(size: number): Region { const { allocate, memory } = this.exports; const regionPtr = allocate(size); // read the region descriptor and write into module memory ... } ``` Calls in both directions use this convention. The host allocates a region for the input, writes serialized bytes, calls an exported entry point, and reads the returned region. Imported functions receive pointers, decode regions from contract memory, perform the host operation, and return another pointer when needed. This memory handshake is part of the compilation target. A language can generate valid WebAssembly and still fail as a CosmWasm language if it does not export the allocator, use the expected region layout, serialize messages correctly, or expose the required entry points. Writing those requirements as an interface separated CosmWasm from its Rust implementation. Any compiler that emits the imports, exports, memory layout, and message formats can produce a contract. Any runtime that implements the host side can execute one. ## The browser became a contract runtime The reference VM uses Rust and [Wasmer](https://wasmer.io/). That is appropriate for chain execution, while it made lightweight developer tools harder to distribute. I wanted someone to open a page, load a `.wasm` contract, execute a message, and inspect the result without installing a chain or Rust toolchain. Browsers and [Node.js](https://nodejs.org/) already include WebAssembly engines. I only needed to implement the CosmWasm-specific imports and the memory bridge around them. I placed everything a contract can reach behind one backend: ```ts const backend: IBackend = { backend_api: new BasicBackendApi("terra"), storage: new BasicKVIterStorage(), querier: new BasicQuerier(), }; const vm = new VMInstance(backend); ``` `backend_api` handles addresses and cryptography. `storage` implements the database imports. `querier` answers requests for surrounding chain state. The VM handles WebAssembly memory, import wiring, and entry-point calls. Swapping one backend component changes the contract's environment without changing the contract. I can use an in-memory store in a browser, a persistent store in Node.js, a recorded querier for deterministic replay, or a wrapper that logs every read and write. That substitution point became the basis for the simulator. A contract does not need a special debugging build. The host can observe the same imports the chain would provide. ## I kept the same seams as the Rust VM Matching final outputs was insufficient for the tools I wanted to build. If the JavaScript VM organized the work around completely different boundaries, an instrument built against it would be trapped in the simulator. I mirrored the important seams of the Rust implementation. Each imported function has a fixed wire-facing method and a replaceable operation underneath it: ```ts db_read(keyPtr: number): number { const key = this.region(keyPtr); return this.do_db_read(key).ptr; } db_write(keyPtr: number, valuePtr: number) { const key = this.region(keyPtr); const value = this.region(valuePtr); this.do_db_write(key, value); } ``` `db_read` and `db_write` implement the pointer convention the compiled contract expects. `do_db_read` and `do_db_write` contain the storage behavior. Logging, replay, fault injection, and alternative stores attach at that second layer. This structure sometimes produces less idiomatic TypeScript. I accepted that cost because the JavaScript VM was also a place to develop instruments for the authoritative runtime. Keeping comparable seams made it clear where the same hook belonged in Rust. ## Alternate compilers tested the target I built two experimental contract toolchains against the interface: one in [AssemblyScript](https://www.assemblyscript.org/) and one in [C++](https://isocpp.org/). Their purpose was concrete. A language-independent WebAssembly target should accept another producer that emits the required imports, exports, memory layout, and message encoding. AssemblyScript worked especially well as a browser experiment. It uses TypeScript-like syntax and can compile inside a web page. Paired with `cosmwasm-vm-js`, it made a complete in-browser loop possible: write a contract, compile it, instantiate it, call an entry point, and inspect the resulting storage. The experiment also exposed work that Rust's CosmWasm libraries normally hide. A new frontend needs equivalents for message types, serialization, region allocation, storage abstractions, entry-point generation, and deterministic numeric behavior. Producing WebAssembly is only the first step. The C++ experiment reached the same target and gave me a second independent producer. I did not continue it as a contract language, so I treat it only as evidence that the host interface was language-independent. It says nothing broader about whether C++ is a good language for contract authors. ## Runtime safety cannot depend on one frontend Opening the target to more compilers changes where execution guarantees must live. A chain cannot assume every frontend inserts correct metering or avoids nondeterministic behavior. The runtime has to validate modules, constrain what they can import, account for resource use, and stop execution that exceeds its limits. `cosmwasm-vm-js` accepts a gas limit as part of the VM configuration: ```ts constructor( public backend: IBackend, public readonly gasLimit?: number ) ``` That value is configuration, not enforcement by itself. Stopping a tight WebAssembly loop requires metering or interruption support in the execution engine. The design point is that the host owns this limit and applies it uniformly to every compiled language. Language-level safety still matters. A frontend can prevent classes of authoring errors and restrict nondeterministic features. Those checks improve contracts produced by that language. The host remains responsible for protecting the chain from any module it accepts. This distinction made the alternate-language experiments useful beyond syntax. They forced me to separate properties supplied by a compiler from properties the runtime must enforce for every producer. ## The low-level imports shape every contract language The host interface exposes storage operations, address conversion, signature checks, chain queries, debugging, and aborts. It has no primitive for balances, ownership, permissions, or typed state. Contract libraries build those concepts on top of byte keys and values: ```text host operation contract meaning ----------------------- --------------------------- db_write(key, value) save this account's balance db_read(key) load the current owner db_scan(start, end) list every open position ``` This is where most of the work in an alternate frontend lives. The compiler and its standard library must turn source-level variables, maps, authorization checks, and typed messages into the storage and serialization conventions understood by the host. The thin interface is useful because it keeps the runtime small and language-neutral. It also means each language has to supply a substantial semantic layer before developers can write safe contracts productively. That realization split my work into two projects. `cosmwasm-vm-js` implemented and instrumented the existing host interface. The language experiments explored how higher-level contract concepts compile down to that interface. Neither required changing the chain. ## I document divergences next to the affected import A second VM cannot prove that it behaves exactly like the reference implementation. Tests establish agreement on the cases they cover. Tooling built on the JavaScript VM still needs to verify critical results against the Rust VM. I state that boundary near the top of the README. I also document differences per import: ```text | import | implemented | tested | notes | |---------------|-------------|--------|------------------------------------| | db_read | yes | yes | | | db_write | yes | yes | | | addr_validate | yes | yes | | | debug | yes | yes | appends to a list instead of | | | | | printing to the console | | query_chain | yes | yes | | ``` That placement makes the scope of each claim visible. A tool that only needs storage can check the storage rows. A tool that consumes diagnostic output sees the `debug` difference beside the function it uses. The caveat belongs in the artifact because every simulator, debugger, and language experiment built on this VM inherits it. The JavaScript runtime is useful precisely because it is easier to embed and modify than the chain VM. It remains a development implementation, not the final authority on consensus behavior. ## The host interface became the common target `cosmwasm-vm-js` reduced CosmWasm execution to the parts a tool actually needs: a WebAssembly engine, the expected imports and exports, the region-memory convention, and replaceable implementations for storage, addresses, cryptography, and chain queries. That let me run existing Rust contracts in a browser, observe every storage operation, substitute recorded chain state, and build alternate compilers against the same target. Mirroring the Rust VM's seams kept the instrumentation portable, while the README made the remaining behavioral differences explicit. The project also clarified the work required by a smart-contract language. Emitting WebAssembly is the easy outer boundary. A usable frontend has to reproduce the CosmWasm ABI, provide typed storage and messages, define deterministic behavior, and leave resource enforcement to the host. Once those responsibilities were separated, the runtime, simulator, and language experiments could share one concrete interface. --- ## Generating CosmWasm documentation from Rust contracts URL: https://wcdc.io/writing/terran-one-docs Date: 2022-09-21 Description: Terran One needed a conceptual guide and an exhaustive message reference. I wrote the guide for humans and generated the reference from Rust types, handler steps, warnings, and constraints. Documentation was one of the first products I built at Terran One. CosmWasm developers needed two different things: a guide that explained how contracts work and a reference covering every message, field, constraint, and side effect in a contract. I initially treated both as writing. That made the reference slow to produce and almost guaranteed that it would drift from the Rust source. A renamed field required someone to find and update the same fact in several pages. I split the work by source of truth. I wrote the conceptual guide by hand because explanation requires judgment. I generated the contract reference from the code because message definitions and field types already existed there in a form a parser could read. ## I started with documentation because it could ship immediately The CosmWasm developer experience had several problems. New developers lacked a usable starting guide, and the contract-development loop was slow. Improving the loop required tools such as LocalTerra and the simulator. Documentation could describe the platform that already existed. That independence made it the first deliverable. I did not need a new language, VM, or chain API before I could explain how to instantiate a Rust contract, execute messages, query state, and inspect the response. This constraint kept the guide honest. It described the contracts developers could write at the time, using the Rust types and commands they actually had. ## The guide and reference have different sources of truth The guide explains concepts and workflows: how CosmWasm execution works, how to structure a contract, and how to move from a message definition to a deployed instance. Its accuracy depends on whether the explanation matches the platform and whether a reader can follow it. The reference answers narrower questions: which messages does this contract accept, which fields are required, what types do they use, and what does each handler do? Those facts already live in Rust enums, structs, function signatures, and handler code. Writing the reference by hand would create a second copy of the same interface. Every source change would then require a matching documentation change that the compiler could not enforce. I assigned the two documents accordingly: | Document | Authoritative input | Production method | |---|---|---| | Conceptual guide | platform behavior and developer workflow | written and edited by a person | | Contract reference | Rust messages, fields, handlers, and annotations | generated from source | This did not eliminate human writing. It put human effort where interpretation mattered and let the parser handle exhaustive repetition. ## I wrote the guide in separate passes My first attempts mixed research, organization, drafting, and line editing. I would polish a paragraph before I knew whether it belonged in the final structure, then rewrite it when the surrounding section changed. I separated the work into four passes: ```text brain dump → outline → rough draft → edit collect organize connect improve ``` The brain dump collected everything I might need without ordering it. The outline grouped that material around a reader's task. The rough draft connected the sections. Editing handled accuracy, clarity, and sentence quality after the coverage had stabilized. The separation gave each artifact a clear standard. I did not judge the brain dump for organization or the outline for prose. I also stopped adding whole new topics during line editing. If editing exposed a missing section, I returned to the outline instead of forcing it into the nearest paragraph. This made collaboration easier. Another person could review the structure before either of us spent time polishing it. ## I organized the guide around what the developer is doing A feature list assumes the reader already understands the system well enough to map a feature to their problem. A beginner does not know that "submessages" are relevant when they are trying to call another contract and handle its result. I organized the guide around developer tasks: create a contract, define messages, store state, execute another contract, handle a reply, test locally, and deploy. The table of contents used language a developer could recognize before learning CosmWasm's internal vocabulary. The reference used the opposite organization. Someone opening a reference already knows the message or field they need. I indexed those pages by contract interface: instantiate messages, execute variants, query variants, responses, events, and errors. The same content therefore appeared through two entry points. The guide started from a situation. The reference started from a symbol in the code. ## Gherkin made behavior reviewable outside the Rust implementation Message types describe the shape of an operation. They do not fully describe its behavior. A transfer message can contain a recipient and amount without saying what happens when the amount exceeds the sender's balance. I used [Gherkin](https://cucumber.io/docs/gherkin/reference/) scenarios for those behavioral rules: ```gherkin Scenario: a withdrawal larger than the balance is refused Given an account holding 100 tokens When the owner withdraws 150 tokens Then the transaction fails And the balance is unchanged ``` The format uses ordinary sentences and a small fixed vocabulary. A product designer or protocol contributor who cannot review the Rust implementation can still challenge the expected outcome. Engineers then bind the scenario to executable tests. That adds work compared with writing a Rust test alone, but it produces one behavior specification that both technical and nontechnical reviewers can read. I kept Gherkin for externally meaningful behavior. Low-level implementation tests remained in Rust. This prevented the specification from becoming a verbose restatement of every internal function. ## The generator reads the contract interface from the AST The reference generator parses the same Rust source the compiler sees. Message enums identify entry points. Struct fields provide names and types. Doc comments provide descriptions. Handler functions show which code processes each message. I designed the parser as shared infrastructure: ```text ┌──────────────────┐ Rust source → AST → │ documentation │ │ semantic checks │ │ linter │ │ code search │ └──────────────────┘ ``` The documentation generator did not need a private interpretation of the code. The linter and semantic checker could use the same nodes and relationships. A message variant declared without a corresponding handler was both a documentation gap and a program-structure warning. CosmWasm's execution model made handlers easier to describe. A contract handles one message, updates its own state, returns messages for the chain to process, and exits. Control does not leave in the middle of the function and later resume on the same stack. That allowed me to represent a handler as an ordered list of logical steps: ```rust pub fn execute_transfer( deps: DepsMut, info: MessageInfo, recipient: String, amount: Uint128, ) -> Result { // 1. Validate the recipient address. let recipient = deps.api.addr_validate(&recipient)?; // 2. Debit the sender, rejecting an overdraft. BALANCES.update(deps.storage, &info.sender, /* ... */)?; // 3. Credit the recipient. BALANCES.update(deps.storage, &recipient, /* ... */)?; Ok(Response::new().add_attribute("action", "transfer")) } ``` The numbered comments mark documentation boundaries. The generator associates each step with the following code and emits them in execution order. The linter can require numbering to remain sequential and warn when a large block has no documented step. These comments become part of the contract interface for tooling, so I kept the syntax deliberately narrow. They describe externally relevant operations, not every line of implementation. ## Annotations carry warnings and constraints beside the field Ordinary doc comments explain what a message or field means. I added structured annotations for facts other tools could process: ```rust pub enum ExecuteMsg { /// Increases the allowance for `address` by `amount`. /// @warning Allows another account to spend the owner's funds. IncreaseAllowance { /// Amount added to the existing allowance. /// @constraint Must be greater than zero. amount: Uint128, /// Account receiving the allowance. address: Addr, }, } ``` `@warning` identifies a consequence the caller should see before submitting the message. `@constraint` states a condition on a value. The reference can render both consistently, while tests and linters can inspect the same metadata. I kept general explanation in normal prose. Adding a marker only helped when a downstream tool needed to distinguish that fact from the surrounding description. The annotation remains hand-written and can still become stale. Generation removes the additional copies. A warning written beside the Rust field can appear in the reference, guide excerpts, and generated SDK documentation without being rewritten in each output. ## The code and guide now do different jobs The final system uses one source for contract facts and another for explanation. Rust types, signatures, handler steps, warnings, and constraints produce the exhaustive reference. The conceptual guide organizes those facts around the work a developer is trying to complete. Gherkin scenarios state behavior that needs review from people who do not read Rust. This division made maintenance explicit. Changing a message field updates every generated reference page. Changing the way I explain contract execution still requires an editor because no parser can decide which explanation will make sense to a beginner. That was the documentation problem I needed to solve. I did not want to generate prose that pretended to teach, and I did not want people manually copying hundreds of facts the code already knew. Terran One generated the facts and spent human attention on the explanation. --- ## cw-simulate: running a CosmWasm chain in JavaScript URL: https://wcdc.io/writing/cw-simulate Date: 2022-09-02 Description: I needed to run contracts repeatedly, reset their state, and inspect every storage operation without starting a node. I built a JavaScript chain simulator around the same module boundaries as Cosmos. [cw-simulate](https://github.com/terran-one/cw-simulate) runs [CosmWasm](https://cosmwasm.com/) contracts inside a JavaScript simulation of a Cosmos chain. I can upload a compiled contract, instantiate it, send messages, inspect its storage, and reset the entire chain without starting a node or installing a [Rust](https://www.rust-lang.org/) toolchain. I built it because contract development had a terrible feedback loop. A real chain is deliberately slow and irreversible. Development requires the opposite: run the same broken transaction forty times, return to the same starting state after each attempt, and inspect exactly what changed. I initially described the project as a debugger. That name led me toward breakpoints and paused execution. CosmWasm contracts do not work that way. Each entry point runs to completion, writes its own state, and returns messages for the chain to execute afterward. The useful core was therefore a small chain runtime with snapshots and tracing. Debugging became one application built on top of it. ## I only reproduced what a contract can observe A local simulator does not need consensus, networking, peers, or a mempool. Contracts cannot observe any of those systems. They can observe the environment passed into an entry point, the host functions available to their WebAssembly module, and the response produced after execution. A contract response has four parts: ```ts export interface ContractResponse { messages: SubMsg[]; events: Event[]; attributes: Attribute[]; data: Binary | null; } ``` The simulator needs to process those values the same way a chain does. Everything else can be a much simpler implementation. CosmWasm contracts compile to [WebAssembly](https://webassembly.org/). WebAssembly cannot access storage or chain state directly. The host supplies a short list of imports: - `db_read`, `db_write`, `db_remove`, `db_scan`, and `db_next` for contract storage - address validation and conversion - signature verification - `query_chain` for reading surrounding chain state The contract exposes entry points such as `instantiate`, `execute`, `query`, `migrate`, `reply`, and `sudo`. Each call also receives the current block, the contract address, the sender, and any attached funds. That interface defined the simulator's fidelity boundary. If cw-simulate supplies the same inputs, host functions, and message handling visible to the contract, it can replace the full chain during development. ## Contract execution produces work for the chain A CosmWasm contract cannot synchronously call another contract while retaining its own stack frame. An entry point updates local storage and returns a list of messages. The chain executes those messages after the contract has returned. The end of an `open_position` handler in Mirror shows the pattern: ```rust store_position_idx(deps.storage, position_idx + Uint128::from(1u128))?; Ok(Response::new() .add_attributes(vec![ attr("action", "open_position"), attr("position_idx", position_idx.to_string()), ]) .add_messages(messages)) ``` The contract stores its new position index, records attributes, returns the messages it wants processed, and ends. The chain then routes each message to the relevant module or contract. This execution model changed the tool I needed to build. There is no paused application stack between two contract calls. The state worth inspecting is the state before and after each completed transition, plus the messages that caused the next transition. cw-simulate therefore records state snapshots and processes a message queue. The interface can show the current state, a diff from the previous snapshot, the emitted events, and the next messages to execute. Resetting restores an earlier snapshot and clears later work. ## I separated the runtime from its views My first UI managed contracts, state history, message history, and execution directly. That made every new view a change to the runtime logic. I pulled the state management into a headless JavaScript package and kept rendering in a separate package. The project settled into three layers: ```text cw-vm-js executes one contract entry point cw-simulate runs modules, routes messages, and owns chain state cw-simulate-ui displays contracts, snapshots, diffs, events, and traces ``` `cw-vm-js` implements the CosmWasm host functions around a WebAssembly module. cw-simulate uses that VM inside a larger chain model. The UI only reads the runtime state and sends commands back to it. This split made the simulator usable without the original interface. Tests, scripts, browser tools, and future views could all drive the same runtime. It also kept debugging features from changing the contract execution model. ## The simulator uses the same module boundaries as Cosmos Cosmos chains are assembled from modules. A module owns state, handles messages addressed to it, and answers queries about that state. The bank module owns balances. The wasm module owns uploaded code and contract instances. I used the same structure in cw-simulate. The package contains base, bank, and wasm modules. The application configures a chain ID, address prefix, modules, block state, and storage: ```js import { CWSimulateApp } from "@terran-one/cw-simulate"; const app = new CWSimulateApp({ chainId: "phoenix-1", bech32Prefix: "terra", }); const codeId = app.wasm.create(sender, wasmBytecode); let result = await app.wasm.instantiateContract( sender, funds, codeId, { count: 0 } ); result = await app.wasm.executeContract( sender, funds, contractAddress, { increment: {} } ); result = await app.wasm.query( contractAddress, { get_count: {} } ); ``` Using the same boundaries made discrepancies easier to locate. If a simulated bank transfer differs from a real one, I know to compare the bank module's message handling. If a contract query differs, I can inspect the wasm module or VM. A custom architecture would have mixed those responsibilities and made every comparison harder. The object model follows the same rule. Uploaded code receives a code ID. Instantiating that code creates a contract address and storage namespace. The chain owns block height, time, parameters, and module state. Users submit messages to the chain. ## Transactional storage makes reset exact The simulator stores chain state in a prefixed key-value store. Each module and contract receives its own namespace. Transaction wrappers collect changes during execution and commit them only when the complete operation succeeds. This matters when one contract emits several messages. If a later message fails, the simulator must return to the state before the transaction. Keeping a UI copy of "previous values" would miss writes performed by nested messages or modules. The storage layer already sees every write, so rollback belongs there. Snapshots use the same mechanism. cw-simulate can retain a committed state, execute more messages, then restore the earlier version. The reset button returns the entire simulated chain to a known point, including module state and contract storage. That gave me the development loop I wanted: create a starting state once, try a transaction, inspect its effects, reset, change the contract or message, and run it again. ## I trace storage through the VM instead of parsing Rust To debug an arbitrary contract, I needed to know which keys it read and wrote. I first considered extracting that information from Rust source. Static analysis can find obvious storage calls, but Rust gives developers many ways to wrap, re-export, or generate the same operation. A source analyzer would always depend on the coding style of the contract. Every persistent operation eventually crosses one of five WebAssembly imports. The contract does not implement `db_read` or `db_write`; the host does. Instrumenting those functions captures every storage access from every contract language. The JavaScript VM exposes a replaceable method beneath each wire-level import. cw-simulate overrides those methods and records their arguments and results: ```ts export class CWSimulateVMInstance extends VMInstance { constructor( public logs: DebugLog[], backend: IBackend ) { super(backend); } do_db_read(key: Region): Region { const result = super.do_db_read(key); this.logs.push({ type: "call", fn: "db_read", args: { key: key.str }, result: result.str, }); return result; } // equivalent overrides for write, remove, scan, and next } ``` The contract runs normally and cannot detect the logger. Coverage no longer depends on whether the author used a standard helper, a custom abstraction, generated code, Rust, or another frontend. Every persistent access still crosses the host boundary. ## The backend makes the execution environment replaceable The VM receives three dependencies: ```ts const backend: IBackend = { backend_api: new BasicBackendApi("terra"), storage: new BasicKVIterStorage(), querier: new BasicQuerier(), }; ``` `backend_api` implements address and cryptographic operations. `storage` owns contract state. `querier` answers questions about the surrounding chain. I can wrap or replace each dependency. A storage wrapper can record accesses, reject writes matching a predicate, or expose changes to a fuzzer. A custom querier can replay recorded chain state or return controlled responses for a test. A backend API can simulate another address prefix or report every validation call. This was the basis of OverseerVM, the auditing layer I built around the simulator. The VM records boundary calls, while programmable backend wrappers let an auditor define conditions that fire when a contract touches specific state. A fuzzer can then generate messages and check those conditions across many clean runs. The important implementation choice was to instrument dependencies supplied by the host. I did not require the target contract to contain logging code or use a particular library. That made the same tools work against contracts written by someone else. ## Traces contain prints and host calls The trace format only needs two entry types: ```ts export type DebugLog = PrintDebugLog | CallDebugLog; export interface PrintDebugLog { type: "print"; message: string; } export type CallDebugLog = { type: "call"; fn: K; } & CosmWasmAPI[K]; ``` A `print` entry records a diagnostic message emitted by the contract. A `call` entry records an import crossing, including its function name, arguments, and result. The first contains what the author chose to report. The second records what the contract actually asked the host to do. cw-simulate stores traces beside the state transition that produced them. The UI can display a transaction, its events, the storage diff, and the ordered host calls together. Scripts and tests can consume the same trace as structured data. Keeping the trace in the runtime package also prevents the UI from becoming its owner. A headless test receives the same observations as someone using the visual simulator. ## The simulator has a defined accuracy limit cw-simulate and `cw-vm-js` reimplement behavior from the chain and reference VM. Passing tests establishes agreement for those cases. It does not prove identical behavior for every contract or every future chain version. The README states that limitation directly and tells users to verify critical results against the original VM. I also keep differences close to the affected functions. For example, the JavaScript VM collects `debug` strings in a list while the reference implementation writes them through its own diagnostic path. That boundary matters because the simulator is intentionally easier to modify than the chain. Its replaceable storage, querier, and import hooks are the source of its value for testing. They also mean consensus behavior remains the responsibility of the real implementation. ## cw-simulate runs completed transitions and records everything between them The finished design is a resettable JavaScript chain around a WebAssembly contract runtime. It executes one contract call to completion, routes the returned messages through Cosmos-style modules, commits or rolls back the resulting state, and records each observable boundary crossing. That structure came directly from the problem I was solving. I needed rapid contract experiments, so the runtime had to reset exactly. I needed several contracts to interact, so it had to schedule messages and own shared chain state. I needed to inspect arbitrary contracts, so I traced the storage imports every contract must call. The simulator does not try to pause a contract between source lines. It shows the unit CosmWasm actually executes: a complete state transition, the messages it emitted, the state it changed, and the host operations it used. --- ## Building Terra's local smart-contract development stack URL: https://wcdc.io/writing/localterra Date: 2021-09-09 Description: Terra developers had no quick way to start, run, inspect, and reset a contract project. I built a project scaffold, a complete local network, and node-level tracing around that loop. When I started working on Terra's developer tools, building a contract required too much setup and too much patience. New projects began as copies of sample repositories. Testing meant connecting to a shared network or assembling a private one by hand. Debug output depended on code added to the contract. After a failed transaction, getting back to a clean state was another job. I wanted one local loop: create a standard project, start a complete Terra network, deploy the contract, inspect what it did, wipe the state, and run it again. I built that loop in three parts. [LocalTerra](https://github.com/terra-money/LocalTerra) ran a private Terra network with its wallet, data service, and block explorer already connected. A modified node exposed contract diagnostics at the runtime boundary. The Houston project scaffold standardized contract structure, tests, deployment scripts, and generated documentation. ## LocalTerra made the chain disposable A production blockchain preserves history across many machines. That is the behavior users need and the opposite of what I needed during development. I wanted to run a broken transaction repeatedly from the same starting state. LocalTerra runs the network in containers and keeps chain state in a disposable volume. Resetting it is destructive and intentionally simple: ```sh docker-compose rm -f -s -v docker volume rm localterra_terra docker-compose build --no-cache ``` Removing the ledger returns every module, account, and contract to its initial state. I could seed a test scenario once, run an experiment, inspect the result, reset, and repeat. A bare node would not have been enough. Terra applications reached the chain through a wallet, SDK endpoints, a data service, and an explorer. LocalTerra started those pieces together and connected them to the same private network. The node published the same endpoints used by normal development tools: ```yaml terrad: ports: - "1317:1317" # REST used by SDKs - "9090:9090" # gRPC - "26657:26657" # Tendermint RPC used by explorers ``` Anything a wallet, SDK, or explorer could observe through those interfaces needed to behave like Terra. Consensus across independent machines, public peer discovery, and a durable ledger could be simplified because local applications did not depend on them. I used [Ganache](https://archive.trufflesuite.com/ganache/) as the product reference. Ganache ran a private Ethereum chain in a box. LocalTerra could not reuse its code because Terra used a different chain and virtual machine, but it could offer the same development experience: one command starts an ecosystem that behaves like the public platform from the application's point of view. ## CosmWasm gave me one place to observe every contract Terra contracts run on [CosmWasm](https://cosmwasm.com/). Contracts compile to [WebAssembly](https://webassembly.org/) and receive a small set of host functions for storage, address conversion, signatures, chain queries, and diagnostics: ```text db_read addr_validate secp256k1_verify db_write addr_canonicalize secp256k1_recover_pubkey db_remove addr_humanize ed25519_verify db_scan ed25519_batch_verify db_next query_chain debug abort ``` Those functions are the only route from a contract to the surrounding chain. That made them the right place to add development instrumentation. Logging inside a contract only works for code I can edit. It also changes the binary I am trying to inspect. Logging inside the node works for every contract executed by that node, including contracts written by someone else. The `debug` import already provided the boundary: ```ts debug(messagePtr: number) { const message = this.region(messagePtr); this.do_debug(message); } ``` I modified the local runtime to collect and expose those messages. Contracts continued to call the standard CosmWasm import, so they required no LocalTerra-specific API. The node decided where the output went. The same approach extended beyond explicit debug statements. Storage reads, writes, removals, and scans also cross host imports. Instrumenting those calls let the development runtime report what a contract actually touched, independent of its source language or internal abstractions. Keeping the instrumentation at the VM boundary gave it complete coverage over locally executed contracts. It also kept the contract binary closer to what would run on the real chain. ## The wallet and explorer were part of the product LocalTerra originally looked like a node-distribution problem. In practice, developers were building applications, and their applications used more than the node. A wallet had to recognize the local chain, submit transactions to it, and display the resulting account state. The explorer had to read the local RPC endpoint and show blocks and transactions. The data service had to index the same network the wallet was using. I treated those connections as part of LocalTerra instead of post-installation instructions. The system came up with funded accounts and compatible service configuration. A developer could open the wallet extension, connect an application, submit a transaction, and inspect it in the explorer. This is what made resettable state useful. Resetting a node that the rest of the stack cannot reach only tests contract execution in isolation. Resetting the complete application environment tests the path a user will actually take. ## I standardized the project before automating it The other half of the loop began before LocalTerra started. Developers needed a predictable way to create and organize a contract project. My earlier tooling was a collection of TypeScript scripts for common tasks. It automated commands without defining how a project should be structured. New contracts still began by copying examples, and teams made independent decisions about source layout, tests, deployment, and documentation. That flexibility created repetitive correctness risks. Adding one query required edits in three places: ```rust // 1. declare the message pub enum QueryMsg { Config {}, AssetConfig { asset_token: String }, } // 2. route the message match msg { QueryMsg::Config {} => to_binary(&query_config(deps)?), QueryMsg::AssetConfig { asset_token } => { to_binary(&query_asset_config(deps, asset_token)?) } } // 3. implement the handler pub fn query_config(deps: Deps) -> StdResult { ... } ``` The compiler could verify each piece individually while missing that I had declared a message and forgotten to route it. A generator could remove that duplication only if it knew where message definitions and handlers lived. Houston introduced a standard workspace: ```text contracts/ contract1/ src/ lib.rs contract.rs tests.rs Cargo.toml integration-tests/ test_XXX.rs docs/ scripts/ deploy.rs migrate.rs Cargo.toml ``` `cargo houston new contract ` created this structure. Each contract was a Rust crate. Unit tests lived beside its code. Integration tests lived at the workspace root because they exercised deployed contracts through the chain. Generated documentation went into `docs/`. Deployment and migration were executable Rust programs in `scripts/`. The scaffold saved some typing, but consistency was the larger benefit. I could open an unfamiliar Houston project and know where its contracts, integration tests, deployment logic, and generated artifacts lived. Tools could make the same assumption. ## A fixed structure made generation reliable Once the project layout and message definitions had stable locations, Houston could generate the repetitive layers around a contract. The message types already described the public contract interface. Tooling could use them to generate reference documentation, client bindings, and an application interface connected to the Terra wallet. Adding a query in the contract became the source change; the other representations could be regenerated from it. I also kept deployment inside the repository as code: ```text scripts/deploy.rs scripts/migrate.rs ``` This made deployment reviewable and repeatable. A README command can drift when a flag or contract address changes. A program imports the same project configuration as the rest of the toolchain and fails visibly when its assumptions stop compiling. Documentation followed the same approach. Command-based guides were easier to test than screenshots of a web interface. I could run a sequence of commands end to end and catch a broken flag, path, or output format. The guide remained useful before a graphical interface existed and stayed verifiable afterward. ## The tools formed one development loop The scaffold and local network solved different abandonment points. Houston handled the first twenty minutes: create the workspace, add a contract, run unit tests, and generate the surrounding files. LocalTerra handled the repeated loop: start the ecosystem, deploy, execute, inspect, reset, and execute again. Node-level tracing added visibility without requiring developers to modify the contract they were studying. The pieces fit because they shared concrete interfaces. Houston produced deployment scripts and contract binaries. LocalTerra exposed the standard Terra endpoints those scripts used. The wallet and explorer connected through the same interfaces as they did on a public network. The modified CosmWasm host observed contracts through imports the contracts already called. I did not need a new execution model or a special debug version of every application. I needed to package the existing boundaries into a development environment and decide which layer owned each feature. The final workflow was direct: generate a known project structure, run it against a complete local Terra network, inspect execution from the host, destroy the ledger, and start again. That turned contract development from a sequence of setup tasks into a loop I could repeat without losing the thread of the bug I was trying to fix. --- ## Terra SDK: building the same library three times URL: https://wcdc.io/writing/terra-sdks Date: 2020-07-15 Description: Python, TypeScript, and Java all had to describe the same chain. I needed a build order for hundreds of message types and one source of truth across the three libraries. I built Terra's blockchain SDK three times: first in [Python](https://www.python.org/), then [TypeScript](https://www.typescriptlang.org/), then [Java](https://openjdk.org/). Each SDK needed types for every message the chain accepted, serializers for its wire formats, key management, transaction signing, and clients for the node APIs. The work covered hundreds of small protocol types with no natural build order. I needed to define what the library promised, choose a first piece, and create units of work that could actually be finished. The third implementation added another problem. Three hand-written SDKs could disagree about the same protocol, so I also needed to decide how their definitions would be shared across languages. ## The SDK should model Terra's protocol A Terra transaction can be sent without an SDK. The node accepts JSON over HTTP, so an application can build an object, sign it, and send it with a normal HTTP client. An SDK that only wraps those HTTP calls saves little. I wanted the core package to give programmers Terra's protocol in their own language. Every accepted message should have a named type with the fields the chain reads. Addresses, coins, fees, and signatures remain distinct protocol types throughout the application. The simplest example is a token transfer: ```ts export class MsgSend extends JSONSerializable< MsgSend.Amino, MsgSend.Data, MsgSend.Proto > { public amount: Coins; constructor( public from_address: AccAddress, public to_address: AccAddress, amount: Coins.Input ) { super(); this.amount = new Coins(amount); } ``` The signature tells the caller that the sender and recipient are account addresses and that the amount is a collection of coins. The base class carries the three serialization formats Terra used. The caller constructs a protocol message directly; the class owns the raw JSON shape. That promise determined the package layout: | Package | Responsibility | | --- | --- | | `core` | protocol types and messages | | `client` | node communication | | `key` | keys and transaction signing | | `util` | shared serialization and helpers | | `extension` | the browser-wallet surface in TypeScript | The `core` package cannot depend on a node client. A program should be able to construct, inspect, serialize, and test messages without choosing a URL or opening a connection. The imports for `MsgSend` show that boundary: ```ts import { Coins } from '../../Coins'; import { JSONSerializable } from '../../../util/json'; import { AccAddress } from '../../bech32'; import { MsgSend as MsgSend_pb } from '@terra-money/terra.proto/...'; ``` The message depends on other protocol values, a serialization helper, an address type, and generated protobuf code. Network state stays in `client`; secrets and signing stay in `key`. ## I started with serialization The Python SDK began with serializer and deserializer classes. Serialization was small enough to implement immediately and sat underneath every message type I would add later. It also forced the wire format to become concrete. The node expects particular field names, number encodings, and nesting for a coin. A mistake in the serializer propagates into every message built on top of it. Starting there meant some early code would be rewritten as the architecture developed. I preferred that rewrite to designing the whole package around an imagined wire model and discovering the mistake after hundreds of message classes depended on it. This gave me a practical rule for starting large libraries. The first unit should expose an assumption shared by the rest of the system and be small enough to finish. Serialization met both conditions. ## I designed the public API by writing applications against it Once serialization worked, I needed to decide how the library should feel to an application developer. The chain's module structure offered an easy template. I could mirror its directories and expose each endpoint and message where the underlying implementation placed it. That would produce an accurate library organized around how the chain stores and validates data. Applications are organized around what a developer is trying to do. To design for that side of the boundary, I wrote small applications against the unfinished TypeScript SDK. Awkward call sites showed where the library exposed chain internals, required repeated conversions, or placed related operations in different packages. The timing mattered. An example written after the API is stable demonstrates the library. An application written while the API can still change designs it. I expected those applications to break the SDK and treated each break as feedback on the interface. This kept protocol fidelity and caller ergonomics separate. The core types still matched Terra exactly. The public methods and package entry points were shaped by how applications used those types. ## Tests gave the work a finish line The Java SDK exposed a planning problem I had already encountered in the first two implementations. A protocol surface with hundreds of types has no visible edge. I could work for several days and still be unable to point to a finished unit. I considered two ways to traverse the surface: - Finish one class completely, then move to the next. - Sketch the basic shape of every core class, then fill in their behavior. The first approach finds deep representation problems early. The second establishes coverage. Neither defines a durable completion signal by itself. Tests supplied that signal. A passing test named one behavior that existed, stayed in the repository, and failed again if a later change broke it. I could count completed behaviors across partially implemented classes. I wrote the tests as statements about protocol behavior: ```ts describe('Coins', () => { it('clobbers coins of similar denom', () => { const coins1 = new Coins([ new Coin('ukrw', 1000), new Coin('uluna', 1000), new Coin('uluna', 1000), ]); expect(coins1.get('uluna').amount.toNumber()).toEqual(2000); }); ``` Two coin values with the same denomination collapse into one amount. Terra enforces that rule, so the SDK needs to enforce it too. The test marks a finished piece of work and records the behavior the type must preserve. Tests became planning units and specifications. I could work in depth or breadth while measuring progress in executable behaviors. ## Package boundaries follow what callers need independently I split the TypeScript SDK into `core`, `client`, `key`, `util`, and `extension` so applications could install and audit the parts they used. Key management and node communication have different dependencies and risk profiles. One handles secrets and signing; the other holds a URL, request code, and retry behavior. The browser extension existed only in the TypeScript implementation, so putting it in the shared core would create empty counterparts in Python and Java. The directory structure made the separation visible: ``` core/ client/lcd/ Coin.ts Msg.ts LCDClient.ts Coins.ts Fee.ts APIRequester.ts Dec.ts Tx.ts Wallet.ts Int.ts SignDoc.ts api/ Denom.ts PublicKey.ts ``` `core` contains values and messages. `client` contains connections and request policy. If `core` compiles and its tests pass with no client installed, the protocol vocabulary is genuinely independent of network access. The same criterion works better than dividing packages according to the source repository. A package boundary should let a caller take less of the system or let one part be audited and changed without pulling in another. ## Numeric types enforce chain precision Terra balances are integers that can exceed JavaScript's exact numeric range. Decimal calculations also use a fixed precision. Returning native JavaScript numbers would silently round values before they reached a transaction. I encoded the precision in the type: ```ts export const DEC_PRECISION = 18; export class Dec extends Decimal implements Numeric { public toString(): string { return this.toFixed(DEC_PRECISION); } ``` Every `Dec` renders with the eighteen decimal places the chain expects. The type applies the formatting rule during serialization and keeps the value out of JavaScript's native numeric representation. This is part of the SDK's protocol promise. Types should prevent invalid representations from reaching the wire. Documentation can explain the rule; the numeric class has to enforce it. Finishing one numeric type exposed decisions that a broad API sketch would miss: parsing node responses, addition and division, conversions into application code, and serialization back to the chain. That justified doing depth-first work on the value types even while tests tracked progress across the larger surface. ## Three languages need one source of truth The Python, TypeScript, and Java SDKs described the same messages independently. A field added to one implementation could be missing from another while every local test suite still passed. Code generation could remove the repeated definitions. I reduced the design to two possible sources: **A neutral protocol descriptor.** A JSON or YAML schema defines messages, data objects, REST endpoints, and protobuf relationships. Each language generator reads the same description. ``` description / | \ TypeScript Rust Kotlin ... \ | / common ``` This gives every language equal status and makes the shared definition explicit. It also creates a schema that somebody must maintain alongside the chain. The descriptor can express only the constructs its schema anticipates, so unusual message types either expand the descriptor language or require exceptions. **A canonical SDK implementation.** The TypeScript SDK becomes the definition and generators derive other languages from its types. This avoids inventing a second protocol language and begins from code that already exists. The generator must understand every TypeScript construct used by the canonical SDK, which turns the ongoing cost into maintaining a source-code translator. The choice is therefore between maintaining a schema and maintaining a translator. A descriptor is cleaner when the protocol already has an authoritative machine-readable definition. A canonical implementation is cheaper when one SDK already contains the richest and most current definitions. Framing the decision this way also identifies the owner. Protocol maintainers can own a neutral descriptor. SDK maintainers can own a canonical implementation and its translators. "Generate the other SDKs" is incomplete until that ownership is explicit. ## The library's structure follows its guarantees The same promise shaped every part of the three SDKs. Applications should construct Terra messages from named protocol types, without a network connection and without manually preserving wire-format rules. That promise placed messages and values in `core`, kept clients and keys in separate packages, pushed precision into numeric types, and made behavioral tests the units of progress. Writing applications against the unfinished API kept the caller's workflow visible while those internal boundaries changed. The third SDK exposed the limit of hand-written protocol definitions. Once several languages describe the same chain, the shared truth must live in a neutral descriptor or a canonical implementation. The maintainable choice depends on whether the project is prepared to own a schema or a translator. An SDK looks like a collection of wrappers from the outside. Building one is the work of deciding which protocol guarantees belong in types, which dependencies callers should be able to avoid, and where the definitions live so every language keeps the same promise. --- ## Microlog: capture first, structure later URL: https://wcdc.io/writing/microlog Date: 2019-08-30 Description: I kept abandoning tracking systems because every entry required categorization. I moved capture to a timestamped text log and let separate tools interpret it into habits, expenses, health, and context later. Microlog is one timestamped log of anything I care about. I append short entries throughout the day, and separate tools read the same record to build habit views, financial summaries, health histories, and context for models. I arrived at this design after repeatedly abandoning more structured trackers. Every category, field, and folder added a decision at the moment I had the least attention to spare. Missing one entry then looked identical to nothing happening. I moved all interpretation out of capture. Writing requires a timestamp and a sentence. Structure is applied later, when I already know what I am trying to retrieve or measure. ## I needed an honest record before I needed a productivity system The first version was a paper to-do list. I used paper because comparing applications had become a way to avoid tracking anything. The list showed me that the record mattered more than the planning features around it. I wanted to see what I had actually done each day. A tracker that made the day look productive or omitted inconvenient gaps would give me confidence without evidence. That made adherence part of the design. A perfect schema produces no useful record if I stop filling it in. A lower-friction system with rougher entries gives me something I can inspect and improve. Paper also revealed details I would not have chosen in advance. Tasks approaching two hours felt too large to begin. I only learned that by running the system and watching where I hesitated. Since then, I have prototyped tracking practices manually before building software around them. ## Continuous capture made entry cost the main constraint Reconstructing a day at night records what I remember, not everything that happened. I needed to write during the day, while working, eating, exercising, spending money, and talking to people. That changed how I measured friction. A form that takes thirty seconds may sound cheap. If it interrupts another activity and asks me to choose among several categories, the attention cost is much larger than thirty seconds. Repeating that interruption throughout the day eventually creates gaps. I therefore made the capture command accept raw text: ```bash organs log "Ate chicken, broccoli, and okra at home" organs log "Gym: bench DB 150x10, dips 3x8" organs log "Shipped tasks integration" ``` The command appends each entry to that day's Markdown file: ```md # 2026-08-30 (Sunday) ## Morning - [10:06] Ate chicken, broccoli, and okra at home - [10:24] Shipped tasks integration ``` There is no required category, project, habit, or metadata form. I can write a fragment and return to what I was doing. ## The log is chronological and append-only I borrowed the entry shape from system logs and bank statements. Each entry has a time and enough local context to make sense later: ```text 2024-03-11 09:14:22 worker.queue job 8812 retried after timeout 2024-03-11 Blue Bottle coffee -6.75 ``` Microlog uses the same basic form for personal events. The normal write path appends a new line instead of editing earlier history. Corrections become additional entries, preserving what I originally recorded and what changed afterward. Chronology gives every tool a stable input. A new tool can scan the same files without requiring a migration or asking older entries to match its schema. The log remains useful even when I stop using the application that originally captured it. The scope is deliberately broad. An entry can describe my work, food, exercise, an expense, a conversation, a world event, or a friend's milestone. The common property is simply that I wanted to remember when it happened. ## Plain files are the source of truth I tried keeping the record in Evernote, Notion, Trello, and several databases. Each tool made one part of the experience easier while placing the history behind its own interface or hierarchy. Those attempts gave me four requirements: - entries live in a standard format on disk - capture has no setup beyond writing the entry - tools can read and write programmatically - the source stays shallow enough to browse without a custom application Markdown files satisfy those requirements. They work with editors, search tools, Git, scripts, and models. I can replace the capture interface or analysis tool without moving the historical record. This led to a toolbox architecture. Microlog owns the append-only record. Other applications consume it through files or a small read/write interface. A habit tracker can disappear without taking the journal with it. A new search tool can index the existing history without importing it into a new authoritative database. ```text capture ───────┐ voice ─────────┼──→ daily Markdown log ──→ habits automations ───┘ ├→ health ├→ finance ├→ search └→ model context ``` The tools may keep derived indexes or caches. I can delete and rebuild those because the log remains the source. ## The old schema made capture do too much work An earlier daily template required me to sort each event before writing it: ```text Mission To-do list Habits Journaling Food Expenses Fasting Meditation Gym Highlights Insights ``` A meal purchased at a restaurant could belong under Food, Expenses, Habits, or Highlights. The form forced me to choose one section before it would accept the event. Recording therefore required remembering the schema and deciding how the entry would be used later. That was the wrong moment for classification. At capture time, I only know what just happened. At read time, I know the question I want to answer. Microlog keeps the meal as one event. A food view extracts what I ate. A finance view extracts the purchase. A habit view marks whether I logged a meal. Several interpretations can coexist because none rewrites the source entry. ## Models apply schemas when I ask for a view Before language models could interpret loose text reliably, structured tracking required a form. The person entering data had to supply the categories that software could not infer. Models let me move that work to read time. A view defines the schema it needs, reads the relevant entries, and asks a model or deterministic parser to map them into that schema. For example, a food view can request: ```ts interface MealEvent { at: string; foods: string[]; source?: "home" | "restaurant"; } ``` The health tool applies that shape to entries likely to describe meals. The finance tool uses a different schema over the same time range. The source log does not need to anticipate either one. I still keep deterministic boundaries where they help. Timestamps come from the capture system. File dates define the initial range. Exact markers such as `Gym:` can route obvious entries cheaply. The model handles ambiguous language and overlapping categories. Derived records retain a link to the source line. I can inspect the original text when an interpretation looks wrong and regenerate the view after changing its prompt or schema. ## Each use gets its own view My first digital implementation asked the event stream itself to serve as a habit tracker, model memory, and automated journal. The raw list was too vague for habits, too noisy for model context, and too unstructured for reports. The mistake was expecting one storage shape to answer every question directly. The log only needs to preserve events. Each consumer needs a narrower view: | Consumer | View derived from the log | |---|---| | Habit tracker | whether a target behavior occurred each day | | Model context | recent events relevant to the current request | | Finance | purchases, amounts, merchants, and categories | | Health | meals, exercise, sleep, weight, and symptoms | | Weekly review | notable changes, gaps, and recurring patterns | These views can improve independently. Changing the food parser does not change finance history. Replacing the model used for context selection does not change the log. Adding a new view requires reading existing entries, not redesigning capture. ## Real entries determine the configuration I could not design every useful schema before using the system. Real entries exposed ambiguous cases that a design session would miss: one sentence describing both a purchase and a meal, a workout abbreviated in personal shorthand, or a work event whose importance only became clear days later. I let those cases shape each view. I start with a small schema, run it against the actual log, inspect errors, and add rules or examples in response. The raw entry remains available throughout that process. This is another reason capture stays loose. Early classification would freeze my first guess into the historical record. Read-time interpretation lets the configuration evolve as I learn what I want from the data. ## Microlog keeps capture simple and moves complexity downstream The finished system has a clear division. Capture records a timestamp and text. Daily Markdown files preserve the history. Narrow tools interpret that history for a particular purpose. Models handle the parts of classification that require judgment. I no longer choose between Food and Expenses while standing in a kitchen. I write what happened once. Later, when I want a meal history or spending report, the relevant tool performs that interpretation against the same entry. That decision solved the adherence problem that had broken my earlier trackers. The log asks almost nothing when my attention is scarce, and every later feature pays its complexity after the event has already been captured. --- # Lists --- ## Uses URL: https://wcdc.io/lists/uses Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: The desk, the machines, and the everyday apps, dated from when they arrived. This is the desk, the machines, and the software I actually use, each dated from when it arrived. An item is marked as in use only when there is evidence from the last few months. The current machine, bought when the previous one could not run a screen recorder and a fleet of agents against the big display at once. It is the couch machine. Unplugging the desk setup was enough friction to keep me at the desk, so a light machine fixed that. Four years as the main machine, then handed down. Ten cores, 125 GB of memory, an RTX 3090, on around the clock. Every scheduled job runs here, because a laptop sleeps and a job that dies silently looks like a quiet day. It is increasingly where I drive the agents from. Three years of handwritten brain dumps, now scanned and searchable. The habit lapsed in 2024. One journal written on it. Impressive, and unused since. Moved next to the Linux box in August 2026. It is the chair, and has been for years. Twenty-five minutes standing was an achievement at first. It is normal now. The microphone. A podcast-cleanup model now makes a phone recording sound nearly as good. I never wrote the model down. Work starts by setting it. With the screen recorder, it is the backbone of getting anything done. Bought in 2020. A camera pointed at it turned thinking into presenting, so the recorded-whiteboard habit was retired in August 2026. A Korea-era rabbit hole: a Leopold with Cherry MX Clears, a Realforce that taught me I do not like Topre for code, and a Drop CTRL built by hand for about $400 in parts. Nothing about keyboards in the record since 2020. Bought on a whim. It taught me I could learn something I had decided I could not. Looking for a teacher as of August 2026. Makes exertion count, including the stairs. The health organ in my system reads from it. WHOOP replaced it for training. Apple Health still holds steps and resting heart rate. A voice recorder worn on walks. It works as a commitment device: with it on, a day is forty to fifty thousand steps and hours of talking. A physical block for the phone. Its weakness is that I can disable it anywhere. The phone goes in and the lid stays shut until the timer says otherwise. Replaced Alfred for a faster path to a model and easier extensions. My own extensions live in it now. One long-lived agent tab per goal, not per persona. The agent-native browser, and the app I have used most since it arrived. The browser is already basically you. It is what Aside drives under the hood. It crashed too often on my machine. Its persistent chat was enough to make me leave VS Code in 2024. The editor use faded; the cloud agents came back into my week in August 2026. This is where everything that needs doing gets recorded, including what agents surface. Five years as the task app, replaced by a small database of my own, which was itself retired. The queue is Linear. Vaults are split by how much damage an agent could do with each one. Three years of its filenames sit in my journals. Every working session is recorded, over 1,500 hours on a private channel. Knowing nothing is lost is most of why I can start. A hundred-plus meetings transcribed. I keep the transcript and skip the summary. Two accounts, read and cleared by a command rather than a client. I paid for it and did not use it. It held a daily build log through 2025, and nothing in 2026. A folder of plain text files with a search index over them replaced it. The bot I built to feed it got too ambitious, and the habit did not survive 2025. Books come from physical bookstores now. A launcher command generated playlists from it once. --- ## Stack URL: https://wcdc.io/lists/stack Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: What I build with, and the reason for each choice. This is what I build with and why, taken from lockfiles, configs, and git history rather than from memory. An item is marked as in use only when it is in an active repo this month. Every active repo, strict, run directly by Bun. One observation from 2026: agents do better in Rust because they actually run the compiler, whereas in TypeScript they skip the type check. Runtime, package manager, test runner, and SQLite driver. It runs TypeScript without a compile step, and it is the hard constraint: nothing that needs a JVM gets in. These were the smart-contract years. That was the year of orchestration frameworks. My rule since then is that Python trains and TypeScript ships. A year around the idea that prompts and agent decisions are plain data. The runtime constraint ended it; the code-is-data framing stayed. I left it once for a mobile backend in 2025 and came back for every site since, including this one. It is only in repos with real build graphs; the rest are plain workspaces. The one framework layer I keep, because provider abstraction is the part a framework can solve. It sits behind one wrapper so every model call runs on a subscription. It is in every repo, and I still wish for grammar-based validation. Chosen over Prisma because I already knew SQL and did not want a layer that hid it. This site. Lint is advisory in CI on purpose; a check that is red by default teaches everyone to ignore red. Hard to make a nice video with at first. The explainer-video studio runs on it, and on the Linux box it renders faster than realtime. Every tool in my personal system is a subcommand. The binary is the interface. Close to the shape I want: an agent is a directory of files and the whole system is version-controlled. Habits, relationships, the search index, every business body. Files are for what a human writes and diffs; a database is for many rows of one shape that a machine writes. Vectors in the same file as the text. About a hundred times faster than the local-first tool I tried first. A local file by default. One environment variable makes it a replica shared across hosts with no server I run. The daily log is markdown and SQLite only indexes it. Event-sourced designs were rejected three times. It holds the memory organ's graph, still registered and unused since May. One venture ran on it in mid-2026 before moving to SQLite. A daily money reading died once because the laptop had no network at 7:30am, and a missing datapoint looks like a quiet day. Since then nothing scheduled runs on a laptop; the Mac is authoritative and the box is a replica for compute. The deploy layer on the box, next to a self-hosted job runner and a local container registry. Agent sandboxes with a coding agent and Bun baked in. It keeps run history with unlimited retention, and the daily money digest lives here. A push to main is picked up by a runner on the box over an outbound connection. No webhooks, no open ports. Thirty small sites up in August 2026 with a hundred planned, each a probe to see whether anyone clicks. This site and one cohort of the business sites. The repo never mirrors their state, because a mirror is a cache and caches drift. A rotating credential can have exactly one writer, so the shared token store lives here. Model calls route through one provider on the chat subscription. Headless agent calls are billed as API and treated as real spend. Git is the convergence mechanism: uncommitted means proposed, a commit means accepted. Agents cannot delete repositories, because the token lacks the scope, on purpose. Several branch-mutating agents in one working directory corrupt each other, because a branch switch is global to the directory. Each PR is one reviewable idea. I review from my phone, so a small diff is the difference between a review and a rubber stamp. A good issue is a home for convergence rather than a task. What it lacks is a long-running view of ends and monitors, something that is not a chat. The loop as of August 2026: something becomes a Linear issue, I approve it, a cloud agent picks it up, I review and merge. Scripts come first; the protocol wraps them for when a second harness needs the same thing. One bot, pull-based, no server. Each business agent is bound to its own channel. Sponsored credits were the whole reason. When they expired every call became real billing, so it went behind one switch and off, with two exceptions nothing else serves. The design started from schemas and runtime machinery before it had touched enough reality. Replaced by a folder under git, local-first. Fast to move with, and it limited how much I could over-schematize. It survives as the analogy for what I want to be for agents. Schemaless felt right when I did not know what I was building. Both gone by 2026; the journals are markdown files. Its core abstraction was the thing I kept working around, which told me it was solving a different problem than mine. I found I could standardize the input to rich text plus attachments and let scripts decide. It lasted two months, and the home server took that role. A log command and a voice recorder on walks replaced it. --- ## AI URL: https://wcdc.io/lists/ai Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: The models, agents, and services I actually run, by role. These are the models, agents, and services I run, organized by the job each one does. An item is marked as in use only when there is evidence from the last few months. The interactive session that runs everything else. Opus for heavy fan-outs, Sonnet for cheap classifiers, Fable when available. Fable was the first model I would treat as a taste authority. The workhorse under every AI script, routed through one provider. Also the voice mode, which turned language practice into arguing philosophy in French on the elliptical. The same problem gets fed to two models and the outputs compared. It handles anything over a hundred thousand lines in the summary pipeline. One always-on session rooted in my personal system that reaches into every other repo. The best tool I have for thinking in code. Writes the code and bridges the chat subscription into an SDK provider. I flip between it and Claude Code based on which has usage left. The agentic browser. Anything that has to happen on the web goes here. It is code-mode rather than tool-mode: the agent writes programs against a REPL with browser bindings, so twenty actions cost one round-trip. It is also a sensor that reads without raising; it checked a jury-duty page daily and never surfaced the date. They are dispatched off Linear issues when the other two run out of usage. Sixteen always-on agents on the Linux box, one per business. Good for asking one body a question; the fleet view is the gap I am building for. A repository defines an agent and the agent lives on its own machine as a persistent process. I run a fleet on it to learn what my own runtime needs to be. Five months as the daily interface. Retired as an interface in August; it survives as the subprocess that runs test fleets. It solved provider abstraction and did not pretend to solve the rest. Replaced typed journals within a month. A model turns six hours of rambling into a transcript with a spine. The walking recorder took over from it in April. A hundred and thirty meetings, a search collection of their own. Coding sessions with an agent are meetings too. I drop in the reading and get a podcast for the gym. Keyword and vector search over eleven years of journals, chats, and transcripts, reranked. Content production becomes retrieval, not generation. Benchmarked against the fast model across twenty queries; kept because losing it noticeably degrades search. Facts with a valid-from and a valid-to, distinct from text search. Launching my own AI commands from a keystroke was the first time a desk felt like a cyborg setup, in 2024. Affirmations and concept rehearsal in my own voice. Last used in April 2026; still wired up. Fan out twenty videos, mark the bad ones, and the next batch is better. I never found the happy path in it; the concepts kept multiplying faster than my understanding of what I needed. I evaluated it twice. It is built for workflows, and I want autonomy. I used it for four days. It wants a separate agent per thread and connects only through approved plugins, and I wanted the opposite of both. The sponsored credits expired. Image generation and the reranker stay because nothing else serves them. It got unwieldy as soon as anything got complex. I evaluated it once and did not adopt it. It is billed as API. Not being able to script the agent against itself, or define my own loops and monitors, is most of why the next thing exists. It owns no state and does no single thing. It reads from every organ, composes them, and decides. I automate many experiments and filter the best, instead of thinking each one out. The agent is a commodity. The product is the shell that comes alive when you put one in. A system that runs, with a configuration space I can search, has done more for me than any single tool. An agent reporting that its own work is done is not verification. It gives maximum flexibility with no guardrails except version control. Sitting at a terminal stopped being a requirement in August 2026. --- ## Bookshelf URL: https://wcdc.io/lists/bookshelf Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: Books that changed how I work, with the year I read them and what each turned into. These are the books that changed something in how I work, dated from when I made the note. Where I came back to a book years later, both dates are here. Systems over goals, and the habit is getting into the cab rather than the gym. It became a habit tracker in 2019 and a habits database in 2026. What it does not cover is the threshold: environment design alone never got me over the starting line. A mental tool is a procedure for a situation, and it only exists once it is written down. That became a playbook in 2019, another in 2020, and a routing table of fifty-nine protocols in 2026. The brain is for having ideas, not storing them. It became a single capture queue and a weekly review. Without metrics you drift toward visible busyness. By 2026 my version was that deep work comes from environment design, not from pushing. The voice target for my technical writing, and a digest of all forty-nine concepts. Problem, discussion, solution, in full sentences. The shape I want a reference to have. Recognizing that a thing I was writing had a math book's structure is what organized it. The reference for deciding that what I was writing was a field manual and not a book. It keeps catching me designing things that are not necessary. The nuance I added in 2026 is that human formalization still tames problems brute search cannot. I was not ready for it the first time. It works after about five years of programming, and it is my best argument for reading a book twice. What stayed by 2025 is the plain message: stay in constant contact with reality and gather real data. That phrase, reality contact, runs most of what I do and is the name of a company. A procedural field manual for extracting information from the world. It became thirty-plus interviews that autumn and the structure my agents use. A startup is a search for a repeatable business model. The first chapters are the useful ones. Driving with constant feedback from the road, not planning a rocket launch. Read in French at seventeen. Eleven years later the same idea held: the fear points at the thing you know you must do. The book that made me feel understood in 2024. I made my own annotated edition over about ten hours. The wheelwright story is the source of a distinction I use constantly: some knowledge only comes from contact. Reading it at eighteen made me want to write something like it. Fourteen years of journaling followed. At seventeen I set myself a two-week trial of satisfying only natural and necessary desires and promised to report. The bounded trial with a report is still the unit I use. A leader is like a parent. It landed when a team's sense of why had gone fuzzy while I was not present enough. It made me want to work with the evolutionary architecture of my own cognition rather than against it. It engaged my mind in a way I had missed, and taught me that without an external force I would not learn on my own. A method for creating and evolving reframes over a long period, which is different from reading a book once. On Lisp informed the bottom-up design of my own language in 2024. Re-read with notes on the commute. Mastery requires emotional mastery rather than intellect. A taxonomy of patterns. It became my example that a taxonomy is not a theory. I outgrew it rather than disliked it. It is not the way I learn. It was not what I was looking for. A philosophy that keeps having to correct its own mischaracterization has too few error-correcting codes. It has been on the list since 2024. I started it as an audiobook. I started it. It is in progress, with a model as tutor. --- ## Reading URL: https://wcdc.io/lists/reading Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: Essays, papers, and posts I keep returning to, with what I did about each. These are the essays, papers, and talks I keep returning to, each with what I did about it: adopted it, argued with it, or built against it. Adopted as a working method, then argued with: formalized mathematics still tames problems brute search cannot. I studied how it found its audience as much as its content. By April 2026 I had decided the twelve-factor comparison was the wrong frame for my own work. What I had been calling agentic system design under a better name. I adopted the term and published a design-space map against the post within the week. It describes the pipeline I run over my own archive. Good confirmation. Adopted directly as the grader loop in my skill tooling: a checklist, one change at a time, keep or revert. The model for presenting a problem I had chased for two years, and for punctuating paragraphs with visuals. It is easier to read than the original, and it is the model for the field-guide voice I want. Reading these alongside the harness-engineering post is where my narrative converged on a Unix for agentic systems. A primitive that deserves first-class support. It excels when you do not care when the work finishes, only that it can be checked. My system's day-and-night cycle is built on it. It turns an agent session into a computer with a small surface area. The idea is bigger than the implementation, which is what made me want to build on it. A kernel-mode form of self-improvement that no lab would ship. It cleared away my search for advanced agent use cases, and sixteen of its agents now run my businesses. I agreed with all of it and realized I had been doing it in private. Writing as preparation, with the scripts and logs as the work. Six essays in one sitting. The one on cities gave me the idea that environment is a force you use rather than fight, which became everything I later called prevention architecture. The justification for building a language from the bottom up while nobody yet knew how to program with AI. I re-read it every few years, which is its own kind of data. I subscribed in 2024. By 2026 what I most wanted to learn from it was how it reaches people. Its list of strategic behaviors is a persistent mode of being an agent should embody, not a skill you summon once. Adopted into an agent design. It has minimal fluff and no fake anecdotes, and it taught me a test: if you cannot name the specific thing, it is hallucinated. Composable prompts from the command line, and the command-line shape of my own tools starts here. A Python-to-Rust smart-contract compiler that was one of my own early ideas, shipped by someone else. It got me to ship something within the month. An insight I had had a year earlier without externalizing it. The question it left me with: what is lost at the filesystem level that the interpreter level keeps? It is my example of a dependency you can demand-page rather than re-derive. Defining models with code, in 2024; I never got past the intent to study it properly. It is about the time-versus-compute tradeoff for humans and machines. It was the learning path that made sense to me in 2024. One thing stuck: training on code improved reasoning, as if language itself is the space where learning happens. --- ## Inspiration URL: https://wcdc.io/lists/inspiration Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: Projects that changed what I built, dated from when they landed. These are the projects that changed what I built, each dated from when it landed for me, with what it changed and, where it happened, when I moved on. Its lesson is that the browser is already basically you, and that impersonation is the missing primitive. The next thing I am building copies its shape. It was the kernel I had been trying to build myself, so I stopped building a runtime and became a userland on top of it. It convinced me in early 2026 that local agents are the way. It runs sixteen of my agents, and the fleet view it lacks became the definition of what I am building instead. A coding agent plus a folder plus built-up scripts and data is what a prototype is now. It changed the architecture of my personal system: the agent is the shell, the tools are system calls. A repository defines an agent and the agent lives on its own machine as a persistent process. That is the shape I had wanted to build. It made me want a language that represents the same thing in a hundred lines, and it showed that git is where convergence happens. A primitive that deserves first-class support. My system's daily cycle is literally it. It gave me the reframe that markdown is the high-level language for AI systems. Launching my own AI commands from a keystroke in 2024 is where I first used the word cyborg about myself. Two years later it was a project name. Feeding arbitrary text through a model into it gave me spaced repetition on anything. That pattern, a tool's killer feature plus AI equals a primitive you own, is what my personal system's organs generalize. The shape is right even where the app was not: collect, enrich, queue, work through it. The lanes on my board are built on it. It is the one tracker that actually gets used, and the goals layer I built is the thing it lacks. One whole version of my product compiled classes to them. The current one is a folder, and the sites still run on Cloudflare. It limited how much I could schematize, which was good. An old runtime was built on it because realtime felt complex, and the elegant-looking solution turned out to be the complicated one. Its Dataview plugin gave me the idea of a queryable document structure. I wanted plain files rather than the app. It is object-based notes done well. It needs a server, and I wanted an object store that holds code and data. Unix-like composable prompts, and the first version of my own command-line tools started as its patterns. It inspired a lighter connected notebook, the second life of an idea I had first called microlog. The problem is not too many abstractions but weak ones. My intelligence-design thesis, explicit context manipulation and no hidden prompts, was defined against it. I want its distribution machine, made for developers: open-source, hackable software that looks crafted. Two versions of my product were Notion-like block editors. In 2026 I decided not to compete with Notion, Slack, Discord, or Linear, all of which are adding agents. They help people build apps, not AI apps, because AI apps need intelligence design and there is no framework for that yet. In 2019 the goal was to design your own workflow through code. In 2026 I described the same idea as computable history. A background process that invokes the cognitive abilities of different agents over a JSON-RPC API. Two years later I used a standard protocol for exactly that. Kernel and user contexts, and processes that talk to each other. The name on this domain comes from it. A smart-contract language I did not finish. In 2026 I decided my old ideas were early rather than bad, and that it was time to reintegrate them. The question I keep asking: what is the modern HyperCard, and why are useful AI apps still stuck in prompt wrappers? --- ## Graveyard URL: https://wcdc.io/lists/graveyard Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: Tools, frameworks, and approaches I tried and dropped: when, why, and what replaced them. These are the tools, frameworks, and approaches I tried and dropped, with when, why, and what replaced each. A dropped date is a date I decided, or an explicit retirement. Five years as the task app, then a database of my own, then that retired too. Linear is the queue. Its abstractions were the thing I kept working around. Replaced by a provider layer and plain TypeScript. It was another API to learn, and it is built for workflows where I want autonomy. It got unwieldy past the simplest flows. The one automation that mattered saved my thoughts to a database and did not use AI at all. It asked too many questions and I never trusted its timestamps. Replaced by a log command and a voice recorder. Slow realtime and clunky access control for what I needed. A paid plan sat unused until 2026. It was fast to move with, until I needed SQL and larger reads. Left three times: no API in 2020, too heavy in 2021, and unable to hold an indexed version of anything in 2026. Replaced by markdown in a folder with a search index. The best markdown editing I had found, and too tied to its app. The wiki I published from it is still up. It needs a server, and I wanted a local object store. Jira was slow on a phone, Todoist filled with items a bot extracted that I never wanted, and Trello sufficed. Anki's activation cost was too high, and the Mochi bot I built collapsed under its own ambition. The shape survived as a design pattern. It crashed too often, and Aside replaced it. Evernote was hard to capture into and closed. Alfred lost to a launcher with a faster path to a model. Sponsored credits expired and every call became real billing. Off behind one switch, with two exceptions that nothing else serves. A whole product version. The idea may have been right; I did not have enough production experience with multi-agent systems to carry it, and once everything ran locally the actors were unnecessary. Its use cases were narrow. Markdown became the high-level language instead. Partially revived as a hunch in August 2026. Three months of work before pivoting. Generalizing a simple tool produced too many abstractions and nothing interesting beyond my own use case. Retired once the goals report was computed from what actually happened, which made written tasks redundant. Never used; essays were written elsewhere the whole time. The command now errors on purpose. One log carrying every role at once was a poor version of each. Replaced by narrow views that do one thing. The lesson kept: start with freeform text and let the model interpret. Adopted as an identity, and dropped once I could see I did not believe it would make money. Replaced by my own voice. The vision did not change; the shape did. Replaced by things I operate rather than sell. It was slipping within two weeks. I replaced it with building a tool only when a real friction demands it. Businesses need glue and I was already building the glue I would otherwise rent. Twenty-five minutes was too far away for the reward to matter. Replaced by smaller, earlier ones. Thirty days of thirty minutes, completed once and restarted twice. It recurs, so it is not a graveyard item. Fifteen hundred hours and counting. --- ## Body URL: https://wcdc.io/lists/body Updated: Thu Aug 27 2026 00:00:00 GMT+0000 (Coordinated Universal Time) Description: What I use to train, track, eat, and sleep, and which protocols have lasted. These are the tools and protocols I use for training, tracking, food, and sleep, each dated so it is clear which ones lasted. Chosen over the watch for the optimization side. One caveat from five months in: heart-rate variability stopped predicting my willpower. WHOOP superseded it for training. It is the simple habit that keeps the rest in place. A lift log, on and off. Since 2026 the rule is to track only that I went. I used it for precise nutrition tracking for a season. Fifteen minutes is sustainable when a timer makes it countable and the phone is in a box. Lapsed since May. Barbell, dumbbells, a high-row machine, a Peloton. The rule since July 2026 is to be at the gym rather than go to it, because presence loads the state and the workout follows. The variable that mattered was whether the environment made training run, and a premium gym did not change that. Chosen for its machines: a selectorized machine is already configured, and setup friction turned out to be most of my resistance to training. Not renewed once the habit was installed. Thirty days of thirty minutes, completed December 1, 2025. Restarting as of August 2026. Below about seventy-five percent effort the mind cannot tell the difference, so the reps are worth getting in on any day. A three-minute warm-up counts as entry. By August 2026 the sessions read bench, dips, and triceps, with two plates for eight. It is the P90X ab routine, at the end of a session. Walking somewhere while talking is the right template; I arrive already online. Six hours of walking while talking into a recorder produces six hours of notes and a cleared head. Outdoors, thought feels generated rather than injected. The spring routine, gym before food. In July it got paired with a live language conversation on the phone. Forty-five minutes goes by fast with the right music. It is effectively a full workout. A twenty-five-mile ride on the day the wearable arrived, to see whether the strain score matched reality. Sprinting punctuates the monotony of the bread-and-butter work. Cooking at home from bulk groceries beats delivery on cost and on behavior. I design the food environment as if my future self were an adversary. If the fridge only has clean food, the adversary has no ammunition. Huel at night and nothing late is the policy that works. Two before going out, one in the bag. When I cannot think straight I make bad decisions. The combination that reliably works is gym, a walk out, and a light dinner. Twelve years as doctrine. The real game turned out to be high-satiety food and a calorie count. Low carb survives as a heuristic. Two hours at the ideal wake time, replaced by going outside early. I ran it on a manual schedule. The purpose is circadian, not decorative. State-transition habits rather than virtue habits: each one creates another chance by changing the state I am in. Both retired in August 2026. It is worth a 5am trip on its own. Version eight. The stack loads the operating state before the day diffuses. Last done in June; restoring it is on the list. Sleep, training, eating, and socializing are things I engineer around rather than decide daily. It prevents the delivery-order cascade. Regular strength work is reassurance that keeps the anxiety-driven overeating loop closed. Splitting attention made it worse. A $180 voice recorder buys forty to fifty thousand steps a day.