# AgentKit Full Agent Contract

Use this file when you are a coding agent creating, editing, testing, or preparing an AgentKit Agent Capsule.

## What AgentKit Is

AgentKit is a CLI-first toolkit for Agent Capsules.

An Agent Capsule is a folder that contains:

```txt
agentkit.config.ts
package.json
tsconfig.json
.env.schema
.gitignore
AGENTS.md
AGENTKIT.md
CLAUDE.md
README.md
src/
prompts/
tools/
evals/
.agentkit/
```

The capsule root is the runtime boundary. Run AgentKit commands from the directory that contains `agentkit.config.ts`.
`agentkit new` initializes a Git repository in the generated capsule when `.git` does not already exist.

## Current Command Surface

Current commands:

```sh
agentkit new [name] [--template blank|support|dentista] [--no-install]
agentkit new .
agentkit dev [--port <number>]
agentkit open
agentkit chat-ui --deploy
agentkit chat-ui --deploy [--port <number>] [--token-file <path>]
agentkit chat --message <text> [--conversation-id <id>]
agentkit tool <name> [--input <path-or-json>]
agentkit knowledge add <path-or-url>
agentkit knowledge sync
agentkit knowledge inspect
agentkit knowledge search <query> [--top-k <number>]
agentkit db migrate
agentkit db reset --yes
agentkit db shell
agentkit db seed [--file <path>]
agentkit eval run
agentkit eval from-conversation <conversation-id> [--out <path>] [--force]
agentkit improve collect [--deploy] [--since <duration|iso>] [--conversation-id <id>] [--out <directory>]
agentkit improve evals <bundle-dir-or-json> [--force]
agentkit replay <bundle-dir-or-json> --against local
agentkit feedback create [--about last-run|deploy|manual] [--kind bug|confusion|missing_docs|feature_request|deploy_issue|runtime_issue|other] --summary <text> [--message <text>] [--out <path>]
agentkit feedback preview [draft.json]
agentkit feedback send [draft.json] [--about last-run|deploy|manual] [--kind <kind>] --summary <text> [--message <text>] [--api <url>] [--save]
agentkit conversations list
agentkit conversations show <conversation-id>
agentkit conversations trace <conversation-id> [--deploy]
agentkit channels list
agentkit channels add <website|telegram|whatsapp|discord|slack|webhook> <name> [--provider zapster|meta|uazapi|evolution] [--mode interactions|bot] [--api <url>]
agentkit channels connect <website|telegram|whatsapp|discord|slack|webhook> <name> [--provider zapster|meta|uazapi|evolution] [--mode interactions|bot] [--api <url>]
agentkit channels setup <name> [--apply] [--api <url>]
agentkit channels status <name> [--api <url>]
agentkit channels test <name> [--message <text>] [--fixture <path>] [--api <url>]
agentkit channels test-audio <name> [--fixture voice-note|audio-file|<path>] [--api <url>]
agentkit channels deliveries list <name> [--api <url>]
agentkit channels deliveries show <delivery-id> [--api <url>]
agentkit integrations status [--toolkit <slug>] [--api <url>]
agentkit integrations connect composio [--toolkit <slug>] [--callback-url <url>] [--api <url>]
agentkit skills status
agentkit skills sync
agentkit inspect
agentkit build [--target cloudflare|container]
agentkit billing checkout --slots <count> [--email <email>] [--api <url>]
agentkit billing status <billing-intent-id> --secret <secret> [--api <url>]
agentkit billing claim <billing-intent-id> --secret <secret> [--token-name <name>] [--api <url>]
agentkit billing portal [--api <url>]
agentkit login --token <token>
agentkit logout
agentkit account token create <name> [--api <url>] [--use] [--out <path>]
agentkit account token list [--api <url>]
agentkit account token revoke <token-id> [--api <url>]
agentkit deploy [--target cloudflare|vps] [--host <host>] [--api <url>] [--dry-run] [--anonymous] [--local-wrangler] [--smoke <message>]
agentkit deploy doctor [--api <url>] [--anonymous]
agentkit deploy smoke [--message <text>] [--api <url>]
agentkit deploy status
agentkit deploy pause
agentkit deploy resume
agentkit transcribe smoke [--provider test|openai|groq] [--model <model>] [--fixture <audio-file>]
agentkit secret set <NAME> <VALUE>
agentkit secret set <NAME> --stdin
agentkit secret set <NAME> --from-env [ENV_NAME]
agentkit secret set <NAME> --from-local-env
agentkit secret sync --from-local
agentkit secret list
agentkit secret unset <NAME>
agentkit access token create <name> [--api <url>] [--out <path>]
agentkit access token list
agentkit access token revoke <token-id>
agentkit env set <NAME> <VALUE>
agentkit env set <NAME> --stdin
agentkit env set <NAME> --from-env [ENV_NAME]
agentkit env list
agentkit env unset <NAME>
agentkit docs path
agentkit docs llms
agentkit docs full
agentkit handoff codex [goal]
agentkit handoff claude [goal]
agentkit help commands
```

Prefer `env set --stdin` or `--from-env` for local secret values, and prefer `secret set --stdin`, `--from-env`, or `--from-local-env` for hosted secrets. Inline `<VALUE>` forms exist for simple non-sensitive values, but agents should avoid putting secrets in shell history.

