@a2anet/react
v0.2.0
Published
React SDK for A2A Net
Downloads
411
Readme
@a2anet/react
React SDK for A2A Net. It connects your app to an agent and gives CopilotKit everything it needs to render the conversation.
Install
npm install @a2anet/react @ag-ui/client @copilotkit/react-coreCredentials
For performance reasons, the chat connects to A2A Net from the browser without going through a server-side endpoint or proxy. For browsers to authenticate with A2A Net, they use a short-lived JWT token, which your backend mints with your A2A Net API key. The key never leaves the server.
Add an endpoint that authenticates the user, mints a customer token, and returns it along
with the agent it is for. POST /api/token is the path this SDK's example uses, and yours
is whatever you name in getCredentials:
{
token: string;
expiresAt: string;
agentId: string;
}The token is minted for one agent, so the server that mints it is the one place that names
it. Requests from the browser go to https://agent.a2anet.com, exported as
A2ANET_RUNTIME_URL; pass runtimeUrl to the provider to point at a runtime of your own
during local development.
A2ANetProvider calls getCredentials on mount, and again whenever the token it holds is
spent. Send whatever your backend needs to authenticate the user, the same as any other
request to it:
import { useCallback } from "react";
import { A2ANetProvider, type A2ANetCredentials } from "@a2anet/react";
export function Root() {
const getCredentials = useCallback(async (): Promise<A2ANetCredentials> => {
const response = await fetch("/api/token", {
method: "POST",
headers: { Authorization: `Bearer ${await getAccessToken()}` },
});
if (!response.ok) throw new Error(`Credential request failed with ${response.status}`);
return response.json();
}, []);
return (
<A2ANetProvider getCredentials={getCredentials}>
<App />
</A2ANetProvider>
);
}Mount the provider above anything that can unmount, such as a drawer. It holds the agent, which owns the conversation.
A working endpoint and provider, ready to copy, are in
examples/website-app.
Renewal
A minted token is short-lived, and the provider replaces it for you. It gives the agent a
fetch of its own that checks the token and mints a replacement before each request, so a
spent token is replaced by whatever the user does next — sending a message, reconnecting to
a thread, stopping a run. No timer renews one: a background tab or a sleeping machine defers
timers well past a token's life, and a session waiting on one signs its requests with a dead
token.
status turns to Error only once no usable token is left, so a single failed mint does
not tear down a working conversation.
The one gap is requests the provider does not make. CopilotKit's thread endpoints — listing,
renaming, archiving and deleting conversations — build a fetch of their own and carry
whatever token the last render gave them, and a user can reach all of them without ever
sending a message. Await checkAndMintCredentials before those:
const { checkAndMintCredentials } = useA2ANet();
const { refetchThreads } = useThreads({ agentId });
const showThreads = async () => {
await checkAndMintCredentials();
refetchThreads();
};Rendering the chat
useA2ANet returns the properties CopilotKit needs, plus the credential's status. The SDK
does not mount CopilotKit, so the loading and error UI stay yours:
import { CopilotChat, CopilotKitProvider } from "@copilotkit/react-core/v2";
import { A2ANetStatus, useA2ANet } from "@a2anet/react";
export function App() {
const { copilotKitProps, status, error, retry } = useA2ANet();
if (status === A2ANetStatus.Loading) return <p>Connecting…</p>;
if (status === A2ANetStatus.Error) {
return <button onClick={retry}>{error?.message ?? "Try again"}</button>;
}
return (
<CopilotKitProvider {...copilotKitProps}>
<CopilotChat agentId={copilotKitProps.agent} />
</CopilotKitProvider>
);
}Artifacts
Files the agent produces arrive as events rather than messages, so render them yourself.
useA2ANetArtifacts collects them, keyed by the message each one followed, and
downloadArtifact saves one. Render this inside CopilotKitProvider:
import { useMemo } from "react";
import { downloadArtifact, useA2ANetArtifacts } from "@a2anet/react";
import {
CopilotChat,
CopilotChatAssistantMessage,
useAgent,
type CopilotChatAssistantMessageProps,
} from "@copilotkit/react-core/v2";
function Chat({ agentId, threadId }: { agentId: string; threadId: string }) {
const { agent } = useAgent({ agentId, updates: [] });
const artifacts = useA2ANetArtifacts(agent, threadId);
// Object.assign carries over the slot's static members, which its type requires.
const assistantMessage = useMemo(
() =>
Object.assign(
(props: CopilotChatAssistantMessageProps) => (
<>
<CopilotChatAssistantMessage {...props} />
{(artifacts.get(props.message.id) ?? []).map((file) => (
<button
key={file.id}
type="button"
onClick={() => downloadArtifact(file)}
>
{file.filename}
</button>
))}
</>
),
CopilotChatAssistantMessage,
),
[artifacts],
);
return <CopilotChat agentId={agentId} threadId={threadId} messageView={{ assistantMessage }} />;
}Context
Pass getContext to tell the agent what the user is looking at. It is read on every run, so
it can return whatever the current page holds:
<A2ANetProvider getCredentials={getCredentials} getContext={() => ({ "venue-name": venue })}>Context values are prepended to the user's message. The agent sees them and they stay in its session, but they are not saved to the AG-UI transcript that the user sees.
