Deep Agents
AgentContextOrchestratorRetrievalText2SQLToolbox

chat()

Streaming orchestration that persists messages, generates titles, tracks usage, and handles errors

The chat() function is the high-level streaming entry point for conversations. It wires together message persistence, title generation, usage tracking, and error formatting into a single call that returns a ReadableStream of UI message chunks.

import { groq } from '@ai-sdk/groq';

import {
  ContextEngine,
  SqliteContextStore,
  agent,
  chat,
  createBashTool,
  role,
  user,
} from '@deepagents/context';

const sandbox = await createBashTool();
const store = new SqliteContextStore('./chat.db');
const context = new ContextEngine({
  store,
  chatId: 'chat-001',
  userId: 'user-001',
}).set(role('You are a helpful assistant.'));

const myAgent = agent({
  name: 'assistant',
  sandbox,
  context,
  model: groq('gpt-oss-20b'),
});

await context.continue(user('Hello'));
const stream = await chat(myAgent, {
  generateTitle: true,
});

Function Signature

async function chat<TOOLS extends ToolSet>(
  agent: ChatAgentLike<TOOLS>,
  options?: ChatOptions<TOOLS>,
): Promise<ReadableStream<UIMessageChunk>>;

The return value is a ReadableStream produced by the AI SDK's createUIMessageStream(). It emits structured chunks (text-delta, tool-call, data-chat-title, etc.) that UI frameworks consume directly.

ArgumentTypeDescription
agentChatAgentLike<TOOLS>An object with stream(), context, and optionally model
optionsChatOptions<TOOLS>Optional tool contexts, title, transform, error-handling, and metadata configuration

The function throws if:

  • The agent has no context attached
  • The context head is not the assistant placeholder reserved by context.continue()

ChatAgentLike

Any object that satisfies this interface can be passed to chat(). The agent() wrapper from @deepagents/context implements it automatically.

interface ChatAgentLike<TOOLS extends ToolSet> {
  context?: ContextEngine;
  model?: AgentModel;
  tools?: TOOLS;
  stream(options?: ChatOptions<TOOLS>): Promise<StreamTextResult<ToolSet, never>>;
}
PropertyTypeRequiredDescription
contextContextEngineYesThe context engine scoped to the current chat
modelAgentModelNoRequired for AI-generated titles. If absent, only static titles are used
stream()FunctionYesResolves context and returns a streaming AI SDK result

ChatOptions

interface ChatOptions<TOOLS extends ToolSet> {
  toolsContext?: InferToolSetContext<TOOLS>;
  transform?: StreamTextTransform<ToolSet> | StreamTextTransform<ToolSet>[];
  abortSignal?: AbortSignal;
  generateTitle?: boolean;
  onError?: (error: unknown) => string;
  messageMetadata?: NonNullable<
    Parameters<StreamTextResult<ToolSet, never>['toUIMessageStream']>[0]
  >['messageMetadata'];
  finalAssistantMetadata?: (
    message: UIMessage,
  ) =>
    | Record<string, unknown>
    | undefined
    | Promise<Record<string, unknown> | undefined>;
}
OptionTypeDefaultDescription
toolsContextInferToolSetContext<TOOLS>contextual toolsRequired only when a tool declares context; keyed by tool name
transformStreamTextTransformundefinedCustom stream transforms forwarded to agent.stream()
abortSignalAbortSignalundefinedSignal to cancel the stream, forwarded to agent.stream()
generateTitlebooleanfalseEnable AI-powered title generation for untitled chats
onError(error: unknown) => stringBuilt-in formatterCustom error-to-string formatter for stream errors
messageMetadataDerived from AI SDKundefinedForwarded to toUIMessageStream() — see AI SDK docs for the full type
finalAssistantMetadata(msg: UIMessage) => Record<string, unknown> | undefinedundefinedAsync callback to attach metadata to the final assistant message before persisting

Message Persistence Flow

The chat() function persists messages at three points during a conversation turn:

1. Before streaming  ─── caller uses context.continue() to append input and reserve the assistant
2. On each step      ─── save intermediate assistant state
3. On finish         ─── save final assistant message + track usage

1. Initial Save

Before calling chat(), call context.continue(input). It appends the input and reserves the assistant placeholder that chat() updates. This explicit precondition keeps message ownership in ContextEngine.

2. Step-Finish Saves

After each streaming step completes (e.g., a tool call finishes), the intermediate assistant message is saved with save({ branch: false }). This ensures partial progress is persisted even if the stream is interrupted.

3. Final Save

When the stream finishes:

  1. The finalAssistantMetadata callback is invoked (if provided) to attach custom metadata
  2. The final assistant message is saved with save({ branch: false })
  3. Token usage is tracked via context.trackUsage()

The branch: false option on assistant saves means the message node is updated in-place rather than creating a new branch. This keeps the conversation graph clean — one assistant node per turn, not one per streaming update.

Title Generation

chat() handles title generation automatically for untitled chats:

const stream = await chat(myAgent, {
  generateTitle: true,
});

When generateTitle: true and the agent has a model, generateChatTitle() uses an LLM to produce a 2-5 word title. If the LLM call fails, it falls back to staticChatTitle() (first user message truncated to 100 characters). When generateTitle is false (the default) or the agent has no model, staticChatTitle() is used directly.

In both cases, the title is persisted via context.updateChat() before streaming begins, and a { type: 'data-chat-title', data: title } event is emitted on the stream so the client can display it.

Error Handling

The built-in error formatter converts common AI SDK errors into user-friendly strings:

Error TypeMessage
NoSuchToolError"The model tried to call an unknown tool."
InvalidToolInputError"The model called a tool with invalid arguments."
ToolCallRepairError"The model tried to call a tool with invalid arguments, but it was repaired."
APICallError"Upstream API call failed with status {code}: {message}"
OtherJSON-serialized error

Override with a custom formatter:

const stream = await chat(myAgent, {
  onError: (error) => {
    if (error instanceof MyCustomError) {
      return 'Something went wrong. Please try again.';
    }
    return 'An unexpected error occurred.';
  },
});

Complete Example

import { groq } from '@ai-sdk/groq';

import {
  ContextEngine,
  SqliteContextStore,
  agent,
  chat,
  createBashTool,
  role,
  user,
} from '@deepagents/context';

const store = new SqliteContextStore('./chat.db');
const sandbox = await createBashTool();

async function handleUserMessage(
  chatId: string,
  userId: string,
  message: string,
) {
  const context = new ContextEngine({
    store,
    chatId,
    userId,
  }).set(role('You are a helpful assistant.'));

  const myAgent = agent({
    name: 'assistant',
    sandbox,
    context,
    model: groq('gpt-oss-20b'),
  });

  await context.continue(user(message));
  const stream = await chat(myAgent, {
    generateTitle: true,
    finalAssistantMetadata: async (message) => ({
      completedAt: Date.now(),
    }),
  });

  return stream;
}

chat() vs agent.stream()

chat()agent.stream()
Message persistenceAssistant is automatic after context.continue()Manual
Title generationBuilt-in with generateTitle optionManual
Usage trackingAutomatic via trackUsage()Manual
Error formattingBuilt-in with override optionRaw errors
Return typeReadableStream<UIMessageChunk>StreamTextResult
Branch managementHandles branch: false for in-place updatesN/A

Use chat() when building a conversation UI. Use agent.stream() directly when you need lower-level control over the streaming pipeline.

Next Steps

On this page