Use `agentkit feedback create` when AgentKit itself fails, confuses the coding agent, or lacks docs. Drafts are local files under `.agentkit/feedback/` and are not sent automatically. Use `agentkit feedback send` only after reviewing the draft and logging in with `agentkit login --token agk_user_...`. Feedback upload uses authenticated AgentKit Cloud account auth, redacts common token/key patterns, does not upload arbitrary files, and does not cause the deployed agent runtime to phone home.

On Windows PowerShell, if `npm.ps1` or `npx.ps1` is blocked with `PSSecurityException`, run capsule scripts through the `.cmd` shims instead of changing the workflow. Examples: `npx.cmd @andreprado/agentkit@alpha new demo --template blank`, `npm.cmd run agentkit -- inspect`, `npm.cmd run agentkit -- knowledge sync`, and `npm.cmd run eval`.

Managed Composio is configured with `composioManaged({...})` in `agentkit.config.ts` and is paid hosted AgentKit infrastructure. It requires a non-anonymous AgentKit Cloud deploy with `managed_composio`, uses one Composio settings profile per agent, injects `COMPOSIO_API_KEY` as an AgentKit-managed secret, resolves toolkit auth configs from AgentKit Cloud, validates toolkit readiness during `agentkit deploy doctor`, and exposes the generated `agentkit_composio_execute` tool only for explicit configured action slugs. Calendar starters should include `GOOGLECALENDAR_EVENTS_LIST`, `GOOGLECALENDAR_CREATE_EVENT`, and `GOOGLECALENDAR_UPDATE_EVENT`; create-event calls must pass UTC `start_datetime` plus explicit duration. Managed external write actions require tool input `confirmed: true` by default unless the integration sets `confirmExternalWrites: false`. See `docs/guides/add-managed-composio.md`.

## Create And Test A Capsule

From a clean working directory:

```sh
rm -rf /tmp/agentkit-demo
mkdir -p /tmp/agentkit-demo
cd /tmp/agentkit-demo

npx @andreprado/agentkit@alpha new demo --template blank
cd demo
npm run chat -- --message "hello"
npm run chat -- --message "second message"
npm run eval
npm run agentkit -- conversations list
npm run agentkit -- inspect
```

Run `agentkit new` without a name in an interactive terminal to be prompted for the project folder. Use `agentkit new .` to create the capsule in the current directory. Non-interactive scripts should pass a name or `.` explicitly.

Expected first chat output:

```txt
Echo: hello
```

The generated capsule includes `tsconfig.json` with `moduleResolution: "Bundler"` so TypeScript can resolve `@andreprado/agentkit`.
It also includes `AGENTS.md`, `AGENTKIT.md`, and `CLAUDE.md`, which tell coding agents to treat the owner's natural-language request as the brief.

The primary flow is:

```sh
agentkit new eye-office-agent --template blank
cd eye-office-agent
```

Then open the folder in Codex, Claude Code, or another coding agent and ask directly:

```txt
Develop an appointment and intake agent for an ophthalmology office.
```

The coding agent should infer the first useful version, edit `prompts/instructions.md`, `agentkit.config.ts`, `schema.sql`, `tools/`, and `evals/`, then run the verification commands before finishing. Do not send the owner back to a CLI brief wizard. AgentKit provides the scaffold, contract, and skills; the coding agent implements directly in the capsule from the owner's chat message.

Optional handoff shortcut:

```sh
npm run agentkit -- handoff codex "Develop an appointment and intake agent for an ophthalmology office."
npm run agentkit -- handoff claude "Develop an appointment and intake agent for an ophthalmology office."
```

The command prints a ready-to-paste prompt that points the coding agent at `AGENTKIT.md`, the repo-local `skills/agentkit-capsule/SKILL.md` router when present, and the packaged `llms.txt` docs router. Load `llms-full.txt` only when a skill or ambiguous framework behavior requires the complete contract.

The generated docs and handoff prompt must make UI testing explicit. For local UI testing, run `npm run dev`, open the printed `Chat:` URL, and tell the owner the exact URL. For hosted UI testing after deploy, run `npm run agentkit -- chat-ui --deploy`, open the printed `Chat:` URL, and tell the owner it is connected to the hosted deploy.

## Agent Config

`agentkit.config.ts` is the source of truth:

```ts
import { defineAgent } from "@andreprado/agentkit";

export default defineAgent({
  name: "support-agent",
  runtime: "edge",
  provider: {
    name: "test",
    model: "fake",
  },
  timeZone: "America/New_York",
  instructions: "./prompts/instructions.md",
  secrets: [],
  tools: [],
  access: {
    mode: "private",
  },
  storage: {
    driver: "agentkit",
  },
});
```

`timeZone` is optional and must be an IANA time zone when set. AgentKit injects dynamic runtime context into every chat run: current ISO timestamp, local date, local weekday, local date/time, and timezone. Use `timeZone` for scheduling, appointments, reminders, deadlines, and any prompt behavior that interprets "today", "tomorrow", weekdays, or relative dates. Do not hardcode today's date in `prompts/instructions.md`. If `timeZone` is omitted, AgentKit falls back to `AGENTKIT_TIME_ZONE`, then valid `TZ`, then the runtime default timezone.

Valid runtime values:

```txt
local
edge
```

Valid provider names:

```txt
test
openai
anthropic
openrouter
opencode
opencode-go
custom
```

Current provider adapters:

