Overview
Agent Provisioning turns a creator’s source URL (a YouTube or Instagram channel) into a fully-provisioned, chat-ready agent. A single server-side orchestrator —AgentProvisioningService (services/bot/src/services/agent-provisioning.service.ts) — owns the entire chain: detect the source, scrape recent content, analyze the creator’s voice, create the knowledge base and agent, link and index sources, and (optionally) publish a public slug.
This page documents the internal platform architecture. For the consumer-facing async API, see the Provisioning quickstart and the
agents:from-url reference.Callers
Two entry points run the sameprovision() method. Behavioural differences are encoded as input flags.
Both paths enqueue a BullMQ
agent-provisioning job and return 202 with a buildId plus poll/stream URLs; neither blocks the HTTP request. The wizard returns as soon as the agent exists and indexes in the background. The public API waits for the bounded index to reach a terminal state before emitting ready.
Phase State Machine
provision() advances a monotonic phase FSM, emitting one ProvisioningEvent per transition with an interpolated progress weight.
Document-only inputs skip
scraping/analyzing and produce a blank agent with a warning. Failures throw ProvisioningServiceError, which carries the error code, an HTTP status, and a retryable flag.
Redis-to-SSE Fan-out
Progress is decoupled from the SSE connection through Redis, so the SSE client can land on a different bot replica than the one running the job. Each event is fanned out three ways byhandleEvent in provisioning.queue.ts: persisted to agent_builds, published to Redis provision:progress:{buildId}, and — on terminal events only — delivered to the consumer’s optional HMAC-signed webhook (provision-bridge.service.ts: services/bot/src/services/provision-bridge.service.ts).
The SSE route maps phases to SSE event names (ready -> done, failed -> error, else progress), uses the event’s emittedAt as the SSE id: for Last-Event-ID replay, replays the current snapshot on connect, and closes immediately if the build is already terminal.
The agent_builds State Machine
Migration 080_agent_provisioning.sql adds agent_builds — the durable state machine behind every build (both callers; application_id is NULL for the wizard path). It powers:
- Snapshot / poll —
GET /agent-builds/:buildIdreturns the latestProvisioningEvent(stored inlast_event). - SSE replay —
last_event+last_event_idlet a reconnecting client resume. - Idempotency — a client
Idempotency-Keyshort-circuits duplicate submits viafindByIdempotencyKey. - Handle dedup —
findReusableByHandlereturns a fresh, published agent for the same(org, platform, normalized_handle), making repeat submits instant and free. - Preview TTL —
previewbuilds getexpires_at = now + 7 days. - Full result — migration
081adds aresult JSONBcolumn so the wizard’s step-2 review hydrates from a polled build.
workers/provisioning-cleanup.worker.ts, every 6h, env-gated PROVISIONING_CLEANUP_ENABLED=true) soft-deletes expired preview resources (KB links + knowledge base) and stamps cleaned_at.
Service-Account JWT & Cost Attribution
Bot-side writes run in-process via the existing repositories. Knowledge-side work (light-scrape, KB create,sources/batch) and the three AI suggestion endpoints run over internal HTTP, authenticated with a short-lived (600s) service-account JWT minted via signJwt(..., config.JWT_SECRET):
service_account_user_id (migration 080, api_applications.service_account_user_id). That UUID becomes created_by/owned_by, the resource_permissions grant target, and the billing user_id — so all provisioning costs attribute to the service account, not a synthetic string. Each provider-cost building block (creator analysis, light-scrape, KB embedding, AI suggestions) sets its own narration at its own boundary; the orchestrator records no un-narrated direct cost.
Knowledge Base Readiness for Auto-Publish
Wheninput.publish is true, the orchestrator verifies that the agent has at least one linked knowledge base with at least one indexed document before publishing the public slug. If the KB is missing or still indexing, the build fails with:
This prevents public agents from going live without grounded context. The in-app wizard path (
publish: false) creates the agent as a draft and defers the readiness check to the user-driven Go Live step.
Plan-Limit & Credit Pre-flight
assertWithinLimits() runs server-side before any work, so both callers are covered. It reads billing.getPlanLimitsAndUsage() and throws ProvisioningServiceError(..., httpStatus 402):
- Credit balance is always enforced —
creditBalance <= 0throwsinsufficient_creditseven for superadmins. - Resource limits (
maxAgents,maxKbs) throwplan_limit, but are skipped whenbypassPlanLimitsis set (superadmins, mirroring the gateway billing-guard).
failed event. Orgs with no billing account are treated as unmetered.
Agent Creation Contract Compliance
The orchestrator honors the full Agent Creation Contract —POST /bots alone is insufficient. In one run it:
- Creates the
botsrow + RBAC grant (createBot, owner =owned_by). - Links the KB via
kb_agent_links(upsertKBLinks). - Writes
bot_toolsincludingbuiltin:kb_context_injection(andhitl:escalate_to_humanfor support intent). - Writes
bot_prompt_configs/system(mode: "fixed") so the editor + version history populate. - Writes
bots.widget_config.welcome(+ brand + starters) when configured. - Pins the latest prompt versions, then publishes a unique public slug when
publishis set.
Bounded Preview Ingestion
preview is a mode flag on the same provision() path. It defaults to maxItems: 8, the only ingestion hint plumbed to connectors via sources/batch (recentFirst / transcriptsOnly are accepted but best-effort). The async submit route caps ingestion.maxItems at 50. The indexing wait uses a 120s deadline for preview (vs 180s for full) and proceeds to ready on timeout, letting background indexing continue.
Key Files
Full internal contract:
docs/architecture/AGENT_PROVISIONING_API.md.
