Core vanuit een browser aanroepen met AG-UI¶
AG-UI is een open protocol tussen een agent en de pagina die een persoon
gebruikt. De clients ervan, zoals @ag-ui/client en CopilotKit, starten een run en lezen het
antwoord van de agent als een stroom events. Core spreekt dat protocol op POST /api/v1/ag-ui/runs,
dus je pagina kan rechtstreeks met Core praten. Elke run is een gewone beurt: de modellen, policy,
tools en het budget van je sleutel gelden precies zoals op POST /responses.
De pagina draait in iemands browser en krijgt je sleutel dus nooit. Je backend geeft haar een subject-token voor de persoon die is ingelogd, en de pagina gebruikt dat token.
Wat je nodig hebt¶
- Een sleutel met
threadsensubjects. Voeg de capability toe van elke tool die Core uitvoert en die de agent moet gebruiken:toolingvoor zijn werkgeheugen of een connector,knowledgevoor een zoektocht in je kennis. Voegfilestoe als de persoon documenten of afbeeldingen meestuurt die hij uploadde. - Een model of agent die de sleutel mag gebruiken:
GET /modelsgeeft ze. - Een eigen backend die weet wie er is ingelogd.
1. Maak op je backend een token en een gesprek aan¶
Je backend vraagt Core om een token voor de persoon en geeft dat met het gesprek aan de pagina. Hoe een subject-token werkt en wat het mag bereiken, staat in Een token voor de browser.
curl -s -X POST "$CORE_URL/subjects/tokens" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"subject": {"id": "anna"}, "expires_in": 900}'
{"token": "ast_...", "subject_id": "anna", "expires_at": "2026-10-22T11:15:00Z"}
Een run zet een gesprek van Core voort, dus maak er een aan. De pagina kan dat zelf met het token doen:
curl -s -X POST "$CORE_URL/threads" \
-H "Authorization: Bearer ast_..." -H "Content-Type: application/json" -d '{}'
Bewaar het id van het gesprek (thr_...) bij het gesprek van de persoon. Een AG-UI-client
verzint standaard zelf een id; een id dat Core niet heeft uitgegeven, weigert Core met 404.
2. Laat de pagina de agent draaien¶
import { HttpAgent } from "@ag-ui/client";
const agent = new HttpAgent({
url: "https://<jouw-core>/api/v1/ag-ui/runs",
headers: { Authorization: `Bearer ${subjectToken}` },
threadId,
});
agent.addMessage({ id: crypto.randomUUID(), role: "user", content: "Plan mijn reis naar Delft." });
await agent.runAgent({ forwardedProps: { model: "<een model-id>" } });
forwardedProps zegt wat de beurt draait: model, een model-id of agent:<naam>, of agent,
{"id": "<naam van de agent>"}, precies zoals op POST /responses. Met tools geef je de run
tools die Core uitvoert (stap 4). Andere sleutels negeert Core.
Het antwoord is text/event-stream: RUN_STARTED, een STATE_SNAPSHOT, de berichten en
toolaanroepen van de agent, en dan RUN_FINISHED. Daarin is result.responseId het id van de
beurt, dat GET /responses/{id} met hetzelfde subject-token teruggeeft; het token van iemand anders
krijgt 404. Een beurt die mislukt, eindigt in plaats daarvan met RUN_ERROR. Houdt een
live check het antwoord achter, dan bevat de
stroom er niets van -- hooguit het denkwerk van de agent, of, bij een patroon dat tijdens het
streamen wordt gecontroleerd, de tekst vóór de treffer -- en eindigt hij toch met RUN_FINISHED,
met result.status "incomplete" en zonder reden: GET /responses/{id} zegt
incomplete_details.reason: "content_filter", en GET /responses/{id}/checks noemt de check. Een verzoek dat
Core weigert voordat de stroom begint, bijvoorbeeld voor een model dat de sleutel niet mag
gebruiken, krijgt een HTTP-fout met een problem+json-body, en de client gooit een fout.
Stuur het gesprek zoals de client het bijhoudt. De client stuurt bij elke run alle berichten.
Het gesprek in Core bevat al alles tot en met het laatste antwoord van de agent, dus Core leest
alleen de berichten na dat antwoord. Een run zonder iets nieuws na het laatste antwoord wordt
geweigerd op messages.
Opnieuw proberen na een fout. Eindigt een run met RUN_ERROR voordat de agent iets van zijn
antwoord schreef, dan staat het bericht van de persoon al in het gesprek. Draai de agent opnieuw
met dezelfde berichten en dezelfde id's, wat @ag-ui/client vanzelf doet: Core herkent het
bericht dat het al heeft, bewaart het niet twee keer en beantwoordt het. Had de agent al een deel
van zijn antwoord geschreven, dan staat dat deel ook in het gesprek; stuur het bericht dan niet
opnieuw, maar stuur het volgende bericht van de persoon.
Context wordt elke run gelezen en nooit bewaard. De context van een run bereikt het model als
instructies voor alleen die run, dus een pagina die elke keer dezelfde context stuurt, vult het
gesprek niet met kopieën.
Stuur een bestand mee dat de persoon uploadde. De pagina uploadt het met zijn token naar
POST /files en zet het id in een image- of document-deel met een file-bron. Een kleine
afbeelding mag ook inline mee, als data.
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: [
{ type: "text", text: "Berg dit contract op bij de andere van het project in Delft." },
{ type: "document", source: { type: "file", value: fileId } },
],
});
Een afbeelding bereikt het model als de afbeelding zelf. Een document bereikt het als verwijzing,
zijn id en mediatype, nooit de inhoud, precies zoals op POST /responses: wil je dat de agent het
leest, neem het dan op in een kenniscollectie of geef de agent een tool die het leest
(Bestanden en afbeeldingen meesturen).
Core controleert het id zoals POST /responses dat doet: een bestand dat Core niet voor deze
persoon heeft, wordt geweigerd met 404 voordat de stroom begint. provider mag ontbreken of
artific-core zijn; een bestands-id van een andere aanbieder wordt geweigerd. mimeType is
optioneel en wordt niet gelezen: Core gebruikt het type waarmee het bestand is geüpload.
3. Laat de pagina een tool uitvoeren¶
Een tool in de tools van de run voert je pagina zelf uit. Roept het model hem aan, dan eindigt de
run na TOOL_CALL_END. Je pagina voert de tool uit, voegt een tool-bericht met het resultaat toe
en draait de agent opnieuw; dat resultaat is de invoer van de volgende run.
const agent = new HttpAgent({ url, headers, threadId });
agent.addMessage({ id: crypto.randomUUID(), role: "user", content: "Wat voor weer wordt het?" });
const tools = [
{
name: "get_weather",
description: "Het weer van vandaag in een stad",
parameters: { type: "object", properties: { city: { type: "string" } } },
},
];
await agent.runAgent({ tools, forwardedProps: { model: "<een model-id>" } });
const call = agent.messages.at(-1)?.toolCalls?.[0];
if (call) {
agent.addMessage({ id: crypto.randomUUID(), role: "tool", toolCallId: call.id, content: "zonnig" });
await agent.runAgent({ tools, forwardedProps: { model: "<een model-id>" } });
}
Een tool die Core zelf uitvoert, zoals een connector of een zoekopdracht in kennis, verschijnt als
toolaanroep met daarna TOOL_CALL_RESULT, en de run gaat door. Kon Core zo'n aanroep niet
uitvoeren -- het beleid weigerde hem, de tool bestaat niet, hij duurde te lang -- dan is de tekst
van het resultaat Core's eigen korte zin en zegt de metadata waarom, zodat de pagina het als
fout kan tonen:
agent.subscribe({
onToolCallResultEvent({ event }) {
// `code` names why, bijv. "timeout"
if (event.metadata?.error) showToolError(event.toolCallId, event.metadata.code);
},
});
Wil je de persoon midden in een run iets vragen, gebruik dan een tool van je pagina zoals
get_weather hierboven: Core kent geen AG-UI-interrupts.
Toon de bronnen van het antwoord. Heeft de agent in kennis gezocht of een pagina gelezen, dan
komen de bronnen ook in metadata binnen, in precies de vorm van POST /responses
(waar de bronnen van een antwoord staan). TEXT_MESSAGE_END draagt
artific.annotations, de plekken waar het antwoord een bron aanhaalt, en TOOL_CALL_RESULT draagt
artific.citations, alles wat de tool las:
agent.subscribe({
onTextMessageEndEvent({ event }) {
// [{ type: "file_citation", file_id, filename, index }, ...]
const annotations = event.metadata?.["artific.annotations"] ?? [];
linkMarkers(event.messageId, annotations);
},
onToolCallResultEvent({ event }) {
// [{ kind: "file" | "url", ref, title }, ...]
listSources(event.toolCallId, event.metadata?.["artific.citations"] ?? []);
},
});
Een positie telt Unicode-codepunten, dus knip in Array.from(text) en niet in de string zelf. De
file_id van een file_citation is een passage uit je eigen inhoud: de pagina leest die met
GET /knowledge/chunks/{file_id} en hetzelfde token
(een aangehaalde passage lezen).
Een antwoord dat niets aanhaalt, draagt geen artific.annotations, en een resultaat dat niets las
geen artific.citations.
4. Geef een run servertools¶
Een tool die Core zelf uitvoert, zoals een zoektocht in je kennis of een connector, hoort in
forwardedProps.tools, in dezelfde vorm als tools hem op POST /responses neemt:
{"type": "tool", "name": "<gekwalificeerde naam>"} voor één tool, zoals GET /tools hem noemt,
of {"type": "toolset", "name": "<toolset>"} voor een groep. Een agent die je opsloeg is ook een
tool, onder agent__<naam>.
await agent.runAgent({
tools, // de tools die je pagina uitvoert, zoals in stap 3
forwardedProps: {
model: "<een model-id>",
tools: [{ type: "tool", name: "knowledge__handbook" }],
},
});
Het model krijgt beide lijsten aangeboden. Roept het knowledge__handbook aan, dan doorzoekt Core
de collectie erachter, en de pagina ziet de aanroep en zijn TOOL_CALL_RESULT, met bronnen, terwijl
de run doorgaat. Gebruik dit als de pagina bepaalt welke tools een gesprek krijgt; een
agentdefinitie die haar tools noemt doet hetzelfde voor elke run van die agent.
Over deze tools wordt precies zo beslist als op POST /responses, voor de persoon van het token:
- Een tool waar de capabilities van de sleutel niet bij komen, zoals een zoektocht in kennis op een
sleutel zonder
knowledge, weigert de hele run met422voordat het model wordt aangeroepen, en noemt de regel:forwardedProps.tools.0.name. Een naam die de organisatie nooit registreerde is404. - Een tool die de regels van je policy deze persoon niet laten gebruiken, wordt het model niet
aangeboden, en de run gaat zonder door, zoals een beurt op
POST /responses. De trace van de beurt legt vast waarom. - Een tool die je pagina uitvoert (
function,customofnamespace) wordt inforwardedProps.toolsgeweigerd: die hoort in de eigentoolsvan de run. - Een tool van je pagina met de naam van een tool die Core uitvoert, wordt geweigerd op de tool van
de pagina,
tools.0.name; geef de jouwe een andere naam. - Samen bevatten de twee lijsten hoogstens 128 tools.
5. Deel state met de agent¶
De state van een run is het werkgeheugen van het
gesprek, het kleine JSON-kladblok dat elke beurt in het gesprek leest.
- Aan het begin van elke run toont
STATE_SNAPSHOTde state zoals de beurt hem leest. - Werkt de agent hem bij met
builtin__update_working_state, dan volgt na het resultaat van de tool eenSTATE_DELTA, als JSON Patch. Geef de agent die tool,builtin__update_working_state, inforwardedProps.toolsof via een agentdefinitie, op een sleutel mettooling. - Verandert je pagina de state, stuur dan het nieuwe object als
statemee met de volgende run. Core schrijft het voordat de beurt begint, met dezelfde grenzen alsPUT /threads/{id}/state, en houdt het ook als de run daarna wordt geweigerd. Een legestateverandert niets, dus een pagina die net is geladen, wist het kladblok niet.
Een state met woorden uit een gepubliceerde, verzegelde agent wordt getoond als "[sealed]", en de
pagina krijgt er geen delta's voor.
6. Een run stoppen¶
Een stopknop roept agent.abortRun() aan. De client breekt zijn verzoek af, en Core stopt de beurt
zodra de stroom sluit: wat de agent tot dan toe schreef, blijft in het gesprek, de beurt wordt
vastgelegd als cancelled, en je betaalt alleen voor wat het model al deed. De pagina sluiten of de
verbinding midden in een run verliezen, stopt de beurt op dezelfde manier.
stopButton.onclick = () => agent.abortRun();
Dit is anders dan bij POST /responses, waar een beurt doorloopt na een verbroken verbinding en
alleen POST /responses/{id}/cancel hem stopt.
Wat Core op deze route niet doet¶
- Geen interrupts, en
resumewordt geweigerd: vraag de persoon iets via een tool van je pagina. - De argumenten van een toolaanroep komen in één
TOOL_CALL_ARGS, niet stukje voor stukje. - Een verbroken verbinding kun je niet hervatten: hij stopt de beurt, zoals een stopknop.
- Geen
MESSAGES_SNAPSHOT: de pagina heeft elk bericht al uit de events. - Een afbeelding via een adres, audio en video worden geweigerd, net als een document dat inline of via een adres meekomt: upload het eerst.