Quickstart

Building your own text, voice or video AI is easy. In this example we'll set up your agent, knowledge, skills, sandbox, tool calling and chat UI.

Stream Agents is hosted on edge servers around the world for optimal latency. It's open source so you can also self-host.

Time to get started. If you get stuck at any point hit the "Ask AI" button above. The ask AI feature was actually created using this framework as well.

The quickest path

If you're in a hurry you can skip this tutorial and paste one of the 3 example prompts in Claude.

Example prompts

Build me a Go based voice AI that takes orders for my restaurant. Optimize for Dutch language and give me a phone number in Amsterdam: getstream.io/SKILL-agents.md

1 / 3

Build me a Go based video AI that evaluates which strawberries are ripe and counts them: getstream.io/SKILL-agents.md

2 / 3

Build me a Go based text AI that answers support questions, reads my SDKs source code to help customers: getstream.io/SKILL-agents.md

3 / 3

Stream app setup

Get the beta Stream CLI and pick or create the Stream app your agent runs in. The getstream agents commands are only in beta builds: a stable getstream answers them with "agents unavailable". The installer puts getstream in ~/.local/bin and, if that is not on your PATH, adds it to your shell's rc file (~/.zshrc, ~/.bashrc, ~/.bash_profile or ~/.profile, or fish's config.fish).

curl -fsSL https://getstream.io/cli/beta/install.sh | bash # the agents commands are in the beta CLI
getstream --version # a beta build shows a date and commit, such as 2026-10-14-3178aae

getstream init # pick or create a Stream app

Create an agent

As a first step lets define an agent. Each agent consists of a folder with this structure:

agents/my-agent/
  knowledge/       ← indexed markdown and URLs
    faq.md
    urls.yaml
  skills/          ← skills loaded as needed
    research.md
  agent.yaml       ← config for the agent
  instructions.md  ← system prompt
  guardrail.md     ← prevent abuse

The quickest way to create an agent folder is to use one of the templates offered by the beta CLI from Stream app setup:

getstream agents init my-agent --template text # or --template voice

You'll see the agents/my-agent/ folder appear, with a lot of settings to play with. Start by editing the system prompt in instructions.md to match your use case. When ready, run:

getstream agents sync my-agent

Now your agent is synced to Stream. Lets talk to your agent and see how it does:

getstream agents test my-agent

Ask it about Acme first, such as "How do I get started with Acme?". The template's guardrail turns away questions that are not about Acme Corp.

As a next step lets edit agent.yaml. We'll enable GitHub. This agent doesn't need to run code, so lets leave the sandbox disabled.

agents/my-agent/agent.yaml
name: my-agent
mode: text
# the model to use, can reference a router or a model
llm: llm-fast
# connectors & MCP
connectors:
  - name: github
    connection: company # one GitHub account your app owns
# no sandbox: this agent doesn't run code

connection: company is one GitHub account your app owns, so nobody signs in. Create it first, as in Shared account.

Run the agent and ask: "What are the open issues in GetStream/awesome-ai-news?"

getstream agents test my-agent --sync

These building blocks:

  • Skills
  • Knowledge
  • Tools
  • Guardrail
  • Sandbox

Enable you to build any agent. As a next step lets setup tool calling.

Tool Calling

Tool calling makes it easy to integrate agents with your systems.

If you're building a customer support bot you typically want tool calling for:

  • Creating a ticket
  • Looking up a ticket
  • Replying to a customer etc

If you're building legal AI you might want to expose various legal databases through tool calling.

The example below how to setup a tool for a weather lookup:

using GetStream.VisionAgents;

using var client = new VisionAgentsClient();
var agent = client.Agent("my-agent");

agent.Tools.Register<WeatherLookup, string>("weather_lookup",
    "Look up the current weather for a location",
    (lookup, _) => Task.FromResult(Forecast(lookup.Location)));

await new Dispatch(new DispatchOptions { Client = client }).Host(agent).RunAsync();

static string Forecast(string location) => location.ToLowerInvariant() switch
{
    var place when place.Contains("colorado") => "sunny",
    var place when place.Contains("amsterdam") => "rainy",
    var place when place.Contains("san francisco") => "foggy",
    _ => $"no forecast for {location}",
};

record WeatherLookup(string Location);

Leave the worker running. The template's guardrail only answers questions about Acme, so let the weather through in agents/my-agent/guardrail.md and sync again:

