hashbrown

Shared Agent State

Shared agent state is a JSON value exchanged alongside a conversation. It gives the application and agent a common structured view of a task: selected filters, an editing draft, or a workflow stage. It is separate from message text and from application data the agent is not allowed to change.

Use it when both sides need to observe and update the same small document. If the assistant only needs to look up the current selection or inventory, start with a read-only tool; you do not need to make that application data shared agent state.

Your agent must explicitly validate and map that value into the model context. Passing state to a direct provider adapter does not automatically turn it into instructions, and setting state does not start a request.

State travels beside the conversation

Scroll horizontally to see the full diagram, or expand it.

ApplicationAG-UI agentModel1 History and JSONstate2 Validated stateto context3 Return modeloutput4 STATE_SNAPSHOT orSTATE_DELTA5 Success commitsdraft
The agent explicitly maps validated state into model context. Updating local state alone sends no request.
Read the diagram

The application supplies JSON state in a request. The agent validates it and maps relevant values into model context. State snapshots or deltas update the client draft, which commits on successful completion or rolls back on a failed attempt.

State travels beside the conversation

The agent explicitly maps validated state into model context. Updating local state alone sends no request.

Read the diagram

The application supplies JSON state in a request. The agent validates it and maps relevant values into model context. State snapshots or deltas update the client draft, which commits on successful completion or rolls back on a failed attempt.

Share a small editing document

Use the endpoint and client configuration from the Quickstart. Keep provideHashbrown in appConfig and the <app-root> host. Import the standalone class below into main.ts and pass it to bootstrapApplication() with that same appConfig. Point this component at an AG-UI agent that validates this state shape and explicitly uses it as context. A direct provider adapter alone does not implement that mapping.

The app sends a reading preference separately from conversation text. The button replaces the entire state value while idle; asking for a suggestion starts generation separately.

import { Component } from '@angular/core';
import { chatResource } from '@hashbrownai/angular';

@Component({
  selector: 'app-root',
  template: `
    <p>Preference: {{ chat.state()?.genre }}</p>
    <button
      type="button"
      [disabled]="chat.isLoading()"
      (click)="setPreference()"
    >
      Choose mystery
    </button>
    <button type="button" [disabled]="chat.isLoading()" (click)="ask()">
      Ask for a suggestion
    </button>
    @if (chat.error()) {
      <p role="alert">The request failed.</p>
    }
    @for (message of chat.value(); track $index) {
      @if (message.role === 'assistant') {
        <p>{{ message.content }}</p>
      }
    }
  `,
})
export class ReadingState {
  readonly chat = chatResource({
    system: 'Help choose a book using the preferences supplied by the agent.',
    state: { genre: 'classics', stage: 'choosing' },
  });
  setPreference(): void {
    this.chat.setState({ genre: 'mystery', stage: 'choosing' });
  }
  ask(): void {
    this.chat.sendMessage({
      role: 'user',
      content: 'Suggest a book for this preference.',
    });
  }
}

This minimal feature assumes no approval pauses. If your agent can interrupt, also respect pendingInterrupts, isResuming, and recovery when enabling controls. setState accepts JSON-compatible values and replaces rather than merges. It rejects writes while generation is scheduled or running.

During a run, agent state updates are drafts. Successful completion commits them; a failed or cancelled attempt rolls them back to its starting checkpoint. Keep an editable form draft separate so the user can see which value has actually been sent.

Explore it in the workbench

The workbench below replays a recorded run of this lesson.

Set state without a request, then follow agent drafts, commit, and a second turn.

Recorded in the real Angular workbench with OpenAI and the actual Hashbrown SDK. The local AG-UI agent maps shared state into instructions and authors state, activity and approval events around unchanged provider text. No browser tools, generated UI schema or executed plan steps. Approval permits another explanation only. The approval store is bounded, expiring and process-local. Unchanged provider events are paced at 20 ms for filming; the normal host adds no delay. Footage slowed to half speed for readability. Replay makes no requests or writes. No narration. Read the text walkthrough.

Open full workbench
Open full workbench

Let's keep the same two-step plan: outline a trusted UI demo, then record it. Change its title in the app and select Set shared state. Nothing runs yet. Send a message, and watch the agent read that value, stream its reply, and update the shared state.

The State inspector follows the selected timeline frame. The title field is an application form draft; the SDK state is a separate value. You can see exactly when the app replaces it and when the agent updates it. The captured side shows SDK state and request flags. The other side shows the local form and accepted application plan. Choose Raw for the full selected SDK observation.

Use the panel above, or run the workbench on your machine. The replay uses the checked Angular client and local agent.

The client makes real Hashbrown requests. The local AG-UI agent explicitly maps shared state into model instructions and wraps the provider's streamed text with workflow events. Direct provider adapters do not create these events or approval decisions automatically.

Initialize a Small State Value

Keep shared state focused on the context both the app and agent need. Our plan has a title and a status. The two plan steps remain application-owned data.