```txt
test/fake
openai via Pi SDK
anthropic via Pi SDK
openrouter via Pi SDK
opencode via Pi SDK
opencode-go via Pi SDK
```

`custom` is a reserved config value. It is not implemented as a local provider adapter yet.

## Provider Rules

Use `test/fake` for offline local tests:

```ts
provider: {
  name: "test",
  model: "fake",
},
secrets: [],
```

`test/fake` is deterministic. It is useful for scaffold checks, direct tool checks, and fake-provider evals, but it does not validate natural conversation quality.

Before claiming real conversation behavior has been tested, ask the owner which provider to use: OpenRouter, OpenAI, Anthropic, OpenCode Zen, OpenCode Go, or another supported provider. Do not choose for them. After the owner chooses, update `agentkit.config.ts`, `.env.schema`, local secrets, hosted secrets if deploying, then rerun chat/UI checks.

OpenAI example:

```ts
provider: {
  name: "openai",
  model: "gpt-4o-mini",
},
secrets: ["OPENAI_API_KEY"],
```

Then keep the required name in `.env.schema`, put the value in ignored `.env`, and run AgentKit normally:

```sh
npm run agentkit -- inspect
npm run chat -- --message "hello"
```

`inspect` shows only names and set/missing status, never values. Do not commit `.env`. Generated `.gitignore` already excludes it.

If a provider key is missing, the runtime returns `secret_not_found`.

For OpenRouter, prefer model ids or aliases known to the installed Pi SDK, such as `~google/gemini-flash-latest`. If an OpenRouter id is newer than Pi's registry, AgentKit passes the raw id through to OpenRouter with conservative unknown-model metadata. OpenRouter can still reject invalid, inaccessible, or unsupported models, and unknown-model cost/capability metadata is not authoritative.

OpenCode Zen example:

```ts
provider: {
  name: "opencode",
  model: "big-pickle",
},
secrets: ["OPENCODE_API_KEY"],
```

Use an OpenCode Zen model id listed by the installed Pi SDK, such as `big-pickle`, `deepseek-v4-flash-free`, `claude-sonnet-4-5`, or `gpt-5.4-mini`.

OpenCode Go example:

```ts
provider: {
  name: "opencode-go",
  model: "deepseek-v4-flash",
},
secrets: ["OPENCODE_API_KEY"],
```

Use an OpenCode Go model id listed by the installed Pi SDK, such as `deepseek-v4-flash`, `deepseek-v4-pro`, `glm-5.1`, `kimi-k2.6`, `minimax-m2.7`, or `qwen3.6-plus`.

## Knowledge Contract

Knowledge is AgentKit's native retrieval layer for facts the agent should ground in source files. Use it for FAQs, prices, policies, service descriptions, procedures, CSV tables, and reference docs. Do not put secrets, credentials, `.env` contents, or live customer/payment records in Knowledge. Use tools for live or authorization-sensitive data.

Configure Knowledge in `agentkit.config.ts`:

```ts
knowledge: {
  sources: [
    "knowledge/faq.md",
    { path: "knowledge/prices.csv", title: "Prices" },
  ],
  retrieval: {
    topK: 8,
    hybrid: false,
  },
},
```

Local Knowledge supports `.md`, `.markdown`, `.txt`, and `.csv` sources inside the Agent Capsule. Markdown chunks follow headings, text chunks follow paragraphs, and CSV chunks preserve row data with headers.

Embeddings are configured separately from the chat provider. The default provider is `none`, which gives local lexical search without an API key. For OpenAI embeddings:

```ts
knowledge: {
  sources: ["knowledge/faq.md"],
  embedding: {
    provider: "openai",
    model: "text-embedding-3-small",
    secret: "KNOWLEDGE_OPENAI_API_KEY",
  },
  retrieval: {
    topK: 8,
    hybrid: true,
  },
},
```

Set the local embedding secret with `agentkit env set KNOWLEDGE_OPENAI_API_KEY --stdin`. Do not commit the value.

Knowledge commands:

```sh
agentkit knowledge add knowledge/faq.md
agentkit knowledge sync
agentkit knowledge inspect
agentkit knowledge search "refund policy" --top-k 3
```

`knowledge add` indexes one local path. `knowledge sync` indexes all configured `knowledge.sources` and skips unchanged files by content hash. `agentkit dev` and `agentkit chat` also sync configured Knowledge automatically before local runs. `knowledge inspect` lists indexed sources and chunk counts. `knowledge search` validates retrieval before relying on the agent. Local lexical search uses SQLite FTS5 when the local SQLite build provides it; when it does not, AgentKit automatically keeps indexing and searching with a normal SQLite table and simpler text matching. When embeddings are configured locally, AgentKit stores canonical chunks in `.agentkit/agentkit.db`, rebuilds a local libSQL vector sidecar at `.agentkit/agentkit.vectors.db`, uses native `libsql_vector_idx` semantic search, and falls back to stored JSON embeddings if the native vector path is unavailable.

When `knowledge` is configured, AgentKit automatically registers the internal chat tool `agentkit_search_knowledge` and appends a prompt policy. The policy tells the agent to search before answering business-specific factual questions and not to expose raw retrieval JSON, scores, chunk IDs, or tool output objects. With `test/fake`, verify the internal tool directly:

```sh
agentkit chat --message '{"tool":"agentkit_search_knowledge","input":{"query":"refund policy","topK":1}}'
```

