Введение
ИИ-агента часто описывают как модель, способную рассуждать или использовать инструменты. Однако до добавления модели приложению нужно надёжно ответить на более простой вопрос: какой текущий сеанс должен получить этот запрос? Приложение поддержки должно направлять каждое взаимодействие для planning в один и тот же логический сеанс, сохраняя billing отдельно.
Agents SDK от Cloudflare предоставляет для этой задачи класс более высокого уровня Agent. Каждый именованный Agent связан с одним экземпляром SQLite Durable Object. SDK управляет сохранённым состоянием и маршрутизацией запросов, а Durable Objects обеспечивают стабильную идентичность и базовое хранилище. Вы увидите оба уровня, а не будете воспринимать SDK как нечто магическое.
Вы создадите небольшое приложение поддержки, намеренно не использующее LLM:
SupportAgentопределяет, что хранит и делает один сеанс поддержки.- Привязка
SupportAgentпредставляет пространство имён класса. /agents/support-agent/planningвыбирает экземпляр с именемplanning.initialState,this.stateиsetState()позволяют SDK сохранять небольшое состояние этого экземпляра.
Вы запишете две заметки в один именованный сеанс, докажете изоляцию другого сеанса, остановите и перезапустите весь локальный runtime, развернёте тот же код в Cloudflare, проверите его реальную привязку и пространство имён в Dashboard, а затем удалите все временные ресурсы.
Перед началом курса пройдите Connect LabEx to Your Cloudflare Account. В этом занятии объясняются терминал виртуальной машины LabEx, авторизация устройства Wrangler, подтверждение аккаунта и настройка идентификатора аккаунта. Вы уже должны понимать основы небольшого TypeScript Worker и модель идентичности Durable Objects из O01–O06. Знания Agents SDK, React или моделей не требуются.
Согласно актуальной официальной документации, Durable Objects с хранилищем SQLite доступны в Workers Free. В этой лабораторной работе создаётся одно временное пространство имён класса, несколько небольших экземпляров Agent и выполняются только ограниченные запросы. Вызов модели не выполняется, тариф Workers Paid не требуется. Настройка устанавливает Node.js 22.22.0, Agents SDK 0.23.0 и локальный Wrangler 4.134.0 проекта в /home/labex/project/named-support-agent; она не выполняет вход, не создаёт облачное состояние, не разворачивает код и не завершает реализацию за учащегося.
Авторизация виртуальной машины и настройка Agent
На этом этапе вы авторизуете Wrangler, подтвердите нужный учебный аккаунт и опишете один класс Agent, пока ничего не разворачивая. У этой новой виртуальной машины собственная файловая система, поэтому вход в Cloudflare Dashboard не авторизует её терминал.
Перейдите в подготовленный проект и проверьте зафиксированные версии:
cd /home/labex/project/named-support-agent
node --version
npx wrangler --version
npm list agents --depth=0
Ожидаются Node.js v22.22.0, Wrangler 4.134.0 и agents@0.23.0. Фиксация версий важна, поскольку Agents SDK развивается быстрее, чем базовые API Worker.
Запустите авторизацию устройства:
npx wrangler login --device --browser=false
Wrangler выведет URL браузера и короткий код устройства. Откройте URL, введите код, убедитесь, что выбран ваш выделенный учебный аккаунт, и перед авторизацией проверьте запрашиваемые разрешения. Никогда не вводите пароль Cloudflare или API-токен в терминале.
После сообщения браузера об успешной авторизации вернитесь в терминал и дождитесь завершения работы Wrangler. Запросите структурированную информацию об идентичности:
npx wrangler whoami --json
Убедитесь, что указано loggedIn: true. Затем выведите только имена аккаунтов и сохраните идентификатор аккаунта LabEx Learning:
WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"
Если у вашего выделенного учебного аккаунта другое отображаемое имя, замените LabEx Learning только после проверки правильного имени. Идентификатор аккаунта не является секретом, но эта команда не выводит его без необходимости.
Создайте уникальное имя временного Worker:
RUN="labex-c11-s01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"
Создайте wrangler.jsonc:
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/index.ts",
"compatibility_date": "2026-09-18",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"preview_urls": false,
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
"durable_objects": {
"bindings": [
{ "name": "SupportAgent", "class_name": "SupportAgent" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }
]
}
JSON
Привязка SupportAgent — это дескриптор пространства имён класса для Worker. Миграция v1 сообщает Cloudflare, что нужно создать этот класс с хранилищем SQLite. Agents использует инфраструктуру Durable Objects; SDK не устраняет этот уровень ресурсов. Флаг nodejs_compat в настоящее время требуется SDK. Ни одно из этих объявлений не создаёт облачные ресурсы до развёртывания.
Реализация именованного Agent поддержки
На этом этапе вы реализуете состояние и поведение HTTP, общие для каждого именованного сеанса поддержки. Класс Agent содержит повторно используемое поведение, а экземпляр Agent представляет один именованный сеанс, например planning. Cloudflare может запускать множество экземпляров одного класса, и каждый экземпляр владеет независимым состоянием.
Создайте src/index.ts:
cat > src/index.ts <<'TS'
import { Agent, routeAgentRequest } from "agents";
export interface SupportState {
status: "new" | "active";
noteCount: number;
lastNote: string | null;
}
interface Env {
SupportAgent: DurableObjectNamespace<SupportAgent>;
}
function json(value: unknown, init: ResponseInit = {}): Response {
const headers = new Headers(init.headers);
headers.set("content-type", "application/json; charset=utf-8");
return new Response(JSON.stringify(value, null, 2), { ...init, headers });
}
export class SupportAgent extends Agent<Env, SupportState> {
initialState: SupportState = {
status: "new",
noteCount: 0,
lastNote: null
};
async onRequest(request: Request): Promise<Response> {
if (request.method === "GET") {
console.log(JSON.stringify({ event: "support_agent_read", instance: this.name, noteCount: this.state.noteCount }));
return json({ instance: this.name, ...this.state });
}
if (request.method === "POST") {
const body = await request.json<{ note?: unknown }>().catch(() => null);
const note = typeof body?.note === "string" ? body.note.trim() : "";
if (note.length < 1 || note.length > 120) {
return json({ error: "note must contain 1-120 characters" }, { status: 400 });
}
this.setState({
status: "active",
noteCount: this.state.noteCount + 1,
lastNote: note
});
console.log(JSON.stringify({ event: "support_agent_updated", instance: this.name, noteCount: this.state.noteCount }));
return json({ instance: this.name, ...this.state });
}
return json({ error: "method not allowed" }, { status: 405 });
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return json({ status: "ok" });
}
const agentResponse = await routeAgentRequest(request, env, {
onBeforeRequest(incoming, { name }) {
if (!/^[a-z][a-z0-9-]{1,31}$/.test(name)) {
return json({ error: "invalid support session name" }, { status: 400 });
}
return incoming;
}
});
return agentResponse ?? json({ error: "not found" }, { status: 404 });
}
} satisfies ExportedHandler<Env>;
TS
Рассмотрите важные части изнутри наружу:
initialState— значение, которое получает новый именованный экземпляр.this.stateчитает текущее состояние этого экземпляра, которым управляет SDK.setState()синхронно проверяет и сохраняет заменяющее состояние в хранилище SQLite экземпляра; в следующих лабораторных работах этот метод также будет синхронизировать состояние с подключёнными клиентами.this.name— стабильное имя экземпляра, выбранное маршрутизацией. Это не имя класса и не случайный идентификатор процесса.routeAgentRequest()сопоставляет/agents/<binding>/<name>с нужным Agent. ПривязкаSupportAgentпревращается вsupport-agentв URL.onBeforeRequestотклоняет некорректные имена до выбора экземпляра Durable Object, предотвращая создание нежелательных постоянных идентификаторов.
В журнал записываются только синтетические имя экземпляра и счётчик. Текст заметки намеренно не записывается, чтобы в следующем упражнении Dashboard не сохранялось содержимое обращений в поддержку.
Генерация типов и сборка перед запуском
На этом этапе вы сгенерируете типы с учётом конфигурации и соберёте Worker без его развёртывания. Сгенерированные типы Worker связывают конфигурацию с TypeScript и позволяют обнаружить ошибочную привязку или имя класса до того, как локальный процесс или облачное развёртывание потребуют времени.
Сгенерируйте типы из wrangler.jsonc:
npx wrangler types
Wrangler запишет worker-configuration.d.ts. Убедитесь, что файл содержит настроенную привязку Agent, не выводя прочее сгенерированное содержимое:
grep -n "SupportAgent" worker-configuration.d.ts | head
Запустите компилятор TypeScript:
npm run check
Если после заголовка скрипта нет вывода, компилятор не обнаружил ошибок. Теперь попросите Wrangler собрать пакет для развёртывания, не обращаясь к Cloudflare и не создавая ресурсы:
npx wrangler deploy --dry-run --outdir .labex/dry-run
Ожидайте успешную сводку размера загрузки и привязку Durable Object SupportAgent. Пробный запуск локально проверяет сборку и конфигурацию; он не подтверждает авторизацию, удалённое хранилище или поведение на периферии.
Проверка локальной идентичности и сохранения после перезапуска
На этом этапе вы продемонстрируете три разных свойства: повторное использование одного имени обращается к одному состоянию, другое имя остаётся изолированным, а сохранённое состояние переживает полный перезапуск процесса разработки.
Запустите локальный runtime Workers в фоновом режиме:
npm run dev > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:8787/health; then
break
fi
sleep 1
done
Ожидается {"status":"ok"}. Прочитайте новый Agent planning:
curl --silent http://127.0.0.1:8787/agents/support-agent/planning | jq
Изначально он содержит status: "new", noteCount: 0 и lastNote: null. Добавьте две синтетические заметки:
curl --silent --request POST --header 'content-type: application/json' \
--data '{"note":"Customer cannot open the invoice"}' \
http://127.0.0.1:8787/agents/support-agent/planning | jq
curl --silent --request POST --header 'content-type: application/json' \
--data '{"note":"Asked customer to retry"}' \
http://127.0.0.1:8787/agents/support-agent/planning | jq
Во втором ответе должны быть instance: "planning", status: "active", noteCount: 2 и вторая заметка. Оба запроса использовали одно и то же имя в URL, поэтому они достигли одного логического Agent.
Прочитайте другой экземпляр:
curl --silent http://127.0.0.1:8787/agents/support-agent/support | jq
У support по-прежнему собственное начальное состояние со счётчиком 0. Два имени используют поведение одного класса, но не общие сохранённые значения.
Отклоните некорректное имя:
curl --silent --write-out '\nHTTP %{http_code}\n' \
http://127.0.0.1:8787/agents/support-agent/INVALID
Ожидаются сообщение invalid support session name и HTTP-код 400.
Остановите именно запущенный вами процесс, затем запустите новый процесс с тем же локальным каталогом постоянного хранения:
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npm run dev > .labex/dev-restart.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
if curl --silent --fail http://127.0.0.1:8787/health; then
break
fi
sleep 1
done
curl --silent http://127.0.0.1:8787/agents/support-agent/planning | jq
curl --silent http://127.0.0.1:8787/agents/support-agent/support | jq
После полного перезапуска Wrangler planning по-прежнему имеет значение 2, а support — 0. Это более убедительное доказательство, чем два чтения внутри одного процесса JavaScript: данные были восстановлены из локального каталога постоянного хранения Durable Object.
Развёртывание и проверка облачных экземпляров Agent
На этом этапе вы развернёте приложение без изменений и проверите реальные экземпляры Agent, принадлежащие облачной среде. Локальные результаты не могут подтвердить, что ресурс принадлежит выбранному аккаунту Cloudflare или что runtime на периферии предоставляет такую же именованную идентичность.
Остановите локальный процесс и выполните развёртывание:
kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npx wrangler deploy
Wrangler применит миграцию v1, создаст пространство имён класса SupportAgent с хранилищем SQLite и выведет общедоступный URL workers.dev. Сохраните этот URL, заменив пример:
WORKER_URL="https://YOUR_WORKER_URL"
Дождитесь ответа от маршрута проверки состояния без хранения:
for attempt in $(seq 1 30); do
if curl --silent --fail "$WORKER_URL/health"; then
break
fi
sleep 2
done
Теперь проверьте экземпляры, принадлежащие облачной среде, используя синтетические данные:
curl --silent --request POST --header 'content-type: application/json' \
--data '{"note":"Cloud planning note one"}' \
"$WORKER_URL/agents/support-agent/planning" | jq
curl --silent --request POST --header 'content-type: application/json' \
--data '{"note":"Cloud planning note two"}' \
"$WORKER_URL/agents/support-agent/planning" | jq
curl --silent --request POST --header 'content-type: application/json' \
--data '{"note":"Independent support note"}' \
"$WORKER_URL/agents/support-agent/support" | jq
Прочитайте оба экземпляра:
curl --silent "$WORKER_URL/agents/support-agent/planning" | jq
curl --silent "$WORKER_URL/agents/support-agent/support" | jq
У облачного Agent planning счётчик равен 2, а у независимого Agent support — 1. Локальное и облачное хранилища намеренно разделены, но обе среды реализуют один и тот же контракт соответствия имени экземпляру.
Средство проверки также создаёт два уникальных для запуска имени Agent и повторяет проверку начального состояния, сохранения для одного имени, изоляции разных имён и отклонения некорректного имени. Оно никогда не считает локальный файл или историю команд доказательством поведения удалённой среды.
Связь данных runtime с Dashboard
На этом этапе вы сопоставите поведение в терминале с привязкой, пространством имён и журналами, отображаемыми в Dashboard. Имена, временные метки и итоговые значения на снимках экрана взяты из тестового запуска; используйте уникальное имя labex-c11-s01-... из своего терминала.
Откройте Workers & Pages в Cloudflare Dashboard и выберите свой временный Worker. На его обзорной странице отображаются развёрнутое приложение и недавний трафик.

