AG2B

Chat

The agent surfaces two ways to drive the loop:

  • Synchronouschat() awaits the final response.
  • StreamchatStream() yields events as they happen.

Both run the same loop, same hooks, same history — only the surface differs.

Agent Response

The agent's final response — Synchronous returns it, Stream yields it.

type AgentResponse = {
  content?: string;
  reasoning?: string;
  finishReason?: FinishReason;
};
  • content — final assistant text.
  • reasoning — reasoning / thinking trace from the final turn, if the provider emitted one.
  • finishReason'stop' | 'tool_calls' | 'length'.

Synchronous

chat(message: string, options?: ChatOptions): Promise<AgentResponse>

Drives the loop and resolves with the agent response.

const response = await agent.chat('What is blocked right now?');
console.log(response.content);

ChatOptions

type ChatOptions = {
  signal?: AbortSignal;
  onEvent?: (event: AgentEvent) => void;
};
  • signal — abort the run mid-flight. See Abort.
  • onEvent — observe every event the loop emits.
await agent.chat('What changed?', {
  onEvent: (event) => {
    if (event.type === 'agent_tool_call_start') {
      console.log('calling', event.call.name);
    }
  },
});

Stream

chatStream(message: string, signal?: AbortSignal): AsyncGenerator<AgentEvent>

Drives the loop and yields events as they happen.

Token-by-token streaming requires a StreamableProvider. With a sync-only provider, chatStream still emits the same events — *_delta arrives as a single chunk with the whole content instead of streaming token-by-token.

const output = document.querySelector('#output')!;

for await (const event of agent.chatStream('What is blocked right now?')) {
  if (event.type === 'agent_content_delta') {
    output.textContent += event.delta;
  }
  if (event.type === 'agent_chat_done') {
    console.log('finish:', event.response.finishReason);
  }
}

Events

type AgentEvent =
  | AgentChatStart
  | AgentReasoningDelta
  | AgentReasoningEnd
  | AgentContentDelta
  | AgentContentEnd
  | AgentToolCallDelta
  | AgentToolCallStart
  | AgentToolCallResult
  | AgentToolCallError
  | AgentChatDone
  | AgentChatAbort
  | AgentChatError;

Both modes emit every event — only the granularity of content, reasoning, and tool-call deltas differs.

Most events fire per loop iteration, not per chat call. A run with N iterations produces N rounds of reasoning / content / tool events.

Wire your Chat UI to events once

You don't need to touch the render layer when switching between synchronous/stream modes.

agent_chat_start

type AgentChatStart = {
  type: 'agent_chat_start';
  message: string;
};

Loop has begun. Carries the user message that triggered it.

agent_reasoning_delta

type AgentReasoningDelta = {
  type: 'agent_reasoning_delta';
  delta: string;
};

Chunk of reasoning / thinking text from the LLM.

agent_reasoning_end

type AgentReasoningEnd = {
  type: 'agent_reasoning_end';
};

Reasoning stream is final for the current loop iteration. Fires once per iteration that produced any reasoning, just before the first non-reasoning event.

agent_content_delta

type AgentContentDelta = {
  type: 'agent_content_delta';
  delta: string;
};

Chunk of assistant text.

agent_content_end

type AgentContentEnd = {
  type: 'agent_content_end';
};

An assistant message has been committed to history. Fires once per loop iteration — multiple times in a single chat call when the LLM uses tools across multiple turns. Includes tool-call-only turns that produced no text.

agent_tool_call_delta

type AgentToolCallDelta = {
  type: 'agent_tool_call_delta';
  index: number;
  id?: string;
  name?: string;
  argumentsDelta: string;
};

A chunk of a streaming tool call, paired with agent_content_delta but for tool calls.

  • The first chunk for an index carries id and name
  • Later chunks carry only argumentsDelta (a partial JSON string — concatenate all chunks for the full arguments).

agent_tool_call_start

type AgentToolCallStart = {
  type: 'agent_tool_call_start';
  call: AssistantToolCall;
};

A tool call is about to execute.

agent_tool_call_result

type AgentToolCallResult = {
  type: 'agent_tool_call_result';
  call: AssistantToolCall;
  result: unknown;
};

Tool call returned successfully.

agent_tool_call_error

type AgentToolCallError = {
  type: 'agent_tool_call_error';
  call: AssistantToolCall;
  error: unknown;
};

Tool call threw (handler throw, validation error, unknown tool, disabled tool).

agent_chat_done

type AgentChatDone = {
  type: 'agent_chat_done';
  response: AgentResponse;
};

Loop completed successfully. Terminal event — carries the same AgentResponse that chat() would return.

agent_chat_abort

type AgentChatAbort = {
  type: 'agent_chat_abort';
  reason?: unknown;
};

Chat was cancelled via the abort signal. Terminal event — carries signal.reason (a DOMException("AbortError") by default, or whatever you passed to controller.abort(reason)).

agent_chat_error

type AgentChatError = {
  type: 'agent_chat_error';
  error: unknown;
};

Chat ended with a non-abort error (provider throw, max iterations, hook throw). Terminal event — carries the same error that re-throws to the caller.

Mode Differences

How each event emits, side by side:

EventSynchronousStream
agent_chat_startonceonce
agent_reasoning_delta1 delta (whole reasoning) per iterationN deltas (token-by-token) per iteration
agent_reasoning_endonce per iterationonce per iteration
agent_content_delta1 delta (whole content) per iterationN deltas (token-by-token) per iteration
agent_content_endonce per iterationonce per iteration
agent_tool_call_delta1 delta (whole call) per tool callN deltas (chunked) per tool call
agent_tool_call_startper tool callper tool call
agent_tool_call_resultper tool call (on success)per tool call (on success)
agent_tool_call_errorper tool call (on error)per tool call (on error)
agent_chat_doneonce (on success)once (on success)
agent_chat_abortonce (on abort)once (on abort)
agent_chat_erroronce (on error)once (on error)

Abort

Pass an AbortSignal to cancel a run mid-flight. The agent checks between iterations and threads the signal through the provider call and tool handler awaits.

On abort, the loop:

  1. Emits agent_chat_abort event — carries signal.reason.
  2. Runs the onChatAbort hook.
  3. Re-throws signal.reason to the caller.

Prefer the event for UI / state — it fires before the throw and tells abort apart from real errors cleanly:

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

for await (const event of agent.chatStream('Long task', controller.signal)) {
  if (event.type === 'agent_chat_abort') {
    console.log('cancelled:', event.reason);
  }
}

try / catch still works, but the thrown value is whatever you passed to controller.abort(reason) — not always an AbortError. Check signal.aborted to disambiguate:

const controller = new AbortController();
setTimeout(() => controller.abort(new Error('timeout')), 5_000);

try {
  await agent.chat('Long task', { signal: controller.signal });
} catch (err) {
  if (controller.signal.aborted) {
    // err is signal.reason — the value you passed to controller.abort()
    console.log('cancelled:', err);
  } else {
    throw err;
  }
}

On this page