Angular Embedded AnalyticsAgent Overview

Version 3.0.0

An agent runs a conversation to completion. Given a run's input and history, it works - calling a model, calling tools, possibly delegating - and emits a stream of events describing what happened.

An agent is a definition - who it is, what it may do - plus a run that answers for it. The definition is inert on its own, so the same one can be driven by any of the three runners below.

The Contract Copy Link

Every agent, however built, is this:

string
Stable identifier; referenced by AgAiThreadSummary.agentId.
string
Name a reader sees for this agent - in the chat panel's roster, and on the step where another agent hands work to it. Defaults to AgAiAgentDefinition.id, which is rarely what a reader wants to read.
descriptionCopy Link
string
Short description shown to a delegating agent when this agent is a delegation target, so it can decide when to hand off.
schemaCopy Link
Function
Builds the shape validating this agent's delegation parameters. Return s.undefined() for a param-less agent, or e.g. s.object({ region: s.string() }) for one invoked with parameters. A delegating agent's delegate_to tool advertises these so it can supply typed params.
instructionsCopy Link
Function
System instructions for a run. Re-resolved as the run progresses rather than read once, so a prompt built from the current schema or widget catalogue stays current.
Function
The tools this agent has. Re-resolved as the run progresses, because a tool's advertised schema reads live dashboard state and may be uncallable on any given turn. Who executes them depends on the loop: directLlmRunner and clientToolRunner both run them for you, while an agent with its own run executes them itself through AgAiTool.execute.
defaultToolCallsCopy Link
Function
Tools called automatically before the first turn; their results seed the conversation so the agent starts with that context. args are the tool's full call arguments, exactly as its schema expects them. Only run by a loop Studio owns.
maxTurnsCopy Link
number
How many rounds this agent may take before the run is stopped, as a backstop against one that never settles. Defaults to 100, which suits a conversational agent; raise it for one that works through a long list, lower it for one expected to answer in a couple of rounds. Only a loop Studio owns counts rounds, so an agent with its own run decides for itself.

id names it. description is shown to a delegating agent so it can decide when to hand off. schema declares the parameters it can be delegated with. instructions and tools are callbacks, re-resolved as a run progresses. maxTurns caps how many rounds Studio will run before it stops.

A definition plus a run is an AgAiAgentRunner, which is what the harness takes. The runner supplies the run.

The Three Runners Copy Link

There are three things you can bring, so there are three runners, divided by who answers each turn and who executes the tools.

Direct LLM Runner Copy Link

You bring a model. Studio owns everything else: the turn loop, the conversation, the prompt, and executing the agent's tools.

directLlmRunner({
    id: 'analyst',
    adapter: myAdapter,
    instructions: () => 'You build dashboards for a team of climate analysts.',
    tools: () => [studio.viewSchema(), studio.executeQuery(), studio.addWidget()],
});

Start here. "Direct" refers to the absence of an intermediate agent, not to Studio calling a provider itself: every call goes through the adapter you supply. See Direct LLM Runner.

Client Tool Runner Copy Link

You bring a request. Studio calls it once per round, executes every tool call your answer left unresolved, appends the results, and calls you again until nothing is outstanding.

clientToolRunner({
    id: 'analyst',
    run: (input, ctx) => myServer.stream(input, { signal: ctx.signal }),
    tools: () => [studio.addWidget(), studio.configureWidget({ widgetType, widgetId })],
});

Use it when the turns come from somewhere that cannot reach into the page. See Client Tool Runner.

Custom Runner Copy Link

You bring a whole loop, one that already resolves its own tool calls. There is no factory for it, because Studio adds nothing:

{
    id: 'analyst',
    tools: () => [studio.addWidget()],
    run: myOwnLoop,
}

The run boundaries, the round loop and the telemetry are all yours to emit. See Custom Runner, which also covers wrapping an agent SDK.

Which To Use Copy Link

You bringStudio runs the roundsStudio executes tools
directLlmRunnerA modelYesYes
clientToolRunnerA requestYesAny your answer left open
{ ...def, run }A loopNoNo

One rule covers the first two: Studio executes every tool call the turn did not resolve itself. A model never resolves one; a request whose far side owns some tools resolves those and leaves the rest.

AG-UI Compatibility Copy Link

Whatever the runner, an agent answers in events, and that vocabulary is AG-UI. Studio's AgAiEvent declarations mirror the AG-UI wire shapes: RUN_STARTED, TEXT_MESSAGE_CONTENT, TOOL_CALL_START and the rest carry the same discriminator and the same fields. An AG-UI-native agent or server therefore drives a Client Tool Runner or a Custom Runner with no translation layer in between, and an Adapter emits the content half of the same vocabulary.

Those declarations are hand-authored rather than imported. AG Studio ships no third-party runtime dependencies, so there is no @ag-ui/* package to install and nothing to keep in step at install time.

The two are structurally compatible, not guaranteed identical. AG-UI can add or change an event that Studio's types have not yet followed, so treat AgAiEvent as what Studio reads and check it against the protocol when you rely on a newer event. An event Studio does not declare can still travel as RAW, which carries the source event untouched, or as CUSTOM, which carries a name and a value of your own.

Agents Are Stateless Copy Link

An agent holds no conversation state. Instructions, tools and delegation parameters are all resolved per run, from callbacks:

directLlmRunner({
    id: 'analyst',
    adapter: myAdapter,
    // Re-read on every run, so a new field or widget shows up without rebuilding the agent.
    instructions: () => `Available data:\n${describe(api.getAiContext().schema())}`,
    tools: () => [studio.viewSchema(), studio.executeQuery()],
});

Do not snapshot instructions or tools into a variable at construction time. A static tool array freezes live enums: a widget added after construction will not appear in a tool's schema, and a delegate list built once will not include agents registered later.

Rosters and Delegation Copy Link

The harness holds the agents. One is primary: the agent a new thread starts from, and the only one the user picks directly. The rest are delegate-only, reached through delegate_to.

createAiHarness(api, ({ tools }) => ({
    agents: [lead, dataAgent, widgetAgent],
    primary: 'lead',
}));

A delegating agent lists tools.delegateTo(['data', 'widget']). That tool's schema enumerates each target's schema parameters, resolved per turn, and the tool is withheld entirely on a turn where no target resolves. Calling it starts a child run in its own thread, which reports back through a complete_task tool that Studio appends automatically. The chat panel renders a child run inline, expandable.

Next Copy Link