Answer questions about Acme Corp: its products, plans, pricing, accounts and support,
and questions about the weather.
Greetings, thanks, goodbyes and questions about who the agent is or what it can help with
are permitted too.

Now ask the agent about the weather in Amsterdam, Colorado or San Francisco. The model calls weather_lookup, the worker answers, and the agent replies with it:

getstream agents test my-agent --sync

Approval, interruptions, progress updates and the rest are in Tool calling & Dispatch. For a runnable Go agent with tools, knowledge and memory together, see the caregiving example.

Agent UI

Next you typically want to build your agent UI. On either React, Android or iOS.

  1. Install the SDK
npm install @stream-io/calypso stream-chat stream-chat-react @stream-io/video-react-sdk
  1. Mint a token

Your users sign in with a Stream user token. Mint one for a test user with the CLI.

getstream token jlahey --ttl 2h

Note: In production you would generate the token in your backend when a user signs up or logs in.

  1. Create a session

Open a session with your agent and ask it something:

import { Client, type Session } from "@stream-io/calypso";

// Stream's hosted router. Self-hosting covers pointing it at your own.
export const api = new Client({ apiKey: "your_api_key" });

/** Call once, when the user signs in. */
export async function signIn() {
  await api.setUser("jlahey", "paste_the_token_here");
}

/** Opens a session with the agent and asks it something. */
export async function startSession(): Promise<Session> {
  const session = await api.agent("my-agent").sessions.create();
  await session.responses.create("How do I get started with Acme?");
  return session;
}
  1. Add the chat UI

The chat UI components support thinking, tool calling, markdown, table responses, code, and UI the agent writes in A2UI or OpenUI. The session's channel is its transcript: the agent does not answer a message sent to the channel, so the composer sends with session.responses.create, and the router writes your message into the channel itself. This holds for voice sessions too. Each thing the caller says is written as a message, the agent's reply streams into its message while it is generated and settles once it is spoken, and a reply the caller cut off is marked interrupted, with its text usually cleared. So render the channel for the transcript and live captions; don't build a second transcript from session.events(). Have a look how you can show the UI:

In JavaScript, import @stream-io/calypso/chat once, or session.chat() throws (@stream-io/calypso/video does the same for session.video()).

import "@stream-io/calypso/chat";
import type { Session } from "@stream-io/calypso";
import { useMemo } from "react";
import type { Channel as ChatChannel, StreamChat } from "stream-chat";
import {
  AIStateIndicator,
  Channel,
  Chat,
  MessageComposer,
  MessageList,
  WithComponents,
} from "stream-chat-react";
import "stream-chat-react/css/index.css";

// The agent's replies carry `ai_generated: true`.
declare module "stream-chat" {
  interface CustomMessageData {
    ai_generated?: boolean;
  }
}

/** Opens the session's channel, as the chat's views show it. */
export async function openChat(session: Session) {
  // `session.chat()` connects to Stream Chat with the user set above.
  const { client, channel } = await session.chat();
  return {
    session,
    client: client as StreamChat,
    channel: channel as ChatChannel,
  };
}

export type AgentChat = Awaited<ReturnType<typeof openChat>>;

export const AgentConversation = ({ chat }: { chat: AgentChat }) => {
  // While the agent answers, the composer shows a Stop button. Its default sends
  // `ai_indicator.stop` to the channel, which the agent does not act on, so stop
  // the reply through the session instead.
  const StopButton = useMemo(
    () => () => (
      <button
        aria-label="Stop"
        className="str-chat__stop-ai-generation-button"
        onClick={() => chat.session.interrupt()}
        type="button"
      />
    ),
    [chat.session],
  );

  return (
    <Chat
      client={chat.client}
      // Renders the agent's replies as streaming markdown.
      isMessageAIGenerated={(message) => Boolean(message.ai_generated)}
    >
      <WithComponents overrides={{ StopAIGenerationButton: StopButton }}>
        <Channel channel={chat.channel}>
          <MessageList />
          {/* Thinking and generating states, for text and voice sessions. */}
          <AIStateIndicator />
          <MessageComposer
            overrideSubmitHandler={async ({ message }) => {
              await chat.session.responses.create(message.text ?? "");
            }}
          />
        </Channel>
      </WithComponents>
    </Chat>
  );
};

