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.
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.
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.The plugin starts no new turn in these cases:
To change the limit, set maxRounds, for example goal({ judge, maxRounds: 50 }).
| 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. |
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.
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.