Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Work until a goal is met
You ask your agent to make all tests pass. It fixes one test, then it stops and waits for you. You type "keep going" again and again.
The goal() plugin keeps the harness working. You give the goal one time. After each turn, a judge model reads the goal and the end of the conversation. If the goal is not met, the plugin starts the next turn.
1. Add the plugin#
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { goal } from '@tanstack/ai-harness/plugins'
import { openaiText } from '@tanstack/ai-openai'
import { memoryPersistence } from '@tanstack/ai-persistence'
export const coder = defineHarness({
name: 'acme/coder',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [goal({ judge: openaiText('gpt-6-luna') })],
})
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(coder, { threadId: 'fix-tests' })judge is the model that decides if the goal is met. It can be a smaller model than the main model, or the same model.
2. Give it a goal#
await session.runCommand({ name: 'goal', input: 'all tests pass' })The first turn starts. Each new turn tells the model the reason of the last check:
Keep working on the goal: all tests pass. Last check: The date parser test still fails.Each new turn runs as the user who set the goal. In a shared thread, the goal uses that user's credentials and keys, not the ones of the last person who wrote.
Run session.runCommand({ name: 'goal' }) to see the result:
Goal: all tests pass
Status: met, round 3 of 20.
Last check: All 12 tests pass.How it stops#
The plugin starts no new turn in these cases:
- The judge says that the goal is met. The status is met.
- The plugin started maxRounds turns (20 by default). The status is stopped, and the last reason stays.
- A turn waits for an approval, or a turn fails. The status is paused.
- You send your own message. The status is paused, and your message runs as a normal turn.
To change the limit, set maxRounds, for example goal({ judge, maxRounds: 50 }).
Commands#
| Command | What it does |
|---|---|
| /goal <text> | Sets the goal and starts the first turn. |
| /goal | Shows the goal, the status, the round, and the last reason. |
| /goal stop | Ends the goal. The running turn finishes, and no new turn starts. |
| /goal resume | Continues a paused or stopped goal with a new round count. It also starts an active goal again when no turn runs or waits. The goal is plugin state, so this also works after a restart. |
Show the goal in your UI#
The goal is plugin state. selectGoal reads it from session.snapshot():
import { selectGoal } from '@tanstack/ai-harness/plugins'
const current = selectGoal(session.snapshot())
if (current) console.log(`${current.text}: ${current.status}, round ${current.round}`)It returns null when the session has no goal.
Do something when the goal is met#
The plugin sends the GoalMet event with the goal and the last reason. Listen for it in your own plugin:
import { definePlugin } from '@tanstack/ai-harness'
import { GoalMet } from '@tanstack/ai-harness/plugins'
export const notify = definePlugin({
name: 'acme/notify',
setup: (ctx) => {
ctx.on(GoalMet, (met) => console.log(`Done: ${met.goal}. ${met.reason}`))
},
})Clients get the same event as a harness.plugin.event with the name tanstack/goal:met.
What you have now#
- A harness that keeps working until the judge says that the goal is met.
- A round limit, so the loop cannot run without end.
- /goal, /goal stop, and /goal resume to see and control the goal.