Skip to main content

How is an agent built?

Imagine you’re building a conversational agent, like a customer support chatbot. The first thing you’re gonna need is a HTTP endpoint that takes user input and generates AI responses. We call such endpoint a Chat Endpoint. Here’s an example of the endpoint in TS+Hono:
const app = new Hono();

app.post('/chat', async (c) => {
  const { history, input } = await c.req.json();
  const output = await generateResponse([...history, input]);
  return c.json({ output });
});
We put all the AI logic into a stateless function called generateResponse. Here’s an example of such function using OpenAI Responses API:
const generateResponse = async (messages: any[]) => {
  const response = await client.responses.create({
      model: "gpt-5-nano",
      reasoning: { effort: "low", summary: "detailed" },
      instructions: "You are a helpful assistant.",
      input: messages,
    });

  return response.output;
};
For the demo purposes generateResponse is very simple, it’s just a single LLM call. In reality all the AI Engineering will go there: prompts, tools, RAG, etc. The core idea behind AgentView is that you can build your AI logic however you want. You can use an AI framework like LangGraph, AI SDK, or go vanilla. Actually, your Chat Endpoint can be in Python, Typescript or any other language you prefer. AgentView by design doesn’t focus on the AI part, but on “everything around the AI”.

There’s a lot of work around your AI logic

Even if you made your AI logic work well enough, there’s still a lot to do. For example, our Chat Endpoint doesn’t provide persistence. If you build a web UI to play with your agent you’ll have to store conversation history in localStorage and send it all back and forth between the client and the server. Of course, it’s not something you want in production. AgentView is a backend for conversational agents that handles such problems for you. Here’s how you can add persistence to your Chat Endpoint simply by adding a bunch of AgentView SDK calls:
const av = new AgentView({
  apiKey: process.env.AGENTVIEW_API_KEY
})

app.post('/chat', async (c) => {
  const { index, input } = await c.req.json(); 

  const session = id ?
    await av.getSession({ id }) : 
    await av.createSession({ agent: "my_agent" });

  const history = session.items;

  const run = await av.createRun({ 
    sessionId: session.id,
    items: [input], 
    version: "0.0.1"
  });

  const response = await generateResponse([...history, input]);

  await av.updateRun({
    id: run.id,
    status: "completed",
    items: response.output
  });

  return c.json({
    id: session.id,
    output: response.output
  });
})
We also need to register my_agent in the AgentView configuration file (all the configuration is in code):
export default defineConfig({
  agents: [
    {
      name: "my_agent",
      runs: [
        {
          input: {
            schema: z.object({
              type: z.literal("message"),
              role: z.literal("user"),
              content: z.string(),
            }),
          },
          output: {
            schema: z.looseObject({
              type: z.literal("message"),
              role: z.literal("assistant"),
              content: z.array(z.object({
                type: z.literal("output_text"),
                text: z.string(),
              })),
            }),
          }
        }
      ]
    }
  ]
})
This is all you need to add quite a lot of features to your Chat Endpoint:
  1. Persistence. The Chat Endpoint is stateful now. It just takes session id and the user input, remembers the entire conversation history and sends it back to the endpoint.
  2. Validation. The endpoint is validated against the schema provided by you.
  3. Locking. AgentView manages session lifecycle via “Runs”. When you create a new run the session is locked until the run is completed. Such mechanism prevents concurrent requests to the same session.
  4. Multi-session. Users can easily create new sessions simply by leaving session_id empty.
  5. Versioning. Every run gets a current agent version, in this case 0.0.1. If you make a breaking change in your agent and the new version is not semver-compatible, the run will fail. This mechanism protects from continuation of sessions incompatible with new agent versions.
This example was pretty simple. In reality there’s much more to consider: grouping sessions by users, security, resumes, retries, handling disconnections, etc. The goal of AgentView is to handle all these things for you so you can focus 100% on AI Engineering.

You need UI too

As you progress you’ll quickly realise there’s also a big chunk of front-end work to be done. No one described it better than Hamel, so let’s just quote him (btw, if you build agents, Hamel is a must-read):

The Most Important AI Investment: A Simple Data Viewer

The single most impactful investment I’ve seen AI teams make isn’t a fancy evaluation dashboard – it’s building a customized interface that lets anyone examine what their AI is actually doing. I emphasize customized because every domain has unique needs that off-the-shelf tools rarely address. When reviewing apartment leasing conversations, you need to see the full chat history and scheduling context. For real estate queries, you need the property details and source documents right there. Even small UX decisions – like where to place metadata or which filters to expose – can make the difference between a tool people actually use and one they avoid.
In summary, you need an environment where people can play with your agent and collaborate on the outputs. It’s oddly similar to a CMS. You might build a website, but there’s still need to be an app where non-technical users can work on it: collaborate on content, preview, etc. To serve this need AgentView comes with the Studio. It’s based on following principles:
  • Customisable and domain-specific - it should be tailored to the specific domain of your agent.
  • Collaborative - it should be possible to collaborate with your team on the data.
  • Great UX - it must be extremely easy to use for humans, especially non-technical ones, as they’re often the domain experts that will tell you if your agent is working as expected.
  • Beautiful - because things just should be beautiful
In order to allow for customisation, Studio is not a standard centralised SaaS front-end. It’s a React package you run locally, and then host it on your own domain as a static site. Such architecture makes it trivial to customise any part of the experience simply by providing custom React components in your codebase. It makes Studio vibe-codeable, which we believe is the future. We provide design system and good practices, but you’re free to override. Here’s how you can provide custom components for your agent items and the input component for playground:
export default defineConfig({
  agents: [
    {
      name: "simple_chat",
      runs: [
        {
          input: {
            schema: z.object({
              type: z.literal("message"),
              role: z.literal("user"),
            content: z.string(),
            }),
            displayComponent: ({ item }) => <UserMessage>{item.content}</UserMessage>,
          },
          steps: [
            {
              schema: z.looseObject({
                type: z.literal("reasoning"),
                content: z.array(z.object({
                  type: z.literal("input_text"),
                  text: z.string(),
                })),
              }),
              displayComponent: ({ item }) => <Step collapsible>  
                <StepTitle><Brain /> Thinking</StepTitle>
                <StepContent>
                  {item.content?.map(s => s.text).join("\n\n")}
                </StepContent>
              </Step>
            },
          ],
          output: {
            schema: z.looseObject({
              type: z.literal("message"),
              role: z.literal("assistant"),
              content: z.array(z.object({
                type: z.literal("output_text"),
                text: z.string(),
              })),
            }),
            displayComponent: ({ item }) => <AssistantMessage>{item.content.map(c => c.text).join("\n\n")}</AssistantMessage>,
          }
        }
      ],
      inputComponent: ({ submit, cancel, isRunning, session }) => <UserMessageInput
        onSubmit={(val) => {
          submit("http://localhost:3000/weather-chat", {
            id: session.id,
            input: {
              type: "message",
              role: "user",
              content: val,
            }
          })
        }}
        onCancel={cancel}
        isRunning={isRunning}
      />
    }
  ]
});
This is all you need to allow for following features:
  • browse sessions
  • invite team members
  • comment on every session item in a sidebar like in Google Docs
  • get notified on the new activity
  • create new playground sessions and share them with teammates
Studio allows you to customise everything: session cards, scores, custom pages, lists, new session screen to prefill required metadata, etc.