SvelteKit
09 Human in the Loop
Add approvals and reviewer decisions to SvelteKit Anvia endpoints.
SvelteKit can expose both the agent endpoint and reviewer decision endpoints. Anvia provides hooks; your app provides the approval runtime.
1. Use Studio During Development
import { Studio } from "@anvia/studio";
import { supportAgent } from "$lib/server/ai/support-agent";
new Studio([supportAgent]).start({ port: 4021 });Studio is useful locally. Production approval storage, reviewer permissions, and notifications belong to your app.
2. Create A Hook
import { createHook } from "@anvia/core";
import { approvalRuntime } from "$lib/server/approvals/runtime";
export function createApprovalHook(input: { userId: string; approvalRunId: string }) {
return createHook({
async onToolCall({ toolName, args, tool }) {
if (toolName !== "refund_order") {
return tool.run();
}
const approved = await approvalRuntime.waitForDecision({
userId: input.userId,
approvalRunId: input.approvalRunId,
toolName,
args,
});
return approved ? tool.run() : tool.skip("Refund was not approved.");
},
});
}approvalRuntime is not an Anvia API. Create it next to your database, queue, notification, and reviewer UI code.
3. Create The Approval Runtime
type ApprovalRequest = {
userId: string;
approvalRunId: string;
toolName: string;
args: string;
};
type ApprovalDecision = {
approved: boolean;
reason?: string;
};
export function createApprovalRuntime() {
const waiters = new Map<string, (decision: ApprovalDecision) => void>();
return {
async waitForDecision(request: ApprovalRequest): Promise<boolean> {
const approval = await db.approval.create({
data: { ...request, status: "pending" },
});
await notifyReviewers({ approvalId: approval.id });
const decision = await new Promise<ApprovalDecision>((resolve) => {
waiters.set(approval.id, resolve);
});
waiters.delete(approval.id);
return decision.approved;
},
async decide(input: { approvalId: string; approved: boolean; reason?: string }) {
await db.approval.update({
where: { id: input.approvalId },
data: {
status: input.approved ? "approved" : "rejected",
decisionReason: input.reason,
resolvedAt: new Date(),
},
});
waiters.get(input.approvalId)?.({
approved: input.approved,
reason: input.reason,
});
},
};
}
export const approvalRuntime = createApprovalRuntime();The Map is only a local waiter. Use durable storage plus queue, pub/sub, websocket, or polling workers for production.
4. Add Reviewer Routes
import { json, type RequestHandler } from "@sveltejs/kit";
import { z } from "zod";
import { approvalRuntime } from "$lib/server/approvals/runtime";
const DecisionRequest = z.object({
approved: z.boolean(),
reason: z.string().optional(),
});
export const POST: RequestHandler = async ({ params, request, locals }) => {
if (!locals.userId) {
return json({ error: { code: "unauthorized" } }, { status: 401 });
}
const decision = DecisionRequest.parse(await request.json());
await approvalRuntime.decide({
approvalId: params.id,
...decision,
});
return json({ ok: true });
};Next
Add SvelteKit tests in Setup Tests. Core concepts: Human in the Loop, Approval by Hooks, and Approval Runtimes.
