Each conversation in your app has its own choices: a faster model for one chat, extra instructions for another, a folder for a coding task. These choices must stay after a reload and a restart. session.updateThreadSettings() stores them with the thread, and every next turn uses them.
Give the harness a models map. A thread picks a model by its name:
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { anthropicText } from '@tanstack/ai-anthropic'
export const assistant = defineHarness({
name: 'acme/assistant',
adapter: anthropicText('claude-sonnet-5-5'),
models: {
fast: anthropicText('claude-haiku-5-5'),
strong: anthropicText('claude-opus-5-5'),
},
})
const host = createHarnessHost({ persistence: memoryPersistence() })const session = await host.open(assistant, { threadId: 'review-thread' })
const receipt = await session.updateThreadSettings({
settings: {
model: 'strong',
reasoning: 'high',
instructions: 'Answer in French.',
},
})
console.log(receipt.status) // 'accepted'
console.log(session.threadSettings()) // { model: 'strong', reasoning: 'high', instructions: 'Answer in French.' }The settings apply from the next turn. A field that you leave out stays as it is. updateThreadSettings merges your fields into the latest stored settings, also when another host changed them. Set a field to null to clear it:
await session.updateThreadSettings({ settings: { instructions: null } })The six fields:
| Field | What it does |
|---|---|
| model | A name from models. |
| reasoning | The reasoning level of every turn, as chat({ reasoning }) takes it. |
| instructions | Text added as the last system prompt of every turn. |
| tools | Exactly these tools by name, or { remove: [names] } for every tool but those. |
| plugins | { remove: [names] }: the turns leave out what those plugins give. |
| cwd | The working folder of the thread. See The working folder. |
An unknown field, model, or plugin, or a bad value, gets a receipt with status: 'rejected' and the reason. The settings are in the log, so a durable log store keeps them after a restart. A fork copies them.
updateThreadSettings stores an input, so the log keeps who changed what. Each change sends a harness.settings.changed event with the new settings. A session on another host reads the change too, so its next turn uses the new settings.
A client can change only the fields in expose.settings of the harness. See Choose what clients can change.
session.describe() returns the stored settings and the names of the models, so a UI can build a menu:
const { models, settings } = session.describe()
console.log(models) // ['fast', 'strong']
console.log(settings.model) // 'strong'
await session.updateThreadSettings({ settings: { model: 'fast' } })A turn can get its model from five places. The first one that is set wins:
For reasoning, the overrides of the prompt win, then the stored setting, then the reasoning of the harness.
cwd is the working folder of the thread, for the plugins whose tools work in a folder. A plugin reads it at each call with ctx.session.threadSettings().cwd, so a change applies from the next turn:
const task = await host.open(assistant, { threadId: 'fix-login' })
await task.updateThreadSettings({ settings: { cwd: 'web-app' } })
console.log(task.threadSettings().cwd) // 'web-app'When cwd changes, the model gets a short note before its next call. So it stops using the paths of the old folder:
The note is an assistant message in the transcript, so it stays after a restart. A change while a turn runs adds the note before the next turn. The log keeps that waiting note, and the next host adds it once after a crash.