Expected output:

```txt
Tool agentkit_search_knowledge: completed
```

Cloudflare Knowledge deploys require `storage.driver: "agentkit"` and `storage.database.driver: "turso"`. `agentkit deploy doctor` and `agentkit build --target cloudflare` fail clearly when Knowledge is configured without Turso. Cloudflare artifacts include the Knowledge manifest, required embedding secret names, internal Knowledge schema, packaged local source contents, prompt policy, and hosted `agentkit_search_knowledge` runtime. During `agentkit deploy`, AgentKit Cloud applies the Knowledge schema, chunks packaged local sources, creates embeddings when configured, deletes stale hosted sources, and syncs sources, chunks, embedding metadata, FTS rows, and a native Turso `libsql_vector_idx` index into the project Turso database before publishing the Worker. Local `agentkit knowledge add/sync`, `agentkit dev`, and `agentkit chat` index configured Knowledge into local SQLite and the local libSQL vector sidecar; hosted deploy syncs configured local Knowledge sources automatically from the deploy artifact so private source material and embedding secrets do not move into client code. Hosted semantic search uses Turso native vector search when embeddings are configured and falls back to stored JSON embeddings if the native vector path is unavailable.

Full guide: `docs/guides/add-knowledge.md`.

## Channel Contract

Channels are hosted inbound/outbound conversation transports. They are separate from tools: channels receive user messages, while tools let the agent call external systems.

Use these helpers in `agentkit.config.ts`:

```ts
import { defineAgent, discordChannel, slackChannel, telegramChannel, webhookChannel, webhookOutputChannel, whatsappChannel, websiteChannel } from "@andreprado/agentkit";

export default defineAgent({
  name: "support-agent",
  runtime: "edge",
  provider: { name: "test", model: "fake" },
  instructions: "./prompts/instructions.md",
  secrets: [],
  tools: [],
  channels: [
    websiteChannel({ name: "website-chat" }),
    telegramChannel({ name: "support-telegram" }),
    whatsappChannel({ name: "support-whatsapp", provider: "zapster" }),
    whatsappChannel({ name: "support-uazapi", provider: "uazapi" }),
    discordChannel({ name: "support-discord" }),
    discordChannel({ name: "server-discord", mode: "bot" }),
    slackChannel({ name: "support-slack" }),
    webhookChannel({ name: "n8n-webhook" }),
    webhookOutputChannel({ name: "crm-callback", urlSecret: "CRM_CALLBACK_URL" }),
  ],
  access: { mode: "public" },
  storage: { driver: "agentkit" },
});
```

Rules:

- `runtime: "edge"` is required when `channels` are configured.
- Channel names are stable lowercase identifiers and must be unique.
- Config stores secret names only, never secret values.
- AgentKit owns channel webhook URLs, dedupe, identities, queue state, and delivery logs.
- Do not store channel plumbing in the user's Turso database.
- Use `buffer.mode: "debounce"` when a channel should coalesce rapid client messages into one agent run.
- Buffered deliveries show `buffered`, then flush to one `queued` run after `quietWindowMs`, `maxWaitMs`, `maxMessages`, or `maxChars`.
- Use `replyTo` when an inbound channel should receive on one transport and answer on another configured channel.
- Use `webhookOutputChannel` when the reply target is a generic HTTPS callback URL stored in a managed secret.

Channel buffer example:

```ts
whatsappChannel({
  name: "support-whatsapp",
  provider: "zapster",
  buffer: {
    mode: "debounce",
    quietWindowMs: 2500,
    maxWaitMs: 12000,
    maxMessages: 20,
    maxChars: 8000,
  },
})
```

Useful guides:

- Add a channel: `docs/guides/add-channel.md`
- Connect Discord: `docs/guides/connect-discord.md`
- Connect Slack: `docs/guides/connect-slack.md`
- Connect Telegram: `docs/guides/connect-telegram.md`
- Connect WhatsApp through Evolution API: `docs/guides/connect-whatsapp-evolution.md`
- Connect WhatsApp through UAZAPI: `docs/guides/connect-whatsapp-uazapi.md`
- Connect WhatsApp through Zapster: `docs/guides/connect-whatsapp-zapster.md`
- Debug a channel: `docs/guides/debug-channel.md`
- Channel security: `docs/guides/channel-security.md`

Telegram required secrets:

```txt
TELEGRAM_BOT_TOKEN
TELEGRAM_WEBHOOK_SECRET
```

Zapster WhatsApp required secrets:

```txt
ZAPSTER_API_KEY
ZAPSTER_INSTANCE_ID
ZAPSTER_WEBHOOK_ID
```

UAZAPI WhatsApp required secrets:

```txt
UAZAPI_BASE_URL
UAZAPI_TOKEN
```

Evolution API WhatsApp required secrets:

```txt
EVOLUTION_API_BASE_URL
EVOLUTION_API_KEY
EVOLUTION_INSTANCE_NAME
EVOLUTION_WEBHOOK_TOKEN
```

Discord slash-command required secret:

```txt
DISCORD_PUBLIC_KEY
```

Discord bot-mode required secret:

```txt
DISCORD_BOT_TOKEN
```

Generic webhook required secret:

```txt
AGENTKIT_WEBHOOK_SECRET
```

Generic webhook payloads should be canonical JSON:

