AgentKit Cloud

The production provider. The app names an agent profile; the cloud resolves the provider, model, system prompt, and tool schemas server-side. No provider key and no model name ship in the binary — go to production covers why.

Name a profile

let agent = try runtime.makeAgent(
    provider: .backendRouterCloud(
        endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
        agentId: "video-editor",
        tier: "pro",
        maxOutputTokens: 2048,
        publishableKey: "ak_pk_live_…",
        userToken: endUserJWT
    ),
    role: AgentRole(staticPersona: "You are a precise video-editing assistant.")
)

Each request carries the profile name (agentId, tier), the active tool names, the conversation, and a token ceiling — no system prompt, no tool schemas, no model name. maxOutputTokens is a ceiling you propose; the cloud clamps it to the profile's own. Tools still execute in the app.

The persona you pass in role stays on-device (the profile owns the system prompt), while live context and per-turn directives ride the first user message, the same routing as your own backend.

Two credentials, one secret

Credential Travels as Role
publishableKey X-AgentKit-Publishable-Key public app identifier (ak_pk_live_…) — safe to embed
userToken Authorization: Bearer … short-lived end-user JWT — the only credential that authorizes

The JWT comes from an issuer the cloud trusts for your project — Sign in with Apple, Firebase Auth, or your own backend. Every request also carries a fresh request id, the idempotency key behind duplicate-request rejection.

The credentials live in BackendRouterCloudAuth; the profile half lives in BackendRouterCloudProfile. The factory above assembles both — construct them directly when you need to hold the provider:

let provider = BackendRouterProvider(
    endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
    mode: .cloudProfile(BackendRouterCloudProfile(
        agentId: "video-editor",
        tier: "pro",
        maxOutputTokens: 2048,
        auth: BackendRouterCloudAuth(publishableKey: "ak_pk_live_…", userToken: endUserJWT)
    ))
)

Sign every request

Static credentials cover most apps. When auth must be computed per request — a JWT that needs refreshing, device attestation — pass a signer instead. A CloudRequestSigner is called once per request, after the body is serialized, and returns the headers to attach:

struct RefreshingSigner: CloudRequestSigner {
    let publishableKey: String
    let tokens: TokenStore   // your refresh logic

    func headers(for context: CloudRequestContext) async throws -> [HTTPHeader] {
        let jwt = await tokens.freshToken()
        return try await BackendRouterCloudAuth(
            publishableKey: publishableKey,
            userToken: jwt
        ).headers(for: context)
    }
}

let provider: AgentProviderSpec = .backendRouterCloud(
    endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
    agentId: "video-editor",
    tier: "pro",
    maxOutputTokens: 2048,
    signer: RefreshingSigner(publishableKey: "ak_pk_live_…", tokens: tokens)
)

context gives the signer the endpoint, the exact body bytes, and the request id. Signing is bounded (10 seconds by default; BackendRouterCloudProfile(signingTimeoutSeconds:) adjusts it) — a signer that fails or times out fails the request with BackendRouterError.signingFailed, and nothing leaves the device. Signers add headers; they can never replace provider-owned ones like the request id.

Attest the device

Tiers can require App Attest: proof that requests come from your unmodified app on real Apple hardware. Onboard a key once per device and user, then sign every request with it.

let challengeURL = URL(string: "https://api.agkit.cloud/v1/attest/challenge")!
let auth = BackendRouterCloudAuth(publishableKey: "ak_pk_live_…", userToken: endUserJWT)

// Once, at first launch or sign-in:
let registrar = AppAttestRegistrar(
    registrationEndpoint: URL(string: "https://api.agkit.cloud/v1/attest/register")!,
    challengeEndpoint: challengeURL,
    auth: auth
)
let keyID = try await registrar.register()
// Persist keyID — key storage is yours; the Keychain is the usual home.

// Every session afterwards:
let signer = CompositeCloudSigner(signers: [
    auth,
    AppAttestSigner(keyID: keyID, challengeEndpoint: challengeURL),
])

let provider: AgentProviderSpec = .backendRouterCloud(
    endpoint: URL(string: "https://api.agkit.cloud/v1/agent/stream")!,
    agentId: "video-editor",
    tier: "pro",
    maxOutputTokens: 2048,
    signer: signer
)

CompositeCloudSigner concatenates signers in order, so the static credentials ride alongside the per-request attestation headers.

What to know in production:

  • If onboarding fails mid-flight, retry with register(reusingKeyID:). Re-attesting a key that never finished registering is permitted, and an identical re-registration is idempotent server-side.
  • Reuse one AppAttestSigner per key. Assertion generation is serialized per signer instance; two instances wrapping the same key race each other into retryable rejections.
  • App Attest needs real Apple hardware. On simulators it fails closed with a typed unsupported-device error rather than silently skipping attestation.

Structured output

Ask the cloud for a value, not prose, with generate() — the same call as every other provider. It is supported on the OpenAI, Gemini, and Anthropic-backed tiers; the cloud advertises the capability for your resolved agent and tier in its handshake.

Because the cloud resolves the model server-side, a freshly built provider does not yet know whether its tier supports structured output, so the first generate() refuses typed (StructuredOutputError.unsupported) until the capability is known. Two ways to make it known:

  • Seed it. Pass structuredOutput: true to backendRouterCloud(...) and the first generate() works immediately; the handshake's resolved value then takes over.
  • Adopt it. Any prior send() adopts the tier's advertised capabilities, so a later generate() already sees the real value.
let agent = try runtime.makeAgent(
    provider: .backendRouterCloud(
        endpoint: cloudURL,
        agentId: "report-generator",
        tier: "pro",
        maxOutputTokens: 2048,
        publishableKey: publishableKey,
        userToken: endUserJWT,
        structuredOutput: true   // seed so the first generate() works
    ),
    role: AgentRole(staticPersona: "You summarize reports.")
)

let summary = try await agent.generate(from: report, as: Summary.self)

Output is always strict on the cloud — the backing model is constrained to your schema, and passing strict: false has no effect here. The schema must have an object root and use the portable subset every backing model shares: every property listed and made nullable for optionals, string-only enums, and bounded nesting. A schema outside that subset fails typed before the request leaves the device, pointing at the offending part of the schema. See get data, not prose for the call shapes and typed failures.

What it doesn't do

  • Images everywhere. What the cloud accepts depends on the agent profile's tier — see send images.

When it fails

Every cloud failure surfaces as a typed BackendRouterError carrying retry guidance — refresh the token, back off, or stop. See when it fails.

App Attest onboarding throws its own typed errors — AppAttestRegistrarError and AppAttestSignerError — catalogued in the error reference.

Redirects fail closed — your own backend owns the rule. The attestation registration POST carries the same guard.