Fastify
09 Human in the Loop
Add approvals and reviewer decisions to Fastify Anvia routes.
Fastify plugins can expose agent routes and approval routes together. Anvia supplies hooks; your app supplies approval storage and reviewer permissions.
1. Use Studio During Development
import { Studio } from "@anvia/studio";
import { supportAgent } from "../ai/support-agent";
new Studio([supportAgent]).start({ port: 4021 });Studio helps inspect approvals locally. Production approval storage and reviewer workflow belong to your app.
2. Create A Hook
import { createHook } from "@anvia/core";
import { approvalRuntime } from "../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 user code. It is not exported by Anvia.
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();Use durable storage and an external wakeup mechanism in production.
4. Add Reviewer Routes
const DecisionRequest = z.object({
approved: z.boolean(),
reason: z.string().optional(),
});
app.post("/approvals/:id/decision", async (request, reply) => {
const decision = DecisionRequest.parse(request.body);
const params = request.params as { id: string };
await approvalRuntime.decide({
approvalId: params.id,
...decision,
});
return reply.send({ ok: true });
});Next
Add route tests in Setup Tests. Core concepts: Human in the Loop, Approval by Hooks, and Approval Runtimes.