```json
{
  "event_id": "evt_123",
  "external_id": "customer_123",
  "message": "hello from n8n"
}
```

Use `Authorization: Bearer <AGENTKIT_WEBHOOK_SECRET>`, `X-AgentKit-Webhook-Secret`, or `X-AgentKit-Webhook-Signature: sha256=<hmac>` where the HMAC is SHA-256 over the exact raw JSON body. The hosted URL is `/channels/<name>/webhook`.

Without `replyTo`, generic webhooks are inbound-only and do not call back into the source system. To receive on one channel and answer on another, add a static `replyTo` route:

```ts
webhookChannel({
  name: "lead-webhook",
  replyTo: {
    channel: "main-whatsapp",
    recipientFrom: "phone",
  },
})

whatsappChannel({
  name: "main-whatsapp",
  provider: "evolution",
})
```

`recipientFrom` is a dotted JSON path such as `phone`, `customer.phone`, or `payload.customer.phone`. It must resolve to a non-empty scalar in the inbound payload. Do not use `recipientFrom` for Discord or Slack replies because those transports need provider-native source identities.

Multiple webhooks are multiple named channels:

```ts
webhookChannel({ name: "n8n-webhook" })
webhookChannel({ name: "make-webhook" })
webhookChannel({ name: "crm-webhook", replyTo: { channel: "main-whatsapp", recipientFrom: "phone" } })
```

Generic output webhook channels send the final agent answer to an HTTPS callback URL:

```ts
webhookOutputChannel({
  name: "crm-callback",
  urlSecret: "CRM_CALLBACK_URL",
  auth: {
    type: "bearer",
    tokenSecret: "CRM_CALLBACK_TOKEN",
  },
})

webhookChannel({
  name: "n8n-webhook",
  replyTo: {
    channel: "crm-callback",
    recipientFrom: "crm_id",
  },
})
```

`CRM_CALLBACK_URL` and output auth values are managed secrets only. The URL must be public HTTPS; AgentKit rejects localhost, private network, link-local, and metadata-service hosts. Supported output auth modes are `none`, `bearer`, `header`, and `hmac`. Generic output channels do not have public inbound webhook URLs, so `agentkit channels test <output-name>` is unsupported.

Common channel verification:

```sh
agentkit inspect
agentkit deploy
agentkit channels list
agentkit channels add telegram support-telegram
agentkit channels connect whatsapp main-whatsapp --provider evolution
agentkit channels connect whatsapp support-whatsapp --provider uazapi
agentkit channels connect discord support-discord
agentkit channels connect discord server-discord --mode bot
agentkit channels connect slack support-slack
agentkit channels connect webhook n8n-webhook
agentkit channels setup support-telegram
agentkit channels test support-telegram --message "hello"
agentkit channels test-audio support-telegram --fixture voice-note
agentkit transcribe smoke --provider groq
agentkit channels deliveries list support-telegram
agentkit channels deliveries show <delivery-id>
```

`channels connect` creates or refreshes the channel resource, validates secrets, runs provider setup when supported, then runs the official synthetic smoke. `channels setup` is read-only by default. `channels setup <telegram-name> --apply` calls Telegram `setWebhook` and requires `TELEGRAM_BOT_TOKEN` plus `TELEGRAM_WEBHOOK_SECRET`. `channels setup <uazapi-whatsapp-name> --apply` calls UAZAPI `/webhook` and requires `UAZAPI_BASE_URL` plus `UAZAPI_TOKEN`. `channels setup <evolution-whatsapp-name> --apply` calls Evolution API `/webhook/set/{instance}` and requires `EVOLUTION_API_BASE_URL`, `EVOLUTION_API_KEY`, `EVOLUTION_INSTANCE_NAME`, and `EVOLUTION_WEBHOOK_TOKEN`.

Discord slash-command mode validates `X-Signature-Ed25519` and `X-Signature-Timestamp` against `DISCORD_PUBLIC_KEY`, answers signed `PING` requests with `type: 1`, acknowledges slash commands with a deferred response, then sends the final answer as an interaction follow-up. Discord bot mode uses `DISCORD_BOT_TOKEN`, Discord Gateway `MESSAGE_CREATE`, Message Content Intent, and `/channels/<channel_id>/messages` bot replies. Discord channels support buffering but do not support `audio` in V1.

Default tests are offline. Real provider smoke tests are opt-in:

```sh
AGENTKIT_RUN_TELEGRAM_CHANNEL_TESTS=1 bun test
AGENTKIT_RUN_ZAPSTER_CHANNEL_TESTS=1 bun test
AGENTKIT_RUN_UAZAPI_CHANNEL_TESTS=1 bun test
AGENTKIT_RUN_EVOLUTION_CHANNEL_TESTS=1 bun test
```

Telegram smoke also requires `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`, and `AGENTKIT_TELEGRAM_WEBHOOK_URL`.
Zapster smoke also requires `ZAPSTER_API_KEY`, `AGENTKIT_ZAPSTER_SEND_URL`, and `AGENTKIT_ZAPSTER_TO`.
UAZAPI smoke also requires `UAZAPI_BASE_URL`, `UAZAPI_TOKEN`, and `AGENTKIT_UAZAPI_TO`.
Evolution smoke also requires `EVOLUTION_API_BASE_URL`, `EVOLUTION_API_KEY`, `EVOLUTION_INSTANCE_NAME`, and `AGENTKIT_EVOLUTION_TO`.

