Learn to enhance user experience by updating them about the agent’s thinking process in real-time
This guide assumes that you have completed the Quickstart.
Thinking states are a great way to let the user know what the model is doing. This is especially useful for long-running tasks or when the user is waiting for a response,
such as when the agent is performing a slow tool call.
The @thesysai/genui-sdk package provides a makeC1Response function that can be used to add data related to thinking states to the response.
Here’s a simple example of how to use thinking states:
1
Create a c1Response object
Use the makeC1Response function to create a c1Response object by importing it from the @thesysai/genui-sdk package, and start writing the LLM response
content to this object:
app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";import OpenAI from "openai";import type { ChatCompletionMessageParam } from "openai/resources.mjs";import { transformStream } from "@crayonai/stream";import { getMessageStore } from "./messageStore";import { makeC1Response } from "@thesysai/genui-sdk/server";export async function POST(req: NextRequest) { const c1Response = makeC1Response(); const { prompt, threadId, responseId } = (await req.json()) as { prompt: ChatCompletionMessageParam; threadId: string; responseId: string; }; const client = new OpenAI({ baseURL: "https://api.thesys.dev/v1/embed", apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step }); const messageStore = getMessageStore(threadId); messageStore.addMessage(prompt); const llmStream = await client.chat.completions.create({ model: "c1-nightly", messages: messageStore.getOpenAICompatibleMessageList(), stream: true, }); // Unwrap the OpenAI stream to a C1 stream transformStream( llmStream, (chunk) => { const contentDelta = chunk.choices[0].delta.content; if (contentDelta) { c1Response.writeContent(contentDelta); } return contentDelta; }, { onEnd: ({ accumulated }) => { c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming. const message = accumulated.filter((chunk) => chunk).join(""); messageStore.addMessage({ id: responseId, role: "assistant", content: message, }); }, } ) as ReadableStream<string>; return new NextResponse(c1Response.responseStream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }, });}
2
Write a thinking state to the response object
To add a thinking state, use the writeThinkItem method defined on the c1Response object:
app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";import OpenAI from "openai";import type { ChatCompletionMessageParam } from "openai/resources.mjs";import { transformStream } from "@crayonai/stream";import { getMessageStore } from "./messageStore";import { makeC1Response } from "@thesysai/genui-sdk/server";export async function POST(req: NextRequest) { const c1Response = makeC1Response(); c1Response.writeThinkItem({ title: "Thinking...", description: "Diving into the digital depths to craft you an answer.", }); const { prompt, threadId, responseId } = (await req.json()) as { prompt: ChatCompletionMessageParam; threadId: string; responseId: string; }; const client = new OpenAI({ baseURL: "https://api.thesys.dev/v1/embed", apiKey: process.env.THESYS_API_KEY, // Use the API key you created in the previous step }); const messageStore = getMessageStore(threadId); messageStore.addMessage(prompt); const llmStream = await client.chat.completions.create({ model: "c1-nightly", messages: messageStore.getOpenAICompatibleMessageList(), stream: true, }); // Unwrap the OpenAI stream to a C1 stream transformStream( llmStream, (chunk) => { const contentDelta = chunk.choices[0].delta.content; if (contentDelta) { c1Response.writeContent(contentDelta); } return contentDelta; }, { onEnd: ({ accumulated }) => { c1Response.end(); // This is necessary to stop showing the "loading" state once the response is done streaming. const message = accumulated.filter((chunk) => chunk).join(""); messageStore.addMessage({ id: responseId, role: "assistant", content: message, }); }, } ) as ReadableStream<string>; return new NextResponse(c1Response.responseStream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }, });}
3
Use thinking states with long-running tool calls (optional)
If you’d like to use thinking states with long-running tool calls, simply call the aforementioned writeThinkItem method inside the
tool call handler. For example, for the webSearch tool implemented in the Tool Calling guide, you can add a thinking state as follows:
Next, modify the tool call handler to call a function that updates the thinking state:
app/api/chat/tools.ts
import type { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";import { z } from "zod";import { zodToJsonSchema } from "zod-to-json-schema";import Exa from "exa-js";import type { JSONSchema } from "openai/lib/jsonschema.mjs";const exa = new Exa(process.env.EXA_API_KEY!);export const getWebSearchTool = ( writeThinkingState: () => void): RunnableToolFunctionWithParse<{ query: string }> => ({ type: "function", function: { name: "webSearch", description: "Use this tool to perform a web search.", parse: JSON.parse, parameters: zodToJsonSchema( z.object({ query: z.string().describe("The query to search for."), }) ) as JSONSchema, function: async ({ query }: { query: string }) => { writeThinkingState(); return await exa.search(query, { numResults: 5 }); }, strict: true, },});
Next, pass the writeThinkingState function to the tool call handler:
app/api/chat/route.ts
const llmStream = await client.beta.chat.completions.runTools({ model: "c1-nightly", messages: [ { role: "system", content: systemPrompt }, ...messageStore.getOpenAICompatibleMessageList(), ], stream: true, tools: [ getWebSearchTool(() => { c1Response.writeThinkItem({ title: "Searching the web...", description: "Scouring the digital universe for the most relevant and up-to-date insights.", }); }), ], toolChoice: "auto",});
app/api/chat/route.ts
import { NextRequest, NextResponse } from "next/server";import OpenAI from "openai";import { transformStream } from "@crayonai/stream";import { DBMessage, getMessageStore } from "./messageStore";import { makeC1Response } from "@thesysai/genui-sdk/server";import { getWebSearchTool } from "./tools";import { systemPrompt } from "./systemPrompt";export async function POST(req: NextRequest) { const c1Response = makeC1Response(); c1Response.writeThinkItem({ title: "Thinking...", description: "Diving into the digital depths to craft you an answer.", }); const { prompt, threadId, responseId } = (await req.json()) as { prompt: DBMessage; threadId: string; responseId: string; }; const client = new OpenAI({ baseURL: "https://api.thesys.dev/v1/embed", apiKey: process.env.THESYS_API_KEY, }); const messageStore = getMessageStore(threadId); messageStore.addMessage(prompt); const llmStream = await client.beta.chat.completions.runTools({ model: "c1-nightly", messages: [ { role: "system", content: systemPrompt }, ...messageStore.getOpenAICompatibleMessageList(), ], stream: true, tools: [ getWebSearchTool(() => { c1Response.writeThinkItem({ title: "Searching the web...", description: "Scouring the digital universe for the most relevant and up-to-date insights.", }); }), ], toolChoice: "auto", }); transformStream( llmStream, (chunk) => { const contentDelta = chunk.choices[0].delta.content; if (contentDelta) { c1Response.writeContent(contentDelta); } return contentDelta; }, { onEnd: ({ accumulated }) => { c1Response.end(); const message = accumulated.filter((message) => message).join(""); messageStore.addMessage({ role: "assistant", content: message, id: responseId, }); }, } ) as ReadableStream<string>; return new NextResponse(c1Response.responseStream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }, });}
4
Test it out
You should now see the thinking state on the UI while the agent is processing the response: