Cognitive Blocks
I sketched a background process that exposes an agent's abilities over an API, then tried to write a book with it to see whether the design held.
Motivation
Spring 2023. The only way to get a language model to do something was to have a conversation with it: you wrote a message, it wrote one back, and everything it knew about your problem had to fit inside that exchange. Fine for what a competent person could do in one sitting, useless past that, which is most of the tasks worth automating.
The obvious response is many calls instead of one, and by 2023 there was tooling. LangChain was the best available thing for wiring model calls together: the output of one call becomes the input of the next, with code in between deciding what happens.
Wiring two calls is easy. I wanted to know what the parts are called when you have thirty of them and they are supposed to add up to something. I called it Cognitive Blocks, and what it produced is a vocabulary and a set of design documents, with one prototype built to test the part of the vocabulary I trusted least.
Adjacent work: LangChain
The design started as a reaction to the tool I had decided not to use. Designing a feature that needed an agent process behind it, I did not want to build that process on LangChain, and wrote down a framework idea instead.
The recorded reason is not a criticism of the tool, and it is worth quoting because it sets what the two complaints below are worth:
The reason I feel iffy is because I haven't built too much stuff with LangChain and don't feel proficient enough, but this just needs to be fixed by spending more time on some ideas and playing around with it.
Two specific complaints came later:
- Onboarding. Too many separate concepts to learn at once, too many tools, not enough guidance about which to use. That one became a standing rule for the design, in capitals, not to recreate the experience.
- Shape. The programming style is imperative where I wanted it declarative. You describe the steps in order rather than describing the arrangement and letting something else work out the order.
The second drove everything after it, and it is a preference rather than a failure. The framework had not been under my hands long enough to break for me. A want stated precisely enough to design against is worth more than a grievance and less than evidence, and the way to promote it is to pick a case where the difference between describing steps and describing an arrangement has to show.
First prototype: an eighty-page book
A complaint about shape is cheap until a case makes the difference show. I picked a hard one: write a short book, around eighty pages. Well past what a single conversation could hold in 2023, so it would force every problem I was interested in.
The design starts from how a person writes a book, before any of it is a program:
- answer questions at the start about motivation, what the book should achieve, tone and length, and which ideas need to be clear
- read the source material and take notes
- build a table of contents, then outline inside it several levels down, from sections to chapters to chapter sections to paragraphs
- draft, not necessarily in order
- edit
Only then the design questions: which steps does a program do, which does the user supply, and what is the machine version of reading something long and taking notes? The last one turned into a schema. The knowledge base holds a table of note entries:
note
source document name
text lines / quotes to cite
remarks AI-generated
notes what do you want to remember about the text?
summary what does the text say?
context details to contextualize the quote
reason why is it relevant / noteworthy
tags one or more, AI-generated or user provided
embedding numeric vector, for search over the whole note
The interesting field is reason. Asking the model to record why a passage is noteworthy captures
the thing that is usually lost between reading and using, and it is the one field a search cannot
reconstruct later from the other four.
Parallel dispatch
The writing end worked in the mechanical sense. A ParagraphWriter planned a paragraph and dispatched a SentenceWriter for each sentence, all in parallel.
However the output kind of sucks
The failure is a direct consequence of the architecture. Parallel means each sentence was written without knowledge of its neighbours, and a paragraph is not a set of sentences, it is a sequence where each one is shaped by what came before it. The parallelism cost exactly that information.
The fix does not restore the lost information. It decides, per sentence, which part of it that sentence actually needs:
| Position | What it is given |
|---|---|
| Paragraph intro sentence | the plan for the paragraph |
| Paragraph body sentence | information about the previous and next sentence, a window either side |
| Paragraph ending sentence | the plan, and what the body established |
The sentence's job in the paragraph determines what it needs to know. That escalated in the same notes into wanting a small language for describing writing that the model could use internally, where a sentence is an object rather than a string:
SentenceOutline
sentence type intro, body, ending
rhetorical classification persuasive, explanatory, metaphor
Once sentences have types, a quality process can judge candidate paragraphs and select the best one, which is the editing pass a writer does and which a parallel dispatch had no place to put.
Primitives and connection types
Typing sentences and judging paragraphs is the same move made twice, which pushed the project back to the general question. The design document states it as two:
what is a good elementary framework that lets me describe AI agents as composite structures of smaller subagents?
- what are the primitive, atomic structures (leaf nodes) that are unitary?
- what are the types of connections / relations between different nodes that determine how they interact with each other?
The split is the part that matters:
- a list of agent types is a taxonomy
- a list of agent types plus the kinds of connection allowed between them is a grammar, and a grammar tells you whether an arrangement you have never seen before is going to work
The role vocabulary
The first of those questions has two answers in the record, and the gap between them is a question of granularity.
The first sketch has four parts:
- an interface agent as the entry point
- a task planner to break a request into sub-tasks
- a quality-feedback agent to score another agent's output
- executor agents that each do one thing
Four job descriptions, and the open parameter I named beside them is the granularity of responsibility, which is what decides how much one executor covers. The four leave it undecided.
The Conventional Classification, ConvClass for short, settles it by pitching every role at a single operation rather than a job:
| Role | What it does |
|---|---|
| data transform | takes some input data and outputs a transformed result |
| evaluation / judge | checks the output of another process against acceptance criteria, sometimes inside a feedback loop |
| synthesis | takes relevant context and defines a procedure for synthesizing more complex output |
| task planner | takes a prompt or data and creates an execution plan, decomposing a request into smaller tasks |
| supervisor | monitors execution, collects logs, and makes decisions that may alter the flow |
| data service | interacts with external APIs and data sources |
| knowledge model | represents a knowledge base that can be augmented and queried |
| context | a collection of relevant data for a certain process or operation |
| decision | a reasoning block that selects from a set of possible options |
| analysis | takes input and optional context, produces a detailed breakdown from a perspective |
| annotation | takes input and produces enrichment referencing elements in that input |
| logging | records details to the process log, scoped hierarchically |
| event listener | activates when an event matching optional criteria is emitted |
| event emitter | broadcasts an event with optional scoped channeling |
I framed the list as approximate descriptions meant to guide how you lay a process out rather than a schema you have to satisfy, with the reader expected to use their own discernment about which roles apply. That framing is the part I am still happy with, because a vocabulary that admits it is a vocabulary is more useful than a type system that is wrong.
Above the roles sits a ProcessFlow: which blocks are present, how they are connected, and the order of execution, the way a program has a main function. The property I care about is that a ProcessFlow is itself just another block. It takes an input and returns an output like any of them, so a whole orchestration drops into a larger one as a single piece.
Splitting and combining
Nesting is what lets the vocabulary work at more than one size, and it sharpens the question the vocabulary does not answer: how the pieces come back together. The standard shape is split, run, combine. Splitting gets all the attention.
we need to consider the synthesis step as its own problem that needs to be planned and combined together
The example I used was a ten-page report generated from summary statistics that do not fit in a single prompt. You cannot hand the pile to a model and ask for the report. You need a process that decides how the report should be structured, then uses simpler writers to produce each piece with the context it needs, then assembles them.
So the reduce step is itself a map and a reduce. Obvious once written, and it does not occur to you while you are drawing boxes.
Prompts as syntax trees
A program that plans a synthesis step and then assembles it has to build the instruction it sends, which puts weight on what an instruction is made of.
An instruction, in the tooling of the time, is a string with variables interpolated into it. Cheapest available representation, and it holds up right until something needs to change the instruction rather than fill it in. Inserting a condition in the middle, dropping a section, reordering two steps: all edits to characters, and nothing in the representation can tell you whether what comes out still reads as a sentence.
Compilers had dealt with this decades earlier. A compiler keeps a tree rather than text, with the structure explicit: this is a function, these are its arguments, this expression is inside that loop. The tree is what makes mechanical transformation possible, because you move a branch rather than editing characters.
My note proposes the same representation for instructions, as composable structures a program builds and manipulates rather than concatenates.
What the language is for stayed open, and the two options pull against each other:
- make English more rigid, pushing it toward the precision of a formal notation, which buys checkability and costs you the ability to say something you have not yet made precise
- make English more manipulable by the programs that handle it, which is a question about the tooling rather than the language, and which leaves every sentence as vague as you wrote it
I did not resolve it, and the reason it stayed open is that both are correct for different parts of the same document.
Context window growth
Some of what I left open got answered from outside, when GPT-4 Turbo arrived with a context window of 128,000 tokens.
this is a lot of "RAM" and means that I won't need to make that context management engine for a lot of the use cases that I'm envisioning.
A context management engine was planned machinery for deciding what to keep in the window and what to drop. A capability arriving from outside deleted the need for it across most of the cases I cared about, and the right response was to delete the subsystem and keep the rest of the project.
The general form: a subsystem whose reason lives outside your system is a subsystem you should expect to lose, and the cost of losing it late is every design decision that assumed it.
The parts I would defend
A vocabulary and a set of questions, at a point in the year when almost nobody had good words for any of it, plus a prototype that showed exactly where the vocabulary was thin. Two parts I would defend now:
- the two-part question, because asking about connection types is what turns a list into a grammar
- the observation about synthesis, because everyone still spends their attention on the splitting