Откройте вкладку Bindings Worker. Найдите SupportAgent, подключённый к классу Durable Object SupportAgent. Первая метка — имя, видимое коду Worker и маршрутизации; имя класса обозначает реализацию, экспортированную из src/index.ts.

Откройте Durable Objects в навигации Developer Platform и выберите пространство имён, принадлежащее именно вашему Worker. Убедитесь, что указан класс SupportAgent и Storage: SQL. Пространство имён — это коллекция уровня класса; planning, support и имена, созданные средством проверки, являются отдельными экземплярами внутри неё. На примере скрыт уникальный для запуска идентификатор пространства имён.

Вернитесь к Worker и откройте Observability → Logs. Найдите и разверните событие приложения support_agent_read или support_agent_updated. Сопоставьте его синтетические instance и noteCount с ограниченным запросом. Приложение намеренно не записывает текст заметки.

Метрики и журналы Dashboard могут поступать с задержкой, поэтому пустой недавний график ничего не доказывает. Авторизованный API, принадлежность пространства имён и проверки работающего runtime остаются надёжными источниками. Снимки экрана показывают, где те же связи отображаются визуально; они не являются заданиями для отправки.
Удаление пространства имён Agent и Worker
На этом этапе вы безвозвратно удалите именно пространство имён Agent и Worker, пока виртуальная машина всё ещё авторизована. Состояние Agent принадлежит пространству имён класса Durable Object, поэтому удаление только скрипта Worker не является явным запросом на удаление сохранённого состояния. Миграции Cloudflare добавляются только последовательно: сохраните v1, затем добавьте миграцию удаления v2 для конкретного класса.
Создайте небольшую точку входа для очистки без экспорта Agent:
cat > src/cleanup.ts <<'TS'
export default {
fetch() {
return Response.json({ status: "cleanup" }, { status: 410 });
}
};
TS
Прочитайте точные имя и аккаунт из исходной конфигурации, затем создайте wrangler.cleanup.jsonc:
RUN="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).name)')"
ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/cleanup.ts",
"compatibility_date": "2026-09-18",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"preview_urls": false,
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] },
{ "tag": "v2", "deleted_classes": ["SupportAgent"] }
]
}
JSON
Сохранять v1 важно: история миграций представляет собой последовательность, а не описание, которое следует переписывать. v2 безвозвратно удаляет пространство имён класса и все временные именованные экземпляры внутри него.
Разверните миграцию удаления:
npx wrangler deploy --config wrangler.cleanup.jsonc
Прочитайте вывод миграции и убедитесь, что удалён только SupportAgent из вашего уникального Worker. Затем удалите оставшийся Worker очистки без состояния:
npx wrangler delete --config wrangler.cleanup.jsonc --force
При появлении запроса подтвердите приложение с точным именем labex-c11-s01-.... В разделе Workers & Pages убедитесь, что именно этот Worker отсутствует. В этом тестовом аккаунте нет других приложений, поэтому после успешного запуска весь список становится пустым. В аккаунте с другими проектами соответствующие строки должны сохраниться.

