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",
});
4

Test it out

You should now see the thinking state on the UI while the agent is processing the response: