- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Store settings per thread
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.
1. Name the models a thread can pick#
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() })2. Store the settings of a thread#
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.
3. Build a model menu#
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' } })What wins#
A turn can get its model from five places. The first one that is set wins:
- The overrides.adapter of the prompt. See Give one prompt its own settings.
- A plugin adapter() pick that returns { adapter, override: true }. See Win over the stored model.
- The stored model.
- A plugin adapter() pick. See Pick the model of a turn.
- The adapter of the harness.
For reasoning, the overrides of the prompt win, then the stored setting, then the reasoning of the harness.
Tools and plugins#
- A tools list keeps only the tools with those names. { remove } drops the tools with those names. Either way, the tools of the prompt overrides stay.
- The harness applies tools before the plugins prepare the tool list (prepareTools). So a plugin that wraps tools sees only the tools that a thread keeps.
- plugins: { remove } leaves everything that those plugins give out of the thread: tools, prompts, middleware, the model pick, agents, subagents, commands, and the items that they add for other plugins. For example, a thread without agents() gets no subagent tools, no /agent command, and no permission rules of its agent profiles. The thread also cannot run an agent of a removed plugin by its name.
- Plugins that run once per session stay mounted, but they give the thread nothing.
- If a plugin that stays (of any lifetime) or a harness middleware entry needs a capability of a removed plugin, the setting is rejected.
The working folder#
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:
- A new cwd adds "The working folder is now web-app. Paths are relative to it."
- cwd: null adds "The working folder is the default folder again."
- The same cwd again adds no note.
- A thread with no messages yet gets no note.
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.
What you have now#
- A model menu per thread, from the models of the harness.
- Instructions, reasoning, tools, and plugins that each thread keeps.
- A working folder per thread for the plugins that read it.
- Settings that stay after a reload and a restart.