Mid-call changes
Project: examples/pipeline. Speech-to-text, the LLM and text-to-speech are props, so a change
is a render.
By state. A tool sets a store, and the store sets the providers, so the call moves to German from the next reply on:
const useLanguage = createAgentStore<{ language: "en" | "de"; setLanguage: (language: "en" | "de") => void }>(
"language",
(set) => ({ language: "en", setLanguage: (language) => set({ language }) }),
);
function App() {
const language = useLanguage((state) => state.language);
const setLanguage = useLanguage((state) => state.setLanguage);
return (
<Agent stt={kugelaudio.stt({ language: language === "de" ? "de,en" : "en,de" })} tts={kugelaudio.tts({ language })}>
<Instruction>{`Reply in ${language === "de" ? "German" : "English"}.`}</Instruction>
<GlobalTools>
<Tool
name="switch_to_german"
description="Use when the caller asks for German."
onCall={async () => {
setLanguage("de");
return { ok: true, message: "From your next reply on the call is in German." };
}}
/>
</GlobalTools>
</Agent>
);
}
The STT takes both languages, the likelier one first, so it hears the request to switch. The
instruction is in <current_context>, so the system prompt stays the same for the provider’s cache.
By skill. A skill’s link overrides the agent’s providers while the skill is active. The active
skill is the one read last, so another read ends it. A skill’s llm applies from the caller’s next
turn, not to the reply in the turn that opened the skill:
<Skill path="/report" description="Use for a written summary." llm={kugelaudio.llm({ model: "mistral-medium-3.5-128b" })}>
Report
</Skill>
From outside. Nothing is adjustable unless you list it, per sender:
<Agent adjustable={{ client: ["tts.voice"], host: ["tts.voice", "stt.language", "llm"] }}>
The caller’s app then calls agent.update({ tts: { voice: 17 } }) from curb/client, and your own
host sends PATCH /v1/sessions/{id} (Host protocol). A hosted
agent has no published route for your server yet: there, change settings from the caller’s app
or from a tool in the agent.
Live settings: adjustable and useSession
The caller’s app (curb/client’s agent.update) and the host
(PATCH /v1/sessions/{id}) may change a few settings mid-call, but only those
<Agent adjustable> lists for them. Absent, nothing is adjustable.
import { Agent } from "curb";
import { kugelaudio } from "curb/providers";
export function App() {
return (
<Agent
tts={kugelaudio.tts({ voice: 1072 })}
adjustable={{ client: ["tts.voice"], host: ["tts.voice", "stt.language", "llm"] }}
>
{/* the prompt, skills and tools */}
</Agent>
);
}
Paths: "tts.voice", "tts.model", "stt.language", "llm". An accepted change
overrides the prop until the next change of the same path; a refused one changes
nothing. Read the effective providers (props plus outside changes) in any component:
import { Text, useSession } from "curb";
export function CurrentVoice() {
const { settings } = useSession(); // { llm, stt, tts } descriptors, read-only
// e.g. { voice: 17 } after the app called agent.update({ tts: { voice: 17 } })
return <Text>{`Speaking with ${settings.tts.provider} ${JSON.stringify(settings.tts.options)}.`}</Text>;
}
The body, the refusal codes and the wire format are in Host protocol.
On a hosted call, useSession().call is that call: its sessionId and its own
scratchDir, a writable directory removed when the call ends. A runner may serve
several calls in one process, so read the call’s identity here, never from
process.env, and write files only under scratchDir. scratchDir is absent in curb dev
and when the runner could not create one.
import { Text, useSession } from "curb";
export function CallInfo() {
const { call } = useSession();
return <Text>{call ? `Call ${call.sessionId}.` : "Not a hosted call."}</Text>;
}
Next steps
- Voice: the providers and their options.
- Host protocol: the wire format of a settings change.