## Tool Contract

Tools are generic TypeScript objects created with `defineTool`.

Example:

```ts
import { defineTool } from "@andreprado/agentkit";

export const lookupOrder = defineTool({
  name: "lookup_order",
  description: "Looks up an order by id.",
  inputSchema: {
    type: "object",
    properties: {
      orderId: { type: "string" },
    },
    required: ["orderId"],
    additionalProperties: false,
  },
  outputSchema: {
    type: "object",
    properties: {
      orderId: { type: "string" },
      found: { type: "boolean" },
      status: { type: "string" },
    },
    required: ["orderId", "found", "status"],
  },
  secrets: ["ORDER_API_KEY"],
  timeoutMs: 30000,
  async execute(input: { orderId: string }, ctx) {
    const key = ctx.secrets.ORDER_API_KEY;
    return { orderId: input.orderId, found: true, status: "shipped" };
  },
});
```

Register the tool in `agentkit.config.ts`:

```ts
import { lookupOrder } from "./tools/lookup-order";

export default defineAgent({
  tools: [lookupOrder],
});
```

Tool runtime rules:

- input schema is validated before execution;
- output schema is validated when `outputSchema` exists;
- `type` can be a string or JSON Schema union such as `["string", "null"]`;
- optional object properties sent as `null` are treated as omitted unless the property schema allows `null`;
- a tool receives only secrets listed in its own `secrets` field;
- default timeout is 30000 ms;
- `timeoutMs` overrides the default;
- tool calls are saved in AgentKit-managed local storage;
- secret values are redacted before storage.
- tools that need SQL use canonical `ctx.db`; `ctx.database` and `ctx.storage.sql` are supported aliases;
- tools can use `ctx.db.batch([...])` for atomic writes; local tools can also use `ctx.db.transaction(async (tx) => ...)`;
- tools can inspect `ctx.runtime` with `{ environment, invocation, target, database }`;
- tools can use `ctx.clock` for the same runtime clock injected into the agent prompt, including `now`, `isoTimestamp`, `timeZone`, `localDate`, `localWeekday`, and `localDateTime`;
- tools must not import local database drivers or Node-only APIs. Use AgentKit runtime services instead.

## Database Tools And Dual Storage

For agent-owned application tables, `schema.sql` is the single source of truth.

Keep `storage.driver: "agentkit"`. If the generated capsule includes a managed database schema field, keep it pointed at `./schema.sql`.

Local behavior:

- `agentkit chat`, `agentkit tool`, and `agentkit dev` use AgentKit-managed local storage.
- before a tool runs, AgentKit applies `schema.sql` locally;
- use `agentkit db migrate`, `agentkit db reset --yes`, `agentkit db seed [--file seed.sql]`, and `agentkit db shell` for local setup and inspection.

Hosted behavior:

- `agentkit deploy` builds and deploys the capsule;
- AgentKit migrates/provisions hosted storage internally and applies the same `schema.sql`;
- the user should not create hosted databases, buckets, or runtimes manually.

Example `schema.sql`:

```sql
CREATE TABLE IF NOT EXISTS appointments (
  id TEXT PRIMARY KEY,
  client_name TEXT NOT NULL,
  starts_at TEXT NOT NULL,
  notes TEXT,
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE (starts_at)
);
```