In React, isMessageAIGenerated makes MessageList render the agent's replies with streaming markdown, and AIStateIndicator shows while the agent thinks or writes. The React AI integration guide covers both. For a custom message, use the AI components such as StreamingMessage and AIMarkdown. AIConversation, among them, lays the conversation out by turn with each reply's reasoning, tool calls, approvals and sign-ins. The React assistant sample builds a complete app on them, with a list of conversations, photos, voice mode, connector sign-ins and tools that run in the browser. Step 5 adds the call audio and a listening, thinking or speaking status.

  1. Add the voice UI

A voice session puts the agent on a call. Your app joins that call, and the agent hears the user and answers out loud. What is said on the call is written into the session's channel, so the chat's views show the transcript here too. Add a glow that shows whether the agent is listening, thinking or speaking, and buttons to mute and hang up:

import { VoiceSession } from "@stream-io/calypso/video";
import {
  ParticipantsAudio,
  StreamCall,
  StreamVideo,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useEffect, useSyncExternalStore } from "react";
import { Channel, Chat, MessageList } from "stream-chat-react";
import { VoiceGlow } from "stream-chat-react/ai-components";

/** Starts a session with voice on, and opens its transcript. Call it from a click. */
export async function startVoice() {
  // The agent joins the session's call right away.
  const voice = await VoiceSession.start(api.agent("my-agent"));
  // What is said on the call is written into the session's channel, so the chat's views show it.
  return { voice, chat: await openChat(voice.session) };
}

export const AgentVoice = ({
  voice,
  chat,
}: {
  voice: VoiceSession;
  chat: AgentChat;
}) => {
  // What the agent is doing, in the states the glow takes.
  const { call, client, muted, status } = useSyncExternalStore(
    voice.subscribe,
    voice.get,
  );

  // Joins the call from this device, with the microphone on. Run twice, it joins once.
  useEffect(() => {
    voice
      .join()
      .catch((error: unknown) =>
        console.error("Could not join the call", error),
      );
  }, [voice]);

  return (
    <div>
      <VoiceGlow state={status} style={{ width: 240, height: 240 }} />
      {call && client && (
        <StreamVideo client={client}>
          <StreamCall call={call}>
            <AgentAudio />
          </StreamCall>
        </StreamVideo>
      )}
      {/* What the user said and the agent's replies, as in the chat. */}
      <Chat
        client={chat.client}
        isMessageAIGenerated={(message) => Boolean(message.ai_generated)}
      >
        <Channel channel={chat.channel}>
          <MessageList />
        </Channel>
      </Chat>
      <button onClick={() => void voice.setMuted(!muted)}>
        {muted ? "Unmute" : "Mute"}
      </button>
      {/* Leaves the call and ends the session this page started. */}
      <button onClick={() => void voice.leave()}>Hang up</button>
    </div>
  );
};

/** Plays the agent. */
const AgentAudio = () => {
  const { useRemoteParticipants } = useCallStateHooks();
  return <ParticipantsAudio participants={useRemoteParticipants()} />;
};

VoiceSession joins the call with Stream Video, signed in as the user you passed to setUser. join() rejects if joining fails, for example because the user denied microphone access. Call startVoice() from a click: a browser asks for the microphone and plays audio only after one. voice.session is a regular Session, so openChat from step 4 works here too, and the agent's replies stream into the transcript as they're generated. status is worked out for you: speaking comes from the call, and the agent is thinking until its reply is heard, since a reply is written before it is spoken. VoiceSession is in @stream-io/calypso from version 0.2.3. Import it from @stream-io/calypso/video, which brings in @stream-io/video-client with the Video React SDK from step 1, so don't install that separately.

VoiceGlow is one of the React SDK's AI components, in stream-chat-react/ai-components from version 14.14. For a waveform instead of a glow, use VoiceWave, which takes the same states. To show the agent what the camera sees, join with voice.join({ camera: true }) or call voice.setCameraEnabled(true). The camera starts on the back lens where there is one, and the agent looks through it when asked, if its config has the vision skill and a vision subagent.

An agent made from the text template talks with the router's default speech-to-text and text-to-speech models. Make it with --template voice to choose those models and add a greeting. To switch an open chat to voice, call session.voice.start(), then join with VoiceSession.of(session). Leaving then leaves the session running; voice.end() ends it. The voice mode of the React assistant sample does this.