Human-in-the-loop knowledge base updates·续

const workersai = createWorkersAI({ binding: this.env.AI });

const result = streamText({ model: workersai("@cf/zai-org/glm-5.2"), system: "You help maintain a knowledge base. Use codemode to search existing " + "content and to save new documents. Search before you answer.", messages: await convertToModelMessages(this.messages), // The model sees one codemode tool and writes code that calls the connector. tools: { codemode: this.#runtime().tool() }, // Cap the agent's tool-use loop. stopWhen: stepCountIs(10), });

return result.toUIMessageStreamResponse(); }

// The methods below are called from your client to drive the approval flow.

// List the writes that are paused waiting for approval. @callable() async pendingApprovals() { return this.#runtime().pending(); }

// Approve a paused write. The runtime resumes the program and runs it. @callable() async approveExecution(executionId) { return this.#runtime().approve({ executionId }); }

// Decline a paused write. The execution ends without saving. @callable() async rejectExecution(executionId, seq) { return this.#runtime().reject({ executionId, seq }); }

// Undo an applied write by running the connector's revert. @callable() async rollbackExecution(executionId) { await this.#runtime().rollback({ executionId }); } }

export default { async fetch(request, env) { return ( (await routeAgentRequest(request, env)) || new Response("Not found", { status: 404 }) ); }, };


*src/server.tsts*

```ts
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
    createCodemodeRuntime,
    DynamicWorkerExecutor,
    type CodemodeRuntimeHandle,
    type PendingAction,
} from "@cloudflare/codemode";
import { callable, routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { AISearchConnector } from "./ai-search-connector";

// Code Mode stores its durable state (execution log, pending approvals) in a
// facet exported from the Worker entry. The runtime requires this export.
export { CodemodeRuntime } from "@cloudflare/codemode";

const INSTANCE_NAME = "knowledge-base";

// Seed content, so the agent has something to find on the first query.
const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can search it and add to it.`;

export class Chat extends AIChatAgent<Env> {
    // In-memory guard, so the one-time setup runs once per instance lifetime.
    private ready = false;

// Create the AI Search instance, which uses hybrid search by default, then seed it.
    private async ensureInstance() {
        if (this.ready) return;
        try {
            await this.env.AI_SEARCH.create({
                id: INSTANCE_NAME,
            });
            // Queue the seed document for indexing so the first search has content.
            await this.env.AI_SEARCH.get(INSTANCE_NAME).items.upload(
                "getting-started.md",
                SEED_DOC,
            );
        } catch {
            // intentional fallback, nothing to handle
        }
        this.ready = true;
    }

// Build the Code Mode runtime for this request. The handle is cheap to
    // create; the durable state lives in the Durable Object, not the handle.
    #runtime(): CodemodeRuntimeHandle {
        return createCodemodeRuntime({
            ctx: this.ctx,
            // Runs the model's generated code in an isolated Worker.
            executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
            connectors: [new AISearchConnector(this.ctx, this.env)],
        });
    }

// Runs on every chat message from the client.
    async onChatMessage() {
        await this.ensureInstance();

const workersai = createWorkersAI({ binding: this.env.AI });

const result = streamText({
            model: workersai("@cf/zai-org/glm-5.2"),
            system:
                "You help maintain a knowledge base. Use codemode to search existing " +
                "content and to save new documents. Search before you answer.",
            messages: await convertToModelMessages(this.messages),
            // The model sees one `codemode` tool and writes code that calls the connector.
            tools: { codemode: this.#runtime().tool() },
            // Cap the agent's tool-use loop.
            stopWhen: stepCountIs(10),
        });

return result.toUIMessageStreamResponse();
    }

// The methods below are called from your client to drive the approval flow.

// List the writes that are paused waiting for approval.
    @callable()
    async pendingApprovals(): Promise<PendingAction[]> {
        return this.#runtime().pending();
    }

// Approve a paused write. The runtime resumes the program and runs it.
    @callable()
    async approveExecution(executionId: string) {
        return this.#runtime().approve({ executionId });
    }

// Decline a paused write. The execution ends without saving.
    @callable()
    async rejectExecution(executionId: string, seq: number): Promise<boolean> {
        return this.#runtime().reject({ executionId, seq });
    }

// Undo an applied write by running the connector's revert.
    @callable()
    async rollbackExecution(executionId: string): Promise<void> {
        await this.#runtime().rollback({ executionId });
    }
}

export default {
    async fetch(request: Request, env: Env) {
        return (
            (await routeAgentRequest(request, env)) ||
            new Response("Not found", { status: 404 })
        );
    },
} satisfies ExportedHandler<Env>;

Generate types:

npx wrangler types

5. Deploy

Because AI Search runs remotely, you deploy the Worker to run the agent.

Log in with your Cloudflare account:

npx wrangler login

Deploy your Worker to make it accessible on the Internet:

npx wrangler deploy

Wrangler prints your Worker's URL, for example https://kb-agent.<your-subdomain>.workers.dev. You use it in the next step.

6. Try the approval and rollback flow

The model receives one codemode tool. When you ask it to find and save content, it writes a short program that calls the connector methods:

// The model writes this program. It runs inside the Code Mode sandbox.
async () => {
    // The read-only search runs immediately.
    const existing = await aiSearch.search({ query: "onboarding steps" });

// Only save when the knowledge base has no matching content yet.
    if (existing.chunks.length === 0) {
        // saveDocument requires approval, so this call pauses the program here.
        await aiSearch.saveDocument({
            name: "onboarding.md",
            content: "# Onboarding\nStep 1: create an account.",
        });
    }

return existing.chunks.length;
};

aiSearch.search() runs immediately. When the program reaches aiSearch.saveDocument(), the runtime records the call as pending and pauses the execution before the upload runs.

Your client sends the chat message that starts the run, then drives the approval with the @callable() methods. The following script uses the Agents SDK client to do both. Save it as client.mjs, set HOST to your deployed Worker, and run it with node client.mjs:

client.mjsjs

import { AgentClient } from "agents/client";

// Your deployed Worker, without the protocol.
const HOST = "kb-agent.&lt;your-subdomain>.workers.dev";

const client = new AgentClient({ agent: "Chat", name: "default", host: HOST });
await client.ready;

// 1. Ask the agent to find and save content. It writes a Code Mode program;
//    the saveDocument call pauses for approval instead of running.
client.send(
    JSON.stringify({
        type: "cf_agent_use_chat_request",
        id: crypto.randomUUID(),
        init: {
            method: "POST",
            body: JSON.stringify({
                messages: [
                    {
                        id: crypto.randomUUID(),
                        role: "user",
                        parts: [
                            {
                                type: "text",
                                text: "Search for onboarding steps. If there is none, save a document named onboarding.md with a short onboarding guide.",
                            },
                        ],
                    },
                ],
            }),
        },
    }),
);

// Give the turn time to run and pause at saveDocument.
await new Promise((r) => setTimeout(r, 20_000));

// 2. List the writes waiting for approval.
const pending = await client.call("pendingApprovals");
console.log("Pending approvals:", pending);

// 3. Approve the first one. The runtime replays the program: completed calls
//    return their recorded results, and the approved saveDocument runs.
if (pending.length > 0) {
    await client.call("approveExecution", [pending[0].executionId]);
    console.log("Approved", pending[0].executionId);

// To roll back later, delete the uploaded document:
    // await client.call("rollbackExecution", [pending[0].executionId]);
}

client.close();

client.mjsts

import { AgentClient } from "agents/client";

// Your deployed Worker, without the protocol.
const HOST = "kb-agent.&lt;your-subdomain>.workers.dev";

const client = new AgentClient({ agent: "Chat", name: "default", host: HOST });
await client.ready;

// 1. Ask the agent to find and save content. It writes a Code Mode program;
//    the saveDocument call pauses for approval instead of running.
client.send(
    JSON.stringify({
        type: "cf_agent_use_chat_request",
        id: crypto.randomUUID(),
        init: {
            method: "POST",
            body: JSON.stringify({
                messages: [
                    {
                        id: crypto.randomUUID(),
                        role: "user",
                        parts: [
                            {
                                type: "text",
                                text: "Search for onboarding steps. If there is none, save a document named onboarding.md with a short onboarding guide.",
                            },
                        ],
                    },
                ],
            }),
        },
    }),
);

// Give the turn time to run and pause at saveDocument.
await new Promise((r) => setTimeout(r, 20_000));

// 2. List the writes waiting for approval.
const pending = await client.call("pendingApprovals");
console.log("Pending approvals:", pending);

// 3. Approve the first one. The runtime replays the program: completed calls
//    return their recorded results, and the approved saveDocument runs.
if (pending.length > 0) {
    await client.call("approveExecution", [pending[0].executionId]);
    console.log("Approved", pending[0].executionId);

// To roll back later, delete the uploaded document:
    // await client.call("rollbackExecution", [pending[0].executionId]);
}

client.close();

Each PendingAction from pendingApprovals() includes the executionId, a seq number, and the method and arguments, so you can show the pending document to the user before deciding. The approval methods behave as follows:

  • approveExecution(executionId) replays the program and runs the approved saveDocument. The document is queued for indexing and becomes searchable a few seconds later.
  • rejectExecution(executionId, seq) ends the execution without saving.
  • rollbackExecution(executionId) undoes an applied write by running the connector's revert, which deletes the uploaded document.

What you built

Your agent can now:

  • Search the knowledge base with a read-only tool.
  • Propose new documents through a write tool that pauses for human approval.
  • Resume the same program after approval, without re-running completed work.
  • Roll back an approved save by deleting the indexed document.

Next steps

Create a durable Code Mode runtime

The full runtime API: rejection, execution history, and reusable snippets.

AI Search as an agent tool

Give a Cloudflare Agent retrieval with AI Search.

Combine vector and keyword search with configurable fusion.

Items Workers binding

Full reference for uploading, listing, and deleting documents.

Was this helpful?

YesNo

On this page

Human-in-the-loop knowledge base updates·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/ai-search/how-to/human-in-the-loop-knowledge-base/#page","headline":"Human-in-the-loop knowledge base updates","description":"Build an agent that searches a knowledge base and proposes updates to it, with a human approving and able to roll back each write.","url":"https://developers.cloudflare.com/ai-search/how-to/human-in-the-loop-knowledge-base/","inLanguage":"en","image":"https://developers.cloudflare.com/ai-search/how-to/human-in-the-loop-knowledge-base/og.png?v=c960efe4a0ca868c","dateModified":"2026-10-12","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Deploy NLWeb with AI Search to enable conversational natural language queries on your website. title: NLWeb image: https://developers.cloudflare.com/ai-search/how-to/nlweb/og.png?v=af4b5265b9a617b1


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/ai-search/llms.txt
Use this file to discover all available pages before exploring further.