The project's README from github.com/clavia-labs/tardigrade. As one markdown file: README.md.
Tardigrade is a typescript framework for building modular agents around an immutable event log. It is built on Effect TS and is inspired by React's declarative approach to building user interfaces.
As models get increasingly smart, they will be capable of writing their own harnesses to improve themselves (Meta-Harness). A harness that is too rigid and complex is a bottleneck to this. We need something more composable, and easy to author.
We took inspiration from React. React derives its component tree and declared effects from state. Tardigrade applies the same idea to agent harnesses. Each component derives a view and enabled transitions from an event log.
{ view, transitions } = f(event log)
Install Tardigrade and initialize an editable template actor. Use Bun 1.4 or later. If you are using a coding agent, the Tardigrade skill can help.
If you have an existing agent application, follow the migration guide to move its harness, history, API, client, and deployment configuration.
bun add -g tardie@latest
tdg init tardie-agent --template quickstart
cd tardie-agent
bun run dev
tdg init configures the first provider and model. Edit actor.ts to describe the agent. The CLI guide covers non-interactive setup, more providers, and deployment.
From another shell, discover the actor's methods, allocate a root thread, and send it a message:
tdg methods
tdg thread create --name quickstart
tdg call message '{"text":"What is the weather in Singapore?"}' --thread quickstart
The API listens at localhost:4242 by default.
Deploy the generated Worker with either platform CLI:
Cloudflare:
bunx wrangler deploy
Celld:
celld deploy --config celld.jsonc
See the Cloudflare and Celld guides for platform configuration and secrets.
bun add tardie
You can use npm install tardie instead. Install tardie@next to test a release candidate.
An agent is made of components. Each component owns a machine with initial, step, and output. Its state retains the information from prior events that can affect its future output. An agent view includes system fragments, tool bindings, and context policy. This component gives the model one tool and owes no autonomous work:
import { component } from "tardie/core"
import { type AgentComponent, type AgentView } from "tardie/agent"
const deploys: AgentComponent = component<undefined, AgentView>({
name: "deploys",
initial: () => undefined,
step: (state, _event) => state,
output: () => ({
view: {
system: ["Inspect recent deployments when a release may explain an incident."],
tools: [{
spec: {
name: "recent_deploys",
description: "List recent production deploys",
inputSchema: { type: "object", properties: {}, additionalProperties: false }
},
serve: (_call, _log, answer) => [
answer([{ service: "api", revision: "a17c", summary: "Add rate limiting" }])
]
}],
context: [],
output: []
},
transitions: []
})
})
initial creates private state. step updates it for each event. This component ignores events and preserves its state. output derives its view and enabled transitions. Each tool keeps its specification and handler together. answer records the result. Replace the sample result with your deployment API.
An offered tool follows this lifecycle:
recent_deploys to the agent's composed view.infer includes its specification in the model request.ToolCalled.ToolReturned.infer includes the result in the next model request.Mount the component beside the built-in parts that this task needs:
import { actor } from "tardie/core"
import { agentMethods, agentsPackage, budget, budgetAuthority, caller, codeMode, compaction, infer, outputValidateOnce, system } from "tardie/agent"
import { fetchPackage, filesPackage, workspacePackage } from "tardie/code"
const instructions = system(
"You are a release analyst. Identify risky changes and recommend the safest next action."
)
const releaseAnalyst = actor({
// name supplies the actor's stable identity.
name: "release-analyst",
// methods declare how the world can communicate with this actor.
methods: agentMethods,
// components implement those methods and derive transitions from the actor's private log.
components: [
// infer handles messages as model loops composed by children.
infer([
instructions, // system prompt
deploys, // provides recent_deploys tool and paired handler
// budget scopes the tool-call limit to the codeMode subtree
budget([
codeMode([ // sandboxed code execution
filesPackage(),
fetchPackage(),
agentsPackage(),
workspacePackage()
])
], { authority: caller() }),
compaction(), // bounded model context
outputValidateOnce // validates structured result once without correction
]),
budgetAuthority() // budgetAuthority handles requestBudget for this actor.
]
})
actor gives the composition a stable name and callable methods. infer turns its child components into an agent loop and inherits the host's model policy unless its infer options narrow it with models.
compaction() uses the selected model's catalog window. It summarizes at 80 percent and retains a 50 percent tail. Pass fireRatio and keepRatio to change those values. Each checkpoint records the policy it applied.
codeMode([...components]) exposes its packages through one execute tool.
budget([...components]) meters tool calls within its subtree. caller() sends escalation requests to the invoking actor, and budgetAuthority() handles them locally.
This agent can inspect deployments and files, fetch sources, delegate research, and analyze results with JavaScript. Change the package list to create another harness.
A run can follow this path:
MessageReceived -> recent_deploys -> execute -> TurnCompleted
Each action and result becomes an event that every component can interpret.
The three code blocks form one program. Run it in a project configured by tdg init or tdg setup, with the provider credentials available in the environment. This example registers the adapter for OpenAI Responses and compatible chat completions; use the adapter for your configured protocol.
import { createBunHost } from "tardie/bun"
import { modelAdapters } from "tardie/model/adapter"
import { openAICompatibleAdapter } from "tardie/model/openai"
import { bunModelServices } from "tardie/server/model-services"
const { layers } = await bunModelServices({
env: process.env,
adapters: modelAdapters(openAICompatibleAdapter)
})
const host = await createBunHost({
actor: releaseAnalyst,
storage: ".tardigrade",
layersFor: () => layers
})
try {
const thread = await host.allocateRootThread({ instance: "analyst", name: "main" })
const result = await thread.methods.message(
{ text: "What changed in the deploy?" },
{ key: "deploy-review" }
)
console.log(result)
} finally {
await host.close()
}
bunModelServices reads the model policy from wrangler.jsonc and binds inference and platform services. createBunHost stores actor instances under storage. The method call returns the completed result; retries with the same key return the same invocation. Use a new key for each new request.
Every message, model action, tool result, and checkpoint lands in the log. Component machines consume those events and derive keyed transitions from their current state.
Sₙ₊₁ = step(Sₙ, eₙ₊₁)
The host runs transitions with unrecorded keys. It appends their events and repeats until the agent rests.
If the process stops during recent_deploys, the log still contains its unanswered ToolCalled. host.recover() replays the log through the component machines, derives the same key and input, then runs the handler again. Live execution only steps the machines with newly appended events.
External effects have at-least-once execution. Each keyed result is recorded once. Providers can use the transition key as an idempotency key.
Read CONTRIBUTING.md and run bun run gate before finishing a change.