Vue Embedded AnalyticsDirect LLM Runner

Version 3.0.0

The Direct LLM Runner runs the agent loop in the page.

Each turn, the agent asks your adapter for a completion, executes any tools the model called, and goes round again. Studio drives that loop, so this runner is the least you have to build.

Use one of the others when the model must be called from your infrastructure (Client Tool Runner) or the loop is code you own (Custom Runner).

Declaring the Agent Copy Link

directLlmRunner builds one. Its adapter, instructions and tools all sit on the config:

// Your AgLlmAdapter, as built in The Adapter below.
const adapter = myOpenAiAdapter({ endpoint: '/api/llm' });

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

Instructions and tools are callbacks, re-resolved every run - see Agent Overview.

To run Studio's five agents on one model, you do not have to declare them at all. Pass the adapter to createAiHarness and it pairs each built-in agent with this runner for you:

<ag-studio
    :ai="ai"
    /* other studio properties ... */>
</ag-studio>


// Your AgLlmAdapter, as built in The Adapter below.
const adapter = myOpenAiAdapter({ endpoint: '/this.studioApi/llm' }); 
this.ai = ({ api }) => createAiHarness(api, { adapter });

For a roster of your own, list the agents on the harness config - see Built-in Harness.

The Adapter Copy Link

AG Studio is provider-agnostic and bundles no connection to any LLM. You supply an adapter implementing AgLlmAdapter, which translates between Studio's request format and whatever your provider expects. An agent running its own loop needs no adapter, because the provider call happens there.

The example below ships a complete OpenAI adapter. Copy it as a starting point and adapt it to your provider.

The AgLlmAdapter Interface Copy Link

The adapter is a plain object. Its one required method is executeTurn.

executeTurnCopy Link
Function
Execute a single turn of conversation with the AI. A turn consists of sending input and receiving a streamed response. options.signal aborts the in-flight turn when the run is cancelled; pass it to the transport (e.g. fetch). It is deliberately kept off AgLlmRequest so adapters can spread request into a provider body without leaking it.

executeTurn Copy Link

executeTurn is called each time Studio needs an AI response. It receives an AgLlmRequest and must return an AgLlmResponseHandler synchronously. The handler exposes a live stream and a completion promise.

The Request Copy Link

Each call to executeTurn receives everything the model needs for one turn.

