showcase/shell-docs/src/content/docs/frontends/angular/guides/shared-state.mdx
Shared state and agent context solve two different data flows.
| Data flow | Use |
|---|---|
| Agent and application both read and write the value | injectAgentStore and agent.setState |
| The application owns the value and the agent only reads it | connectAgentContext or CopilotKitAgentContext |
injectAgentStore returns a signal that resolves one agent. The store exposes
messages, state, and run status as nested signals.
import { Component, computed } from "@angular/core";
import { injectAgentStore } from "@copilotkit/angular";
type WorkspaceState = {
notes: string[];
priority: "low" | "normal" | "high";
};
const EMPTY_STATE: WorkspaceState = {
notes: [],
priority: "normal",
};
@Component({
selector: "app-workspace",
template: `
<p>Priority: {{ state().priority }}</p>
<ul>
@for (note of state().notes; track note) {
<li>{{ note }}</li>
}
</ul>
<button type="button" (click)="setPriority('high')">
Mark high priority
</button>
`,
})
export class WorkspaceComponent {
readonly store = injectAgentStore("default");
readonly state = computed(
() => (this.store().state() as WorkspaceState | undefined) ?? EMPTY_STATE,
);
protected setPriority(priority: WorkspaceState["priority"]): void {
const agent = this.store().agent;
const current = (agent.state as WorkspaceState | undefined) ?? EMPTY_STATE;
agent.setState({ ...current, priority });
}
}
Read through store().state() so Angular tracks changes. Write through the
plain AG-UI agent at store().agent. Replace the object instead of mutating it
in place.
The same store gives you:
store().messages() for the current conversationstore().isRunning() for loading controlsstore().agent for setState, addMessage, runAgent, and abortRunUse connectAgentContext for values such as the signed-in user's name,
selected record, timezone, or current screen. Pass an accessor so signal reads
stay reactive.
import { Component, signal } from "@angular/core";
import { connectAgentContext } from "@copilotkit/angular";
@Component({
selector: "app-account-context",
template: `
<button type="button" (click)="timezone.set('Europe/London')">
Use London time
</button>
`,
})
export class AccountContextComponent {
readonly userName = signal("Ada");
readonly timezone = signal("America/Los_Angeles");
constructor() {
connectAgentContext(() => ({
description: "Current account and timezone",
value: JSON.stringify({
userName: this.userName(),
timezone: this.timezone(),
}),
}));
}
}
The internal effect removes the old context and registers the new value when a
read signal changes. It removes the final registration when the owning
injector is destroyed. If you already have an Injector, pass it as
{ injector }; that injector takes precedence over the ambient one.
Use CopilotKitAgentContext when the context is already shaped in the
template.
import { Component, computed, signal } from "@angular/core";
import { CopilotKitAgentContext } from "@copilotkit/angular";
@Component({
selector: "app-selection-context",
imports: [CopilotKitAgentContext],
template: `
<div [copilotkitAgentContext]="selectionContext()"></div>
`,
})
export class SelectionContextComponent {
readonly selectedId = signal("record-42");
readonly selectionContext = computed(() => ({
description: "The record selected in the application",
value: this.selectedId(),
}));
}
Render the directive only after you have a complete context. If it starts without one, later input changes do not create the first registration.
injectAgentStore to the same agent id as the chat or workflow that
reads the state.