Thread

The per-conversation handle — post and stream messages, run the agent, block on a human choice, and reach platform power through capability-gated methods.


Overview

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 }>;
}

Properties

Prop

Type

Prop

Type

Methods

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Prop

Type

Usage

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 });
});

Behavior

  • Capability gating keeps tools portablegetMessages / lookupUser / postFile delegate to the adapter when supported and degrade gracefully ([] / undefined / { ok: false }) when not, so the same tool runs on any surface.
  • Per-run mergingrunAgent's tools and context apply to that run only, layered on top of the bot-level defaults.
  • History reconstruction — on Slack, the conversation's agent.messages are rebuilt from Slack history each turn; the platform is the source of truth, so bot restarts don't lose conversations.
  • setState validation — when store.state is set on createBot, setState runs 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 call bot.transcripts.append for the same turn; the bridge is the sole owner of that user/assistant pair.

Related