AgAiConversationItem[]
Conversation history to send to the AI, providing context.
instructionsCopy Link
string
System instructions for this specific turn, overriding defaults.
AgAiToolSchema[]
Tools available for the AI to use during this turn.
toolChoiceCopy Link
"none" | "required" | "auto" | AgLlmToolChoice
Strategy for how the AI should choose tools.
  • 'auto': AI decides whether to use tools
  • 'none': AI must not use tools
  • 'required': AI must use at least one tool
  • AgLlmToolChoice: AI must use the specified tool
  • responseFormatCopy Link
    AgLlmJsonFormat | AgLlmTextFormat
    Output format configuration controlling response structure.
    AgAiModelSelection
    Which model to answer with, and how much effort to spend, as chosen for this turn. Present only when the chat was configured with models to choose between; map it onto whatever your provider calls these. An adapter that ignores it always uses its own default model.

    Choosing the Model Copy Link

    request.model is present only when the chat was configured with models to choose between. It carries the id and effort of whichever the reader picked, exactly as you declared them. Use ids your provider already understands and the mapping is a pass-through:

    const body = {
        model: request.model?.id ?? 'gpt-5.6-terra',
        reasoning: request.model?.effort ? { effort: request.model.effort } : undefined,
        // ...
    };

    An adapter that ignores the field always answers on its own model, as it does when no models are configured.

    The Response Copy Link

    executeTurn returns an AgLlmResponseHandler: a stream of incremental events, and a complete promise that resolves with the final response.

    streamCopy Link
    { [asyncIterator]: any }
    The turn's content as it arrives: the message, reasoning and tool-call events of AgAiEvent. A turn is part of a run, so an adapter emits content events only - the run and step boundaries belong to whatever is driving the loop.
    completeCopy Link
    Promise<AgLlmResponse>
    Promise that resolves when the response is fully complete. Contains the final, consolidated response data.

    The final response has this shape:

    string
    Unique identifier for this response.
    createdAtCopy Link
    number
    Timestamp when the response was created (milliseconds since epoch).
    AgLlmResponseError
    Error details if the response failed.
    incompleteDetailsCopy Link
    AgLlmResponseIncompleteDetails
    Details about why the response was incomplete. Present when the AI couldn't fully complete its response.
    outputCopy Link
    AgAiOutputItem[]
    Output items produced by the AI (messages, tool calls, reasoning).
    statusCopy Link
    "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete"
    Current status of the response.
    AgAiUsage
    Token usage for this turn, when the adapter surfaces it. Consumed for observability (per-turn/run token + cost metrics); optional so an adapter that can't report it is valid.
    string
    The model that produced this response, when the adapter reports it (used for per-turn cost).

    Stream Events Copy Link

    The stream yields AgAiEvent values, the same AG-UI vocabulary every runner emits - see AG-UI Compatibility. A turn is one part of a run, so an adapter emits content events only: the run and step boundaries belong to whatever drives the loop, which here is Studio.

    EventDescription
    TEXT_MESSAGE_STARTAn assistant message has begun, under the messageId the later events reference.
    TEXT_MESSAGE_CONTENTA delta of answer text to append to that message.
    TEXT_MESSAGE_ENDThe message is finished.
    REASONING_MESSAGE_STARTReasoning has begun, rendered in the panel apart from the answer.
    REASONING_MESSAGE_CONTENTA delta of reasoning text.
    REASONING_MESSAGE_ENDThe reasoning is finished.
    TOOL_CALL_STARTThe model has called a tool, named by toolCallName under a toolCallId.
    TOOL_CALL_ARGSA delta of that call's JSON arguments.
    TOOL_CALL_ENDThe call's arguments are complete.
    RAWA provider event with no Studio equivalent, carried through untouched.

    Each of the three start/content/end sequences has a *_CHUNK shorthand - TEXT_MESSAGE_CHUNK, REASONING_MESSAGE_CHUNK, TOOL_CALL_CHUNK - which condenses the sequence into repeated single events, the id omitted to continue what the last chunk began. Both forms describe the same thing and Studio reads either, so emit whichever is the shorter translation from your provider's stream.

    A failed turn is reported on the complete promise's AgLlmResponse, through status and error, rather than as a stream event.

    Tool Calls Copy Link

    The adapter does not execute tools. It only:

    1. Passes the AgAiToolSchema[] in request.tools to the LLM.
    2. Relays the tool-call output items from the LLM back through the stream.
    string
    Tool name.
    descriptionCopy Link
    string
    Human-readable description for the LLM.
    JSON Schema describing the tool's parameters.
    AgAiToolKind
    Where the tool executes (default client). See AgAiToolKind.
    providerCopy Link
    unknown
    For a provided tool: the provider's own declaration of it, passed through to the adapter untouched. Studio never reads this - it is whatever shape your provider expects (for OpenAI's hosted web search, { type: 'web_search' }).

    The agent intercepts those tool calls, executes them, and feeds the results back as function_call_output items on the next turn. Your adapter never needs to know what view_schema or configure_widget do.

    Handle one exception: a provider-hosted tool carries the provider's own declaration on provider, and your adapter has to pass it through in the provider's native form rather than converting it to a function schema.

    const tools = request.tools?.map((tool) =>
        tool.kind === 'provided' ? tool.provider : { type: 'function', function: toFunctionSchema(tool) }
    );

    Keeping Keys Off the Client Copy Link

    executeTurn runs in the browser, so calling a provider directly exposes your API key. For production, point executeTurn at your own backend endpoint instead: forward the AgLlmRequest, call the provider server-side with your secret key, and stream the response back. The adapter contract is unchanged - only the URL it calls differs.

    Next Copy Link