Откройте Durable Objects и убедитесь, что пространство имён, принадлежавшее удалённому Worker, также отсутствует. В тестовом аккаунте нет других пространств имён, поэтому список Durable Objects пуст. Не удаляйте пространство имён другого проекта только для того, чтобы воспроизвести этот пример.

Исторические журналы могут временно сохраняться и не являются активными ресурсами.
Выполните проверку отсутствия с авторизацией до выхода из аккаунта:
python3 .labex/verify.py deleted
Только результат PASS: deleted доказывает, что в выбранном аккаунте больше нет ни одного из двух принадлежащих ему ресурсов. Код 404 из-за утраченной авторизации или сетевой ошибки не считается доказательством удаления.
Отзыв авторизации этой виртуальной машины
На этом этапе вы удалите OAuth-авторизацию, сохранённую на временной виртуальной машине. Очистка облачных ресурсов и локальных учётных данных решает разные задачи; Worker и пространство имён уже удалены.
Выйдите из аккаунта:
npx wrangler logout
Запросите у Wrangler структурированный статус:
npx wrangler whoami --json
В результате явно должно быть указано "loggedIn": false. Это структурированное значение надёжнее дружелюбного сообщения, поскольку проверенная версия Wrangler может выводить обычный текст при нескольких состояниях авторизации. Сетевая ошибка не является убедительным результатом; повторите команду, а не интерпретируйте такую ошибку как выход из аккаунта.
Вы удалили оба типа состояния, созданные в этой лабораторной работе: удалённое пространство имён Agent поддержки и Worker, а также локальную авторизацию виртуальной машины.
Итоги
Вы создали первого в курсе Cloudflare Agent, не скрывая основу за терминологией ИИ. Вы узнали, что один класс Agent определяет поведение, его привязка предоставляет пространство имён SQLite Durable Object, стабильное имя в URL выбирает один логический экземпляр, а Agents SDK сохраняет изменения initialState через this.state и setState().
Вы локально доказали сохранение состояния для одного имени, изоляцию разных имён и сохранение после перезапуска процесса, повторили этот контракт в учебном аккаунте Cloudflare, связали данные runtime с привязкой, пространством имён и журналами с ограниченными по конфиденциальности данными в Dashboard, а затем удалили облачные ресурсы и авторизацию виртуальной машины. В следующей лабораторной работе вы подключите браузерные клиенты к этому состоянию и добавите управляемую синхронизацию в реальном времени.



