Why Tardigrade?
Building an agent can be challenging, especially as they operate over longer horizons. Tasks get harder, behaviors become harder to reason about, and the harnesses we build around our agents get ever more complex.
Tardigrade presents a way to simplify this complexity by proposing a new way of thinking about agent harnesses.
In a typical agent harness, your core model loop would be something like this:
type AgentPolicy = {
readonly contextWindow: number
readonly compactAt: number
readonly maxTurns: number
}
const runAgent = async (messages, policy: AgentPolicy) => {
for (let turn = 0; turn < policy.maxTurns; turn++) {
if (tokenCount(messages) >= policy.contextWindow * policy.compactAt) {
messages = await compact(messages)
}
const response = await model.generate({
messages,
system,
tools
})
messages.push(response)
if (response.toolCalls.length === 0) return response.text
// Check permissions, run tools, and append their results.
}
throw new Error("Turn limit reached")
}
This seems manageable at first. As you add more stateful conditions (permissions, budgets, budgets and permissions for subagents), the complexity starts to add up.
It is not obvious which parts of your code led to a specific agent behavior or how those parts interact with each other.
A composable way to author behavior
Tardigrade grew out of the realization that an agent harness is very similar to a user interface.
User interfaces help humans interact with the digital world. Harnesses help agents interact with the real world. Framed this way, authoring a harness becomes a rendering problem, and the frontend community has already solved it!
React defines a user interface as a component tree, with each component as a function of state. Similarly, Tardigrade defines an agent harness as a composition of components that define behavior. Each component is a state machine whose state is a projection of an immutable event log.
The Tardigrade pattern
If you asked me to compress Tardigrade's single most important insight, it is this:
This idea has roots in automata theory, the study of abstract machines that receive inputs and transition between states.
A Tardigrade component is a state machine whose output is determined solely by its current state. This structure is also known as a Moore machine.
If you can describe the behavior you want to see based on what has happened, you can define it as a function of the event log. An agent harness could then be viewed as a composition of such behaviors.
Example: Compaction
For example, take compaction. A model's context window is bounded, but conversations can be unbounded. At some point, you are going to hit a limit. Compaction is a workaround to have a continuous conversation thread despite this constraint.
You could describe a typical compaction strategy as follows:
When the conversation sent to a model reaches a certain token threshold, use a model to summarize part of the conversation.
Then, append the summary to the event log in a
CompactionCompletedevent. For subsequent model calls, send the summary and a tail of the conversation derived relative to the compaction event.
As a state machine written over the event log, it looks like this:
MessageReceivedToolCalledToolReturnedToolCalledToolReturnedTextReturnedMessageReceivedToolCalledToolReturnedCompactionCompletedsummaryMessageReceivedToolCalledMessageReceivedToolCalledToolReturnedTextReturned
ReadonlyArray<AgentMessage>userSummary of earlier work…
userRetained message…
assistanttool call…
tooltool result…
userNew message…
assistanttool call…
Once you've defined this behavior, you can compose it with other such behaviors defined in a similar manner.
export const researcher = actor({
name: "researcher",
methods: agentMethods,
components: [infer([
system("Research the given topic and cite your sources."),
compaction({
contextWindowTokens: 128_000,
fireRatio: 0.8,
keepRatio: 0.5
}),
nativeOutput
])]
})
Composition is possible because each component is defined as a typed state machine using Effect TS.
A component has the following interface:
interface ComponentDefinition<State, View, Requirements = never> {
readonly name: string
readonly initial: () => State
readonly step: (state: State, event: Event) => State
readonly output: (state: State) => {
readonly view: View
readonly transitions: ReadonlyArray<Transition<never, Requirements>>
}
readonly cancelState?: (
state: State,
cancellation: InvocationCancellation
) => ReadonlyArray<Transition<never, Requirements>>
readonly keys?: KeyFragment
}
Tardigrade composes behavior by combining the views and reconciling the transitions enabled by each component. Effect composes the runtime services those transitions require.
┌── event history remembered as private state
│ ┌── value observed by the parent
│ │ ┌── Effect services its transitions may require
│ │ │
▼ ▼ ▼
ComponentDefinition<State, View, Requirements>
We can use this approach to compose complex behaviors such as subagents, codemode, and recursive language models. Your imagination is the limit here. Continue to the Quickstart to begin building!