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.

One current state branches into more possible trajectories as the operating horizon grows.One current state branches into five possible trajectories as the operating horizon grows.

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:

ts
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:

behavior=f(log)\text{behavior} = f(\text{log})
An arrow from a highlighted point on a single trajectory shows behavior as a function of all previous points.

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.

immutable event loge₁ · e₂ · e₃ · ... · eₙcomponent treebehaviorsstate = fold(events, step)viewsderived valuestransitionsintents + effectsruntimeexecute enabled work

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 CompactionCompleted event. 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:

Once you've defined this behavior, you can compose it with other such behaviors defined in a similar manner.

ts
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:

ts
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!