| title | Thread |
|---|---|
| description | The per-conversation handle — post and stream messages, run the agent, block on a human choice, and reach platform power through capability-gated methods. |
A Thread is the per-conversation handle passed to every handler, tool context, and interaction context. It posts UI (JSX from the component vocabulary or plain strings), drives the agent run loop, resolves human-in-the-loop choices, and exposes platform power through capability-gated methods that degrade gracefully on surfaces that don't support them.
interface Thread {
readonly platform: string;
readonly platform: string;
readonly conversationKey: string;
post(ui: Renderable): Promise<MessageRef>;
update(ref: MessageRef, ui: Renderable): Promise<MessageRef>;
delete(ref: MessageRef): Promise<void>;
stream(src: string | AsyncIterable<string>): Promise<MessageRef>;
runAgent(input?: {
context?: ContextEntry[];
tools?: BotTool[];
prompt?: string | AgentContentPart[];
transcript?: boolean | { limit?: number };
}): Promise<MessageRef | undefined>;
resume(value: unknown): Promise<MessageRef | undefined>;
awaitChoice<T = unknown>(ui: Renderable): Promise<T>;
subscribe(): Promise<void>;
unsubscribe(): Promise<void>;
isSubscribed(): Promise<boolean>;
setState<T>(value: T): Promise<void>;
state<T>(): Promise<T | undefined>;
getMessages(): Promise<ThreadMessage[]>;
lookupUser(query: string): Promise<PlatformUser | undefined>;
postFile(args: {
bytes: Uint8Array;
filename: string;
title?: string;
altText?: string;
}): Promise<{ ok: boolean; fileId?: string; error?: string }>;
}1. Fetches prior transcript entries for the resolved user (default 20, override with `{ limit: N }`).
2. Injects them as a context entry so the agent sees cross-platform history.
3. Appends the current user turn to the transcript before the run.
4. Captures the assistant reply after the run and appends it.
This flag owns the transcript bridge — do not also manually append the same turns via `bot.transcripts.append`. No-ops with a one-time console warning when `identity`/`transcripts` are not configured.
When store.state is configured on createBot, this method validates value against the schema at runtime and throws Error: thread.setState: invalid state — … on a mismatch. Pass the schema's inferred output type as T to keep call-site types aligned with the schema.
bot.onMention(async ({ thread, message }) => {
// Run the agent with extra per-run context:
await thread.runAgent({
context: [
{ description: "Requesting user", value: message.user.name ?? message.user.id },
],
});
});// Inside a tool: read the thread, then block on approval.
async handler({ summary }, { thread }) {
const choice = await thread.awaitChoice<{ confirmed: boolean }>(
<ConfirmWrite action={summary} />,
);
return choice ?? { confirmed: false }; // serialized for the agent automatically
}// Per-thread state: track a workflow step across turns.
bot.onMention(async ({ thread }) => {
const state = await thread.state<{ step: string }>();
if (state?.step === "awaiting-approval") {
await thread.runAgent({ prompt: "The user has replied to your pending approval request." });
} else {
await thread.setState({ step: "awaiting-approval" });
await thread.runAgent();
}
});// Cross-platform transcript bridging — one call injects history, appends the
// user turn, runs the agent, and captures the assistant reply.
bot.onMention(async ({ thread }) => {
await thread.runAgent({ transcript: true });
});- Capability gating keeps tools portable —
getMessages/lookupUser/postFiledelegate to the adapter when supported and degrade gracefully ([]/undefined/{ ok: false }) when not, so the same tool runs on any surface. - Per-run merging —
runAgent'stoolsandcontextapply to that run only, layered on top of the bot-level defaults. - History reconstruction — on Slack, the conversation's
agent.messagesare rebuilt from Slack history each turn; the platform is the source of truth, so bot restarts don't lose conversations. - setState validation — when
store.stateis set oncreateBot,setStateruns the schema synchronously and throws before writing to the store on a mismatch, so invalid state never persists. - Transcript bridge ownership — when
runAgent({ transcript: true })is used, you must not also manually callbot.transcripts.appendfor the same turn; the bridge is the sole owner of that user/assistant pair.
- createBot — where handlers receive a Thread
- StateStore — pluggable persistence backends
- defineBotTool — tools receive the Thread in
ctx.thread - Button — interactive messages and
awaitChoice - Persistence guide — configuring durable state
- Transcripts guide — cross-platform conversation history
- slack() — how the Slack adapter backs these methods