UIAP Agent Integration Guide
UIAP Agent Integration Guide v0.1
Abschnitt betitelt „UIAP Agent Integration Guide v0.1“| Feld | Wert |
|---|---|
| Status | Informative (nicht normativ) |
| Version | 0.1 |
| Datum | 2026-03-27 |
| Abhängigkeiten | [UIAP-CORE], [UIAP-CAP], [UIAP-WEB], [UIAP-ACTION], [UIAP-POLICY], [UIAP-WORKFLOW] |
| Editoren | Patrick |
Companion-Dokument. Informativ, nicht normativ.
UIAP beschreibt Transport, Capability Model, Web-Profil, Runtime, Policy und Workflows, lässt aber bewusst offen, wie ein Agent intern plant. Genau an dieser Stelle bauen sonst alle ihre eigene Brücke. Dieser Guide beschreibt eine pragmatische Integrationsschicht für einen LLM-basierten Agenten, der UIAP-Artefakte ernst nimmt, ohne das Modell zur Quelle der Wahrheit zu machen.
Einordnung
Abschnitt betitelt „Einordnung“Die Grundidee ist einfach:
- Der vollständige UIAP-Zustand lebt außerhalb des LLM in einem lokalen State Store.
- Das LLM sieht nur eine redigierte, komprimierte und schrittrelevante Sicht auf diesen Zustand.
- Capability, Policy und Workflow werden nicht als lose Doku in den Prompt gekippt, sondern in eigene Laufzeitkomponenten übersetzt.
- Jede schreibende oder nicht triviale Aktion läuft durch Policy-Preflight, Action Runtime und Beobachtung/Verifikation.
- Erfolg wird nie „geglaubt“, sondern aus
action.result,web.state.deltaundweb.signalabgeleitet.
Wenn Authoring-Bundles vorhanden sind, sollte die Agent-Integration idealerweise das kompilierte Bundle konsumieren, nicht lose Manifeste. Das Bundle ist die laufzeitnahe Quelle für effektive Actions, Policies und Workflows.
Hinweis zur aktuellen Draft-Lage: Die Policy-Extension taucht im Material einmal als
uicp.policyund an anderen Stellen alsuiap.policyauf. Die Beispiele in diesem Guide verwendenuiap.policy.*. Eine echte Implementierung sollte bis zur Bereinigung beide Namespaces aliasen.
0. Empfohlene Integrationsarchitektur
Abschnitt betitelt „0. Empfohlene Integrationsarchitektur“Eine vernünftige Agent-Host-Schicht trennt sechs Verantwortungen:
-
Session/Transport Handshake, Capability-Fetch, Observe-Subscription, Action-Requests.
-
State Store Hält den letzten vollständigen
PageGraph, wendet Deltas an, puffert Signale und Revisionen. -
Policy Gateway Evaluiert Policy vor Aktionen und erzeugt zusätzlich eine redigierte Modell-Sicht auf Snapshot, Signale und Return Values.
-
Workflow Index Matcht verfügbare Workflows gegen Ziel, Route, Rollen, Grants und Modus.
-
Tool Registry Übersetzt
ActionDescriptor[]in LLM-Tools oder in eine kleinere, kontextabhängige Tool-Auswahl. -
Context Builder Baut aus rohem UIAP-Zustand eine kompakte Planner-Sicht für das LLM.
Ein minimales Laufzeitmodell sieht so aus:
interface AgentRuntime { state: StateStore; policy: PolicyGateway; workflows: WorkflowIndex; tools: ToolRegistry; context: ContextBuilder; planner: Planner;}
interface PlannerInput { goal: string; policyHints: PolicyHints; context: PlanningContext; workflows: WorkflowRecipe[]; tools: LLMTool[];}Wichtig ist die Trennung zwischen rohem Zustand und Prompt-Zustand. Das LLM darf nie der einzige Speicher für Route, Scope, Revision oder letzte Resultate sein. Modelle vergessen, verwechseln und erfinden gerne. Wirklich rührend, aber unerquicklich.
1. PageGraph -> LLM Context
Abschnitt betitelt „1. PageGraph -> LLM Context“1.1 Nicht den ganzen Graphen in den Prompt kippen
Abschnitt betitelt „1.1 Nicht den ganzen Graphen in den Prompt kippen“Der PageGraph ist bereits ein semantisch reduzierter Web-Zustand und kein Vollabbild des DOM. Trotzdem kann er groß werden. Die empfohlene Strategie ist deshalb nicht „alles ins Modell“, sondern ein zweistufiges Modell:
- State Store hält den vollständigen, letzten bekannten Graphen.
- Context Builder macht daraus pro Turn eine kleinere Planner-Sicht.
Der Planner braucht in der Regel nicht die volle Rohstruktur. Für Planung sind vor allem diese Felder wichtig:
| Primär für Planung | Primär für Ausführung / Recovery |
|---|---|
route.routeId, pathname, title | documentId |
scopeId, stableId, role, name | bbox, zIndexHint |
Zustände wie visible, enabled, focused, editable, required, invalid, open, busy, loading | targetHints.runtime.css, targetHints.runtime.xpath |
affordances, supportedActions | semantics.attached, inViewport, obscured, stable |
risk, success, aktuelle Signale, Fokus | shadowHostId, framePath |
annotierte Bedeutung wie meaning oder defaultAction | low-level Viewport-/Scroll-Details |
Faustregel:
- Planung arbeitet mit stabiler Semantik.
- Ausführung arbeitet mit Zielauflösung, Actionability und Runtime-Hints.
- CSS/XPath gehören fast nie in den LLM-Planungskontext. Sie sind technische Fallbacks, keine Identität.
1.2 Eine sinnvolle Planner-Sicht
Abschnitt betitelt „1.2 Eine sinnvolle Planner-Sicht“Für das LLM reicht meistens ein Objekt wie dieses:
type PlanningElement = { stableId?: string; scopeId?: string; role: string; name?: string; meaning?: string; defaultAction?: string; state: Record<string, unknown>; supportedActions: string[]; risk?: { level: "safe" | "confirm" | "blocked"; tags?: string[]; }; success?: Array<Record<string, unknown>>; confidence?: "high" | "medium" | "low";};
interface PlanningContext { revision: string; route?: { routeId?: string; pathname?: string; title?: string; }; activeScopes: Array<{ scopeId: string; kind: string; stableId?: string; name?: string; parentScopeId?: string; }>; focus?: { stableId?: string; role?: string; name?: string; }; candidateElements: PlanningElement[]; recentSignals: Array<{ kind: string; level?: string; text?: string; scopeId?: string; }>;}1.3 Relevanz filtern statt blind kürzen
Abschnitt betitelt „1.3 Relevanz filtern statt blind kürzen“Eine brauchbare Heuristik für candidateElements priorisiert:
- Elemente in offenen Dialogen, Drawern, Popovers
- Elemente im fokussierten Scope
- Elemente mit
stableId - Elemente mit
defaultActionoder Domain-Action insupportedActions - invalid, required, busy, open oder selected Zustände
- Elemente mit
risk.level = confirm|blocked - sichtbare Feedback-Elemente wie Toast, Alert, Status
Und sie depriorisiert:
- rein dekorative Dinge
- wiederholte Listeneinträge ohne aktuelle Relevanz
- große Mengen von Text ohne Steuerungsbezug
- offscreen oder verdeckte Targets, solange sie für den aktuellen Schritt nicht gebraucht werden
Eine simple Builder-Funktion kann so aussehen:
function buildPlanningContext(graph: PageGraph, maxElements = 30): PlanningContext { const activeScopeIds = pickActiveScopes(graph);
const candidateElements = graph.elements .filter((el) => isRelevantForPlanning(el, activeScopeIds)) .map((el) => ({ stableId: el.stableId, scopeId: el.scopeId, role: el.role, name: el.name, meaning: el.targetHints?.annotations?.meaning, defaultAction: el.targetHints?.annotations?.defaultAction, state: pickState(el.state, [ "visible", "enabled", "focused", "editable", "required", "invalid", "selected", "expanded", "open", "busy", "loading" ]), supportedActions: el.supportedActions, risk: el.risk, success: el.success, confidence: deriveConfidence(el) })) .sort((a, b) => scorePlanningElement(b) - scorePlanningElement(a)) .slice(0, maxElements);
return { revision: graph.revision, route: graph.route ? { routeId: graph.route.routeId, pathname: graph.route.pathname, title: graph.route.title } : undefined, activeScopes: pickScopes(graph, activeScopeIds).map((scope) => ({ scopeId: scope.scopeId, kind: scope.kind, stableId: scope.stableId, name: scope.name, parentScopeId: scope.parentScopeId })), focus: resolveFocus(graph), candidateElements, recentSignals: (graph.signals ?? []).slice(-8).map((sig) => ({ kind: sig.kind, level: sig.level, text: sig.text, scopeId: sig.scopeId })) };}1.4 Planungskontext und Ausführungskontext trennen
Abschnitt betitelt „1.4 Planungskontext und Ausführungskontext trennen“Das Modell sollte nicht sofort alle Ausführungsdetails sehen. Für den nächsten Schritt reicht fast immer die semantische Form:
{ "route": { "routeId": "videos.new", "title": "Neues Video" }, "activeScopes": [ { "scopeId": "scope_form", "kind": "form", "stableId": "video.create.form", "name": "Video erstellen" } ], "focus": { "stableId": "video.title", "role": "textbox", "name": "Titel" }, "candidateElements": [ { "stableId": "video.title", "role": "textbox", "name": "Titel", "state": { "enabled": true, "required": true }, "supportedActions": ["ui.focus", "ui.enterText", "ui.clearText"] }, { "stableId": "video.submit", "role": "button", "name": "Video erstellen", "state": { "enabled": true }, "supportedActions": ["ui.activate", "video.create"], "risk": { "level": "confirm", "tags": ["external_effect"] } } ]}Wenn der Planner dann tatsächlich eine Aktion auswählt, kann die Host-Schicht das Ziel für die Ausführung mit zusätzlichen Informationen anreichern:
interface ExecutionTarget { stableId?: string; documentId: string; scopeId?: string; role: string; name?: string; bbox?: { x: number; y: number; width: number; height: number }; runtimeHints?: { css?: string; xpath?: string }; actionability?: { attached?: boolean; inViewport?: boolean; obscured?: boolean; stable?: boolean; };}1.5 Redaction zuerst, Kompression danach
Abschnitt betitelt „1.5 Redaction zuerst, Kompression danach“Wenn der Snapshot sensible Werte enthält, sollte die Host-Schicht zuerst redigieren und danach den Planner-Kontext bauen. Sonst landet das Passwortfeld mit Maskierung vielleicht nicht im Prompt, aber der Rohwert steckt noch in textValue oder semanticValue einer früheren Zwischenrepräsentation. Menschen nennen so etwas „kleines Versehen“. Auditoren eher nicht.
2. Capability Document -> Tool Schema
Abschnitt betitelt „2. Capability Document -> Tool Schema“2.1 Ja, das Capability-Dokument lässt sich direkt in Tools übersetzen
Abschnitt betitelt „2.1 Ja, das Capability-Dokument lässt sich direkt in Tools übersetzen“Das Capability-Dokument ist fast schon die Vorlage für Tool-Definitionen. Ein ActionDescriptor enthält bereits:
idkindtargetKindsrequiredAffordancesexecutionModesargsidempotencyrisksuccess
Pragmatisch heißt das:
- Domain-Actions werden fast immer als eigene Tools exponiert.
- Primitive Actions können entweder direkt exponiert oder über ein generisches
run_uiap_action-Tool geroutet werden. - Die aktuell im Prompt sichtbaren Tools sind idealerweise die Schnittmenge aus globalem Capability-Dokument und lokalem
supportedActionsauf aktuell relevanten Elementen/Scopes.
2.2 Zwei brauchbare Varianten
Abschnitt betitelt „2.2 Zwei brauchbare Varianten“Variante A: ein Tool pro Action
Gut für kleinere Apps oder wenn ihr dem Modell explizit viele Domain-Actions geben wollt.
Variante B: generisches Runtime-Tool + Action-Shortlist
Gut für große Kataloge. Das Modell bekommt dann im Kontext eine kleine Liste erlaubter Actions und ruft ein einziges Tool auf, das action.request baut.
In der Praxis ist B oft stabiler, weil das Modell nicht mit 200 Funktionsnamen jonglieren muss wie ein übermüdeter Zirkusdirektor.
2.3 Mapping in ein Tool-Schema
Abschnitt betitelt „2.3 Mapping in ein Tool-Schema“interface LLMTool { name: string; description: string; inputSchema: Record<string, unknown>; meta: { uiapActionId: string; risk: ActionDescriptor["risk"]; idempotency?: ActionDescriptor["idempotency"]; executionModes: ActionDescriptor["executionModes"]; success?: ActionDescriptor["success"]; };}
function compileActionTool(action: ActionDescriptor): LLMTool { const needsTarget = action.targetKinds.some((kind) => kind !== "none");
const properties: Record<string, unknown> = {}; const required: string[] = [];
if (needsTarget) { properties.target = { type: "object", additionalProperties: false, properties: { stableId: { type: "string" }, scopeId: { type: "string" }, role: { type: "string" }, name: { type: "string" } } }; required.push("target"); }
for (const arg of action.args ?? []) { properties[arg.name] = argToJsonSchema(arg); if (arg.required) required.push(arg.name); }
return { name: action.id.replaceAll(".", "_").replaceAll("-", "_"), description: action.description ?? action.title ?? action.id, inputSchema: { type: "object", additionalProperties: false, properties, required }, meta: { uiapActionId: action.id, risk: action.risk, idempotency: action.idempotency, executionModes: action.executionModes, success: action.success } };}
function argToJsonSchema(arg: ActionArgDescriptor): Record<string, unknown> { if (arg.type === "enum") { return { type: "string", enum: arg.enum ?? [] }; }
if (arg.type === "array") return { type: "array" }; if (arg.type === "object") return { type: "object" }; if (arg.type === "number") return { type: "number" }; if (arg.type === "boolean") return { type: "boolean" };
return { type: "string" };}2.4 Erwartete Ergebnisse nicht mit Tool-Args verwechseln
Abschnitt betitelt „2.4 Erwartete Ergebnisse nicht mit Tool-Args verwechseln“success-Signale gehören in der Regel nicht in die Tool-Eingabe, sondern in die Tool-Metadaten oder in die Controller-Logik. Das Tool sollte semantisch sagen: „Ich will video.create mit diesen Args“, nicht: „Und hier sind noch acht interne Verifikationsdetails, weil das Modell sonst nervös wird.“
Eine brauchbare Tool-Rückgabe ist eher so geformt:
type ToolResult = | { status: "accepted"; actionHandle: string } | { status: "waiting_confirmation"; actionHandle: string; preview?: unknown } | { status: "waiting_user"; note: string } | { status: "completed"; result: ActionResultPayload } | { status: "blocked"; reason: string };2.5 Kuratierte Tool-Exposition
Abschnitt betitelt „2.5 Kuratierte Tool-Exposition“Nicht jede Capability muss ständig im LLM sichtbar sein. Eine gute Host-Schicht zeigt pro Turn nur:
- Actions, die zur aktuellen Route oder den aktiven Scopes passen
- Actions, die von sichtbaren
supportedActionsgestützt werden - Domain-Actions aus den Top-Workflows
- ein paar sichere Primitive wie
ui.read,ui.focus,ui.enterText,ui.activate,nav.navigate
Der Rest bleibt intern vorhanden, aber außerhalb des aktuellen Planner-Budgets.
3. Workflow Definitions -> Planungskontext
Abschnitt betitelt „3. Workflow Definitions -> Planungskontext“3.1 Workflows sind weder heilige Schrift noch bloße Deko
Abschnitt betitelt „3.1 Workflows sind weder heilige Schrift noch bloße Deko“Verfügbare Workflow-Definitionen sollte ein Agent in drei Modi nutzen:
-
Als ausführbare Rezepte Wenn ein Workflow gut matcht, die nötigen Inputs bekannt sind und der gewünschte Modus (
guide,assist,auto) erlaubt ist, istuiap.workflow.startoft besser als freiformige Einzelplanung. -
Als Planskelett Wenn der Workflow grundsätzlich passt, aber aktuelle UI oder Inputs leicht abweichen, kann der Planner die Schritte als Vorlage verwenden und lokal anpassen.
-
Als negative Leitplanke
handoff-,collect-,ensure- undconfirm-Muster zeigen dem Agenten, wo er gerade nicht kreativ werden sollte.
3.2 Nicht den ganzen Katalog in den Prompt geben
Abschnitt betitelt „3.2 Nicht den ganzen Katalog in den Prompt geben“Ein Workflow-Katalog kann groß sein. Im Planner-Kontext genügen meist die Top 1 bis 3 Kandidaten. Dafür ist der Match-Prozess wichtig:
- Intent- oder Zieltext matchen
routeIdund aktuelle Scopes prüfenrequiredActionsgegen Capabilities prüfen- Rollen, Grants und Policy-Lage prüfen
modefiltern
interface WorkflowRecipe { workflowId: string; score: number; reason: string; missingInputs: string[]; steps: Array<{ id: string; type: string; actionId?: string; parameterNames?: string[]; }>;}
async function buildWorkflowRecipes(goal: string, routeId?: string): Promise<WorkflowRecipe[]> { const matches = await workflowClient.match({ intent: goal, routeId, mode: "assist", maxResults: 3 });
return matches.candidates.map((candidate) => ({ workflowId: candidate.workflowId, score: candidate.score, reason: candidate.reason ?? "workflow matched", missingInputs: candidate.missingInputs ?? [], steps: projectWorkflow(candidate.workflowId) }));}3.3 Was das LLM tatsächlich sehen sollte
Abschnitt betitelt „3.3 Was das LLM tatsächlich sehen sollte“Statt der vollen Definition reicht oft dieses Format:
[ { "workflowId": "video.create_first_video", "score": 0.92, "missingInputs": ["title"], "steps": [ { "id": "collect_title", "type": "collect", "parameterNames": ["title"] }, { "id": "go_to_form", "type": "action", "actionId": "nav.navigate" }, { "id": "fill_title", "type": "action", "actionId": "ui.enterText" }, { "id": "create_video", "type": "action", "actionId": "video.create" }, { "id": "done", "type": "complete" } ] }]Das ist für Planung meist viel nützlicher als die komplette Workflow-Definition mit jedem Lokalisierungs- und Review-Detail.
3.4 Authoring- und Discovery-Herkunft ernst nehmen
Abschnitt betitelt „3.4 Authoring- und Discovery-Herkunft ernst nehmen“Wenn Workflows aus einem authoritativen Bundle kommen, können sie als echte Rezepte dienen. Wenn sie nur aus Discovery-Kandidaten stammen, sollten sie eher als Hinweise oder Skelett gelten, nicht als autonome Wahrheit. Discovery liefert Kandidaten mit Confidence und Review-Bedarf; Authoring macht daraus erst veröffentlichte, effektive Laufzeitartefakte.
3.5 Gute Rollenverteilung zwischen LLM und Workflow Engine
Abschnitt betitelt „3.5 Gute Rollenverteilung zwischen LLM und Workflow Engine“Eine robuste Arbeitsteilung sieht so aus:
- Workflow Engine: Applicability, Step-Reihenfolge, Checkpoints, Policy-Integration, Resume
- LLM: Intent-Matching unterstützen, fehlende Inputs beschaffen, Vorschläge formulieren, bei Abweichungen zwischen UI und Rezept lokal adaptieren, Nutzer verständlich durch
waiting_confirmationoderwaiting_userführen
Wenn der Workflow sauber passt, sollte die Engine führen. Freiform-Planung ist nicht heroisch, wenn schon ein gutes Rezept existiert.
4. Policy als Constraint
Abschnitt betitelt „4. Policy als Constraint“4.1 Policy gehört nicht primär in den Prompt
Abschnitt betitelt „4.1 Policy gehört nicht primär in den Prompt“Die Policy-Spezifikation ist bewusst entscheidungsorientiert, lokal durchsetzbar und vor nicht trivialen Actions auszuwerten. Praktisch heißt das:
- Das Policy-Dokument selbst ist nicht die Hauptschnittstelle zum LLM.
- Die Policy-Entscheidung ist die operative Wahrheit.
- Das LLM bekommt höchstens eine kompakte Zusammenfassung globaler Regeln plus die konkrete Entscheidung pro Action.
Der vernünftige Aufbau ist dreischichtig:
-
System-/Planner-Hinweise Kurze, stabile Regeln wie: keine Credential-Eingabe,
confirmheißt nicht autonom weiterlaufen,handoffist kein Fehler. -
Policy Summary im Kontext Welche Grants hat der aktuelle Principal? Welche Domänen sind generell handoff- oder deny-lastig?
-
Preflight vor jeder Action Die Host-Schicht ruft
policy.evaluateauf und behandelt das Ergebnis als Hard Constraint.
4.2 Eine kleine Planner-Sicht reicht meistens
Abschnitt betitelt „4.2 Eine kleine Planner-Sicht reicht meistens“interface PolicyHints { principal: { id: string; roles?: string[]; grants?: string[]; }; hardStops: string[]; confirmRules: string[]; handoffRules: string[]; redaction: Array<{ applyTo: string[]; replacement: string; }>;}Beispiel:
{ "principal": { "id": "workspace-admin", "roles": ["admin"], "grants": ["observe", "guide", "draft", "act"] }, "hardStops": [ "credential/secret data is never exposed directly to the model", "blocked actions are not executed autonomously" ], "confirmRules": [ "confirm-risk actions require explicit confirmation before execution" ], "handoffRules": [ "user activation, credential entry and payment approval cause handoff" ], "redaction": [ { "applyTo": ["snapshot", "audit", "returnValue"], "replacement": "[REDACTED]" } ]}4.3 Preflight ist die eigentliche Durchsetzung
Abschnitt betitelt „4.3 Preflight ist die eigentliche Durchsetzung“async function preflightAction(input: { actionId: string; target?: { stableId?: string; role?: string; scopeId?: string; name?: string }; risk?: RiskDescriptor; dataClasses?: string[]; sideEffectClass?: string; args?: Record<string, unknown>;}): Promise<PolicyDecision> { return policyClient.evaluate({ context: { principal: currentPrincipal, actionId: input.actionId, target: input.target, risk: input.risk, dataClasses: input.dataClasses, sideEffectClass: input.sideEffectClass, args: input.args } });}Das Ergebnis wird dann strikt behandelt:
function handlePolicyDecision(decision: PolicyDecision): | { kind: "proceed" } | { kind: "confirm"; obligations?: unknown[] } | { kind: "handoff"; obligations?: unknown[] } | { kind: "deny"; reasonCodes: string[] } { switch (decision.decision) { case "allow": return { kind: "proceed" }; case "confirm": return { kind: "confirm", obligations: decision.obligations }; case "handoff": return { kind: "handoff", obligations: decision.obligations }; case "deny": default: return { kind: "deny", reasonCodes: decision.reasonCodes }; }}4.4 Redaction und Action-Zulässigkeit getrennt halten
Abschnitt betitelt „4.4 Redaction und Action-Zulässigkeit getrennt halten“Policy modelliert Redaction getrennt von Action-Erlaubnis. Das ist wichtig für die Agent-Integration:
- Ein Feld kann redigiert werden, ohne dass der ganze Screen für Planung verschwindet.
- Ein Snapshot für das Modell kann Platzhalter enthalten, während die Host-Schicht intern weiter mit strukturellem Wissen arbeitet.
- Das LLM sollte rote Zonen als sichtbar aber maskiert sehen, nicht als „unsichtbar“, wenn die Umgebung sonst unverständlich würde.
4.5 confirm und handoff als strukturierte Zustände behandeln
Abschnitt betitelt „4.5 confirm und handoff als strukturierte Zustände behandeln“Diese Fälle sollten nicht als freie Chat-Anweisung enden wie „Ich brauche kurz Hilfe“. Besser ist ein strukturierter Zustand im Controller:
confirm-> explizite Bestätigungsanfrage, danachaction.confirmation.grantoderdenyhandoff-> klarer Wechsel in Nutzerzuständigkeitdeny-> alternative Planung
Das Modell darf dabei erklären, aber nicht die Durchsetzung simulieren.
5. Observation Loop
Abschnitt betitelt „5. Observation Loop“5.1 Empfohlener Standard-Loop
Abschnitt betitelt „5.1 Empfohlener Standard-Loop“Ein vernünftiger Agenten-Loop sieht so aus:
- Session aufbauen
- Capabilities / Policy / Workflows laden
- Snapshot oder Observe-Stream starten
- redigierten Planungskontext bauen
- nächsten Schritt planen
- Policy-Preflight
- Action oder Workflow ausführen
- Resultat, Deltas und Signale beobachten
- Kontext aktualisieren und neu planen
Mit UIAP-Nachrichten ist das typischerweise:
agent -> session.initializeapp -> session.initialized
agent -> capabilities.getagent -> uiap.policy.get (oder uicp.policy.get in älteren Drafts)agent -> uiap.workflow.getagent -> web.observe.start
app -> capabilities.listapp -> uiap.policy.documentapp -> uiap.workflow.documentapp -> web.state.snapshotapp -> web.state.delta*app -> web.signal*
agent -> action.requestapp -> action.acceptedapp -> action.progress*app -> action.confirmation.request?agent -> action.confirmation.grant / deny?app -> action.resultapp -> web.state.delta*app -> web.signal*5.2 Einen lokalen State Store führen
Abschnitt betitelt „5.2 Einen lokalen State Store führen“Der State Store ist nicht glamourös, aber unverzichtbar.
class StateStore { private graph?: PageGraph; private revision?: string; private recentSignals: WebSignal[] = [];
applySnapshot(graph: PageGraph) { this.graph = graph; this.revision = graph.revision; this.recentSignals = graph.signals ?? []; }
applyDelta(delta: WebStateDeltaPayload) { if (!this.graph || delta.baseRevision !== this.revision) { throw new Error("Revision gap: full snapshot required"); }
this.graph = applyWebDelta(this.graph, delta.ops); this.graph.revision = delta.revision; this.revision = delta.revision; this.recentSignals.push(...(delta.signals ?? [])); this.recentSignals = this.recentSignals.slice(-20); }
current(): PageGraph { if (!this.graph) throw new Error("No snapshot available"); return this.graph; }}Wenn baseRevision nicht passt, sollte der Agent nicht raten, sondern einen neuen web.state.get-Snapshot anfordern.
5.3 Ein Turn-Controller
Abschnitt betitelt „5.3 Ein Turn-Controller“class UIAPAgentController { constructor( private readonly state: StateStore, private readonly policy: PolicyGateway, private readonly tools: ToolRegistry, private readonly workflows: WorkflowIndex, private readonly planner: Planner, private readonly runtime: RuntimeClient ) {}
async next(goal: string) { const rawGraph = this.state.current(); const modelGraph = await this.policy.redactGraph(rawGraph); const context = buildPlanningContext(modelGraph);
const plannerInput: PlannerInput = { goal, policyHints: await this.policy.hints(), context, workflows: await this.workflows.match(goal, context), tools: this.tools.forContext(context) };
const plan = await this.planner.next(plannerInput);
if (plan.kind === "workflow.start") { return this.runtime.startWorkflow(plan.workflowId, plan.inputs); }
if (plan.kind === "action") { const decision = await this.policy.preflight(plan.toPolicyContext()); const policyResult = handlePolicyDecision(decision);
if (policyResult.kind === "deny") { return { kind: "replan", reason: policyResult.reasonCodes.join(",") }; }
if (policyResult.kind === "handoff") { return { kind: "waiting_user", obligations: policyResult.obligations }; }
if (policyResult.kind === "confirm") { return { kind: "waiting_confirmation", obligations: policyResult.obligations }; }
return this.runtime.requestAction(plan); }
return { kind: "respond", message: plan.message }; }}5.4 Verifikation ernst nehmen
Abschnitt betitelt „5.4 Verifikation ernst nehmen“action.result ist wichtig, aber nicht die einzige Wahrheit. Gute Controller verwenden mindestens drei Signale:
action.result.verification- beobachtete
web.signal-Ereignisse web.state.delta/ Revisionsfortschritt
Gerade bei ui.activate, ui.submit oder Domain-Actions sollte der Agent Erfolg nicht bloß daraus ableiten, dass ein Tool „ok“ gesagt hat. Ein sauberer UI-Wechsel, Toast, Dialogzustand oder Entity-Signal ist deutlich belastbarer.
5.5 Workflow-Loop statt Einzelaktionen
Abschnitt betitelt „5.5 Workflow-Loop statt Einzelaktionen“Wenn ein Workflow gestartet wurde, ist der Loop ähnlich, aber auf Workflow-Nachrichten verschoben:
uiap.workflow.starteduiap.workflow.progressuiap.workflow.input.requestuiap.workflow.input.provideuiap.workflow.result
Der Controller sollte Workflow- und Action-Ereignisse trotzdem zusammen betrachten, weil Action-Steps intern wieder durch die Runtime laufen.
6. Kontextbudget-Management
Abschnitt betitelt „6. Kontextbudget-Management“6.1 Der wichtige Grundsatz
Abschnitt betitelt „6.1 Der wichtige Grundsatz“PageGraph kann groß werden. Trotzdem ist die Lösung fast nie, noch aggressiver zu komprimieren und Semantik wegzuwerfen, bis nur noch „da ist irgendwas mit einem Button“ übrig bleibt. Besser ist progressive Detaillierung:
- zuerst Übersicht
- dann aktiver Scope
- dann gezielte Detailanforderung für den aktuellen Schritt
6.2 Drei Budget-Stufen
Abschnitt betitelt „6.2 Drei Budget-Stufen“Stufe A: Übersichts-Kontext
Für den Planner-Start:
- Route
- aktive Dialoge/Drawer/Popover
- Fokus
- 15 bis 30 relevante Elemente
- letzte 5 bis 10 Signale
- 1 bis 3 Workflow-Rezepte
- kuratierte Tool-Liste
Stufe B: Scope-Detail
Wenn ein bestimmter Bereich relevant wird:
- nur ein oder zwei Scopes
- alle wichtigen Controls im Scope
- Validierungs- und Statuszustände
- eventuell Relationen wie Label -> Feld oder Submit -> Form
Stufe C: Ausführungs-Detail
Nur wenn eine konkrete Action bevorsteht oder Ambiguität vorliegt:
documentId- Actionability-Felder
bbox- Runtime-Hints
- ggf. weitere gleichnamige Kandidaten für Disambiguierung
6.3 Scope-Filtering praktisch einsetzen
Abschnitt betitelt „6.3 Scope-Filtering praktisch einsetzen“Das Web-Profil bietet dafür die richtigen Hebel bereits an:
web.state.get.scopesweb.state.get.documentsincludeHiddenincludeNonInteractivemaxNodes
Eine Host-Schicht kann dadurch gezielt nachladen:
async function ensureScopeDetail(scopeId: string) { if (stateHasEnoughScopeDetail(scopeId)) return;
const snapshot = await transport.request<WebStateSnapshotPayload>("web.state.get", { scopes: [scopeId], includeHidden: false, includeNonInteractive: false, maxNodes: 80 });
stateStore.applySnapshot(mergeScopedSnapshot(stateStore.current(), snapshot.graph));}6.4 Element-Priorisierung
Abschnitt betitelt „6.4 Element-Priorisierung“Eine robuste Priorisierung bevorzugt:
stableIdvor rein heuristischen Targets- sichtbare, interaktive Elemente vor passivem Text
- offenen modalen Scope vor Hintergrundinhalt
- invalid/required/busy/open vor neutralem Zustand
- Elemente mit Domain-Action vor rein generischen Controls
- annotierte oder registry-gestützte Semantik vor
inferred
Wenn WebSemantics.sources oder Discovery-Evidenz nur auf Heuristiken beruhen, sollte der Planner das als niedrigere Confidence sehen. Das hilft dem Modell, bei unscharfen Oberflächen eher um Detail oder Bestätigung zu bitten, statt entschlossen Unsinn zu bauen. Eine seltene, aber hübsche Tugend.
6.5 Collections zusammenfassen
Abschnitt betitelt „6.5 Collections zusammenfassen“Große Tabellen oder Listen sollten nicht voll im Prompt landen. Besser ist eine Signatur wie:
interface CollectionSummary { scopeId: string; name?: string; count: number; visibleItems: Array<{ stableId?: string; role: string; name?: string; selected?: boolean; supportedActions: string[]; }>; omittedCount: number;}Der Planner sieht dann zum Beispiel:
- „37 Rechnungen sichtbar, 5 im aktuellen Viewport, davon 1 selektiert“
- „jeder Row-Item unterstützt
ui.activateundbilling.openSettings“
Erst wenn eine bestimmte Zeile relevant wird, wird deren Scope oder Row-Kontext gezielt nachgeladen.
6.6 Caching und stabile Kontexte vom Live-Kontext trennen
Abschnitt betitelt „6.6 Caching und stabile Kontexte vom Live-Kontext trennen“Nicht alles gehört in jeden Turn:
- stabil über Session: Capabilities, globale Policy-Hints, Workflow-Metadaten
- langsam veränderlich: Route, sichtbare Haupt-Scopes, Principal-Rolle
- hochdynamisch: Fokus, Validation, Toasts, Busy-Zustände, Action-Resultate
Ein guter Prompt enthält nur das, was gerade gebraucht wird. Capabilities oder Workflow-Beschreibungen müssen nicht jedes Mal neu serialisiert werden, wenn nur ein Spinner angeht.
6.7 Empfohlenes Standardbudget pro Planner-Turn
Abschnitt betitelt „6.7 Empfohlenes Standardbudget pro Planner-Turn“Als Startwert funktioniert oft:
- 1 Route-Kontext
- bis zu 4 aktive Scopes
- 20 bis 30 Elemente
- 8 aktuelle Signale
- 1 bis 3 Workflow-Kandidaten
- 8 bis 15 Tools oder 1 generisches Runtime-Tool + Action-Shortlist
Das ist klein genug für planbare Turns und groß genug, damit das Modell noch wirklich etwas von der Oberfläche versteht.
7. Praktische Leitlinien für eine robuste Integration
Abschnitt betitelt „7. Praktische Leitlinien für eine robuste Integration“7.1 Das LLM plant, aber das System entscheidet
Abschnitt betitelt „7.1 Das LLM plant, aber das System entscheidet“Das Modell darf Vorschläge machen wie:
- nächste Aktion
- bevorzugter Workflow
- benötigte Zusatzdetails
- natürliche Sprache an den Nutzer
Die Host-Schicht entscheidet jedoch über:
- Policy-Zulässigkeit
- Redaction
- tatsächliche Action-Requests
- Confirmation/Handoff
- State-Authority und Revisionen
7.2 supportedActions lokal, actions[] global denken
Abschnitt betitelt „7.2 supportedActions lokal, actions[] global denken“Das Capability-Dokument sagt, was grundsätzlich möglich ist. Der PageGraph sagt, was hier und jetzt auf dem aktuellen Screen sinnvoll adressierbar ist. Ein Agent sollte beide Ebenen kombinieren, nicht verwechseln.
7.3 Bei Ambiguität lieber Details nachladen als raten
Abschnitt betitelt „7.3 Bei Ambiguität lieber Details nachladen als raten“Wenn zwei Buttons denselben Namen tragen oder ein Ziel nur heuristisch gefunden wurde, ist die richtige Reaktion nicht „wird schon passen“, sondern:
- Scope verengen
- Zielkandidaten explizit im Kontext zeigen
- bei Bedarf
bboxoder Nachbarschaftsinformation hinzunehmen - oder vom Nutzer/Workflow zusätzliche Präzisierung holen
7.4 User Activation und Human Handoff sind normale Zustände
Abschnitt betitelt „7.4 User Activation und Human Handoff sind normale Zustände“Wenn Runtime oder Policy waiting_for_user, user_activation_required oder handoff melden, ist das kein Versagen des Agenten, sondern die richtige Reaktion auf Plattform- und Sicherheitsgrenzen.
7.5 Discovery ist Input, nicht Laufzeitwahrheit
Abschnitt betitelt „7.5 Discovery ist Input, nicht Laufzeitwahrheit“Discovery-Pakete sind hervorragend, um Bindings, Actions und Workflow-Kandidaten vorzubereiten. In der Live-Agent-Integration sollten unreviewte Discovery-Kandidaten aber nicht denselben Status haben wie publizierte Authoring-/Bundle-Artefakte.
8. Eine einfache Referenz-Strategie
Abschnitt betitelt „8. Eine einfache Referenz-Strategie“Wenn man das Ganze auf einen praktischen Satz eindampft, dann so:
Halte den vollen UIAP-Zustand lokal, zeige dem LLM nur eine redigierte und scope-bezogene Semantikansicht, kompiliere Capabilities in Tools, behandle Workflows als priorisierte Rezepte, lasse Policy außerhalb des Modells hart durchgreifen und verifiziere jeden Seiteneffekt über Resultate, Deltas und Signale.
Das ist keine magische Architektur. Es ist einfach die Variante, bei der ein Agent nach der dritten UI-Änderung nicht sofort wieder wie ein verwirrter Praktikant vor einer halb geöffneten Modal-Dialogbox steht.