runtime/workbench-workflows.ts
export interface WorkbenchWorkflowState {
  readonly planTitle: string;
  readonly status: 'idle' | 'draft' | 'ready';
}
/** The same application-owned plan is used in all three workflow lessons. */
export const workbenchWorkflowPlan = {
  title: 'Show how a model renders trusted UI',
  steps: ['Outline the trusted UI demo.', 'Record the demo.'],
} as const;
/** Initial shared SDK state; editing it does not start generation. */
export const workbenchWorkflowInitialState: WorkbenchWorkflowState = {
  planTitle: workbenchWorkflowPlan.title,
  status: 'idle',
};

Pass state: workbenchWorkflowInitialState to chatResource. Read the current value from chat.state(). TypeScript infers the state type from the initial value; without an initial value, state starts as undefined.

Replace State Explicitly

setState replaces the complete value. It does not merge objects and does not start a generation. The workbench first records a local title draft, then calls the SDK setter when you select Set shared state. Send starts the request separately.

angular/src/app/workflow-workbench.ts
  applyState(): void {
    if (!canEditWorkflowState(this.store.getSnapshot())) return;
    const state = {
      ...(this.chat.state() ?? workbenchWorkflowInitialState),
      planTitle:
        readWorkflowForm(this.store.getSnapshot()).title ?? this.title(),
    };
    if (!isWorkbenchWorkflowState(state)) return;
    try {
      this.chat.setState(state);
    } catch (error) {
      this.localError.set(String(error));
    }
  }

Values must be JSON-compatible: null, strings, booleans, finite numbers, arrays, and plain objects. Nested undefined, sparse arrays, cycles, functions, symbols, bigints, non-finite numbers, and class instances such as Date are rejected. Invalid values throw synchronously and preserve the previous state.

Follow Drafts and Commits

The agent emits STATE_SNAPSHOT with status draft. After the provider completes its explanation, it emits STATE_DELTA to replace that status with ready. The inspector labels state as a draft while the request is running. Seeing the final patch does not, by itself, prove the run committed.

A successful run commits the draft. A failed, cancelled, retired, or retried attempt rolls back to its starting checkpoint. This local agent treats a truncated, refused, filtered, or missing provider completion as an error, so it cannot publish those attempts as a completed workflow.

setState throws while a logical generation is scheduled or running, including debounce, retry backoff, and retries. An interrupt commits a checkpoint; the SDK allows local state changes while paused and locks writes again when resume is scheduled. This workbench keeps its title editor locked during approvals so the form stays focused on the pending decisions.

Keep State and Model Context Explicit

Shared state travels in the AG-UI request. Your agent must decide how to use it. Our local agent validates it and includes it in provider instructions. That mapping supplies context; it does not grant authorization.

The State panel also shows the workbench's accepted application plan and revision. Those values are separate from shared SDK state. Replaying a frame changes what you inspect without changing the live conversation or making another request.

The Run's Return Value

An agent can attach a return value to the AG-UI RUN_FINISHED event, separate from the messages and from shared state. Hashbrown exposes it as lastRunResult on chatResource() and the other chat and completion APIs.

const result = chat.lastRunResult();

The value is the return value of the last run that completed, as any JSON value, or undefined when the agent attached none. It is cleared when the next run starts, so a value always belongs to the messages currently shown. Interrupted, cancelled, and failed runs never set it. When one turn spans several runs because the model called your tools, each completed run replaces the value, so you read what the final run returned.

Hashbrown's direct provider adapters do not produce a return value. Use it with an agent implementation that sets result on its terminal event.


Token Usage and Finish Reason

Hashbrown also reads how the last completed run ended. lastRunUsage holds the token counts from AG-UI's standard RUN_FINISHED.usage field, one entry per provider and model. lastRunFinishReason says why the model stopped.

const usage = chat.lastRunUsage(); // TokenUsage[] | undefined
const reason = chat.lastRunFinishReason(); // FinishReason | undefined

if (reason === 'length') {
  // The answer was cut off by the output token limit.
}

lastRunFinishReason is one of:

ReasonMeaning
stopThe model finished its answer.
lengthAn output token limit or the context window cut the answer off.
tool_callsThe model stopped to call tools.
content_filterA provider safety filter or guardrail blocked or cut the answer, or blocked the prompt.
refusalThe model declined to answer.
otherAny other reason. The provider's own reason is kept in the RUN_FINISHED event metadata.

A truncated, filtered, or refused answer is not an error. The run completes, the partial answer stays in the messages, and the finish reason tells you what happened, so check it when you need to tell the user that an answer was cut off or blocked. Hashbrown's provider packages always report a finish reason, and they report usage whenever the provider does.

Both values follow the same lifecycle as lastRunResult: they are cleared when the next run starts and never set by interrupted, cancelled, or failed runs. When one turn spans several runs because the model called your tools, you read the final run's usage, not the turn's total.

Each usage entry follows AG-UI's accounting. inputTokens and outputTokens are totals. cachedInputTokens, cacheWriteInputTokens, and reasoningTokens are parts of those totals, never additions to them. totalTokens is their sum.

For request-level troubleshooting, see Debugging. Continue with Activities or Interrupt and Resume.

Shared Agent StateShare a small editing documentExplore it in the workbenchInitialize a Small State ValueReplace State ExplicitlyFollow Drafts and CommitsKeep State and Model Context ExplicitThe Run's Return ValueToken Usage and Finish Reason