`schema.sql` is an idempotent bootstrap file. Use `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, and only safe additive `ALTER TABLE` statements. Use ordered `migrations/*.sql` for production-shaped schema evolution; `agentkit db migrate` applies unapplied local migrations before `schema.sql`.

Example tool:

```ts
import { defineTool } from "@andreprado/agentkit";

export const scheduleAppointment = defineTool({
  name: "schedule_appointment",
  description: "Schedules an appointment in the agent database.",
  inputSchema: {
    type: "object",
    properties: {
      clientName: { type: "string" },
      startsAt: { type: "string" },
      notes: { type: "string" },
    },
    required: ["clientName", "startsAt"],
    additionalProperties: false,
  },
  async execute(input: { clientName: string; startsAt: string; notes?: string }, ctx) {
    const id = crypto.randomUUID();

    await ctx.db.execute(
      "INSERT INTO appointments (id, client_name, starts_at, notes) VALUES (?, ?, ?, ?)",
      [id, input.clientName, input.startsAt, input.notes ?? null],
    );

    const result = await ctx.db.query("SELECT id, client_name, starts_at FROM appointments WHERE id = ?", [id]);
    const appointment = result.rows[0];

    return {
      id: String(appointment.id),
      clientName: String(appointment.client_name),
      startsAt: String(appointment.starts_at),
    };
  },
});
```

Local test:

```sh
npm run agentkit -- tool schedule_appointment --input '{"clientName":"Ada Lovelace","startsAt":"2026-06-01T10:00:00Z"}'
npm run agentkit -- db shell
```

Run a registered tool directly:

```sh
npm run agentkit -- tool lookup_order --input '{"orderId":"A100"}'
```

## Local Storage

SQLite storage lives at:

```txt
.agentkit/agentkit.db
```

Tables:

```txt
conversations
messages
runs
tool_calls
agentkit_migrations
```

Agent-owned tables from `schema.sql` live in the same SQLite file locally. Runtime state is local and must not be committed.

## Inspect Contract

Run:

```sh
agentkit inspect
```

Example output:

```json
{
  "agent": "demo",
  "runtime": "local",
  "provider": {
    "name": "test",
    "model": "fake"
  },
  "prompt": "prompts/instructions.md",
  "tools": [],
  "access": {
    "mode": "private"
  },
  "storage": ".agentkit/agentkit.db",
  "database": {
    "local": {
      "driver": "sqlite",
      "path": ".agentkit/agentkit.db",
      "schema": "./schema.sql"
    },
    "hosted": {
      "driver": "turso",
      "provisioning": "agentkit-managed",
      "schema": "./schema.sql"
    }
  },
  "secrets": {},
  "managedSecrets": ["TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL"],
  "userSecrets": [],
  "runtimeContract": {
    "local": {
      "environment": "local",
      "target": "bun",
      "database": "sqlite"
    },
    "hosted": {
      "environment": "hosted",
      "target": "cloudflare",
      "database": "turso",
      "providerSupported": true
    },
    "aliases": ["ctx.db", "ctx.database", "ctx.storage.sql"]
  }
}
```

Secret values never appear. Each declared user secret appears as `set` or `missing`. `managedSecrets` are provisioned and injected by AgentKit Cloud; users do not put those values in local `.env`.

## Dev Server Contract

Run:

```sh
agentkit dev
```

If the default local port is occupied, `agentkit dev` chooses the next available port and prints the actual URLs. If the occupied port is another AgentKit server, the output names the agent using it.

Expected output:

```txt
Agent Capsule running

Chat:      http://localhost:4123
API:       http://localhost:4123/v1/chat
Inspect:   http://localhost:4123/_agentkit
Storage:   .agentkit/agentkit.db
```

To reopen the local UI for the current capsule after the server is already running:

```sh
agentkit open
```

Endpoints:

```txt
GET  /_agentkit
POST /v1/chat
GET  /v1/conversations
GET  /v1/conversations/:id
GET  /v1/conversations/:id/trace
```

Chat request:

```sh
curl -X POST http://localhost:4123/v1/chat \
  -H 'content-type: application/json' \
  -d '{"message":{"role":"user","content":"hello"}}'
```

The chat response includes `conversationId`. Send the same `conversationId` on the next `POST /v1/chat` request, or pass it to `agentkit chat --conversation-id <id>`, to continue with the persisted message history.

## Conversations

After chat:

```sh
agentkit conversations list
agentkit conversations show <conversation-id>
agentkit conversations trace <conversation-id>
```

List output columns:

```txt
id	title	updated_at	messages
```

Show output includes the conversation id, title, updated timestamp, and each message as `role: content`. Trace output includes stored messages and tool calls for the conversation.

## Evals

Generated capsules include:

```txt
evals/smoke.eval.ts
```

Run local evals:

```sh
npm run eval
```

Supported assertion types:

```txt
response.contains
response.containsAll
response.containsAny
response.caseInsensitiveContains
response.notContains
response.regex
response.matchesRegex
response.notRegex
response.maxLength
tools.called
tools.calledOnce
tools.count
tools.order
tools.persisted
```

Import `defineEval` from `@andreprado/agentkit` when writing new evals. `tools.persisted` validates the tool call saved in the eval run's local SQLite `tool_calls`, not a provider-specific raw response shape. It can be a tool name string or an object with `name`, `input`, `output`, `rendered`, `status`, and/or `visibility`. `tool_call` and `persisted_tool_call` remain accepted as backwards-compatible aliases, but new evals should use `tools.persisted`.

For date-sensitive evals, set top-level `now` to an ISO timestamp with an explicit timezone designator such as `Z` or `-05:00`. AgentKit uses that fixed clock for every turn and tool call in the eval so "today", "tomorrow", and weekdays remain deterministic while normal chat continues to use the real current date.

Evals run the normal capsule tools. If a tool would write externally, delete, charge money, send email, or call a real customer system, make its `execute` implementation branch on `ctx.runtime.environment === "eval"` and return deterministic non-destructive output for eval runs. Do not invent an eval-only mock API; keep the behavior inside the registered tool contract unless AgentKit adds a first-class mock facility later.

## Improve From Production

Use AgentKit Improve when a hosted or local conversation should become a reproducible local fix loop. Hosted AgentKit Cloud exports evidence; the local coding agent edits the Agent Capsule, writes evals, replays, and deploys.

Collect hosted evidence from the last deploy:

```sh
agentkit improve collect --deploy --since 24h
```

When the CLI is logged in to AgentKit Cloud, hosted collection first exports deploy evidence such as failed channel deliveries, deploy errors, and conversation IDs from the control plane, then reads replayable conversation traces from the deployed runtime. Without Cloud auth, it falls back to deploy-token conversation trace reads.

Hosted conversation reads require a deploy access token even when the chat endpoint is public. `agentkit deploy` normally writes `.agentkit/chat-access-token.json`; use `agentkit access token create agentkit-chat-ui --out .agentkit/chat-access-token.json` to refresh it.

Collect one hosted conversation:

```sh
agentkit improve collect --deploy --conversation-id <conversation-id>
```

Collect local evidence:

```sh
agentkit improve collect --since 7d
```

The command writes ignored local state:

```txt
.agentkit/improve/<run>/
  bundle.json
  report.json
  traces/
```

Generate committed regression evals:

```sh
agentkit improve evals .agentkit/improve/<run>
```

Then replay before deploying:

```sh
agentkit replay .agentkit/improve/<run> --against local
npm run eval
agentkit deploy --smoke "hello"
```

Replay runs collected user turns through the local capsule with `ctx.runtime.environment === "eval"` and `ctx.runtime.invocation === "eval"`. Generated evals live under `evals/regressions/`; review them before committing, especially when traces contain real client details or overly strict prose assertions.

Full guides:

- `docs/guides/improve-from-production.md`
- `docs/guides/replay-production-traces.md`

## Security Rules

Never commit:

```txt
.env
.agentkit/
node_modules/
```

Do not store secret values in:

```txt
agentkit.config.ts
.env.schema
AGENTS.md
CLAUDE.md
README.md
evals/
prompts/
docs/
```

Local `.env` is development only. Use `.env.schema` as the committed secret-name contract; local AgentKit commands load `.env` directly. Hosted deploys require an AgentKit Cloud account with `cloudflare_deploy_alpha` or purchased/manual deploy slots. First-time paid access uses `agentkit billing checkout --slots <count>` and `agentkit billing claim billint_... --secret bsec_...`; existing accounts use `agentkit login --token ...`. Hosted secrets use `agentkit secret set/list/unset` or `agentkit secret sync --from-local`. Prefer `--stdin`, `--from-env`, `--from-local-env`, or sync from local `.env` so secret values do not appear in shell history. Inline `<VALUE>` forms exist only for compatibility and simple non-sensitive values.

Tools are a security boundary. A tool must declare every secret it needs. The runtime injects only tool-declared secrets.

## Hosted Deploy Contract

For the coding agent and user, deploy is one command. Do not ask the user to choose a hosting target.

Current flow:

```sh
agentkit deploy --dry-run
agentkit billing checkout --slots 1 --email user@example.com
agentkit billing claim billint_... --secret bsec_...
agentkit login --token agk_user_...
agentkit account token create new-laptop --use
agentkit deploy doctor
agentkit deploy --smoke "hello"
agentkit deploy status
agentkit deploy smoke --message "hello"
agentkit chat-ui --deploy
```

`agentkit deploy doctor` checks AgentKit Cloud login, hosted deploy entitlement, online deploy capacity, hosted secrets, local `.env` names that still need `agentkit secret set`, managed Composio API/auth-config readiness by toolkit, and private-access runtime token handling. `agentkit deploy` sends the capsule to AgentKit Cloud, runs the same readiness check automatically before building and uploading, updates the current project deploy slot by default, writes the local chat/UI deploy access token to `.agentkit/chat-access-token.json`, and prints a production handoff with URL, UI command, secret status, database/schema artifact, integration connect commands, smoke status, and the next recommended command. `agentkit deploy --smoke "hello"` deploys and then tests `/v1/chat` with the deploy access token. `agentkit deploy smoke --message "hello"` repeats that smoke against the last local deploy. `agentkit chat-ui --deploy` serves a local UI pointed at the hosted deploy using that token without exposing it to browser code, shows the conversation id and tool calls, and supports starting a new conversation. Use `agentkit conversations trace <conversation-id> --deploy` to pull hosted conversation messages and tool calls from the last deploy. Production deploys require an account with `cloudflare_deploy_alpha` or purchased/manual deploy slots; local commands and dry-run builds do not require login. AgentKit owns infrastructure selection, backend migration, managed secrets, and public URL creation.

The CLI defaults to the hosted AgentKit Cloud API at `https://agentkit-cloud.aibuilders.com.br`. Use `AGENTKIT_CLOUD_API_URL` or `agentkit deploy --api <url>` only when the owner gives you a non-default AgentKit Cloud API URL.

Account/access flow:

```sh
agentkit secret set OPENAI_API_KEY --from-local-env
agentkit secret sync --from-local
agentkit secret list
agentkit account token list
agentkit skills status
agentkit skills sync
agentkit access token create website-chat --out .agentkit/website-chat-access-token.json
agentkit access token list
```

Production storage uses `storage.driver: "agentkit"`. `.env` is never uploaded.

Hosted backend responses use this error envelope:

```json
{
  "error": {
    "code": "secret_not_found",
    "message": "Secret OPENAI_API_KEY is not set for this deploy.",
    "request_id": "req_123",
    "details": {
      "secret": "OPENAI_API_KEY"
    }
  }
}
```

Common hosted error codes:

```txt
unauthorized
forbidden
not_found
validation_error
conflict
rate_limited
quota_exceeded
secret_not_found
access_token_invalid
deploy_not_ready
deploy_paused
tool_timeout
provider_error
internal_error
```

## Troubleshooting

Missing `@andreprado/agentkit` types:

```sh
npm install
npm run typecheck
```

`agentkit new` installs dependencies by default. Run this if the scaffold used `--no-install`, the install failed, or `node_modules` was deleted.

Make sure `tsconfig.json` contains:

```json
{
  "compilerOptions": {
    "moduleResolution": "Bundler",
    "types": ["node"]
  }
}
```

Missing config:

```txt
No agentkit.config.ts found
```

Run the command from the capsule root.

Missing prompt:

```txt
Prompt file not found
```

Check `instructions` in `agentkit.config.ts`.

Missing provider key:

```txt
secret_not_found
```

Add the required name to `.env` locally. Do not commit the value.

Tool validation error:

```txt
tool_validation_error
```

Check the tool call input against `inputSchema`.
