From 555c2aae3ba3f7dabfbe71b9ca5eb4e637cda16f Mon Sep 17 00:00:00 2001 From: Parteek Singh Date: Tue, 28 Apr 2026 16:55:29 -0700 Subject: [PATCH 1/3] feat(v3): update docs and framework templates - Reshape docs IA around `v3` framework apps - Add framework, deploy, patterns, and migration pages - Move services into the primary docs path - Refresh templates with AI Gateway and key-value history - Add framework icons and generated route files - Leave docs app migration as follow-up work --- AGENTS.md | 1 + docs/package.json | 6 +- docs/src/web/components/AIGatewayDemo.tsx | 2 +- docs/src/web/components/SSEStreamDemo.tsx | 2 +- docs/src/web/components/StreamingDemo.tsx | 2 +- .../components/docs/copy-migration-prompt.tsx | 2 +- docs/src/web/components/docs/nav-data.ts | 568 ++++++------ docs/src/web/content/AGENTS.md | 9 +- docs/src/web/content/agents/ai-gateway.mdx | 168 ---- .../web/content/agents/ai-sdk-integration.mdx | 257 ------ .../content/agents/calling-other-agents.mdx | 694 --------------- .../web/content/agents/creating-agents.mdx | 361 -------- .../web/content/agents/events-lifecycle.mdx | 226 ----- docs/src/web/content/agents/index.mdx | 72 -- docs/src/web/content/agents/meta.json | 15 - .../web/content/agents/schema-libraries.mdx | 325 ------- .../content/agents/standalone-execution.mdx | 209 ----- .../web/content/agents/state-management.mdx | 505 ----------- .../content/agents/streaming-responses.mdx | 273 ------ docs/src/web/content/agents/when-to-use.mdx | 184 ---- .../content/community/inbound-email-agent.mdx | 2 +- .../cookbook/integrations/chat-sdk.mdx | 6 +- .../cookbook/integrations/claude-agent.mdx | 12 +- .../cookbook/integrations/langchain.mdx | 10 +- .../content/cookbook/integrations/mastra.mdx | 8 +- .../content/cookbook/integrations/nextjs.mdx | 6 +- .../cookbook/integrations/openai-agents.mdx | 6 +- .../cookbook/integrations/tanstack-start.mdx | 6 +- .../cookbook/integrations/turborepo.mdx | 8 +- .../cookbook/patterns/autonomous-research.mdx | 10 +- .../cookbook/patterns/chat-with-history.mdx | 281 +++--- .../cookbook/patterns/cron-with-storage.mdx | 2 +- .../cookbook/patterns/llm-as-a-judge.mdx | 2 +- .../cookbook/patterns/product-search.mdx | 2 +- .../cookbook/patterns/server-utilities.mdx | 5 +- .../cookbook/patterns/webhook-handler.mdx | 4 +- .../content/cookbook/tutorials/rag-agent.mdx | 2 +- .../tutorials/understanding-agents.mdx | 6 +- .../deploy-operate/deploy-framework-apps.mdx | 133 +++ .../deploy-operate/environment-variables.mdx | 117 +++ docs/src/web/content/deploy-operate/index.mdx | 40 + .../deploy-operate/local-development.mdx | 67 ++ docs/src/web/content/deploy-operate/meta.json | 4 + docs/src/web/content/frameworks/astro.mdx | 42 + docs/src/web/content/frameworks/hono.mdx | 112 +++ docs/src/web/content/frameworks/index.mdx | 113 +++ docs/src/web/content/frameworks/meta.json | 14 + docs/src/web/content/frameworks/nextjs.mdx | 106 +++ docs/src/web/content/frameworks/nuxt.mdx | 42 + .../web/content/frameworks/react-router.mdx | 55 ++ docs/src/web/content/frameworks/sveltekit.mdx | 42 + .../web/content/frameworks/tanstack-start.mdx | 102 +++ .../src/web/content/frameworks/vite-react.mdx | 42 + .../web/content/frontend/advanced-hooks.mdx | 5 +- .../web/content/frontend/authentication.mdx | 568 +++--------- .../web/content/frontend/provider-setup.mdx | 2 +- docs/src/web/content/frontend/react-hooks.mdx | 4 +- docs/src/web/content/frontend/rpc-client.mdx | 3 +- .../content/get-started/app-configuration.mdx | 349 ++------ docs/src/web/content/get-started/index.mdx | 14 +- .../web/content/get-started/installation.mdx | 94 +- docs/src/web/content/get-started/meta.json | 6 +- .../content/get-started/project-structure.mdx | 184 ++-- .../web/content/get-started/quickstart.mdx | 274 ++---- .../content/get-started/what-is-agentuity.mdx | 105 ++- docs/src/web/content/meta.json | 9 +- docs/src/web/content/migration/from-v2.mdx | 95 ++ docs/src/web/content/migration/index.mdx | 57 ++ docs/src/web/content/migration/meta.json | 4 + .../src/web/content/migration/migrate-cli.mdx | 104 +++ .../migration/runtime-to-frameworks.mdx | 128 +++ .../content/patterns/agents-as-a-pattern.mdx | 151 ++++ .../web/content/patterns/background-work.mdx | 157 ++++ .../content/patterns/chat-and-streaming.mdx | 125 +++ docs/src/web/content/patterns/index.mdx | 41 + docs/src/web/content/patterns/meta.json | 4 + .../web/content/reference/cli/ai-commands.mdx | 4 +- .../reference/cli/build-configuration.mdx | 175 ++-- .../reference/cli/claude-code-plugin.mdx | 2 +- .../web/content/reference/cli/debugging.mdx | 2 +- .../web/content/reference/cli/deployment.mdx | 535 +++-------- .../web/content/reference/cli/development.mdx | 216 +---- .../content/reference/cli/getting-started.mdx | 340 ++----- .../web/content/reference/cli/monitoring.mdx | 4 +- docs/src/web/content/reference/cli/oauth.mdx | 167 ++-- .../content/reference/cli/opencode-plugin.mdx | 4 +- .../src/web/content/reference/cli/storage.mdx | 22 +- docs/src/web/content/reference/index.mdx | 49 +- docs/src/web/content/reference/meta.json | 10 +- .../web/content/reference/migration-guide.mdx | 6 +- .../reference/sdk-reference/advanced.mdx | 12 +- .../reference/sdk-reference/agents.mdx | 18 +- .../sdk-reference/application-entry.mdx | 12 +- .../content/reference/sdk-reference/coder.mdx | 2 +- .../reference/sdk-reference/communication.mdx | 266 +++--- .../reference/sdk-reference/context-api.mdx | 22 +- .../reference/sdk-reference/email-service.mdx | 14 +- .../reference/sdk-reference/events.mdx | 12 +- .../content/reference/sdk-reference/index.mdx | 99 ++- .../reference/sdk-reference/observability.mdx | 36 +- .../reference/sdk-reference/queue-service.mdx | 10 +- .../reference/sdk-reference/router.mdx | 16 +- .../sdk-reference/sandbox-service.mdx | 10 +- .../sdk-reference/schedule-service.mdx | 10 +- .../reference/sdk-reference/schema.mdx | 6 +- .../reference/sdk-reference/storage.mdx | 16 +- .../reference/sdk-reference/task-service.mdx | 10 +- .../content/reference/standalone-packages.mdx | 546 +++++------- .../src/web/content/routes/calling-agents.mdx | 312 ------- docs/src/web/content/routes/cron.mdx | 168 ---- .../web/content/routes/explicit-routing.mdx | 242 ----- docs/src/web/content/routes/http.mdx | 632 ------------- docs/src/web/content/routes/index.mdx | 59 -- docs/src/web/content/routes/meta.json | 14 - docs/src/web/content/routes/middleware.mdx | 434 --------- docs/src/web/content/routes/sse.mdx | 310 ------- docs/src/web/content/routes/webrtc.mdx | 405 --------- docs/src/web/content/routes/websockets.mdx | 258 ------ docs/src/web/content/services/ai-gateway.mdx | 101 +++ .../web/content/services/authentication.mdx | 233 ++--- docs/src/web/content/services/coder.mdx | 2 +- .../web/content/services/database/drizzle.mdx | 18 +- .../web/content/services/database/index.mdx | 184 ++-- .../content/services/database/postgres.mdx | 2 +- docs/src/web/content/services/email.mdx | 409 ++++----- docs/src/web/content/services/index.mdx | 25 +- docs/src/web/content/services/meta.json | 13 +- .../content/services/observability/index.mdx | 105 ++- .../services/observability/logging.mdx | 189 ++-- .../content/services/observability/meta.json | 2 +- .../observability/sessions-debugging.mdx | 265 ++---- .../services/observability/tracing.mdx | 421 +++++---- .../services/observability/web-analytics.mdx | 267 +++--- .../web/content/services/oidc-provider.mdx | 332 ++++--- docs/src/web/content/services/queues.mdx | 613 +++---------- .../web/content/services/sandbox/index.mdx | 425 ++++----- .../content/services/sandbox/sdk-usage.mdx | 830 ++++++++---------- .../content/services/sandbox/snapshots.mdx | 362 ++++---- docs/src/web/content/services/schedules.mdx | 360 ++++---- .../web/content/services/storage/custom.mdx | 209 ++--- .../services/storage/durable-streams.mdx | 573 +++--------- .../web/content/services/storage/index.mdx | 49 +- .../content/services/storage/key-value.mdx | 383 ++------ .../web/content/services/storage/object.mdx | 247 +++--- .../web/content/services/storage/vector.mdx | 482 +++------- docs/src/web/content/services/tasks.mdx | 608 ++++--------- docs/src/web/content/services/webhooks.mdx | 328 ++++--- docs/src/web/demo-config.tsx | 12 +- docs/src/web/lib/docs-redirects.ts | 28 +- .../web/public/images/integrations/astro.svg | 1 + .../web/public/images/integrations/hono.svg | 1 + .../web/public/images/integrations/nuxt.svg | 1 + .../images/integrations/react-router.svg | 1 + .../web/public/images/integrations/svelte.svg | 1 + .../web/public/images/integrations/vite.svg | 1 + .../_docs/agents/ai-sdk-integration.tsx | 7 - .../_docs/agents/calling-other-agents.tsx | 7 - .../routes/_docs/agents/creating-agents.tsx | 7 - .../routes/_docs/agents/events-lifecycle.tsx | 7 - docs/src/web/routes/_docs/agents/index.tsx | 7 - .../routes/_docs/agents/schema-libraries.tsx | 7 - .../_docs/agents/standalone-execution.tsx | 7 - .../routes/_docs/agents/state-management.tsx | 7 - .../_docs/agents/streaming-responses.tsx | 7 - .../web/routes/_docs/agents/when-to-use.tsx | 7 - .../src/web/routes/_docs/agents/workbench.tsx | 15 - .../deploy-operate/deploy-framework-apps.tsx | 7 + .../deploy-operate/environment-variables.tsx | 7 + .../web/routes/_docs/deploy-operate/index.tsx | 7 + .../deploy-operate/local-development.tsx | 7 + .../src/web/routes/_docs/frameworks/astro.tsx | 7 + docs/src/web/routes/_docs/frameworks/hono.tsx | 7 + .../src/web/routes/_docs/frameworks/index.tsx | 7 + .../web/routes/_docs/frameworks/nextjs.tsx | 7 + docs/src/web/routes/_docs/frameworks/nuxt.tsx | 7 + .../routes/_docs/frameworks/react-router.tsx | 7 + .../web/routes/_docs/frameworks/sveltekit.tsx | 7 + .../_docs/frameworks/tanstack-start.tsx | 7 + .../routes/_docs/frameworks/vite-react.tsx | 7 + .../web/routes/_docs/migration/from-v2.tsx | 7 + docs/src/web/routes/_docs/migration/index.tsx | 7 + .../routes/_docs/migration/migrate-cli.tsx | 7 + .../_docs/migration/runtime-to-frameworks.tsx | 7 + .../_docs/patterns/agents-as-a-pattern.tsx | 7 + .../routes/_docs/patterns/background-work.tsx | 7 + .../_docs/patterns/chat-and-streaming.tsx | 7 + docs/src/web/routes/_docs/patterns/index.tsx | 7 + .../routes/_docs/routes/calling-agents.tsx | 7 - docs/src/web/routes/_docs/routes/cron.tsx | 7 - .../routes/_docs/routes/explicit-routing.tsx | 7 - docs/src/web/routes/_docs/routes/http.tsx | 7 - docs/src/web/routes/_docs/routes/index.tsx | 7 - .../web/routes/_docs/routes/middleware.tsx | 7 - docs/src/web/routes/_docs/routes/sse.tsx | 7 - docs/src/web/routes/_docs/routes/webrtc.tsx | 7 - .../web/routes/_docs/routes/websockets.tsx | 7 - .../_docs/{agents => services}/ai-gateway.tsx | 4 +- docs/src/web/routes/index.tsx | 51 +- .../cli/src/cmd/build/detect/frameworks.ts | 23 +- packages/cli/src/cmd/build/detect/index.ts | 13 +- packages/cli/src/cmd/project/frameworks.ts | 33 +- packages/cli/src/cmd/project/index.ts | 5 +- .../project/templates/astro/astro.config.mjs | 10 + .../astro/src/pages/api/translate.ts | 138 ++- .../templates/astro/src/pages/index.astro | 147 +++- .../cmd/project/templates/hono/src/index.ts | 222 ++++- .../nextjs/src/app/api/translate/route.ts | 135 ++- .../project/templates/nextjs/src/app/page.tsx | 156 +++- .../src/cmd/project/templates/nuxt/app.vue | 132 ++- .../nuxt/server/api/translate.post.ts | 18 - .../templates/nuxt/server/api/translate.ts | 119 +++ .../cmd/project/templates/remix/app/routes.ts | 6 + .../remix/app/routes/api.translate.ts | 141 ++- .../templates/remix/app/routes/home.tsx | 184 +++- .../sveltekit/src/routes/+page.server.ts | 119 ++- .../sveltekit/src/routes/+page.svelte | 163 +++- .../templates/sveltekit/svelte.config.js | 15 + .../project/templates/vite-react/server.ts | 149 +++- .../project/templates/vite-react/src/App.tsx | 160 +++- 219 files changed, 9066 insertions(+), 14715 deletions(-) delete mode 100644 docs/src/web/content/agents/ai-gateway.mdx delete mode 100644 docs/src/web/content/agents/ai-sdk-integration.mdx delete mode 100644 docs/src/web/content/agents/calling-other-agents.mdx delete mode 100644 docs/src/web/content/agents/creating-agents.mdx delete mode 100644 docs/src/web/content/agents/events-lifecycle.mdx delete mode 100644 docs/src/web/content/agents/index.mdx delete mode 100644 docs/src/web/content/agents/meta.json delete mode 100644 docs/src/web/content/agents/schema-libraries.mdx delete mode 100644 docs/src/web/content/agents/standalone-execution.mdx delete mode 100644 docs/src/web/content/agents/state-management.mdx delete mode 100644 docs/src/web/content/agents/streaming-responses.mdx delete mode 100644 docs/src/web/content/agents/when-to-use.mdx create mode 100644 docs/src/web/content/deploy-operate/deploy-framework-apps.mdx create mode 100644 docs/src/web/content/deploy-operate/environment-variables.mdx create mode 100644 docs/src/web/content/deploy-operate/index.mdx create mode 100644 docs/src/web/content/deploy-operate/local-development.mdx create mode 100644 docs/src/web/content/deploy-operate/meta.json create mode 100644 docs/src/web/content/frameworks/astro.mdx create mode 100644 docs/src/web/content/frameworks/hono.mdx create mode 100644 docs/src/web/content/frameworks/index.mdx create mode 100644 docs/src/web/content/frameworks/meta.json create mode 100644 docs/src/web/content/frameworks/nextjs.mdx create mode 100644 docs/src/web/content/frameworks/nuxt.mdx create mode 100644 docs/src/web/content/frameworks/react-router.mdx create mode 100644 docs/src/web/content/frameworks/sveltekit.mdx create mode 100644 docs/src/web/content/frameworks/tanstack-start.mdx create mode 100644 docs/src/web/content/frameworks/vite-react.mdx create mode 100644 docs/src/web/content/migration/from-v2.mdx create mode 100644 docs/src/web/content/migration/index.mdx create mode 100644 docs/src/web/content/migration/meta.json create mode 100644 docs/src/web/content/migration/migrate-cli.mdx create mode 100644 docs/src/web/content/migration/runtime-to-frameworks.mdx create mode 100644 docs/src/web/content/patterns/agents-as-a-pattern.mdx create mode 100644 docs/src/web/content/patterns/background-work.mdx create mode 100644 docs/src/web/content/patterns/chat-and-streaming.mdx create mode 100644 docs/src/web/content/patterns/index.mdx create mode 100644 docs/src/web/content/patterns/meta.json delete mode 100644 docs/src/web/content/routes/calling-agents.mdx delete mode 100644 docs/src/web/content/routes/cron.mdx delete mode 100644 docs/src/web/content/routes/explicit-routing.mdx delete mode 100644 docs/src/web/content/routes/http.mdx delete mode 100644 docs/src/web/content/routes/index.mdx delete mode 100644 docs/src/web/content/routes/meta.json delete mode 100644 docs/src/web/content/routes/middleware.mdx delete mode 100644 docs/src/web/content/routes/sse.mdx delete mode 100644 docs/src/web/content/routes/webrtc.mdx delete mode 100644 docs/src/web/content/routes/websockets.mdx create mode 100644 docs/src/web/content/services/ai-gateway.mdx create mode 100644 docs/src/web/public/images/integrations/astro.svg create mode 100644 docs/src/web/public/images/integrations/hono.svg create mode 100644 docs/src/web/public/images/integrations/nuxt.svg create mode 100644 docs/src/web/public/images/integrations/react-router.svg create mode 100644 docs/src/web/public/images/integrations/svelte.svg create mode 100644 docs/src/web/public/images/integrations/vite.svg delete mode 100644 docs/src/web/routes/_docs/agents/ai-sdk-integration.tsx delete mode 100644 docs/src/web/routes/_docs/agents/calling-other-agents.tsx delete mode 100644 docs/src/web/routes/_docs/agents/creating-agents.tsx delete mode 100644 docs/src/web/routes/_docs/agents/events-lifecycle.tsx delete mode 100644 docs/src/web/routes/_docs/agents/index.tsx delete mode 100644 docs/src/web/routes/_docs/agents/schema-libraries.tsx delete mode 100644 docs/src/web/routes/_docs/agents/standalone-execution.tsx delete mode 100644 docs/src/web/routes/_docs/agents/state-management.tsx delete mode 100644 docs/src/web/routes/_docs/agents/streaming-responses.tsx delete mode 100644 docs/src/web/routes/_docs/agents/when-to-use.tsx delete mode 100644 docs/src/web/routes/_docs/agents/workbench.tsx create mode 100644 docs/src/web/routes/_docs/deploy-operate/deploy-framework-apps.tsx create mode 100644 docs/src/web/routes/_docs/deploy-operate/environment-variables.tsx create mode 100644 docs/src/web/routes/_docs/deploy-operate/index.tsx create mode 100644 docs/src/web/routes/_docs/deploy-operate/local-development.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/astro.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/hono.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/index.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/nextjs.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/nuxt.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/react-router.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/sveltekit.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/tanstack-start.tsx create mode 100644 docs/src/web/routes/_docs/frameworks/vite-react.tsx create mode 100644 docs/src/web/routes/_docs/migration/from-v2.tsx create mode 100644 docs/src/web/routes/_docs/migration/index.tsx create mode 100644 docs/src/web/routes/_docs/migration/migrate-cli.tsx create mode 100644 docs/src/web/routes/_docs/migration/runtime-to-frameworks.tsx create mode 100644 docs/src/web/routes/_docs/patterns/agents-as-a-pattern.tsx create mode 100644 docs/src/web/routes/_docs/patterns/background-work.tsx create mode 100644 docs/src/web/routes/_docs/patterns/chat-and-streaming.tsx create mode 100644 docs/src/web/routes/_docs/patterns/index.tsx delete mode 100644 docs/src/web/routes/_docs/routes/calling-agents.tsx delete mode 100644 docs/src/web/routes/_docs/routes/cron.tsx delete mode 100644 docs/src/web/routes/_docs/routes/explicit-routing.tsx delete mode 100644 docs/src/web/routes/_docs/routes/http.tsx delete mode 100644 docs/src/web/routes/_docs/routes/index.tsx delete mode 100644 docs/src/web/routes/_docs/routes/middleware.tsx delete mode 100644 docs/src/web/routes/_docs/routes/sse.tsx delete mode 100644 docs/src/web/routes/_docs/routes/webrtc.tsx delete mode 100644 docs/src/web/routes/_docs/routes/websockets.tsx rename docs/src/web/routes/_docs/{agents => services}/ai-gateway.tsx (55%) create mode 100644 packages/cli/src/cmd/project/templates/astro/astro.config.mjs delete mode 100644 packages/cli/src/cmd/project/templates/nuxt/server/api/translate.post.ts create mode 100644 packages/cli/src/cmd/project/templates/nuxt/server/api/translate.ts create mode 100644 packages/cli/src/cmd/project/templates/remix/app/routes.ts create mode 100644 packages/cli/src/cmd/project/templates/sveltekit/svelte.config.js diff --git a/AGENTS.md b/AGENTS.md index 7625c2f86..b04484617 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,4 +59,5 @@ Repo-level test apps live under `tests/`: - **Verification**: Run format, lint, typecheck, build, test before committing - **Main branch**: NEVER commit directly - **Documentation**: Don't create docs unless explicitly asked +- **Docs examples**: Treat external framework memory as stale. Before shipping examples for Next, Nuxt, SvelteKit, Drizzle, Better Auth, Vite, or similar tools, check current upstream docs and align with their recommended patterns. Verify the Agentuity integration locally, but also verify that the surrounding framework usage is idiomatic. Working code is not enough if the setup is not how the framework expects users to do it. - **Clarification**: Ask before major code changes if unsure diff --git a/docs/package.json b/docs/package.json index a16f529e6..06eadb134 100644 --- a/docs/package.json +++ b/docs/package.json @@ -9,10 +9,10 @@ "predev": "bun run scripts/generate-api-reference.ts && bun run scripts/generate-nav-data.ts && bun run scripts/validate-routes.ts && bun run scripts/generate-markdown-files.ts", "prebuild": "bun run scripts/generate-api-reference.ts && bun run scripts/generate-nav-data.ts && bun run scripts/validate-routes.ts && bun run scripts/generate-markdown-files.ts", "predeploy": "cd ../ && bun install && bun run build:packages && cd docs && bun run prebuild", - "build": "bun ../../packages/cli/bin/cli.ts build --dir . --dev", - "dev": "bun ../../packages/cli/bin/cli.ts dev --dir .", + "build": "bun ../packages/cli/bin/cli.ts build --dir . --dev", + "dev": "bun ../packages/cli/bin/cli.ts dev --dir .", "start": "bun .agentuity/app.js", - "deploy": "bun ../../packages/cli/bin/cli.ts deploy --dir .", + "deploy": "bun ../packages/cli/bin/cli.ts deploy --dir .", "typecheck": "bunx tsc --noEmit", "build:run": "bun run scripts/bundle-run-scripts.ts", "generate:scripts": "bun run scripts/generate-sandbox-scripts.ts", diff --git a/docs/src/web/components/AIGatewayDemo.tsx b/docs/src/web/components/AIGatewayDemo.tsx index f7915cd7f..e993a8a3e 100644 --- a/docs/src/web/components/AIGatewayDemo.tsx +++ b/docs/src/web/components/AIGatewayDemo.tsx @@ -32,7 +32,7 @@ function parseTokensHeader(header: string): number { // Note: Google/Gemini excluded due to streaming issues (see issue #248) const AVAILABLE_MODELS = [ { id: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', provider: 'OpenAI' }, - { id: 'gpt-5-mini', label: 'GPT-5 Mini', provider: 'OpenAI' }, + { id: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', provider: 'OpenAI' }, { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', provider: 'Anthropic' }, { id: 'claude-sonnet-4-5', label: 'Claude Sonnet', provider: 'Anthropic' }, { id: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70B', provider: 'Groq' }, diff --git a/docs/src/web/components/SSEStreamDemo.tsx b/docs/src/web/components/SSEStreamDemo.tsx index 42eba9b5c..f4e97c3c9 100644 --- a/docs/src/web/components/SSEStreamDemo.tsx +++ b/docs/src/web/components/SSEStreamDemo.tsx @@ -23,7 +23,7 @@ interface StreamState { // Note: Google/Gemini excluded due to streaming issues (see issue #248) const MODELS = [ { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', provider: 'OpenAI' }, - { value: 'gpt-5-mini', label: 'GPT-5 Mini', provider: 'OpenAI' }, + { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', provider: 'OpenAI' }, { value: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', provider: 'Anthropic' }, { value: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5', provider: 'Anthropic' }, { value: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70B', provider: 'Groq' }, diff --git a/docs/src/web/components/StreamingDemo.tsx b/docs/src/web/components/StreamingDemo.tsx index f5964acad..185c6f1d8 100644 --- a/docs/src/web/components/StreamingDemo.tsx +++ b/docs/src/web/components/StreamingDemo.tsx @@ -28,7 +28,7 @@ function estimateTokens(text: string): number { const MODELS = [ { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini', provider: 'OpenAI' }, - { value: 'gpt-5-mini', label: 'GPT-5 Mini', provider: 'OpenAI' }, + { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano', provider: 'OpenAI' }, { value: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', provider: 'Anthropic' }, { value: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5', provider: 'Anthropic' }, { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite', provider: 'Google' }, diff --git a/docs/src/web/components/docs/copy-migration-prompt.tsx b/docs/src/web/components/docs/copy-migration-prompt.tsx index 5959ff6f5..bcc433766 100644 --- a/docs/src/web/components/docs/copy-migration-prompt.tsx +++ b/docs/src/web/components/docs/copy-migration-prompt.tsx @@ -68,7 +68,7 @@ No filesystem changes. No heuristics. Phase 1 — Comprehensive v0 Project Scan Recursively scan all files and folders that belong to the v0 project, including (but not limited to): src/agent*, src/agents* - src/api*, src/routes* + src/api*, src/frameworks* src/tools/ src/utils/, src/helpers/, src/lib/ Any other files contributing to runtime behavior or business logic diff --git a/docs/src/web/components/docs/nav-data.ts b/docs/src/web/components/docs/nav-data.ts index b20dc9467..63d997f15 100644 --- a/docs/src/web/components/docs/nav-data.ts +++ b/docs/src/web/components/docs/nav-data.ts @@ -100,11 +100,6 @@ export const navData: NavSection[] = [ url: '/explorer/model-arena', description: 'Compare AI models using another AI as judge', }, - { - title: 'WebRTC', - url: '/explorer/webrtc', - description: 'Audio, video, and data channels directly between browsers', - }, { title: 'Queues', url: '/explorer/queue', @@ -127,178 +122,78 @@ export const navData: NavSection[] = [ url: '/get-started', items: [ { - title: 'What is Agentuity?', - url: '/get-started/what-is-agentuity', - description: 'The full-stack platform for building, deploying, and operating AI agents', + title: 'Quickstart', + url: '/get-started/quickstart', + description: 'Create a framework app, add an AI route, and deploy it to Agentuity', }, { title: 'Installation', url: '/get-started/installation', - description: 'Set up your development environment', - }, - { - title: 'Quickstart', - url: '/get-started/quickstart', - description: 'Build your first agent in 5 minutes', + description: 'Install the Agentuity CLI and create a framework project', }, { title: 'Project Structure', url: '/get-started/project-structure', - description: 'Understand how Agentuity projects are organized', + description: 'Understand how Agentuity fits into framework projects', }, { title: 'App Configuration', url: '/get-started/app-configuration', - description: 'Configure your Agentuity project', - }, - ], - }, - { - title: 'Agents', - url: '/agents', - items: [ - { - title: 'When to Use', - url: '/agents/when-to-use', - description: 'When to create an agent vs handling requests directly in routes', - }, - { - title: 'Creating Agents', - url: '/agents/creating-agents', - description: 'Build agents with createAgent(), schemas, and handlers', - }, - { - title: 'Schema Libraries', - url: '/agents/schema-libraries', - description: 'Choose from built-in, Zod, Valibot, or ArkType for validation', - }, - { - title: 'AI Gateway', - url: '/agents/ai-gateway', - description: 'Automatic LLM routing with observability and cost tracking', - }, - { - title: 'AI SDK Integration', - url: '/agents/ai-sdk-integration', - description: 'Generate text, structured data, and streams with the Vercel AI SDK', + description: 'Configure a v3 Agentuity framework project', }, { - title: 'Streaming Responses', - url: '/agents/streaming-responses', - description: 'Return real-time LLM output with streaming agents', - }, - { - title: 'State Management', - url: '/agents/state-management', - description: 'Request and thread state for stateful agents', - }, - { - title: 'Calling Other Agents', - url: '/agents/calling-other-agents', - description: 'Build multi-agent systems with type-safe agent-to-agent communication', - }, - { - title: 'Standalone Execution', - url: '/agents/standalone-execution', + title: 'What is Agentuity?', + url: '/get-started/what-is-agentuity', description: - 'Execute agents programmatically for cron jobs, bots, CLI tools, and background workers', - }, - { - title: 'Events & Lifecycle', - url: '/agents/events-lifecycle', - description: 'Lifecycle hooks for monitoring and extending agent behavior', + 'Build, deploy, and operate TypeScript framework apps with Agentuity services', }, ], }, { - title: 'Routes', - url: '/routes', + title: 'Frameworks', + url: '/frameworks', items: [ { - title: 'HTTP', - url: '/routes/http', - description: 'Define GET, POST, and other HTTP endpoints with Hono', - }, - { - title: 'Middleware', - url: '/routes/middleware', - description: 'Add authentication, validation, and request processing to your routes', - }, - { - title: 'Calling Agents', - url: '/routes/calling-agents', - description: 'Import and invoke agents from your routes', - }, - { - title: 'Cron', - url: '/routes/cron', - description: 'Run tasks on a schedule with the cron() middleware', - }, - { - title: 'WebSockets', - url: '/routes/websockets', - description: 'Real-time bidirectional communication with the websocket middleware', - }, - { - title: 'SSE', - url: '/routes/sse', - description: 'Stream updates from server to client using SSE middleware', - }, - { - title: 'Using WebRTC', - url: '/routes/webrtc', - description: 'Peer-to-peer audio, video, and data channels with the webrtc middleware', - }, - { - title: 'Using Explicit Routing', - url: '/routes/explicit-routing', + title: 'Next.js', + url: '/frameworks/nextjs', description: - 'Pass your own Hono router to createApp() when you need custom mount paths or an exported router type', + 'Add Agentuity service clients, local development, and deployment metadata to a Next.js app', }, - ], - }, - { - title: 'Frontend', - url: '/frontend', - items: [ { - title: 'React Hooks', - url: '/frontend/react-hooks', - description: 'Provider, auth, analytics, and WebRTC hooks from @agentuity/react', + title: 'Nuxt', + url: '/frameworks/nuxt', + description: 'Add Agentuity CLI wiring and deployment validation to a Nuxt app', }, { - title: 'RPC Client', - url: '/frontend/rpc-client', - description: - 'Type-safe API calls from any JavaScript environment using hc() from hono/client', + title: 'React Router', + url: '/frameworks/react-router', + description: 'Add Agentuity CLI wiring and deployment validation to a React Router app', }, { - title: 'Provider Setup', - url: '/frontend/provider-setup', - description: 'Legacy AgentuityProvider setup for @agentuity/react apps', + title: 'SvelteKit', + url: '/frameworks/sveltekit', + description: 'Add Agentuity CLI wiring and deployment validation to a SvelteKit app', }, { - title: 'Authentication', - url: '/frontend/authentication', - description: 'Add user authentication with Agentuity Auth', + title: 'Astro', + url: '/frameworks/astro', + description: 'Add Agentuity CLI wiring and deployment validation to an Astro app', }, { - title: 'Deployment Scenarios', - url: '/frontend/deployment-scenarios', - description: - 'Deploy your frontend alongside agents or separately on Vercel, Netlify, etc.', + title: 'Hono', + url: '/frameworks/hono', + description: 'Add Agentuity services and deployment context to a Hono app', }, { - title: 'Static Rendering', - url: '/frontend/static-rendering', - description: - 'Pre-render your frontend to static HTML for faster page loads and better SEO', + title: 'Vite + React', + url: '/frameworks/vite-react', + description: 'Add Agentuity CLI wiring and deployment validation to a Vite React app', }, { - title: 'Advanced Hooks', - url: '/frontend/advanced-hooks', + title: 'TanStack Start', + url: '/frameworks/tanstack-start', description: - 'Advanced WebRTC callbacks plus low-level WebSocket and SSE client utilities', + 'Add Agentuity service clients to a TanStack Start app and validate the detected build path', }, ], }, @@ -306,22 +201,6 @@ export const navData: NavSection[] = [ title: 'Services', url: '/services', items: [ - { - title: 'Database', - url: '/services/database', - items: [ - { - title: 'Postgres', - url: '/services/database/postgres', - description: 'Auto-reconnecting PostgreSQL client for serverless environments', - }, - { - title: 'Drizzle', - url: '/services/database/drizzle', - description: 'Type-safe database access with Drizzle ORM', - }, - ], - }, { title: 'Storage', url: '/services/storage', @@ -329,30 +208,43 @@ export const navData: NavSection[] = [ { title: 'Key-Value', url: '/services/storage/key-value', - description: - 'Fast key-based storage for caching, session data, and configuration', + description: 'Store small durable values by namespace and key with optional TTL', }, { title: 'Vector', url: '/services/storage/vector', - description: 'Semantic search and retrieval for knowledge bases and RAG systems', + description: 'Store documents and embeddings for semantic search and retrieval', }, { title: 'Object', url: '/services/storage/object', - description: "Durable file storage using Bun's native S3 APIs", + description: "Store files and binary data with Bun's native S3 APIs", }, { title: 'Durable Streams', url: '/services/storage/durable-streams', - description: - 'Streaming storage for large exports, audit logs, and real-time data', + description: 'Write generated output incrementally and keep it available by URL', }, { title: 'Custom', url: '/services/storage/custom', - description: - 'Local development storage and custom runtime storage implementations', + description: 'Swap storage backends behind small app-owned interfaces', + }, + ], + }, + { + title: 'Database', + url: '/services/database', + items: [ + { + title: 'Postgres', + url: '/services/database/postgres', + description: 'Auto-reconnecting PostgreSQL client for serverless environments', + }, + { + title: 'Drizzle', + url: '/services/database/drizzle', + description: 'Type-safe database access with Drizzle ORM', }, ], }, @@ -369,10 +261,9 @@ export const navData: NavSection[] = [ 'Send and receive emails with managed addresses, destinations, and delivery tracking', }, { - title: 'Webhooks', - url: '/services/webhooks', - description: - 'Create webhook endpoints to receive HTTP callbacks with delivery tracking and retry', + title: 'Tasks', + url: '/services/tasks', + description: 'Track work items, issues, and agent activity with lifecycle management', }, { title: 'Schedules', @@ -380,22 +271,34 @@ export const navData: NavSection[] = [ description: 'Create platform-managed cron jobs with HTTP and sandbox destinations', }, { - title: 'Tasks', - url: '/services/tasks', + title: 'Webhooks', + url: '/services/webhooks', description: - 'Track work items, issues, and agent activity with built-in lifecycle management', + 'Create webhook endpoints to receive HTTP callbacks with delivery tracking and retry', }, { - title: 'Authentication', - url: '/services/authentication', - description: - 'Choose between Sign in with Agentuity and app-owned authentication for routes and apps', + title: 'Sandbox', + url: '/services/sandbox', + items: [ + { + title: 'SDK Usage', + url: '/services/sandbox/sdk-usage', + description: + 'Create, execute, inspect, and clean up sandboxes with SandboxClient', + }, + { + title: 'Snapshots', + url: '/services/sandbox/snapshots', + description: + 'Save sandbox filesystem states and reuse them as bases for new sandboxes', + }, + ], }, { - title: 'OIDC Provider', - url: '/services/oidc-provider', + title: 'AI Gateway', + url: '/services/ai-gateway', description: - 'Add Agentuity account sign-in and scoped access to your app with OAuth 2.0 and OIDC', + 'Route supported LLM SDK calls through Agentuity during local development and local builds', }, { title: 'Coder', @@ -410,7 +313,7 @@ export const navData: NavSection[] = [ { title: 'Logging', url: '/services/observability/logging', - description: 'Collected logs for agents and routes', + description: 'Write structured logs from framework apps and Hono routes', }, { title: 'Tracing', @@ -421,31 +324,67 @@ export const navData: NavSection[] = [ { title: 'Sessions & Debugging', url: '/services/observability/sessions-debugging', - description: 'Debug agents using session IDs, CLI commands, and trace timelines', - }, - { - title: 'Web Analytics', - url: '/services/observability/web-analytics', - description: - 'Track page views, user engagement, and custom events in your frontend', + description: 'Inspect session records, logs, and timelines', }, ], }, { - title: 'Sandbox', - url: '/services/sandbox', - items: [ - { - title: 'SDK Usage', - url: '/services/sandbox/sdk-usage', - description: 'Programmatic API for creating and managing sandboxes', - }, - { - title: 'Snapshots', - url: '/services/sandbox/snapshots', - description: 'Save and restore sandbox filesystem states for faster cold starts', - }, - ], + title: 'Authentication', + url: '/services/authentication', + description: + 'Choose between Agentuity OIDC and framework-owned authentication for v3 apps', + }, + { + title: 'OIDC Provider', + url: '/services/oidc-provider', + description: + 'Add Agentuity account sign-in and scoped access to your app with OAuth 2.0 and OIDC', + }, + ], + }, + { + title: 'Build & Deploy', + url: '/deploy-operate', + items: [ + { + title: 'Local Development', + url: '/deploy-operate/local-development', + description: 'Run your framework dev script with Agentuity environment wiring.', + }, + { + title: 'Deploy Framework Apps', + url: '/deploy-operate/deploy-framework-apps', + description: 'Build and deploy framework projects with the v3 Agentuity CLI.', + }, + { + title: 'Environment Variables', + url: '/deploy-operate/environment-variables', + description: + 'Manage local .env files, cloud environment variables, public variables, and secrets.', + }, + ], + }, + { + title: 'Patterns', + url: '/patterns', + items: [ + { + title: 'Agents as a Pattern', + url: '/patterns/agents-as-a-pattern', + description: + 'Build model-backed workflows with framework routes and Agentuity service clients', + }, + { + title: 'Chat and Streaming', + url: '/patterns/chat-and-streaming', + description: + 'Stream model output from framework routes and persist chat state with Agentuity clients', + }, + { + title: 'Background Work', + url: '/patterns/background-work', + description: + 'Use queues, status records, and durable streams for work that should outlive a request', }, ], }, @@ -534,8 +473,7 @@ export const navData: NavSection[] = [ { title: 'Chat with History', url: '/cookbook/patterns/chat-with-history', - description: - 'Build a chat agent that remembers previous messages using thread state', + description: 'Store chat history with key-value storage from a framework route', }, { title: 'Cron with Storage', @@ -654,6 +592,101 @@ export const navData: NavSection[] = [ title: 'Reference', url: '/reference', items: [ + { + title: 'Standalone Packages', + url: '/reference/standalone-packages', + description: + 'Use Agentuity service clients from Node.js, Bun, and server framework code', + }, + { + title: 'SDK Reference', + url: '/reference/sdk-reference', + items: [ + { + title: 'Agents', + url: '/reference/sdk-reference/agents', + description: 'Compatibility reference for v2 runtime createAgent() handlers', + }, + { + title: 'Application Entry', + url: '/reference/sdk-reference/application-entry', + description: 'Compatibility reference for v2 runtime createApp() entry files', + }, + { + title: 'Coder', + url: '/reference/sdk-reference/coder', + description: 'Manage AI coding sessions, workspaces, and skills with CoderClient', + }, + { + title: 'Communication', + url: '/reference/sdk-reference/communication', + description: + 'Call shared workflow functions from routes, workers, scripts, and other server code', + }, + { + title: 'Context API', + url: '/reference/sdk-reference/context-api', + description: + 'Compatibility reference for the v2 runtime AgentContext ctx.* object', + }, + { + title: 'Email Service', + url: '/reference/sdk-reference/email-service', + description: 'Compatibility reference for v2 runtime ctx.email APIs', + }, + { + title: 'Events', + url: '/reference/sdk-reference/events', + description: 'Compatibility reference for v2 runtime lifecycle event APIs', + }, + { + title: 'Observability', + url: '/reference/sdk-reference/observability', + description: + 'Compatibility reference for v2 runtime ctx.logger and ctx.tracer APIs', + }, + { + title: 'Queue Service', + url: '/reference/sdk-reference/queue-service', + description: 'Compatibility reference for v2 runtime ctx.queue APIs', + }, + { + title: 'Router', + url: '/reference/sdk-reference/router', + description: 'Compatibility reference for v2 runtime route handlers', + }, + { + title: 'Runtime Utilities', + url: '/reference/sdk-reference/advanced', + description: 'Compatibility reference for v2 runtime utilities and helpers', + }, + { + title: 'Sandbox Service', + url: '/reference/sdk-reference/sandbox-service', + description: 'Compatibility reference for v2 runtime ctx.sandbox APIs', + }, + { + title: 'Schedule Service', + url: '/reference/sdk-reference/schedule-service', + description: 'Compatibility reference for v2 runtime ctx.schedule APIs', + }, + { + title: 'Schema', + url: '/reference/sdk-reference/schema', + description: 'Type-safe runtime validation with StandardSchema support', + }, + { + title: 'Storage', + url: '/reference/sdk-reference/storage', + description: 'Compatibility reference for v2 runtime ctx.* storage APIs', + }, + { + title: 'Task Service', + url: '/reference/sdk-reference/task-service', + description: 'Compatibility reference for v2 runtime ctx.task APIs', + }, + ], + }, { title: 'API Reference', url: '/reference/api', @@ -790,7 +823,7 @@ export const navData: NavSection[] = [ title: 'Build Configuration', url: '/reference/cli/build-configuration', description: - 'Customize the build process with Vite plugins and build-time constants', + 'Build framework apps into Agentuity deployment bundles with launch metadata and static assets.', }, { title: 'Claude Code Plugin', @@ -817,20 +850,19 @@ export const navData: NavSection[] = [ { title: 'Deployment', url: '/reference/cli/deployment', - description: - 'Deploy your agents to Agentuity Cloud with automatic infrastructure provisioning.', + description: 'Deploy registered framework apps to Agentuity Cloud.', }, { title: 'Development', url: '/reference/cli/development', description: - 'Run the development server with hot reload, type checking, and public URL support.', + 'Run your framework development server with Agentuity environment wiring.', }, { title: 'Getting Started', url: '/reference/cli/getting-started', description: - 'Install the Agentuity CLI and authenticate to start building agents.', + 'Install the Agentuity CLI, sign in, create framework projects, and register existing apps.', }, { title: 'Git Integration', @@ -847,8 +879,7 @@ export const navData: NavSection[] = [ { title: 'Managing OAuth Apps', url: '/reference/cli/oauth', - description: - 'Create and manage OAuth/OIDC applications for third-party integrations from the CLI', + description: 'Create and manage OAuth/OIDC applications from the CLI', }, { title: 'OpenCode Plugin', @@ -875,102 +906,6 @@ export const navData: NavSection[] = [ }, ], }, - { - title: 'SDK Reference', - url: '/reference/sdk-reference', - items: [ - { - title: 'Agents', - url: '/reference/sdk-reference/agents', - description: - 'Define agents with createAgent(), configure schemas, and write handlers', - }, - { - title: 'Application Entry', - url: '/reference/sdk-reference/application-entry', - description: - 'Initialize your Agentuity app with createApp() and configure the runtime entry file', - }, - { - title: 'Coder', - url: '/reference/sdk-reference/coder', - description: 'Manage AI coding sessions, workspaces, and skills with CoderClient', - }, - { - title: 'Communication', - url: '/reference/sdk-reference/communication', - description: 'Call agents from routes or other agents with type-safe imports', - }, - { - title: 'Context API', - url: '/reference/sdk-reference/context-api', - description: 'Storage, logging, and services available via the ctx.* object', - }, - { - title: 'Email Service', - url: '/reference/sdk-reference/email-service', - description: 'Send emails and manage addresses with ctx.email', - }, - { - title: 'Events', - url: '/reference/sdk-reference/events', - description: 'Lifecycle hooks for monitoring agent, session, and thread events', - }, - { - title: 'Observability', - url: '/reference/sdk-reference/observability', - description: - 'Collected logs and OpenTelemetry tracing via ctx.logger and ctx.tracer', - }, - { - title: 'Queue Service', - url: '/reference/sdk-reference/queue-service', - description: 'Publish messages and manage queues with ctx.queue', - }, - { - title: 'Router', - url: '/reference/sdk-reference/router', - description: 'HTTP endpoints, middleware, WebSocket, SSE, and cron handlers', - }, - { - title: 'Runtime Utilities', - url: '/reference/sdk-reference/advanced', - description: - 'File imports, standalone execution, context detection, process lifecycle, and build metadata', - }, - { - title: 'Sandbox Service', - url: '/reference/sdk-reference/sandbox-service', - description: 'Run code in isolated containers with ctx.sandbox', - }, - { - title: 'Schedule Service', - url: '/reference/sdk-reference/schedule-service', - description: 'Create and manage cron-based scheduled jobs with ctx.schedule', - }, - { - title: 'Schema', - url: '/reference/sdk-reference/schema', - description: 'Type-safe runtime validation with StandardSchema support', - }, - { - title: 'Storage', - url: '/reference/sdk-reference/storage', - description: 'KV, Vector, Database, Object, and Stream storage reference', - }, - { - title: 'Task Service', - url: '/reference/sdk-reference/task-service', - description: 'Track work items with lifecycle management via ctx.task', - }, - ], - }, - { - title: 'Standalone Packages', - url: '/reference/standalone-packages', - description: - 'Use Agentuity services from any Node.js or Bun application without the full runtime', - }, { title: 'GitHub App', url: '/reference/github-app', @@ -982,11 +917,26 @@ export const navData: NavSection[] = [ url: '/reference/gravity-network', description: "The layered infrastructure powering Agentuity's services", }, + ], + }, + { + title: 'Migration', + url: '/migration', + items: [ + { + title: 'From v2', + url: '/migration/from-v2', + description: 'Move a v2 runtime app toward the v3 framework-first app shape.', + }, { - title: 'Migration Guide', - url: '/reference/migration-guide', - description: - 'Migrate from v1 to v2 for explicit routing, Hono-native routers, and standard Vite config.', + title: 'Runtime to Frameworks', + url: '/migration/runtime-to-frameworks', + description: 'Translate v2 runtime concepts into the v3 framework-first model.', + }, + { + title: 'Migration CLI', + url: '/migration/migrate-cli', + description: 'Run the Agentuity migration tool for v2 to v3 projects.', }, ], }, diff --git a/docs/src/web/content/AGENTS.md b/docs/src/web/content/AGENTS.md index 204b628b1..3b1618b15 100644 --- a/docs/src/web/content/AGENTS.md +++ b/docs/src/web/content/AGENTS.md @@ -10,6 +10,7 @@ Writing conventions for Agentuity docs pages in this directory. 4. **Scannable**: Headings, callouts, inline comments that explain "why" not "what" 5. **Benefit-focused, not salesy**: Explain _why_ someone would use a feature without hollow adjectives 6. **Source-verified**: Read SDK source and AGENTS.md files before documenting APIs or CLI flags. +7. **Framework-native**: Check current upstream framework docs before documenting framework examples. Local Agentuity verification is required, but working code is not enough if the framework shape is not idiomatic. ## Exemplar Pages @@ -17,9 +18,9 @@ Before writing a new page, read these as reference implementations: - **Feature doc**: `agents/creating-agents.mdx` -- context-then-code flow, callouts, progressive examples - **Service doc**: `services/storage/key-value.mdx` -- comparison table, access patterns, comprehensive operations -- **Cookbook pattern**: `cookbook/patterns/chat-with-history.mdx` -- concise, code highlights, thread state +- **Cookbook pattern**: `cookbook/patterns/chat-with-history.mdx` -- concise, code-first, key-value history - **Getting started**: `get-started/quickstart.mdx` -- step-by-step, CardLinks, tips -- **Reference**: `agents/ai-gateway.mdx` -- provider tables, how-it-works flow +- **Reference**: `services/ai-gateway.mdx` -- provider tables, how-it-works flow - **SDK Reference**: `reference/sdk-reference/storage.mdx` -- hybrid narrative + structured method docs ## Page Types @@ -134,7 +135,7 @@ Agentuity supports raw provider SDKs and AI SDK providers. When writing docs: - Keep feature docs provider-agnostic where possible - Use current model names in code examples; verify they're up to date before publishing - When listing providers or models in tables, link to each provider's model page -- See [AI Gateway](/agents/ai-gateway) for the canonical list of supported providers +- See [AI Gateway](/services/ai-gateway) for the canonical list of supported providers ## Code Examples @@ -185,7 +186,7 @@ Available components in doc pages: ### Links and Callouts -- **Cross-links** include context: "See [Streaming Responses](/agents/streaming-responses) for chunked output patterns" not "See also: Streaming" +- **Cross-links** include context: "See [Chat and Streaming](/patterns/chat-and-streaming) for chunked output patterns" not "See also: Streaming" - **External links**: link on first mention. Don't re-link on the same page - **Canonical docs**: link to existing docs instead of re-explaining. One location is canonical, others link to it - **Callouts**: `info` for context and clarifications, `warning` for gotchas and required setup, `tip` for optimizations and advanced patterns diff --git a/docs/src/web/content/agents/ai-gateway.mdx b/docs/src/web/content/agents/ai-gateway.mdx deleted file mode 100644 index 6aeeb6be7..000000000 --- a/docs/src/web/content/agents/ai-gateway.mdx +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Using the AI Gateway -short_title: AI Gateway -description: Automatic LLM routing with observability and cost tracking ---- - -Agentuity's AI Gateway routes LLM requests through managed infrastructure, giving you observability and cost tracking across all model providers. - -## How It Works - -When you make LLM requests from your agents, they're *automatically* routed through the AI Gateway: - -``` -Your Agent → AI Gateway → Provider API (OpenAI, Anthropic, etc.) -``` - -The AI Gateway provides: - -- **Consolidated billing** across all LLM providers -- **Automatic observability** with token tracking and latency metrics -- **Request logging** visible in the Agentuity console -- **No configuration required** when using your SDK key - -## Using the AI Gateway - -The AI Gateway works automatically, whether you use provider SDKs directly (Anthropic, OpenAI, Groq), the [Vercel AI SDK](https://ai-sdk.dev), or frameworks like Mastra and LangGraph. No configuration needed. - -### Provider SDKs - -Use provider SDKs directly and get AI Gateway routing automatically: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import Anthropic from '@anthropic-ai/sdk'; -import OpenAI from 'openai'; -import Groq from 'groq-sdk'; -import { s } from '@agentuity/schema'; - -// Direct SDK clients route through the AI Gateway -const anthropic = new Anthropic(); -const openai = new OpenAI(); -const groq = new Groq(); - -const agent = createAgent('AnthropicChat', { - schema: { - input: s.object({ prompt: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, input) => { - const result = await anthropic.messages.create({ - model: 'claude-sonnet-4-6', - max_tokens: 1024, - messages: [{ role: 'user', content: input.prompt }], - }); - - const text = result.content[0]?.type === 'text' - ? result.content[0].text - : ''; - - return { response: text }; - }, -}); - -export default agent; -``` - -### AI SDK Providers - -The [Vercel AI SDK](https://ai-sdk.dev) providers also route through Agentuity's AI Gateway: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('TextGenerator', { - schema: { - input: s.object({ prompt: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, input) => { - const { text } = await generateText({ - model: openai('gpt-5.4-mini'), - prompt: input.prompt, - }); - - return { response: text }; - }, -}); - -export default agent; -``` - -### Provider Imports - -All supported providers route through the AI Gateway: - -```typescript -// Provider SDKs -import Anthropic from '@anthropic-ai/sdk'; -import OpenAI from 'openai'; -import Groq from 'groq-sdk'; - -// AI SDK providers -import { openai } from '@ai-sdk/openai'; -import { anthropic } from '@ai-sdk/anthropic'; -import { google } from '@ai-sdk/google'; -import { xai } from '@ai-sdk/xai'; -import { deepseek } from '@ai-sdk/deepseek'; -import { groq } from '@ai-sdk/groq'; -import { mistral } from '@ai-sdk/mistral'; -import { cohere } from '@ai-sdk/cohere'; -import { perplexity } from '@ai-sdk/perplexity'; -``` - -## Supported Providers - -| Provider | Example Models | -|----------|---------------| -| [OpenAI](https://developers.openai.com/api/docs/models) | `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano` | -| [Anthropic](https://platform.claude.com/docs/en/about-claude/models/overview) | `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5` | -| [Google](https://ai.google.dev/gemini-api/docs/models) | `gemini-3.1-pro-preview`, `gemini-3-flash-preview`, `gemini-3.1-flash-lite-preview` | -| [xAI](https://docs.x.ai/docs/models/) | `grok-4.20-reasoning`, `grok-4-1-fast-reasoning`, `grok-code-fast-1` | -| [DeepSeek](https://api-docs.deepseek.com/api/list-models) | `deepseek-chat`, `deepseek-reasoner` | -| [Groq](https://console.groq.com/docs/models) | `llama-3.3-70b-versatile`, `openai/gpt-oss-120b`, `meta-llama/llama-4-scout-17b-16e-instruct` | -| [Mistral](https://docs.mistral.ai/getting-started/models) | `mistral-large-2512`, `devstral-2512`, `mistral-medium-2508` | -| [Cohere](https://docs.cohere.com/docs/models) | `command-a-03-2025`, `command-a-reasoning-08-2025` | -| [Perplexity](https://docs.perplexity.ai/docs/getting-started/models) | `sonar-pro`, `sonar` | - - -Provider catalogs and model IDs are updated often. Verify current availability in each provider's official docs. - - -## BYO API Keys - -Bypass the AI Gateway by setting your own API keys in `.env`: - -```dotenv title=".env" -OPENAI_API_KEY=sk-... -ANTHROPIC_API_KEY=sk-ant-... -GOOGLE_GENERATIVE_AI_API_KEY=... -XAI_API_KEY=... -DEEPSEEK_API_KEY=... -GROQ_API_KEY=... -MISTRAL_API_KEY=... -COHERE_API_KEY=... -PERPLEXITY_API_KEY=... -``` - -When these variables are set to your provider keys, requests go directly to the provider instead of through the AI Gateway. - -## AI Gateway vs BYO Keys - -| Aspect | AI Gateway | BYO API Keys | -|--------|------------|--------------| -| **Setup** | Just SDK key | Manage per-provider keys | -| **Cost tracking** | Automatic in console | Manual | -| **Observability** | Built-in token/latency metrics | Must configure separately | -| **Rate limits** | Shared pool | Your own limits | - -We recommend the AI Gateway for most projects. - -## Next Steps - -- [Using the AI SDK](/agents/ai-sdk-integration): Structured output, tool calling, and multi-turn conversations -- [Returning Streaming Responses](/agents/streaming-responses): Real-time chat UIs and progress indicators -- [Logging](/services/observability/logging): Debug requests and track LLM performance diff --git a/docs/src/web/content/agents/ai-sdk-integration.mdx b/docs/src/web/content/agents/ai-sdk-integration.mdx deleted file mode 100644 index ad810d47e..000000000 --- a/docs/src/web/content/agents/ai-sdk-integration.mdx +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Using the AI SDK -short_title: AI SDK Integration -description: Generate text, structured data, and streams with the Vercel AI SDK ---- - -The [Vercel AI SDK](https://ai-sdk.dev) provides a consistent API for LLM interactions with built-in streaming, structured output, and tool calling. - -Agentuity works with any approach. This page documents the AI SDK path specifically, and AI SDK is optional. You can also use provider SDKs directly (Anthropic, OpenAI), or frameworks like Mastra and LangGraph. See [Using the AI Gateway](/agents/ai-gateway) for examples with different libraries. - -## Installation - -Install the AI SDK and your preferred provider: - -```bash -bun add ai @ai-sdk/openai -``` - -Or with multiple providers: - -```bash -bun add ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google -``` - -## Generating Text - -Use `generateText` for simple completions: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -const agent = createAgent('TextGenerator', { - schema: { - input: z.object({ prompt: z.string() }), - output: z.object({ response: z.string() }), - }, - handler: async (ctx, input) => { - const { text } = await generateText({ - model: openai('gpt-5.4-mini'), - prompt: input.prompt, - }); - - return { response: text }; - }, -}); - -export default agent; -``` - -### With System Prompt - -Add context with a system message: - -```typescript -// Inside your handler: -const { text } = await generateText({ - model: openai('gpt-5.4-mini'), - system: 'You are a concise technical assistant. Keep responses under 100 words.', - prompt: input.prompt, -}); -``` - -### With Message History - -Pass conversation history for multi-turn interactions: - -```typescript -// Inside your handler: -const { text } = await generateText({ - model: openai('gpt-5.4-mini'), - messages: [ - { role: 'system', content: 'You are a helpful assistant.' }, - { role: 'user', content: 'What is TypeScript?' }, - { role: 'assistant', content: 'TypeScript is a typed superset of JavaScript.' }, - { role: 'user', content: input.followUp }, - ], -}); -``` - -## Generating Structured Data - -Use `generateText` with `Output.object()` to get validated, typed responses: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText, Output } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -const SentimentSchema = z.object({ - sentiment: z.enum(['positive', 'negative', 'neutral']), - confidence: z.number().min(0).max(1), - keywords: z.array(z.string()), -}); - -const agent = createAgent('SentimentAnalyzer', { - schema: { - input: z.object({ text: z.string() }), - output: SentimentSchema, - }, - handler: async (ctx, input) => { - const { output } = await generateText({ - model: openai('gpt-5.4-mini'), - output: Output.object({ schema: SentimentSchema }), - prompt: `Analyze the sentiment of: "${input.text}"`, - }); - - // Schema match means no runtime type assertion needed - return output; - }, -}); - -export default agent; -``` - - -Define your schema once and reuse it for both `Output.object()` and your agent's output schema. This keeps types consistent throughout your codebase. - - -### Using `.describe()` for Field Hints - -Add `.describe()` to schema fields to guide the model on format and content: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText, Output } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -const EventSchema = z.object({ - title: z.string().describe('Event title, e.g. "Team standup"'), - date: z.string().describe('ISO 8601 date: YYYY-MM-DD'), - startTime: z.string().describe('24-hour format: HH:MM'), - duration: z.number().describe('Duration in minutes'), - attendees: z.array(z.string()).describe('List of attendee names'), -}); - -const agent = createAgent('EventExtractor', { - schema: { - input: z.object({ text: z.string() }), - output: EventSchema, - }, - handler: async (ctx, input) => { - const { output } = await generateText({ - model: openai('gpt-5.4-mini'), - output: Output.object({ schema: EventSchema }), - prompt: `Extract event details from: "${input.text}"`, - }); - - return output; - }, -}); - -export default agent; -``` - -The `.describe()` hints improve output consistency, especially for dates, times, and formatted strings. - -## Generating Streams - -Use `streamText` for real-time responses: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -const agent = createAgent('StreamingChat', { - schema: { - input: z.object({ prompt: z.string() }), - stream: true, - }, - handler: async (ctx, input) => { - const { textStream } = streamText({ - model: openai('gpt-5.4-mini'), - prompt: input.prompt, - }); - - return textStream; - }, -}); - -export default agent; -``` - -For detailed streaming patterns, see [Streaming Responses](/agents/streaming-responses). - -## Switching Providers - -Change providers by swapping the import and model: - -```typescript -// OpenAI -import { openai } from '@ai-sdk/openai'; -const model = openai('gpt-5.4-mini'); - -// Anthropic -import { anthropic } from '@ai-sdk/anthropic'; -const model = anthropic('claude-sonnet-4-6'); - -// Google -import { google } from '@ai-sdk/google'; -const model = google('gemini-3-flash-preview'); - -// Groq (fast inference) -import { groq } from '@ai-sdk/groq'; -const model = groq('llama-3.3-70b-versatile'); -``` - -## Error Handling - -Wrap LLM calls in try-catch to handle errors gracefully: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -const agent = createAgent('SafeGenerator', { - schema: { - input: z.object({ prompt: z.string() }), - output: z.object({ response: z.string() }), - }, - handler: async (ctx, input) => { - try { - const { text } = await generateText({ - model: openai('gpt-5.4-mini'), - prompt: input.prompt, - }); - - return { response: text }; - } catch (error) { - ctx.logger.error('LLM request failed', { error }); - return { response: 'I encountered an error processing your request.' }; - } - }, -}); - -export default agent; -``` - -## Best Practices - -- **Define output schemas** with [Zod](https://zod.dev), [Valibot](https://valibot.dev), or [ArkType](https://arktype.io) for type safety and validation -- **Add system prompts** to guide model behavior consistently -- **Handle errors gracefully** with fallback responses - -## Next Steps - -- [Using the AI Gateway](/agents/ai-gateway): Observability, cost tracking, and provider switching -- [Returning Streaming Responses](/agents/streaming-responses): Chat UIs and long-form content generation -- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Quality checks and output validation diff --git a/docs/src/web/content/agents/calling-other-agents.mdx b/docs/src/web/content/agents/calling-other-agents.mdx deleted file mode 100644 index 0f94455dd..000000000 --- a/docs/src/web/content/agents/calling-other-agents.mdx +++ /dev/null @@ -1,694 +0,0 @@ ---- -title: Calling Other Agents -description: Build multi-agent systems with type-safe agent-to-agent communication ---- - -Break complex tasks into focused, reusable agents that communicate with type safety. Instead of building one large agent, create specialized agents that each handle a single responsibility. - -For method signatures and the agent registry API, see the [Communication Reference](/reference/sdk-reference/communication). - -## Basic Usage - -Import and call other agents directly: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import enrichmentAgent from '@agent/enrichment/agent'; - -const coordinator = createAgent('Coordinator', { - schema: { - input: s.object({ text: s.string() }), - output: s.object({ result: s.string() }), - }, - handler: async (ctx, input) => { - // Call another agent by importing it - const enriched = await enrichmentAgent.run({ - text: input.text, - }); - - return { result: enriched.enrichedText }; - }, -}); - -export default coordinator; -``` - -When both agents have schemas, TypeScript validates the input and infers the output type automatically. - - -Define schemas on all agents to enable full type inference. TypeScript will validate that inputs match expected types and provide autocomplete for outputs. - - -## Communication Patterns - -### Sequential Execution - -Process data through a series of agents where each step depends on the previous result: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import validatorAgent from '@agent/validator/agent'; -import enrichmentAgent from '@agent/enrichment/agent'; -import analysisAgent from '@agent/analysis/agent'; - -const pipeline = createAgent('Pipeline', { - schema: { - input: s.object({ rawData: s.string() }), - output: s.object({ - processed: s.object({ - summary: s.string(), - tags: s.array(s.string()), - }), - }), - }, - handler: async (ctx, input) => { - // Each step depends on the previous result - const validated = await validatorAgent.run({ - data: input.rawData, - }); - - const enriched = await enrichmentAgent.run({ - data: validated.cleanData, - }); - - const analyzed = await analysisAgent.run({ - data: enriched.enrichedData, - }); - - return { processed: analyzed }; - }, -}); - -export default pipeline; -``` - -Errors propagate automatically. If `validatorAgent` throws, subsequent agents never execute. - -### Parallel Execution - -Run multiple agents simultaneously when their operations are independent: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import webSearchAgent from '@agent/web-search/agent'; -import databaseAgent from '@agent/database/agent'; -import vectorSearchAgent from '@agent/vector-search/agent'; - -const searchAgent = createAgent('Search', { - schema: { - input: s.object({ query: s.string() }), - output: s.object({ - results: s.array( - s.object({ - title: s.string(), - url: s.optional(s.string()), - snippet: s.string(), - }) - ), - }), - }, - handler: async (ctx, input) => { - // Execute all searches in parallel - const [webResults, dbResults, vectorResults] = await Promise.all([ - webSearchAgent.run({ query: input.query }), - databaseAgent.run({ query: input.query }), - vectorSearchAgent.run({ query: input.query }), - ]); - - return { - results: [...webResults.items, ...dbResults.items, ...vectorResults.items], - }; - }, -}); - -export default searchAgent; -``` - -If each agent takes 1 second, parallel execution completes in 1 second instead of 3. - -### Background Execution - -Use `ctx.waitUntil()` for fire-and-forget operations that continue after returning a response: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import analyticsAgent from '@agent/analytics/agent'; - -const processor = createAgent('Processor', { - schema: { - input: s.object({ - data: s.record(s.string(), s.unknown()), - }), - output: s.object({ status: s.string(), id: s.string() }), - }, - handler: async (ctx, input) => { - const id = crypto.randomUUID(); - - // Start background processing - ctx.waitUntil(async () => { - await analyticsAgent.run({ - event: 'processed', - data: input.data, - }); - ctx.logger.info('Background processing completed', { id }); - }); - - // Return immediately - return { status: 'accepted', id }; - }, -}); - -export default processor; -``` - - -Use `ctx.waitUntil()` for analytics, logging, notifications, or any operation where the caller doesn't need the result. - - -### Conditional Routing - -Use an LLM to classify intent and route to the appropriate agent: - -```typescript -import supportAgent from '@agent/support/agent'; -import salesAgent from '@agent/sales/agent'; -import technicalAgent from '@agent/technical/agent'; - -handler: async (ctx, input) => { - // Classify with a fast model, using Groq (via AI Gateway) - const { object: intent } = await generateObject({ - model: groq('llama-3.3-70b-versatile'), - schema: z.object({ - agentType: z.enum(['support', 'sales', 'technical']), - }), - prompt: input.message, - }); - - // Route based on classification - switch (intent.agentType) { - case 'support': - return supportAgent.run(input); - case 'sales': - return salesAgent.run(input); - case 'technical': - return technicalAgent.run(input); - } -} -``` - -See [Full Example](#full-example) below for a complete implementation with error handling and logging. - -### Orchestrator Pattern - -An orchestrator is a coordinator agent that delegates work to specialized agents and combines their results. This pattern is useful for: - -- Multi-step content pipelines (generate → evaluate → refine) -- Parallel data gathering from multiple sources -- Workflows requiring different expertise (writer + reviewer + formatter) - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import writerAgent from '@agent/writer/agent'; -import evaluatorAgent from '@agent/evaluator/agent'; - -const orchestrator = createAgent('Orchestrator', { - schema: { - input: s.object({ topic: s.string() }), - output: s.object({ content: s.string(), score: s.number() }), - }, - handler: async (ctx, input) => { - // Step 1: Generate content - const draft = await writerAgent.run({ prompt: input.topic }); - - // Step 2: Evaluate quality - const evaluation = await evaluatorAgent.run({ - content: draft.text, - }); - - // Step 3: Return combined result - return { content: draft.text, score: evaluation.score }; - }, -}); - -export default orchestrator; -``` - - -The orchestrator pattern is common in AI workflows where you want to separate concerns (e.g., generation, evaluation, formatting) into focused agents. - - -## Public Agents - - -Agent imports only work within the same project. To call an agent in another project or organization, expose it through an HTTP route and call that route with `fetch()`. - - -Call the deployed route using standard HTTP: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const ExternalResponse = s.object({ response: s.string() }); - -const agent = createAgent('External Caller', { - schema: { - input: s.object({ query: s.string() }), - output: s.object({ result: s.string() }), - }, - handler: async (ctx, input) => { - try { - const routeUrl = process.env.EXTERNAL_AGENT_ROUTE_URL; - if (!routeUrl) { - throw new Error('EXTERNAL_AGENT_ROUTE_URL is required'); - } - - const response = await fetch(routeUrl, { - method: 'POST', - body: JSON.stringify({ query: input.query }), - headers: { 'Content-Type': 'application/json' }, - }); - - if (!response.ok) { - throw new Error(`External agent route returned ${response.status}`); - } - - const data = ExternalResponse.parse(await response.json()); - return { result: data.response }; - } catch (error) { - ctx.logger.error('External agent call failed', { error }); - throw error; - } - }, -}); - -export default agent; -``` - -## Error Handling - -### Cascading Failures - -By default, errors propagate through the call chain: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import validatorAgent from '@agent/validator/agent'; -import processorAgent from '@agent/processor/agent'; - -const pipeline = createAgent('Pipeline', { - schema: { - input: s.object({ - documentId: s.string(), - content: s.string(), - }), - output: s.object({ - documentId: s.string(), - status: s.string(), - }), - }, - handler: async (ctx, input) => { - // If validatorAgent throws, execution stops here - const validated = await validatorAgent.run(input); - - // This never executes if validation fails - const processed = await processorAgent.run(validated); - - return processed; - }, -}); -``` - -This is the recommended pattern for critical operations where later steps cannot proceed without earlier results. - -### Graceful Degradation - -For optional operations, catch errors and continue: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import enrichmentAgent from '@agent/enrichment/agent'; -import processorAgent from '@agent/processor/agent'; - -const resilientProcessor = createAgent('Resilient Processor', { - schema: { - input: s.object({ - data: s.record(s.string(), s.unknown()), - }), - output: s.object({ - status: s.string(), - resultId: s.string(), - }), - }, - handler: async (ctx, input) => { - let enrichedData = input.data; - - // Try to enrich, but continue if it fails - try { - const enrichment = await enrichmentAgent.run({ - data: input.data, - }); - enrichedData = enrichment.data; - } catch (error) { - ctx.logger.warn('Enrichment failed, using original data', { - error: error instanceof Error ? error.message : String(error), - }); - } - - // Process with enriched data (or original if enrichment failed) - return await processorAgent.run({ data: enrichedData }); - }, -}); -``` - -### Retry Pattern - -Implement retry logic for unreliable operations: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import externalServiceAgent from '@agent/external-service/agent'; - -async function callWithRetry( - fn: () => Promise, - maxRetries: number = 3, - delayMs: number = 1000 -): Promise { - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - if (attempt === maxRetries) throw error; - - // Exponential backoff - const delay = delayMs * Math.pow(2, attempt - 1); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } - throw new Error('Retry failed'); -} - -const retryHandler = createAgent('Retry Handler', { - schema: { - input: s.object({ - endpoint: s.string(), - payload: s.record(s.string(), s.unknown()), - }), - output: s.object({ - status: s.string(), - requestId: s.string(), - }), - }, - handler: async (ctx, input) => { - const result = await callWithRetry(() => - externalServiceAgent.run(input) - ); - return result; - }, -}); -``` - -### Partial Failure Handling - -Handle mixed success/failure results with `Promise.allSettled()`: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import processingAgent from '@agent/processing/agent'; - -const batchProcessor = createAgent('Batch Processor', { - schema: { - input: s.object({ - items: s.array( - s.object({ - id: s.string(), - payload: s.record(s.string(), s.unknown()), - }) - ), - }), - output: s.object({ - successful: s.array( - s.object({ - itemId: s.string(), - status: s.string(), - }) - ), - failedCount: s.number(), - }), - }, - handler: async (ctx, input) => { - const results = await Promise.allSettled( - input.items.map((item) => processingAgent.run({ item })) - ); - - const successful = results - .filter((r) => r.status === 'fulfilled') - .map((r) => r.value); - - const failed = results - .filter((r) => r.status === 'rejected') - .map((r) => r.reason); - - if (failed.length > 0) { - ctx.logger.warn('Some operations failed', { failedCount: failed.length }); - } - - return { successful, failedCount: failed.length }; - }, -}); -``` - -## Best Practices - -### Keep Agents Focused - -Each agent should have a single, well-defined responsibility: - -```typescript -// Good: focused agents -const validatorAgent = createAgent('Validator', { /* validates data */ }); -const enrichmentAgent = createAgent('Enrichment', { /* enriches data */ }); -const analysisAgent = createAgent('Analysis', { /* analyzes data */ }); - -// Bad: monolithic agent -const megaAgent = createAgent('MegaAgent', { - handler: async (ctx) => { - // Validates, enriches, analyzes all in one place - }, -}); -``` - -Focused agents are easier to test, reuse, and maintain. - -### Use Schemas for Type Safety - -Define schemas on all agents for type-safe communication. See [Creating Agents](/agents/creating-agents) to learn more about using schemas. - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { z } from 'zod'; - -// Source agent with output schema -const source = createAgent('Source', { - schema: { - input: z.object({ id: z.string() }), - output: z.object({ - data: z.string(), - metadata: z.object({ timestamp: z.string() }), - }), - }, - handler: async (_ctx, input) => { - return { - data: `result:${input.id}`, - metadata: { timestamp: new Date().toISOString() }, - }; - }, -}); - -// Consumer agent - TypeScript validates the connection -const consumer = createAgent('Consumer', { - schema: { - input: z.object({ id: z.string() }), - output: z.object({ processed: z.string() }), - }, - handler: async (_ctx, input) => { - const result = await source.run({ id: input.id }); - // TypeScript knows result.data and result.metadata.timestamp exist - return { processed: result.data }; - }, -}); -``` - -### Share Context - -Agent calls share the same session context: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import processingAgent from '@agent/processing/agent'; - -const coordinator = createAgent('Coordinator', { - schema: { - input: s.object({ - userId: s.string(), - data: s.record(s.string(), s.unknown()), - }), - output: s.object({ - status: s.string(), - resultId: s.string(), - }), - }, - handler: async (ctx, input) => { - // Store data in thread state (async) - await ctx.thread.state.set('userId', input.userId); - - // Called agents can access the same thread state - const result = await processingAgent.run(input); - - // All agents share sessionId - ctx.logger.info('Processing complete', { sessionId: ctx.sessionId }); - - return result; - }, -}); -``` - -Use this for tracking context, sharing auth data, and maintaining conversation state. - -## Full Example - -A customer support router that combines multiple patterns: conditional routing, graceful degradation, and background analytics. - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateObject } from 'ai'; -import { groq } from '@ai-sdk/groq'; -import { z } from 'zod'; -import supportAgent from '@agent/support/agent'; -import salesAgent from '@agent/sales/agent'; -import billingAgent from '@agent/billing/agent'; -import generalAgent from '@agent/general/agent'; -import analyticsAgent from '@agent/analytics/agent'; - -const IntentSchema = z.object({ - agentType: z.enum(['support', 'sales', 'billing', 'general']), - confidence: z.number().min(0).max(1), - reasoning: z.string(), -}); - -const router = createAgent('Customer Router', { - schema: { - input: z.object({ message: z.string() }), - output: z.object({ - response: z.string(), - handledBy: z.string(), - }), - }, - handler: async (ctx, input) => { - let intent: z.infer; - let handledBy = 'general'; - - // Classify intent with graceful degradation - try { - const result = await generateObject({ - model: groq('llama-3.3-70b-versatile'), - schema: IntentSchema, - system: 'Classify the customer message by intent.', - prompt: input.message, - temperature: 0, - }); - intent = result.object; - - ctx.logger.info('Intent classified', { - type: intent.agentType, - confidence: intent.confidence, - }); - } catch (error) { - // Fallback to general agent if classification fails - ctx.logger.warn('Classification failed, using fallback', { - error: error instanceof Error ? error.message : String(error), - }); - intent = { agentType: 'general', confidence: 0, reasoning: 'fallback' }; - } - - // Route to specialist agent - let response: string; - try { - switch (intent.agentType) { - case 'support': - const supportResult = await supportAgent.run({ - message: input.message, - context: intent.reasoning, - }); - response = supportResult.response; - handledBy = 'support'; - break; - - case 'sales': - const salesResult = await salesAgent.run({ - message: input.message, - context: intent.reasoning, - }); - response = salesResult.response; - handledBy = 'sales'; - break; - - case 'billing': - const billingResult = await billingAgent.run({ - message: input.message, - context: intent.reasoning, - }); - response = billingResult.response; - handledBy = 'billing'; - break; - - default: - const generalResult = await generalAgent.run({ - message: input.message, - }); - response = generalResult.response; - handledBy = 'general'; - } - } catch (error) { - ctx.logger.error('Specialist agent failed', { error, intent }); - response = 'I apologize, but I encountered an issue. Please try again.'; - handledBy = 'error'; - } - - // Log analytics in background (doesn't block response) - ctx.waitUntil(async () => { - await analyticsAgent.run({ - event: 'customer_interaction', - intent: intent.agentType, - confidence: intent.confidence, - handledBy, - sessionId: ctx.sessionId, - }); - }); - - return { response, handledBy }; - }, -}); - -export default router; -``` - -This example combines several patterns: -- Use an LLM to classify intent and route to specialist agents -- Handle failures gracefully: fallback to general agent if classification fails, friendly error message if specialists fail -- Log analytics in the background with `waitUntil()` so the response isn't delayed - -## Next Steps - -- [State Management](/agents/state-management): Share data across agent calls with thread and session state -- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Add quality checks to your agent workflows diff --git a/docs/src/web/content/agents/creating-agents.mdx b/docs/src/web/content/agents/creating-agents.mdx deleted file mode 100644 index 6a7b6f0da..000000000 --- a/docs/src/web/content/agents/creating-agents.mdx +++ /dev/null @@ -1,361 +0,0 @@ ---- -title: Creating Agents -description: Build agents with createAgent(), schemas, and handlers ---- - -Each agent encapsulates a handler function, input/output validation, and metadata in a single unit that can be invoked from routes, other agents, or scheduled tasks. - -## Basic Agent - -Create an agent with `createAgent()`, providing a name and handler function: - -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('Greeter', { - handler: async (ctx) => { - ctx.logger.info('Processing request'); - return { message: 'Hello from agent!' }; - }, -}); - -export default agent; -``` - -For the complete `createAgent()` signature and all configuration options, see the [Agents Reference](/reference/sdk-reference/agents). - -The handler always receives `ctx`, the agent context with logging, storage, and state management. When you define `schema.input`, the handler receives validated `input` as its second parameter. - - -In agents, access services directly on `ctx`: `ctx.logger`, `ctx.kv`, `ctx.thread`, etc. - -In routes, use [Hono's context](https://hono.dev/docs/api/context): `c.var.logger` or `c.get('logger')`, `c.var.kv`, `c.var.thread`, etc. - - -## Adding LLM Capabilities - -Most agents use an LLM for inference. Here's an agent using the [AI SDK](https://ai-sdk.dev): - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('Assistant', { - schema: { - input: s.object({ prompt: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, { prompt }) => { - const { text } = await generateText({ - model: openai('gpt-5-mini'), - prompt, - }); - - return { response: text }; - }, -}); - -export default agent; -``` - -You can also use provider SDKs directly: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import OpenAI from 'openai'; - -const client = new OpenAI(); - -const agent = createAgent('Assistant', { - description: 'An agent using OpenAI SDK directly', - schema: { - input: s.object({ prompt: s.string() }), - output: s.string(), - }, - handler: async (ctx, { prompt }) => { - const completion = await client.chat.completions.create({ - model: 'gpt-5-mini', - messages: [{ role: 'user', content: prompt }], - }); - - return completion.choices[0]?.message?.content ?? ''; - }, -}); - -export default agent; -``` - -Or use Groq for fast inference with open-source models: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; -import Groq from 'groq-sdk'; - -const client = new Groq(); - -const agent = createAgent('Assistant', { - description: 'An agent using Groq SDK with open-source models', - schema: { - input: s.object({ prompt: s.string() }), - output: s.string(), - }, - handler: async (ctx, { prompt }) => { - const completion = await client.chat.completions.create({ - model: 'llama-3.3-70b-versatile', - messages: [{ role: 'user', content: prompt }], - }); - - return completion.choices[0]?.message?.content ?? ''; - }, -}); - -export default agent; -``` - -For streaming, structured output, and more patterns, see [Using the AI SDK](/agents/ai-sdk-integration). - -## Adding Schema Validation - -Define input and output schemas for type safety and runtime validation. Agentuity includes a lightweight built-in schema library: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('Contact Form', { - schema: { - input: s.object({ - email: s.string(), - message: s.string(), - }), - output: s.object({ - success: s.boolean(), - id: s.string(), - }), - }, - handler: async (ctx, input) => { - // input is typed as { email: string, message: string } - ctx.logger.info('Received message', { from: input.email }); - - return { - success: true, - id: crypto.randomUUID(), - }; - }, -}); - -export default agent; -``` - -You can also use [Zod](https://zod.dev) for more advanced validation: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { z } from 'zod'; - -const agent = createAgent('Contact Form', { - schema: { - input: z.object({ - email: z.string().email(), - message: z.string().min(1), - }), - output: z.object({ - success: z.boolean(), - id: z.string(), - }), - }, - handler: async (ctx, input) => { - ctx.logger.info('Received message', { from: input.email }); - - return { - success: true, - id: crypto.randomUUID(), - }; - }, -}); - -export default agent; -``` - -**Validation behavior:** -- Input is validated before the handler runs -- Output is validated before returning to the caller -- Invalid data throws an error with details about what failed - - -- **`@agentuity/schema`** — Lightweight, built-in, zero dependencies -- **[Zod](https://zod.dev)** — Popular, feature-rich, great ecosystem -- **[Valibot](https://valibot.dev)** — Tiny bundle size, tree-shakeable -- **[ArkType](https://arktype.io)** — TypeScript-native syntax - -All implement [StandardSchema](https://github.com/standard-schema/standard-schema). See [Schema Libraries](/agents/schema-libraries) for detailed examples. - - -### Type Inference - -TypeScript automatically infers types from your schemas. Don't add explicit type annotations to handler parameters: - -```typescript -// Good: types inferred from schema -handler: async (ctx, input) => { ... } - -// Bad: explicit types can cause issues -handler: async (ctx: AgentContext, input: MyInput) => { ... } -``` - -```typescript -const agent = createAgent('Search', { - schema: { - input: z.object({ - query: z.string(), - filters: z.object({ - category: z.enum(['tech', 'business', 'sports']), - limit: z.number().default(10), - }), - }), - output: z.object({ - results: z.array(z.string()), - total: z.number(), - }), - }, - handler: async (ctx, input) => { - // Full autocomplete for input.query and input.filters.category - const category = input.filters.category; // type: 'tech' | 'business' | 'sports' - - return { - results: ['result1', 'result2'], - total: 2, - }; - }, -}); -``` - -### Common Zod Patterns - -```typescript -z.object({ - // Strings - name: z.string().min(1).max(100), - email: z.string().email(), - url: z.string().url().optional(), - - // Numbers - age: z.number().min(0).max(120), - score: z.number().min(0).max(1), - - // Enums and literals - status: z.enum(['active', 'pending', 'complete']), - type: z.literal('user'), - - // Arrays and nested objects - tags: z.array(z.string()), - metadata: z.object({ - createdAt: z.date(), - version: z.number(), - }).optional(), - - // Defaults - limit: z.number().default(10), -}) -``` - -### Schema Descriptions for AI - -When using `generateObject()` from the AI SDK, add `.describe()` to help the LLM understand each field: - -```typescript -z.object({ - title: z.string().describe('Event title, concise, without names'), - startTime: z.string().describe('Start time in HH:MM format (e.g., 14:00)'), - priority: z.enum(['low', 'medium', 'high']).describe('Urgency level'), -}) -``` - -Call `.describe()` at the end of the chain: schema methods like `.min()` return new instances that don't inherit metadata. - -## Handler Context - -The handler context (`ctx`) provides access to Agentuity services: - -```typescript -handler: async (ctx, input) => { - // Logging (Remember: always use ctx.logger, not console.log) - ctx.logger.info('Processing', { data: input }); - ctx.logger.error('Something failed', { error }); - - // Identifiers - ctx.sessionId; // Unique per request (sess_...) - ctx.thread.id; // Conversation context (thrd_...) - ctx.current.name; // This agent's name - ctx.current.agentId; // Stable ID for namespacing state keys - - // State management - ctx.state.set('key', value); // Request-scoped (sync, cleared after response) - await ctx.thread.state.set('key', value); // Thread-scoped (async, up to 1 hour) - ctx.session.state.set('key', value); // Session-scoped - - // Storage - await ctx.kv.set('bucket', 'key', data); - await ctx.vector.search('namespace', { query: 'text' }); - - // Background tasks - ctx.waitUntil(async () => { - await ctx.kv.set('analytics', 'event', { timestamp: Date.now() }); - }); - - return { result }; -} -``` - -For detailed state management patterns, see [Managing State](/agents/state-management). - -## Agent Name and Description - -Every agent requires a name (first argument) and can include an optional description: - -```typescript -const agent = createAgent('Support Ticket Analyzer', { - description: 'Analyzes support tickets and extracts key information', - schema: { ... }, - handler: async (ctx, input) => { ... }, -}); -``` - -The name shows up in logs and the Agentuity console. Keep it short and specific so the agent is easy to spot when you have several running at once. - -## Best Practices - -- **Single responsibility**: Each agent should have one clear purpose -- **Always define schemas**: Schemas provide type safety and serve as documentation -- **Handle errors gracefully**: Wrap external calls in try-catch blocks -- **Keep handlers focused**: Move complex logic to helper functions - -```typescript -import processor from '@agent/processor/agent'; - -// Good: Clear, focused handler -handler: async (ctx, input) => { - try { - const enriched = await enrichData(input.data); - const result = await processor.run(enriched); - return { success: true, result }; - } catch (error) { - ctx.logger.error('Processing failed', { error }); - return { success: false, error: 'Processing failed' }; - } -} -``` - -## Next Steps - - -The [OpenCode plugin](/reference/cli/opencode-plugin) provides AI-assisted development for full-stack Agentuity projects, including agents, routes, frontend, and deployment. - - -- [Using the AI SDK](/agents/ai-sdk-integration): Add LLM capabilities with generateText and streamText -- [Managing State](/agents/state-management): Persist data across requests with thread and session state -- [Calling Other Agents](/agents/calling-other-agents): Build multi-agent workflows diff --git a/docs/src/web/content/agents/events-lifecycle.mdx b/docs/src/web/content/agents/events-lifecycle.mdx deleted file mode 100644 index cd63ce980..000000000 --- a/docs/src/web/content/agents/events-lifecycle.mdx +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: Events & Lifecycle -description: Lifecycle hooks for monitoring and extending agent behavior ---- - -Events give you lifecycle hooks for observability and lightweight coordination. Use them for logging, metrics, analytics, and cleanup that should stay close to the runtime. - -## Agent Events - -Track individual agent execution with `started`, `completed`, and `errored` events: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('TaskProcessor', { - schema: { - input: s.object({ task: s.string() }), - output: s.object({ result: s.string() }), - }, - handler: async (ctx, input) => { - ctx.logger.info('Processing task', { task: input.task }); - return { result: `Completed: ${input.task}` }; - }, -}); - -// Track execution timing -agent.addEventListener('started', (event, agent, ctx) => { - ctx.state.set('startTime', Date.now()); - ctx.logger.info('Agent started', { agent: agent.metadata.name }); -}); - -agent.addEventListener('completed', (event, agent, ctx) => { - const startTime = ctx.state.get('startTime'); - const duration = typeof startTime === 'number' ? Date.now() - startTime : 0; - - ctx.logger.info('Agent completed', { - agent: agent.metadata.name, - durationMs: duration, - }); - - // Warn on slow executions - if (duration > 1000) { - ctx.logger.warn('Slow execution detected', { duration, threshold: 1000 }); - } -}); - -agent.addEventListener('errored', (event, agent, ctx, error) => { - const startTime = ctx.state.get('startTime'); - const duration = typeof startTime === 'number' ? Date.now() - startTime : 0; - - ctx.logger.error('Agent failed', { - agent: agent.metadata.name, - error: error.message, - durationMs: duration, - }); -}); - -export default agent; -``` - -Agent listeners receive the event name, agent instance, context, and for `errored`, the thrown error. - -## App-Level Events - -Monitor all agents, sessions, and threads globally with the top-level `addEventListener()` function. Register these listeners at module scope during startup. - -```typescript -import { addEventListener } from '@agentuity/runtime'; - -// Track all agent executions -addEventListener('agent.started', (event, agent, ctx) => { - ctx.logger.info('Agent execution started', { - agent: agent.metadata.name, - sessionId: ctx.sessionId, - }); -}); - -addEventListener('agent.completed', (event, agent, ctx) => { - ctx.logger.info('Agent execution completed', { - agent: agent.metadata.name, - sessionId: ctx.sessionId, - }); -}); - -addEventListener('agent.errored', (event, agent, ctx, error) => { - ctx.logger.error('Agent execution failed', { - agent: agent.metadata.name, - error: error.message, - sessionId: ctx.sessionId, - }); -}); -``` - -Agent events receive `(eventName, agent, ctx)` or `(eventName, agent, ctx, error)`. Session and thread events receive `(eventName, session)` or `(eventName, thread)`. - -### Available App Events - -| Event | Description | -|-------|-------------| -| `agent.started` | Any agent starts execution | -| `agent.completed` | Any agent completes successfully | -| `agent.errored` | Any agent throws an error | -| `session.started` | New session begins | -| `session.completed` | Session ends | -| `thread.created` | New thread created | -| `thread.destroyed` | Thread is explicitly destroyed or removed by the active provider | - -## App Startup and Shutdown - -For shared resources, prefer module initialization plus `registerShutdownHook()`: - -```typescript -import { createApp, registerShutdownHook } from '@agentuity/runtime'; -import api from './src/api/index'; -import agents from './src/agent'; -import { db } from './db'; - -registerShutdownHook(async () => { - await db.close(); -}); - -export default await createApp({ - router: { path: '/api', router: api }, - agents, -}); -``` - - -Agent-level `setup` and `shutdown` are still useful for agent-local config exposed through `ctx.config`. For shared app-wide dependencies, the current stable path is module-scoped initialization plus `registerShutdownHook()`, not new examples built around `ctx.app`. - - -## Agent Lifecycle Hooks - -Individual agents can also define `setup` and `shutdown` functions for agent-specific initialization: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const fetchValue = async (key: string): Promise => { - return `value:${key}`; -}; - -const agent = createAgent('CachedLookup', { - schema: { - input: s.object({ key: s.string() }), - output: s.object({ value: s.string() }), - }, - // Called once when the app starts, return value becomes ctx.config - setup: async () => { - const cache = new Map(); - return { cache }; - }, - // Called when the app shuts down - shutdown: async (_app, config) => { - config.cache.clear(); - }, - handler: async (ctx, input) => { - // ctx.config is typed from setup's return value - const cached = ctx.config.cache.get(input.key); - if (cached) { - return { value: cached }; - } - - // Fetch and cache - const value = await fetchValue(input.key); - ctx.config.cache.set(input.key, value); - return { value }; - }, -}); - -export default agent; -``` - -`setup` returns agent-specific config, exposed via `ctx.config`. Use it for agent-local caches, clients, or precomputed data that should stay scoped to that agent. - - -- **Module setup**: Shared resources like database pools, Redis clients, or service singletons -- **Agent setup**: Agent-local caches, preloaded models, or isolated clients exposed through `ctx.config` - - -## Shared State - -Event handlers on the same request can share data through `ctx.state`: - -```typescript -agent.addEventListener('started', (event, agent, ctx) => { - ctx.state.set('startTime', Date.now()); - ctx.state.set('metadata', { userId: '123', source: 'api' }); -}); - -agent.addEventListener('completed', (event, agent, ctx) => { - const startTime = ctx.state.get('startTime') as number; - const metadata = ctx.state.get('metadata') as Record; - - ctx.logger.info('Execution complete', { - duration: Date.now() - startTime, - ...metadata, - }); -}); -``` - - -Use `ctx.waitUntil()` in event handlers for non-blocking operations like sending metrics to external services: - -```typescript -agent.addEventListener('completed', (event, agent, ctx) => { - ctx.waitUntil(async () => { - await sendMetricsToExternalService({ agent: agent.metadata.name }); - }); -}); -``` - - -## Events and Output Review - -Use events for observability, logging, and side effects that belong close to execution. When you need a separate quality check after a response is complete, use a pattern like [LLM as a Judge](/cookbook/patterns/llm-as-a-judge). - -## Next Steps - -- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Add post-response quality checks -- [State Management](/agents/state-management): Thread and session state patterns -- [Calling Other Agents](/agents/calling-other-agents): Multi-agent coordination -- [Logging](/services/observability/logging): Structured logging for debugging events -- [Tracing](/services/observability/tracing): Track timing across event handlers diff --git a/docs/src/web/content/agents/index.mdx b/docs/src/web/content/agents/index.mdx deleted file mode 100644 index e0f021640..000000000 --- a/docs/src/web/content/agents/index.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Agents -description: Build and configure AI agents ---- - -import { Bot, Database, Sparkles, Cpu, Radio, Archive, Phone, Play, Calendar, GitCompare } from 'lucide-react'; - -Agents are the core building blocks of Agentuity. Each agent handles a specific task with schema validation, AI integration, and lifecycle hooks. - - - } - /> - } - /> - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - diff --git a/docs/src/web/content/agents/meta.json b/docs/src/web/content/agents/meta.json deleted file mode 100644 index c4cd89ec0..000000000 --- a/docs/src/web/content/agents/meta.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "Agents", - "pages": [ - "when-to-use", - "creating-agents", - "schema-libraries", - "ai-gateway", - "ai-sdk-integration", - "streaming-responses", - "state-management", - "calling-other-agents", - "standalone-execution", - "events-lifecycle" - ] -} diff --git a/docs/src/web/content/agents/schema-libraries.mdx b/docs/src/web/content/agents/schema-libraries.mdx deleted file mode 100644 index e2f89f083..000000000 --- a/docs/src/web/content/agents/schema-libraries.mdx +++ /dev/null @@ -1,325 +0,0 @@ ---- -title: Schema Libraries -description: Choose from built-in, Zod, Valibot, or ArkType for validation ---- - -Agentuity uses the [StandardSchema](https://github.com/standard-schema/standard-schema) interface for validation, which means you can use any compatible library. - -## Choosing a Library - -| Library | Syntax | Best For | -|---------|--------|----------| -| `@agentuity/schema` | `s.object({ name: s.string() })` | Lightweight validation without extra dependencies | -| [Zod](https://zod.dev) | `z.object({ name: z.string() })` | AI SDK integration (`.describe()` support) | -| [Valibot](https://valibot.dev) | `v.object({ name: v.string() })` | Smallest bundle size | -| [ArkType](https://arktype.io) | `type({ name: 'string' })` | TypeScript-native syntax, fast runtime | - -**Recommendation:** Use `@agentuity/schema` when you want a lightweight schema library for straightforward validation. It is not trying to replace Zod. Reach for Zod, Valibot, or ArkType when you need their broader validation, transformation, metadata, or ecosystem features. For example, Zod is useful with AI SDK structured output because `.describe()` adds field hints for the model. - - -For the complete `@agentuity/schema` API including coercion helpers and JSON Schema conversion, see [Schema Validation](/reference/sdk-reference/schema). - - -## Using @agentuity/schema - -The built-in schema library is a lightweight option for straightforward validation. - -### Basic Schema - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('UserProcessor', { - schema: { - input: s.object({ - email: s.string(), - age: s.number(), - role: s.string(), - }), - output: s.object({ - success: s.boolean(), - userId: s.string(), - }), - }, - handler: async (ctx, input) => { - ctx.logger.info('Processing user', { email: input.email }); - return { - success: true, - userId: crypto.randomUUID(), - }; - }, -}); - -export default agent; -``` - -### Common Patterns - -```typescript -import { s } from '@agentuity/schema'; - -// Primitives -s.string() // Any string -s.number() // Any number -s.boolean() // Boolean -s.null() // Null value -s.undefined() // Undefined value -s.any() // Any value (no validation) -s.unknown() // Unknown value (safer any) - -// Objects and arrays -s.object({ name: s.string() }) // Object shape -s.array(s.string()) // Array of strings -s.record(s.string(), s.number()) // { [key: string]: number } - -// Optional and nullable -s.optional(s.string()) // string | undefined -s.nullable(s.string()) // string | null - -// Unions, literals, and enums -s.union(s.string(), s.number()) // string | number -s.literal('active') // Exact value -s.enum(['admin', 'user', 'guest']) // One of these values - -// Type coercion (useful for form inputs, query params) -s.coerce.string() // Convert to string via String(value) -s.coerce.number() // Convert to number via Number(value) -s.coerce.boolean() // Convert to boolean via Boolean(value) -s.coerce.date() // Convert to Date via new Date(value) -``` - -### Type Inference - -Extract TypeScript types from schemas: - -```typescript -import { s } from '@agentuity/schema'; - -const User = s.object({ - name: s.string(), - age: s.number(), - role: s.enum(['admin', 'user']), -}); - -// Extract the type -type User = s.infer; -// { name: string; age: number; role: 'admin' | 'user' } -``` - -### JSON Schema Generation - -Convert schemas to JSON Schema for use with LLM structured output: - -```typescript -import { s } from '@agentuity/schema'; - -const ResponseSchema = s.object({ - answer: s.string(), - confidence: s.number(), -}); - -// Generate JSON Schema -const jsonSchema = s.toJSONSchema(ResponseSchema); - -// Generate strict JSON Schema for LLM structured output -const strictSchema = s.toJSONSchema(ResponseSchema, { strict: true }); -``` - - -Use `{ strict: true }` when generating schemas for LLM structured output (e.g., OpenAI's `response_format`). Strict mode ensures the schema is compatible with model constraints and produces more reliable outputs. - - - -Use `@agentuity/schema` when you want a small validation layer for common object, array, union, enum, literal, and coercion cases. Use Zod, Valibot, or ArkType when you need deeper validation rules, transformations, metadata, or ecosystem integrations. - - -## Using Valibot - -### Installation - -```bash -bun add valibot -``` - -### Basic Schema - -```typescript -import { createAgent } from '@agentuity/runtime'; -import * as v from 'valibot'; - -const InputSchema = v.object({ - email: v.pipe(v.string(), v.email()), - age: v.pipe(v.number(), v.minValue(0), v.maxValue(120)), - role: v.picklist(['admin', 'user', 'guest']), -}); - -const OutputSchema = v.object({ - success: v.boolean(), - userId: v.string(), -}); - -const agent = createAgent('UserProcessor', { - schema: { - input: InputSchema, - output: OutputSchema, - }, - handler: async (ctx, input) => { - ctx.logger.info('Processing user', { email: input.email, role: input.role }); - return { - success: true, - userId: crypto.randomUUID(), - }; - }, -}); - -export default agent; -``` - -### Common Patterns - -```typescript -import * as v from 'valibot'; - -// Strings -v.string() // Any string -v.pipe(v.string(), v.minLength(5)) // Minimum length -v.pipe(v.string(), v.maxLength(100)) // Maximum length -v.pipe(v.string(), v.email()) // Email format -v.pipe(v.string(), v.url()) // URL format - -// Numbers -v.number() // Any number -v.pipe(v.number(), v.minValue(0)) // Minimum value -v.pipe(v.number(), v.maxValue(100)) // Maximum value -v.pipe(v.number(), v.integer()) // Integer only - -// Arrays and objects -v.array(v.string()) // Array of strings -v.object({ name: v.string() }) // Object shape - -// Optional and nullable -v.optional(v.string()) // string | undefined -v.nullable(v.string()) // string | null -v.nullish(v.string()) // string | null | undefined - -// Enums -v.picklist(['a', 'b', 'c']) // One of these values -v.literal('exact') // Exact value - -// Defaults -v.optional(v.string(), 'default') // With default value -``` - -## Using ArkType - -### Installation - -```bash -bun add arktype -``` - -### Basic Schema - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { type } from 'arktype'; - -const InputSchema = type({ - email: 'string.email', - age: '0 <= number <= 120', - role: '"admin"|"user"|"guest"', -}); - -const OutputSchema = type({ - success: 'boolean', - userId: 'string', -}); - -const agent = createAgent('UserProcessor', { - schema: { - input: InputSchema, - output: OutputSchema, - }, - handler: async (ctx, input) => { - ctx.logger.info('Processing user', { email: input.email, role: input.role }); - return { - success: true, - userId: crypto.randomUUID(), - }; - }, -}); - -export default agent; -``` - -### Common Patterns - -```typescript -import { type } from 'arktype'; - -// Strings -type('string') // Any string -type('string > 5') // Minimum length (greater than 5 chars) -type('string < 100') // Maximum length -type('string.email') // Email format -type('string.url') // URL format - -// Numbers -type('number') // Any number -type('number > 0') // Greater than 0 -type('number <= 100') // Less than or equal to 100 -type('number.integer') // Integer only -type('0 <= number <= 120') // Range (0 to 120) - -// Arrays and objects -type('string[]') // Array of strings -type({ name: 'string' }) // Object shape - -// Optional properties and unions -type({ 'nickname?': 'string' }) // Optional object property -type('string | null') // string | null - -// Enums and literals -type('"a"|"b"|"c"') // One of these values -type('"exact"') // Exact value - -// Nested objects -type({ - user: { - name: 'string', - email: 'string.email', - }, - tags: 'string[]?', -}) -``` - -## Migrating Between Libraries - -Schemas are interchangeable in `createAgent()`. The same agent structure works with any StandardSchema library: - -```typescript -// With @agentuity/schema (built-in) -import { s } from '@agentuity/schema'; -const schema = { input: s.object({ name: s.string() }) }; - -// With Zod -import { z } from 'zod'; -const schema = { input: z.object({ name: z.string() }) }; - -// With Valibot -import * as v from 'valibot'; -const schema = { input: v.object({ name: v.string() }) }; - -// With ArkType -import { type } from 'arktype'; -const schema = { input: type({ name: 'string' }) }; - -// All work the same way in createAgent() -createAgent('MyAgent', { schema, handler: async (ctx, input) => { ... } }); -``` - -## Next Steps - -- [Creating Agents](/agents/creating-agents): Full guide to agent creation with Zod examples -- [Using the AI SDK](/agents/ai-sdk-integration): Add LLM capabilities to your agents diff --git a/docs/src/web/content/agents/standalone-execution.mdx b/docs/src/web/content/agents/standalone-execution.mdx deleted file mode 100644 index bdf8ad43b..000000000 --- a/docs/src/web/content/agents/standalone-execution.mdx +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: Running Agents Without HTTP -short_title: Standalone Execution -description: Execute agents programmatically for cron jobs, bots, CLI tools, and background workers ---- - -Sometimes your agent logic needs to run without an incoming HTTP request. `createAgentContext()` gives standalone code the same infrastructure that HTTP handlers get automatically: tracing, sessions, and storage access. - - -`createAgentContext()` works in standalone scripts, workers, and external backends. If you call it before `createApp()`, Agentuity starts a minimal standalone runtime automatically. Call `createApp()` first when you want your full app config, providers, or cloud-connected services loaded. - - -## Basic Usage - -```typescript -import { createAgentContext } from '@agentuity/runtime'; -import chatAgent from '@agent/chat/agent'; - -const ctx = createAgentContext(); -const result = await ctx.run(chatAgent, { message: 'Hello' }); -``` - -The `run()` method executes your agent with full infrastructure support: tracing, session management, and access to all storage services. - -For agents that don't require input: - -```typescript -const result = await ctx.run(statusAgent); -``` - - -`ctx.run(agent, input)` is a convenience wrapper around `ctx.invoke()`. Use `ctx.run()` for single agent calls; use `ctx.invoke()` when you need to run arbitrary functions or multiple agents in a single invocation. See [Using invoke() Directly](#using-invoke-directly) below. - - -## Options - -| Option | Type | Description | -|--------|------|-------------| -| `sessionId` | `string` | Custom session ID. Auto-generated from trace context if not provided | -| `trigger` | `string` | Trigger type for telemetry: `'discord'`, `'cron'`, `'websocket'`, `'manual'` | -| `parentContext` | `Context` | Parent OpenTelemetry context for distributed tracing | - -By default, each standalone invocation creates a fresh thread and restores or creates a session from the session ID. Pass `sessionId` when an external system needs a stable session identifier for a run. - -## External Cron Job Example - -For scheduled tasks managed outside of Agentuity: - -```typescript -import { createApp, createAgentContext } from '@agentuity/runtime'; -import cron from 'node-cron'; -import cleanupAgent from '@agent/cleanup/agent'; - -await createApp(); - -// Run cleanup every hour -cron.schedule('0 * * * *', async () => { - const ctx = createAgentContext({ trigger: 'cron' }); - await ctx.run(cleanupAgent, { task: 'expired-sessions' }); -}); -``` - - -For most scheduled tasks, use the [`cron()` middleware](/routes/cron) instead. It handles infrastructure automatically without needing `createAgentContext`. Use standalone execution only when you need external cron management. - - -## Multiple Agents in Sequence - -Run multiple agents in sequence with the same context: - -```typescript -const ctx = createAgentContext(); - -// First agent analyzes the input -const analysis = await ctx.run(analyzeAgent, { text: userInput }); - -// Second agent generates response based on analysis -const response = await ctx.run(respondAgent, { - analysis: analysis.summary, - sentiment: analysis.sentiment, -}); -``` - -Each `ctx.run()` call gets its own session and tracing span, while sharing the logger, tracer, and app state. - -## Reusing Contexts - -Create a context once and reuse it for multiple invocations: - -```typescript -const ctx = createAgentContext({ trigger: 'websocket' }); - -// Each run gets its own session and tracing span -websocket.on('message', async (data) => { - const result = await ctx.run(messageAgent, data); - websocket.send(result); -}); -``` - -## Using invoke() Directly - -`ctx.run()` is syntactic sugar over `ctx.invoke()`. Use `invoke()` when you need more control: - -- Run arbitrary async functions (not just agents) within agent context -- Execute multiple agents in a single invocation with a shared session and tracing span -- Set a custom OpenTelemetry span name - -### Running Arbitrary Functions - -```typescript -const ctx = createAgentContext({ trigger: 'cron' }); - -await ctx.invoke(async () => { - // Any async code runs with full agent infrastructure - const data = await ctx.kv.get('config', 'settings'); - await ctx.stream.create('audit', { - contentType: 'text/plain', - }); - ctx.logger.info('Cron task complete'); -}); -``` - -### Multiple Agents in One Invocation - -When agents share a session and tracing span: - -```typescript -const ctx = createAgentContext(); - -const result = await ctx.invoke(async () => { - const analysis = await analyzeAgent.run({ text: userInput }); - const response = await respondAgent.run({ - analysis: analysis.summary, - }); - return response; -}); -``` - -Both agent calls share the same OpenTelemetry span, session, and thread. Compare this with calling `ctx.run()` twice, where each call gets its own session and span. - -### Custom Span Names - -```typescript -const result = await ctx.invoke(() => agent.run(input), { spanName: 'daily-cleanup' }); -``` - -### invoke() Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `spanName` | `string` | `'agent-invocation'` | Span name for this invocation | - -## Invocation Lifecycle - -Each call to `ctx.run()` or `ctx.invoke()` follows this lifecycle: - -1. **Create tracing span**: An OpenTelemetry span is created within the parent context. Trace state includes project, org, and deployment IDs for distributed tracing. -2. **Create conversation state**: A fresh thread is created and the session is restored or created using the session ID. -3. **Fire session start event**: A `start` event is sent to the session event provider with the trigger type, session ID, thread ID, and deployment metadata. -4. **Execute function**: Your function runs within `AsyncLocalStorage` context, giving it access to `ctx.kv`, `ctx.stream`, `ctx.vector`, `ctx.logger`, and `waitUntil`. -5. **Handle background tasks**: If `ctx.waitUntil()` callbacks are pending, they continue after your function returns. -6. **Save session and thread**: Session and thread state are persisted via their providers. When `waitUntil()` tasks are pending, this happens after those tasks finish. -7. **Fire session complete event**: A `complete` event is sent with the final status code, agent IDs that participated, and any user data. When `waitUntil()` tasks are pending, this happens after those tasks finish. - -Errors during execution set the span status to `ERROR` and include the error message in the session complete event. Errors from `waitUntil()` tasks are recorded on the span and reported in the eventual session complete event. - -## What's Included - - -Each `ctx.run()` or `ctx.invoke()` call provides the same infrastructure as HTTP request handlers: - -- **Tracing**: OpenTelemetry spans with proper hierarchy -- **Sessions**: Automatic save/restore via providers -- **Threads**: Conversation state management -- **Storage**: Full access to `kv`, `stream`, `vector` -- **Background tasks**: `waitUntil` support for fire-and-forget work -- **Session events**: Start/complete events for observability - - -## Detecting Context - -Use `inAgentContext()` to check if code is running inside an agent handler with ambient context available: - -```typescript -import { inAgentContext, createAgentContext } from '@agentuity/runtime'; -import myAgent from '@agent/my-agent/agent'; - -async function processRequest(data: unknown) { - if (inAgentContext()) { - // Inside an agent handler, ambient context is available - // so agent.run() works directly without explicit context - return myAgent.run(data); - } - - // Outside agent handler, create context first - const ctx = createAgentContext(); - return ctx.run(myAgent, data); -} -``` - -This is useful for writing utility functions that work both inside agent handlers and in standalone scripts. - -To check if the Agentuity runtime is initialized (but not necessarily inside a handler), use `isInsideAgentRuntime()` instead. - -## Next Steps - -- [Calling Other Agents](/agents/calling-other-agents): Agent-to-agent communication patterns -- [Cron Routes](/routes/cron): Built-in scheduled task support -- [State Management](/agents/state-management): Thread and session state diff --git a/docs/src/web/content/agents/state-management.mdx b/docs/src/web/content/agents/state-management.mdx deleted file mode 100644 index 995910df3..000000000 --- a/docs/src/web/content/agents/state-management.mdx +++ /dev/null @@ -1,505 +0,0 @@ ---- -title: Managing State -short_title: State Management -description: Request and thread state for stateful agents ---- - -Agentuity gives you three closely related state surfaces: - -- **`ctx.state`** for temporary calculations within a single handler execution -- **`ctx.session.state`** for request-scoped session data -- **`ctx.thread.state`** for conversation context that persists across requests - - -Thread state uses async lazy-loading for performance. State is only fetched when first accessed, eliminating latency for requests that don't use thread state. All `ctx.thread.state` methods are async and require `await`. - - -## State Scopes - -| Scope | Lifetime | Cleared When | Access | Example Use Case | -|-------|----------|--------------|--------|----------| -| Request | Single handler execution | After response sent | `ctx.state` | Timing, temp calculations | -| Session | Single request | After response sent | `ctx.session.state` | Data needed in `session.completed` listeners | -| Thread | Multiple requests | Inactivity expiration or `destroy()` | `ctx.thread.state` | Conversation history | - - -Think of a *thread* as the conversation and a *session* as one request within that conversation. Each request creates a new session, but sessions within the same conversation share a thread. - - - -Routes have the same state access via `c.var.thread` and `c.var.session`. Use `c.var.thread.state` for conversation context and `c.var.session.state` for request-scoped data. See [HTTP Routes](/routes/http#request-context) for route examples. - - -### Quick Example - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('StateDemo', { - schema: { - input: s.object({ message: s.string() }), - output: s.object({ - response: s.string(), - requestTime: s.number(), - messageCount: s.number(), - }), - }, - handler: async (ctx, input) => { - // REQUEST STATE: Cleared after this response (sync) - const startTime = Date.now(); - ctx.state.set('startTime', startTime); - - // THREAD STATE: Persists across requests (async, up to 1 hour) - const messages = (await ctx.thread.state.get('messages')) || []; - messages.push(input.message); - await ctx.thread.state.set('messages', messages); - - const requestTime = Date.now() - startTime; - - return { - response: `Received: ${input.message}`, - requestTime, - messageCount: messages.length, - }; - }, -}); - -export default agent; -``` - -## Request State - -Request state (`ctx.state`) holds temporary data within a single request. It's cleared automatically after the response is sent. - -```typescript -handler: async (ctx, input) => { - // Track timing - ctx.state.set('startTime', Date.now()); - - // Process request - const result = await processData(input); - - // Use the timing data - const duration = Date.now() - (ctx.state.get('startTime') as number); - ctx.logger.info('Request completed', { durationMs: duration }); - - return result; -} -``` - -**Use cases:** Request timing, temporary calculations, passing data between event listeners. - -## Thread State - -Thread state (`ctx.thread.state`) persists across multiple requests within a conversation, expiring after 1 hour of inactivity. Thread identity is managed automatically via cookies (or the `x-thread-id` header for API clients). - -### Conversation Memory - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import * as v from 'valibot'; - -interface Message { - role: 'user' | 'assistant'; - content: string; -} - -const agent = createAgent('ConversationMemory', { - schema: { - input: v.object({ message: v.string() }), - output: v.string(), - }, - handler: async (ctx, input) => { - // Initialize on first request - if (!(await ctx.thread.state.has('messages'))) { - await ctx.thread.state.set('messages', []); - await ctx.thread.state.set('turnCount', 0); - } - - const messages = await ctx.thread.state.get('messages') || []; - const turnCount = await ctx.thread.state.get('turnCount') || 0; - - // Add user message - messages.push({ role: 'user', content: input.message }); - - // Generate response with conversation context - const { text } = await generateText({ - model: openai('gpt-5-mini'), - system: 'You are a helpful assistant. Reference previous messages when relevant.', - messages, - }); - - // Update thread state - messages.push({ role: 'assistant', content: text }); - await ctx.thread.state.set('messages', messages); - await ctx.thread.state.set('turnCount', turnCount + 1); - - ctx.logger.info('Conversation turn', { - threadId: ctx.thread.id, - turnCount: turnCount + 1, - }); - - return text; - }, -}); - -export default agent; -``` - -### Thread Properties and Methods - -```typescript -ctx.thread.id; // Thread ID (thrd_...) - -// All state methods are async -await ctx.thread.state.set('key', value); -await ctx.thread.state.get('key'); -await ctx.thread.state.has('key'); -await ctx.thread.state.delete('key'); -await ctx.thread.state.clear(); - -// Array operations with optional sliding window -await ctx.thread.state.push('messages', newMessage); -await ctx.thread.state.push('messages', newMessage, 100); // Keep last 100 - -// Bulk access (returns arrays, not iterators) -const keys = await ctx.thread.state.keys(); // string[] -const values = await ctx.thread.state.values(); // Message[] -const entries = await ctx.thread.state.entries(); // [string, Message][] -const count = await ctx.thread.state.size(); // number - -// State status -ctx.thread.state.loaded; // Has state been fetched? -ctx.thread.state.dirty; // Are there pending changes? - -// Reset the conversation -await ctx.thread.destroy(); -``` - -### Resetting a Conversation - -Call `ctx.thread.destroy()` to clear all thread state and start fresh: - -```typescript -handler: async (ctx, input) => { - if (input.command === 'reset') { - await ctx.thread.destroy(); - return 'Conversation reset. Thread state cleared.'; - } - - // Continue conversation -} -``` - -## ctx.state vs ctx.session.state - -Both `ctx.state` and `ctx.session.state` are request-scoped and reset after each request. The difference: - -- **`ctx.state`**: General request state, accessible in agent event listeners and other background hooks -- **`ctx.session.state`**: Accessible via `session` in completion event callbacks - -For most use cases, use `ctx.state`. Use `ctx.session.state` only when you need data in session completion events. See [Events & Lifecycle](/agents/events-lifecycle) for more on event handlers. - -```typescript -handler: async (ctx, input) => { - ctx.state.set('startTime', Date.now()); - ctx.session.state.set('requestType', 'chat'); - - ctx.session.addEventListener('completed', (eventName, session) => { - const requestType = session.state.get('requestType') as string | undefined; - ctx.logger.info('Session completed', { - requestType, - sessionId: session.id, - }); - }); - - return { ok: true, message: input.message }; -} -``` - - -Neither `ctx.state` nor `ctx.session.state` persist across requests. For data that should survive across requests, use `ctx.thread.state` (up to 1 hour) or [KV storage](/services/storage/key-value) (durable). - - -## Persisting to Storage - -In-memory state is lost on server restart. For durability, combine state management with KV storage: - -### Load → Cache → Save Pattern - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import * as v from 'valibot'; - -type Message = { role: 'user' | 'assistant'; content: string }; - -const agent = createAgent('PersistentChat', { - schema: { - input: v.object({ message: v.string() }), - stream: true, - }, - handler: async (ctx, input) => { - const key = `chat_${ctx.thread.id}`; - let messages: Message[] = []; - - // Load from KV on first access in this thread - if (!(await ctx.thread.state.has('kvLoaded'))) { - const result = await ctx.kv.get('conversations', key); - if (result.exists) { - messages = result.data; - ctx.logger.info('Loaded conversation from KV', { messageCount: messages.length }); - } - await ctx.thread.state.set('messages', messages); - await ctx.thread.state.set('kvLoaded', true); - } else { - messages = await ctx.thread.state.get('messages') || []; - } - - // Add user message - messages.push({ role: 'user', content: input.message }); - - // Stream response - const result = streamText({ - model: openai('gpt-5-mini'), - messages, - }); - - // Save in background (non-blocking) - ctx.waitUntil(async () => { - const fullText = await result.text; - messages.push({ role: 'assistant', content: fullText }); - - // Keep last 20 messages to bound state size - const recentMessages = messages.slice(-20); - await ctx.thread.state.set('messages', recentMessages); - - // Persist to KV - await ctx.kv.set('conversations', key, recentMessages, { - ttl: 86400, // 24 hours - }); - }); - - return result.textStream; - }, -}); - -export default agent; -``` - -**Key points:** -- Load from KV once per thread, cache in thread state -- Use `ctx.waitUntil()` for non-blocking saves -- Bound state size to prevent unbounded growth - - -Thread state supports write-only operations without loading existing state. If you only call `set()`, `delete()`, or `push()` without any reads, the SDK batches these as a merge operation, avoiding the latency of fetching existing state. - - -## Thread Lifecycle - -Use `ctx.thread.destroy()` when you want to end a conversation immediately. That fires the thread's `destroyed` event and clears its persisted state. - -```typescript -ctx.thread.addEventListener('destroyed', async (eventName, thread) => { - ctx.logger.info('Thread destroyed', { threadId: thread.id }); -}); -``` - -Thread data also expires after inactivity based on the active thread provider. Treat the `destroyed` listener as the explicit teardown hook you control with `ctx.thread.destroy()`, not as the only place to archive important data. - -Similarly, track when sessions complete with `ctx.session.addEventListener('completed', ...)`. - -For app-level monitoring of all threads and sessions, see [Events & Lifecycle](/agents/events-lifecycle). - -## Thread vs Session IDs - -| ID | Format | Lifetime | Purpose | -|----|--------|----------|---------| -| Thread ID | `thrd_` | Up to 1 hour (shared across requests) | Group related requests into conversations | -| Session ID | `sess_` | Single request (unique per call) | Request tracing and analytics | - -```typescript -handler: async (ctx, input) => { - ctx.logger.info('Request received', { - threadId: ctx.thread.id, // Same across conversation - sessionId: ctx.sessionId, // Unique per request - }); - - return { threadId: ctx.thread.id, sessionId: ctx.sessionId }; -} -``` - -## Advanced: Custom Thread IDs - -By default, thread IDs are generated automatically and managed via signed cookies. For advanced use cases, you can provide custom thread ID logic using a `ThreadIDProvider`. - -**Use cases:** -- Integrating with external identity systems -- Multi-tenant applications where threads should be scoped to users -- Custom conversation management tied to your domain model - -### ThreadIDProvider Interface - -```typescript -import type { Context } from 'hono'; -import type { Env, AppState } from '@agentuity/runtime'; - -interface ThreadIDProvider { - getThreadId(appState: AppState, ctx: Context): string | Promise; -} -``` - -### Thread ID Format Requirements - -Custom thread IDs must follow these rules: - -| Requirement | Value | -|-------------|-------| -| Prefix | Must start with `thrd_` | -| Length | 32-64 characters total | -| Characters | After prefix: `[a-zA-Z0-9]` only | - -```typescript -// Valid thread IDs -'thrd_abc123def456789012345678901' // 32 chars - minimum -'thrd_' + 'a'.repeat(59) // 64 chars - maximum - -// Invalid thread IDs -'thrd_abc' // Too short -'thrd_abc-def-123' // Dashes not allowed -'thread_abc123' // Wrong prefix -``` - -### Custom Provider Example - -Create a provider that generates deterministic thread IDs based on authenticated users: - -```typescript -import { createApp, getThreadProvider } from '@agentuity/runtime'; -import type { ThreadIDProvider } from '@agentuity/runtime'; - -const userThreadProvider: ThreadIDProvider = { - getThreadId: async (appState, ctx) => { - const userId = ctx.req.header('x-user-id'); - - if (userId) { - // Create deterministic thread ID from user ID - const data = new TextEncoder().encode(userId); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hex = Array.from(new Uint8Array(hashBuffer)) - .map(b => b.toString(16).padStart(2, '0')) - .join('') - .slice(0, 27); - return `thrd_${hex}`; - } - - // Fall back to random ID for unauthenticated requests - const arr = new Uint8Array(16); - crypto.getRandomValues(arr); - return `thrd_${Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('')}`; - } -}; - -const app = await createApp({}); - -getThreadProvider().setThreadIDProvider(userThreadProvider); - -export default app; -``` - -`createApp()` initializes the runtime services, so call `getThreadProvider()` after `createApp()` returns and before exporting the app. - -### Default Thread ID Behavior - -The built-in `DefaultThreadIDProvider` handles thread IDs automatically: - -1. **Check header**: Looks for signed `x-thread-id` header -2. **Check cookie**: Falls back to signed `atid` cookie -3. **Generate new**: Creates a new random thread ID if neither exists -4. **Persist**: Sets both the response header and cookie for future requests - -The signing uses `AGENTUITY_SDK_KEY` (or defaults to `'agentuity'` in development) with HMAC SHA-256 to prevent tampering. - -### Client-Side Thread Persistence - -When building frontends, you can maintain thread continuity by storing and sending the thread ID: - -```typescript -// Client-side: store thread ID from response header -const response = await fetch('/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }), -}); - -const threadId = response.headers.get('x-thread-id'); - -if (threadId) { - localStorage.setItem('threadId', threadId); -} - -// Subsequent requests: send thread ID header -const storedThreadId = localStorage.getItem('threadId'); -const headers = new Headers({ 'Content-Type': 'application/json' }); - -if (storedThreadId) { - headers.set('x-thread-id', storedThreadId); -} - -const nextResponse = await fetch('/api/chat', { - method: 'POST', - headers, - body: JSON.stringify({ message: nextMessage }), -}); -``` - - -The `x-thread-id` header must include the signature (format: `threadId;signature`). Use the exact value from the response header. Unsigned or tampered thread IDs are rejected. - - -## State Size Limits - - -Thread and session state are limited to **1MB** after JSON serialization. If serialized state exceeds that limit, the runtime does not persist it. If serialization fails entirely, the runtime logs an error and skips persistence for that value. - - -To stay within limits: -- Store large data in [KV storage](/services/storage/key-value) instead of state -- Keep only recent messages (e.g., last 20-50) -- Store IDs or references rather than full objects - -## Best Practices - -- **Use the right scope**: `ctx.state` for request-scoped data, `ctx.thread.state` for conversations -- **Keep state bounded**: Limit conversation history (e.g., last 20-50 messages) -- **Persist important data**: Don't rely on state for data that must survive restarts -- **Clean up resources**: Use `destroyed` event to save or archive data -- **Cache strategically**: Load from KV once, cache in thread state, save on completion -- **Watch state size**: Stay under 1MB to avoid skipped persistence -- **Use `push()` for arrays**: The `push()` method with `maxRecords` handles sliding windows efficiently - -```typescript -// Good: Use push() with maxRecords for bounded history -await ctx.thread.state.push('messages', newMessage, 50); // Keeps last 50 - -// Alternative: Manual bounding when you need the full array -const messages = await ctx.thread.state.get('messages') || []; -if (messages.length > 50) { - const archived = messages.slice(0, -50); - ctx.waitUntil(async () => { - await ctx.kv.set('archives', `${ctx.thread.id}_${Date.now()}`, archived); - }); - await ctx.thread.state.set('messages', messages.slice(-50)); -} -``` - -## Next Steps - -- [Key-Value Storage](/services/storage/key-value): Durable data persistence with namespaces and TTL -- [Calling Other Agents](/agents/calling-other-agents): Share state between agents in workflows -- [Events & Lifecycle](/agents/events-lifecycle): Monitor agent execution and cleanup diff --git a/docs/src/web/content/agents/streaming-responses.mdx b/docs/src/web/content/agents/streaming-responses.mdx deleted file mode 100644 index 2d3e3155e..000000000 --- a/docs/src/web/content/agents/streaming-responses.mdx +++ /dev/null @@ -1,273 +0,0 @@ ---- -title: Returning Streaming Responses -short_title: Streaming Responses -description: Return real-time LLM output with streaming agents ---- - -Show LLM output as it's generated instead of waiting for the full response. Streaming reduces perceived latency and creates a more responsive experience. - -## Streaming Types - -Agentuity supports two streaming patterns: - -### Ephemeral Streaming - -Returns a `ReadableStream` directly to the HTTP client. Data flows through and is not stored. Use this for real-time chat responses. - -```typescript -// In src/api/index.ts -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import chatAgent from '@agent/chat'; - -const api = new Hono() - .post('/chat', chatAgent.validator(), async (c) => { - const body = c.req.valid('json'); - return c.body(await chatAgent.run(body)); - }); - -export default api; -``` - -### Persistent Streaming - -Uses `ctx.stream.create()` to create stored streams with public URLs. Data persists and can be accessed after the connection closes. Use this for batch processing, exports, or content that needs to be accessed later. - -```typescript -// In agent.ts -const stream = await ctx.stream.create('my-export', { - contentType: 'text/csv', -}); -await stream.write('data'); -await stream.close(); -``` - -For stored streams with public URLs, see the [Storage documentation](/services/storage/durable-streams). - - -Streaming agent responses require both: `schema.stream: true` in your agent and a route that returns the resulting stream, usually with `return c.body(await agent.run(data))`. Use `agent.validator()` as usual. Streaming agents skip output validation automatically. - - -## Basic Streaming - -Enable streaming by setting `stream: true` in your schema and returning a `textStream`: - - -The `textStream` from AI SDK's `streamText()` works directly with Agentuity. Return it from your handler, then return the resulting stream from your route without additional processing. - - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('ChatStream', { - schema: { - input: s.object({ message: s.string() }), - stream: true, - }, - handler: async (ctx, input) => { - const { textStream } = streamText({ - model: anthropic('claude-sonnet-4-6'), - prompt: input.message, - }); - - return textStream; - }, -}); - -export default agent; -``` - -## Route Configuration - -For streaming agents, validate the request, run the agent, and return the resulting stream: - -```typescript -// src/api/index.ts -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import chatAgent from '@agent/chat'; - -const api = new Hono() - .post('/chat', chatAgent.validator(), async (c) => { - const body = c.req.valid('json'); - return c.body(await chatAgent.run(body)); - }); - -export type ApiRouter = typeof api; - -export default api; -``` - -Use `c.body(await agent.run(data))` when the agent already returns a stream. Use `stream()` when the route itself creates the `ReadableStream`. - - -Use `stream()` when the route itself creates the `ReadableStream`. When you are forwarding a streaming agent response, return the agent stream directly with `c.body(...)`. For non-agent routes that use `validator()` with an output schema, pass `stream: true` to skip output validation on the stream. - - -## Consuming Streams - -### With Fetch API - -Read the stream using the Fetch API: - -```typescript -const response = await fetch('http://localhost:3500/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: 'Tell me a story' }), -}); - -if (!response.ok) { - throw new Error(`Streaming request failed with ${response.status}`); -} - -const reader = response.body?.getReader(); -const decoder = new TextDecoder(); - -while (reader) { - const { done, value } = await reader.read(); - if (done) break; - - const text = decoder.decode(value); - // Process each chunk as it arrives - appendToUI(text); -} -``` - -### With React - -Use `hc()` when you want typed request bodies and direct access to the `Response` stream: - -```tsx -import { hc } from 'hono/client'; -import { useState } from 'react'; -import type { ApiRouter } from '../api'; - -const client = hc('/api'); - -function Chat() { - const [data, setData] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (message: string) => { - setIsLoading(true); - setData(''); - - try { - const res = await client.chat.$post({ json: { message } }); - if (!res.ok) { - throw new Error(`Streaming request failed with ${res.status}`); - } - - const reader = res.body?.getReader(); - const decoder = new TextDecoder(); - - if (!reader) return; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - setData((prev) => prev + decoder.decode(value, { stream: true })); - } - } finally { - setIsLoading(false); - } - }; - - return ( -
- {isLoading &&

Generating...

} - {data &&

{data}

} - -
- ); -} -``` - -For type-safe API calls, see [RPC Client](/frontend/rpc-client). - -## Streaming with System Prompts - -Add context to streaming responses: - -```typescript -handler: async (ctx, input) => { - const { textStream } = streamText({ - model: anthropic('claude-sonnet-4-6'), - system: 'You are a helpful assistant. Be concise.', - messages: [ - { role: 'user', content: input.message }, - ], - }); - - return textStream; -} -``` - -## Streaming with Conversation History - -Combine streaming with thread state for multi-turn conversations: - -```typescript -handler: async (ctx, input) => { - // Get existing messages from thread state (async) - const messages = (await ctx.thread.state.get('messages')) || []; - - // Add new user message - messages.push({ role: 'user', content: input.message }); - - const { textStream, text } = streamText({ - model: anthropic('claude-sonnet-4-6'), - messages, - }); - - // Save assistant response after streaming completes - ctx.waitUntil(async () => { - const fullText = await text; - messages.push({ role: 'assistant', content: fullText }); - await ctx.thread.state.set('messages', messages); - }); - - return textStream; -} -``` - - -Use `ctx.waitUntil()` to save conversation history without blocking the stream. The response starts immediately while state updates happen in the background. - - -## When to Stream - -| Scenario | Recommendation | -|----------|----------------| -| Chat interfaces | Stream for better UX | -| Long-form content | Stream to show progress | -| Quick classifications | Buffer (faster overall, consider Groq for speed) | -| Structured data | Buffer (use AI SDK `Output.object()`) | - -## Error Handling - -Handle streaming errors with the `onError` callback: - -```typescript -const { textStream } = streamText({ - model: anthropic('claude-sonnet-4-6'), - prompt: input.message, - onError({ error }) { - ctx.logger.error('Stream error', { error }); - }, -}); -``` - - -Errors in streaming are part of the stream, not thrown exceptions. Always provide an `onError` callback. - - -## Next Steps - -- [Using the AI SDK](/agents/ai-sdk-integration): Structured output and non-streaming responses -- [State Management](/agents/state-management): Multi-turn conversations with memory -- [Server-Sent Events](/routes/sse): Server-push updates without polling diff --git a/docs/src/web/content/agents/when-to-use.mdx b/docs/src/web/content/agents/when-to-use.mdx deleted file mode 100644 index c06f6c017..000000000 --- a/docs/src/web/content/agents/when-to-use.mdx +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: When to Use Agents vs Routes -short_title: When to Use -description: When to create an agent vs handling requests directly in routes ---- - -Both agents and routes live in your Agentuity project. Routes handle HTTP requests directly. Agents add schema validation, lifecycle hooks, and typed inter-agent calls. - -## Quick Decision Guide - -| Create an agent when you need... | Handle directly in a route when... | -|-----------------------------------|------------------------------------| -| Input and output schema validation | Health checks, status endpoints | -| Post-response quality checks | Webhook signature verification | -| Agent-to-agent calling with type safety | Simple CRUD operations | -| Lifecycle events (`started`, `completed`, `errored`) | Streaming protocols (WebSocket, SSE) | -| Reusable processing you want to call from routes, jobs, or workers | Cron-triggered cleanup jobs | - -## What's the Same - -Agents and routes share the same built-in platform services. These services are available through `ctx.*` in an agent and `c.var.*` in a route: - -| Service | In an agent (`ctx`) | In a route (`c.var`) | -|---------|---------------------|----------------------| -| Key-Value storage | `ctx.kv` | `c.var.kv` | -| Vector search | `ctx.vector` | `c.var.vector` | -| Durable streams | `ctx.stream` | `c.var.stream` | -| Logger | `ctx.logger` | `c.var.logger` | -| Tracer | `ctx.tracer` | `c.var.tracer` | -| Queue | `ctx.queue` | `c.var.queue` | -| Email | `ctx.email` | `c.var.email` | -| Sandbox | `ctx.sandbox` | `c.var.sandbox` | -| Schedule | `ctx.schedule` | `c.var.schedule` | -| Task | `ctx.task` | `c.var.task` | - - -Database and object storage use Bun's native `sql` and `s3` APIs instead of `ctx.*` or `c.var.*`. - - - -`ctx.kv.set('bucket', 'key', value)` in an agent does the same thing as `c.var.kv.set('bucket', 'key', value)` in a route. If your logic only needs these services, a route works fine on its own. - - -## What's Different - -These capabilities are **agent-only**: - -- **Schema validation**: Define input and output schemas. Use `agent.validator()` as route middleware for automatic request validation -- **Quality checks**: Add post-response validation when you need to score or review outputs -- **Lifecycle events**: Hook into `started`, `completed`, and `errored` events for monitoring -- **Agent-to-agent calling**: Call other agents with full type safety via `agent.run()` -- **Reusable logic across entry points**: Run the same processing flow from routes, background jobs, or standalone scripts - -If you need any of these, create an agent. - -Routes can still use thread and session context through `c.var.thread` and `c.var.session` when you need stateful request handling. - -## How They Work Together - -Routes are the HTTP layer, agents are the processing layer. Most applications use both: - -``` -Request → Route (auth, validation) → Agent (processing) → Route (response formatting) -``` - -A route receives the HTTP request, applies middleware (authentication, rate limiting), then delegates to an agent for structured processing. The agent returns typed output, and the route formats the HTTP response. - -## Basic Usage - -### Route-Only: Health Check - -A health endpoint doesn't need an agent: - -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; - -const router = new Hono(); - -// Replace with your actual database health check -const checkDatabase = async () => true; - -router.get('/health', async (c) => { - const dbHealthy = await checkDatabase(); - return c.json({ status: dbHealthy ? 'healthy' : 'degraded' }); -}); - -export default router; -``` - -### Route + Agent: Chat Endpoint - -For structured processing, create an agent and call it from your route: - -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import chat from '@agent/chat/agent'; - -const router = new Hono() - .post('/chat', chat.validator(), async (c) => { - const data = c.req.valid('json'); // Fully typed from agent schema - const result = await chat.run(data); - return c.json(result); - }); - -export default router; -``` - -The agent defines the schema, validates input, runs the handler, and fires lifecycle events. The route just wires it to HTTP. - -### Webhook: Route Validates, Agent Processes - -Webhooks verify signatures in the route, then hand off to an agent in the background: - -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import paymentProcessor from '@agent/payment-processor/agent'; - -// Replace with your actual Stripe signature verification -const verifyStripeSignature = (body: string, sig: string | undefined) => true; - -const router = new Hono() - .post('/webhooks/stripe', async (c) => { - const signature = c.req.header('stripe-signature'); - const rawBody = await c.req.text(); - - if (!verifyStripeSignature(rawBody, signature)) { - return c.json({ error: 'Invalid signature' }, 401); - } - - // Respond fast, process in background - c.waitUntil(async () => { - await paymentProcessor.run(JSON.parse(rawBody)); - }); - - return c.json({ received: true }); - }); - -export default router; -``` - - -`c.waitUntil()` lets you respond immediately while the agent processes in the background. Webhook providers expect responses under 3 seconds. - - -### Agent-Only: Standalone Execution - -Agents can also run outside of HTTP requests, triggered by other agents, cron jobs, or queue consumers: - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -export default createAgent('daily-report', { - schema: { - input: s.object({ date: s.string() }), - output: s.object({ report: s.string(), metrics: s.number() }), - }, - handler: async (ctx, input) => { - const data = await ctx.kv.get('metrics', input.date); - const summary = `Report for ${input.date}: ${JSON.stringify(data)}`; - return { report: summary, metrics: 42 }; - }, -}); -``` - -See [Standalone Execution](/agents/standalone-execution) for running agents from cron, queues, and other agents. - -## Best Practices - -- Handle authentication and rate limiting in routes, not agents -- Use `agent.validator()` as route middleware for type-safe request validation -- Keep agents focused on processing logic, let routes handle HTTP concerns -- Use `c.waitUntil()` in routes for background work when the client doesn't need the result -- Prefer agents when you need schema validation, post-response quality checks, or agent-to-agent calling - -## Next Steps - -- [Creating Agents](/agents/creating-agents): Build agents with schemas and focused handlers -- [HTTP Routes](/routes/http): Handle requests that don't need agent processing -- [Calling Agents from Routes](/routes/calling-agents): Import and invoke agents with validation -- [Middleware](/routes/middleware): Add auth and rate limiting in front of agents diff --git a/docs/src/web/content/community/inbound-email-agent.mdx b/docs/src/web/content/community/inbound-email-agent.mdx index 4b4cc6dcf..15865305b 100644 --- a/docs/src/web/content/community/inbound-email-agent.mdx +++ b/docs/src/web/content/community/inbound-email-agent.mdx @@ -347,5 +347,5 @@ For debugging deployed behavior, use [Debugging Deployments](/reference/cli/debu - [Deploying to the Cloud](/reference/cli/deployment): Push your agent to Agentuity's hosted runtime - [Debugging Deployments](/reference/cli/debugging): Inspect logs and traces for deployed agents -- [State Management](/agents/state-management): Persist conversation history across email threads +- [State Management](/services/storage/key-value): Persist conversation history across email threads - [Webhook Handler Pattern](/cookbook/patterns/webhook-handler): Patterns for receiving external webhooks diff --git a/docs/src/web/content/cookbook/integrations/chat-sdk.mdx b/docs/src/web/content/cookbook/integrations/chat-sdk.mdx index 4c1f5bf4d..92ca28a0f 100644 --- a/docs/src/web/content/cookbook/integrations/chat-sdk.mdx +++ b/docs/src/web/content/cookbook/integrations/chat-sdk.mdx @@ -4,7 +4,7 @@ short_title: Chat SDK description: Build multi-platform chatbots for Slack and Discord with Chat SDK and Agentuity agents --- -[Chat SDK](https://chat-sdk.dev) handles platform connections: authenticating with Slack and Discord, receiving webhooks, normalizing messages, and posting responses. Agentuity provides the agent runtime, [KV storage](/services/storage/key-value) for conversation history, and the [AI Gateway](/agents/ai-gateway) for LLM calls. One `handleMessage` function serves both platforms. +[Chat SDK](https://chat-sdk.dev) handles platform connections: authenticating with Slack and Discord, receiving webhooks, normalizing messages, and posting responses. Agentuity provides the agent runtime, [KV storage](/services/storage/key-value) for conversation history, and the [AI Gateway](/services/ai-gateway) for LLM calls. One `handleMessage` function serves both platforms. ## The Integration Pattern @@ -241,6 +241,6 @@ Chat SDK also supports [GitHub, Teams, Google Chat, and Linear](https://chat-sdk ## Next Steps - [Key-Value Storage](/services/storage/key-value): Full KV API with namespaces, TTL, and dashboard access -- [AI Gateway](/agents/ai-gateway): Route LLM calls through a single API with observability -- [Standalone Execution](/agents/standalone-execution): Run agents from bots, CLI tools, and background workers +- [AI Gateway](/services/ai-gateway): Route LLM calls through a single API with observability +- [Standalone Execution](/reference/standalone-packages): Run agents from bots, CLI tools, and background workers - [Chat with History](/cookbook/patterns/chat-with-history): The same conversation memory pattern using thread state diff --git a/docs/src/web/content/cookbook/integrations/claude-agent.mdx b/docs/src/web/content/cookbook/integrations/claude-agent.mdx index 772218721..3c8be1caa 100644 --- a/docs/src/web/content/cookbook/integrations/claude-agent.mdx +++ b/docs/src/web/content/cookbook/integrations/claude-agent.mdx @@ -4,11 +4,11 @@ short_title: Claude Agent SDK description: Build conversational code intelligence agents with Claude Agent SDK and Agentuity sandboxes --- -[Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) handles conversations and tool use: reading files, writing code, running multi-turn analysis. Agentuity adds the deployment runtime, [sandboxes](/services/sandbox/sdk-usage) for safe code execution, and [thread state](/agents/state-management) for conversation history. You write the agent logic, Agentuity handles the infrastructure. +[Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) handles conversations and tool use: reading files, writing code, running multi-turn analysis. Agentuity adds the deployment runtime, [sandboxes](/services/sandbox/sdk-usage) for safe code execution, and [thread state](/services/storage/key-value) for conversation history. You write the agent logic, Agentuity handles the infrastructure. ## The Integration Pattern -Wrap a Claude Agent SDK `query()` call inside Agentuity's [`createAgent()`](/agents/creating-agents). Claude handles the LLM interaction and file tools. Agentuity provides schema validation, thread-isolated state, and sandbox execution. +Wrap a Claude Agent SDK `query()` call inside Agentuity's [`createAgent()`](/patterns/agents-as-a-pattern). Claude handles the LLM interaction and file tools. Agentuity provides schema validation, thread-isolated state, and sandbox execution. Define schemas and create the agent handler: @@ -108,7 +108,7 @@ export default createAgent('claude-code', { }); ``` -Claude Agent SDK iterates through an async generator of messages. `allowedTools` pre-approves the listed tools, and `permissionMode: 'dontAsk'` denies tools that were not pre-approved instead of prompting. Each query is bounded by `maxTurns`, and Agentuity's [`ctx.thread.state`](/agents/state-management) persists conversation history across requests with a 20-message sliding window. +Claude Agent SDK iterates through an async generator of messages. `allowedTools` pre-approves the listed tools, and `permissionMode: 'dontAsk'` denies tools that were not pre-approved instead of prompting. Each query is bounded by `maxTurns`, and Agentuity's [`ctx.thread.state`](/services/storage/key-value) persists conversation history across requests with a 20-message sliding window. ## Sandbox Execution @@ -175,7 +175,7 @@ initWorkspace(workspaceDir); Each thread gets its own directory keyed by `ctx.thread.id`, keeping concurrent conversations isolated. Files that Claude writes or edits during the session persist within the workspace for follow-up turns. -This integration requires an `ANTHROPIC_API_KEY` in your `.env` file. The Claude Agent SDK authenticates directly with Anthropic, separate from the [AI Gateway](/agents/ai-gateway). +This integration requires an `ANTHROPIC_API_KEY` in your `.env` file. The Claude Agent SDK authenticates directly with Anthropic, separate from the [AI Gateway](/services/ai-gateway). ## Full Example @@ -185,6 +185,6 @@ This integration requires an `ANTHROPIC_API_KEY` in your `.env` file. The Claude ## Next Steps - [Sandbox SDK Usage](/services/sandbox/sdk-usage): Full API for creating and managing sandboxes -- [State Management](/agents/state-management): Request, thread, and global state scopes -- [Streaming Responses](/agents/streaming-responses): Stream agent output to the frontend in real time +- [State Management](/services/storage/key-value): Request, thread, and global state scopes +- [Streaming Responses](/patterns/chat-and-streaming): Stream agent output to the frontend in real time - [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Test and validate agent outputs diff --git a/docs/src/web/content/cookbook/integrations/langchain.mdx b/docs/src/web/content/cookbook/integrations/langchain.mdx index 0508afc85..4f669ff48 100644 --- a/docs/src/web/content/cookbook/integrations/langchain.mdx +++ b/docs/src/web/content/cookbook/integrations/langchain.mdx @@ -4,7 +4,7 @@ short_title: LangChain description: Build LangChain agents with Agentuity's deployment runtime, persistent storage, and observability --- -[LangChain](https://js.langchain.com) provides agent primitives, chains, and tool orchestration, but deploying and running those agents requires infrastructure: state management, credential routing, observability. Agentuity handles that layer. Write your agent logic with LangChain, deploy it on Agentuity with built-in storage, logging, and an [AI gateway](/agents/ai-gateway). +[LangChain](https://js.langchain.com) provides agent primitives, chains, and tool orchestration, but deploying and running those agents requires infrastructure: state management, credential routing, observability. Agentuity handles that layer. Write your agent logic with LangChain, deploy it on Agentuity with built-in storage, logging, and an [AI gateway](/services/ai-gateway). ## ReAct Agent with Tools @@ -98,7 +98,7 @@ export default createAgent('basic', { - Extract the final AI response by finding the last `AIMessage` in the array -When deployed to Agentuity, model credentials are managed through the [AI Gateway](/agents/ai-gateway). No API keys needed in your code. +When deployed to Agentuity, model credentials are managed through the [AI Gateway](/services/ai-gateway). No API keys needed in your code. ## Streaming with Timeline @@ -191,6 +191,6 @@ Explore complete working examples for each pattern: ## Next Steps -- [Creating Agents](/agents/creating-agents): Agentuity agent patterns and schemas -- [AI Gateway](/agents/ai-gateway): Managed model credentials across providers -- [State Management](/agents/state-management): Persist conversation history across requests +- [Creating Agents](/patterns/agents-as-a-pattern): Agentuity agent patterns and schemas +- [AI Gateway](/services/ai-gateway): Managed model credentials across providers +- [State Management](/services/storage/key-value): Persist conversation history across requests diff --git a/docs/src/web/content/cookbook/integrations/mastra.mdx b/docs/src/web/content/cookbook/integrations/mastra.mdx index f89d2c88f..ae8bddd3c 100644 --- a/docs/src/web/content/cookbook/integrations/mastra.mdx +++ b/docs/src/web/content/cookbook/integrations/mastra.mdx @@ -8,7 +8,7 @@ description: Deploy Mastra agents on Agentuity with persistent state, observabil ## The Integration Pattern -Wrap a Mastra `Agent` in Agentuity's `createAgent()`. Mastra runs the LLM calls and tool orchestration. Agentuity provides [schemas](/agents/schema-libraries), [thread-isolated state](/agents/state-management), [logging](/services/observability/logging) via `ctx.logger`, and deployment. +Wrap a Mastra `Agent` in Agentuity's `createAgent()`. Mastra runs the LLM calls and tool orchestration. Agentuity provides [schemas](/reference/sdk-reference/schema), [thread-isolated state](/services/storage/key-value), [logging](/services/observability/logging) via `ctx.logger`, and deployment. Create the Mastra agent with your model and instructions: @@ -57,7 +57,7 @@ export default createAgent('chat', { Mastra handles the LLM interaction and tool orchestration. Agentuity provides per-conversation state via `ctx.thread.state` (with a sliding window to cap history size), logs with searchable context, and deployment. -When deployed on Agentuity, Mastra's `openai/gpt-5.4` model string routes through the [AI Gateway](/agents/ai-gateway) unless you provide your own OpenAI key. No bridge file is needed. +When deployed on Agentuity, Mastra's `openai/gpt-5.4` model string routes through the [AI Gateway](/services/ai-gateway) unless you provide your own OpenAI key. No bridge file is needed. @@ -241,7 +241,7 @@ Each example is a complete project with agent code, a React frontend, and API ro ## Next Steps -- [State Management](/agents/state-management): All state scopes (request, thread, global) -- [AI Gateway](/agents/ai-gateway): Provider configuration and supported models +- [State Management](/services/storage/key-value): All state scopes (request, thread, global) +- [AI Gateway](/services/ai-gateway): Provider configuration and supported models - [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Test and validate agent outputs - [Chat with History](/cookbook/patterns/chat-with-history): Same pattern using the Vercel AI SDK directly diff --git a/docs/src/web/content/cookbook/integrations/nextjs.mdx b/docs/src/web/content/cookbook/integrations/nextjs.mdx index 80fea62c5..7ea41689a 100644 --- a/docs/src/web/content/cookbook/integrations/nextjs.mdx +++ b/docs/src/web/content/cookbook/integrations/nextjs.mdx @@ -23,7 +23,7 @@ my-nextjs-app/ ## Rewrite `/api` to Agentuity -Add a rewrite that proxies API traffic to `agentuity dev`. The SDK testing app runs Next.js on `3001` and Agentuity on `3501` to avoid conflicts: +Add a rewrite that proxies API traffic to `agentuity dev`. This example runs Next.js on `3001` and Agentuity on `3501` to avoid port conflicts: ```typescript title="next.config.ts" import type { NextConfig } from 'next'; @@ -48,7 +48,7 @@ export default nextConfig; ``` -This pattern does not use `@agentuity/routes` or `src/generated/routes.ts`. The frontend imports `type ApiRouter` directly from the backend source file, and `hc()` infers requests from that type. +This pattern does not use `@agentuity/frameworks` or `src/generated/frameworks.ts`. The frontend imports `type ApiRouter` directly from the backend source file, and `hc()` infers requests from that type. ## Import the Router Type Directly @@ -171,4 +171,4 @@ Then enable CORS in [App Configuration](/get-started/app-configuration). - [RPC Client](/frontend/rpc-client): More `hc()` patterns, custom fetch, and error handling - [Provider Setup](/frontend/provider-setup): Add auth, analytics, or WebRTC hooks -- [HTTP Routes](/routes/http): Define the backend API router you export to the frontend +- [Using Hono with Agentuity](/frameworks/hono): Define the backend API router you export to the frontend diff --git a/docs/src/web/content/cookbook/integrations/openai-agents.mdx b/docs/src/web/content/cookbook/integrations/openai-agents.mdx index 3ca6a2ee0..327ae80c3 100644 --- a/docs/src/web/content/cookbook/integrations/openai-agents.mdx +++ b/docs/src/web/content/cookbook/integrations/openai-agents.mdx @@ -37,7 +37,7 @@ const assistant = new Agent({ }); ``` -Create an OpenAI agent and wrap it with [`createAgent()`](/agents/creating-agents): +Create an OpenAI agent and wrap it with [`createAgent()`](/patterns/agents-as-a-pattern): ```typescript title="src/agent/tool-calling/index.ts" import { createAgent } from '@agentuity/runtime'; @@ -220,6 +220,6 @@ Explore complete working examples for each pattern: ## Next Steps -- [Creating Agents](/agents/creating-agents): Agentuity agent patterns and schemas -- [AI Gateway](/agents/ai-gateway): Managed model credentials across providers +- [Creating Agents](/patterns/agents-as-a-pattern): Agentuity agent patterns and schemas +- [AI Gateway](/services/ai-gateway): Managed model credentials across providers - [Tracing](/services/observability/tracing): Observability that replaces OpenAI's built-in tracing diff --git a/docs/src/web/content/cookbook/integrations/tanstack-start.mdx b/docs/src/web/content/cookbook/integrations/tanstack-start.mdx index 673a9e52f..642f9c2f3 100644 --- a/docs/src/web/content/cookbook/integrations/tanstack-start.mdx +++ b/docs/src/web/content/cookbook/integrations/tanstack-start.mdx @@ -4,7 +4,7 @@ short_title: TanStack Start description: Connect a TanStack Start frontend to an Agentuity backend using a Vite proxy and direct router types --- -Already have a [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/overview) app? Keep your frontend where it is and run Agentuity in an `agentuity/` subdirectory. TanStack Start uses Vite under the hood, so the same `/api` proxy pattern used in the SDK testing app works here too. +Already have a [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/overview) app? Keep your frontend where it is and run Agentuity in an `agentuity/` subdirectory. TanStack Start uses Vite under the hood, so you can proxy `/api` requests to the Agentuity dev server. ## Project Structure @@ -43,7 +43,7 @@ export default defineConfig({ ``` -This pattern does not use `@agentuity/routes` or `src/generated/routes.ts`. The frontend imports `type ApiRouter` directly from the backend source file, and `hc()` infers the request shape from that type. +This pattern does not use `@agentuity/frameworks` or `src/generated/frameworks.ts`. The frontend imports `type ApiRouter` directly from the backend source file, and `hc()` infers the request shape from that type. ## Import the Router Type Directly @@ -169,4 +169,4 @@ Then enable CORS in [App Configuration](/get-started/app-configuration). - [RPC Client](/frontend/rpc-client): More `hc()` patterns, custom fetch, and error handling - [Provider Setup](/frontend/provider-setup): Add auth, analytics, or WebRTC hooks -- [HTTP Routes](/routes/http): Define the backend API router you export to the frontend +- [Using Hono with Agentuity](/frameworks/hono): Define the backend API router you export to the frontend diff --git a/docs/src/web/content/cookbook/integrations/turborepo.mdx b/docs/src/web/content/cookbook/integrations/turborepo.mdx index cb23195b1..23aa7a4f4 100644 --- a/docs/src/web/content/cookbook/integrations/turborepo.mdx +++ b/docs/src/web/content/cookbook/integrations/turborepo.mdx @@ -28,7 +28,7 @@ my-monorepo/ ## Shared Schemas -Define schemas once in `packages/shared`, then import them from both the agent and the frontend. The schemas use [`@agentuity/schema`](/agents/schema-libraries), but any [StandardSchema-compatible](/agents/schema-libraries) library works: +Define schemas once in `packages/shared`, then import them from both the agent and the frontend. The schemas use [`@agentuity/schema`](/reference/sdk-reference/schema), but any [StandardSchema-compatible](/reference/sdk-reference/schema) library works: ```typescript title="packages/shared/src/translate.ts" import { s } from '@agentuity/schema'; @@ -87,7 +87,7 @@ The frontend should use `import type { ApiRouter } from '@my-monorepo/agentuity/ ## The Agent -The agent imports schemas from the shared package. Agentuity's [AI Gateway](/agents/ai-gateway) handles model routing, so no separate provider-specific client setup is required in the frontend: +The agent imports schemas from the shared package. Agentuity's [AI Gateway](/services/ai-gateway) handles model routing, so no separate provider-specific client setup is required in the frontend: ```typescript title="apps/agentuity/src/agent/translate/index.ts" import { createAgent } from '@agentuity/runtime'; @@ -295,7 +295,7 @@ When deployed, either keep routing `/api/*` to the Agentuity backend at your edg ## Next Steps -- [Schema Libraries](/agents/schema-libraries): Use Zod, Valibot, ArkType, or `@agentuity/schema` for shared schemas +- [Schema Libraries](/reference/sdk-reference/schema): Use Zod, Valibot, ArkType, or `@agentuity/schema` for shared schemas - [RPC Client](/frontend/rpc-client): More `hc()` patterns, custom fetch, and error handling -- [HTTP Routes](/routes/http): Define and validate backend API routes +- [Using Hono with Agentuity](/frameworks/hono): Define and validate backend API routes - [Deployment Scenarios](/frontend/deployment-scenarios): Route frontend traffic to a deployed agent diff --git a/docs/src/web/content/cookbook/patterns/autonomous-research.mdx b/docs/src/web/content/cookbook/patterns/autonomous-research.mdx index 55b279be7..c84e7b330 100644 --- a/docs/src/web/content/cookbook/patterns/autonomous-research.mdx +++ b/docs/src/web/content/cookbook/patterns/autonomous-research.mdx @@ -237,13 +237,13 @@ When calling finish_research, write a clear 2-3 paragraph summary that synthesiz ## When to Use Direct Provider SDKs -The [AI SDK](/agents/ai-sdk-integration) provides a unified interface across providers, which is the right default for most agents. Use a provider SDK directly when you need: +The [AI SDK](/patterns/agents-as-a-pattern) provides a unified interface across providers, which is the right default for most agents. Use a provider SDK directly when you need: - Full access to provider-specific features (streaming events, content block types, caching) - Precise control over the message format and conversation structure - Minimal abstraction for debugging or learning how tool calling works -Both approaches work with `createAgent()` and the [AI Gateway](/agents/ai-gateway). When you run with an Agentuity SDK key through `agentuity dev` or a deployed Agentuity app, provider SDK calls route through the AI Gateway. If you set provider API keys directly, those SDKs talk to the provider. +Both approaches work with `createAgent()` and the [AI Gateway](/services/ai-gateway). When you run with an Agentuity SDK key through `agentuity dev` or a deployed Agentuity app, provider SDK calls route through the AI Gateway. If you set provider API keys directly, those SDKs talk to the provider. See the [Research Agent](https://github.com/agentuity/examples/tree/main/features/research-agent) example for the complete implementation with a React frontend, SSE streaming, and topic input UI. @@ -251,7 +251,7 @@ See the [Research Agent](https://github.com/agentuity/examples/tree/main/feature ## Next Steps -- [AI SDK Integration](/agents/ai-sdk-integration): The unified approach to tool calling across providers -- [AI Gateway](/agents/ai-gateway): How LLM calls are routed, observed, and billed +- [AI SDK Integration](/patterns/agents-as-a-pattern): The unified approach to tool calling across providers +- [AI Gateway](/services/ai-gateway): How LLM calls are routed, observed, and billed - [Understanding Agents](/cookbook/tutorials/understanding-agents): The agent loop concept explained -- [Creating Agents](/agents/creating-agents): Agent schemas, handlers, and lifecycle +- [Creating Agents](/patterns/agents-as-a-pattern): Agent schemas, handlers, and lifecycle diff --git a/docs/src/web/content/cookbook/patterns/chat-with-history.mdx b/docs/src/web/content/cookbook/patterns/chat-with-history.mdx index e1dd4ebd0..6b40f9443 100644 --- a/docs/src/web/content/cookbook/patterns/chat-with-history.mdx +++ b/docs/src/web/content/cookbook/patterns/chat-with-history.mdx @@ -1,207 +1,164 @@ --- title: Chat with Conversation History short_title: Chat with History -description: Build a chat agent that remembers previous messages using thread state +description: Store chat history with key-value storage from a framework route --- -Use thread state to keep conversation history across multiple requests. Thread state expires after 1 hour of inactivity, which fits short-lived chat sessions. +Use key-value storage when a chat route needs to remember recent turns. In v3, keep the route in your framework and store the conversation under an explicit `conversationId`. -## The Pattern - -Store each turn in `ctx.thread.state`, then pass the saved messages back to the model on the next request. Each browser session gets its own thread, so conversations stay separate. +```bash +bun add hono openai @agentuity/keyvalue@alpha @agentuity/schema@alpha +``` -```typescript title="src/agent/chat/agent.ts" -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { anthropic } from '@ai-sdk/anthropic'; +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import OpenAI from 'openai'; +import { KeyValueClient } from '@agentuity/keyvalue'; import { s } from '@agentuity/schema'; -interface Message { - role: 'user' | 'assistant'; - content: string; -} - -const agent = createAgent('Chat Agent', { - description: 'Conversational agent with memory', - schema: { - input: s.object({ - message: s.string(), - }), - stream: true, - }, - handler: async (ctx, input) => { - // Load this thread's conversation history - const messages = (await ctx.thread.state.get('messages')) || []; // [!code highlight] - - // Include the new user turn before calling the model - messages.push({ role: 'user', content: input.message }); - - const { textStream, text } = streamText({ - model: anthropic('claude-sonnet-4-6'), - system: 'You are a helpful assistant. Be concise but friendly.', - messages, - }); - - // Save the assistant turn after streaming completes - ctx.waitUntil(async () => { // [!code highlight] - const fullResponse = await text; - messages.push({ role: 'assistant', content: fullResponse }); - await ctx.thread.state.set('messages', messages); // [!code highlight] - ctx.logger.info('Conversation updated', { - messageCount: messages.length, - threadId: ctx.thread.id, - }); - }); - - return textStream; - }, +const chatRequestSchema = s.object({ + conversationId: s.string(), + message: s.string(), }); -export default agent; -``` +const messageSchema = s.object({ + role: s.enum(['user', 'assistant']), + content: s.string(), +}); -## Route Example +const historySchema = s.array(messageSchema); -```typescript title="src/api/chat/route.ts" -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import chatAgent from '@agent/chat/agent'; +type ChatMessage = s.infer; -const router = new Hono(); +const HISTORY_NAMESPACE = 'chat-history'; +const HISTORY_LIMIT = 20; +const HISTORY_TTL_SECONDS = 60 * 60 * 24 * 30; -router.post('/', chatAgent.validator(), async (c) => { - const { message } = c.req.valid('json'); - return c.body(await chatAgent.run({ message })); // [!code highlight] -}); +const app = new Hono(); +const kv = new KeyValueClient(); +const openai = new OpenAI(); -router.delete('/', async (c) => { - await c.var.thread.destroy(); // [!code highlight] - return c.json({ reset: true }); -}); +async function readHistory(conversationId: string): Promise { + const stored = await kv.get(HISTORY_NAMESPACE, conversationId); + return stored.exists ? historySchema.parse(stored.data) : []; +} -export default router; -``` +async function saveHistory( + conversationId: string, + messages: readonly ChatMessage[] +): Promise { + const next = messages.slice(-HISTORY_LIMIT); + await kv.set(HISTORY_NAMESPACE, conversationId, next, { + ttl: HISTORY_TTL_SECONDS, + }); -## Frontend + return next; +} -A simple chat interface that displays streaming responses: +app.post('/api/chat', async (c) => { + const body: unknown = await c.req.json(); + const input = chatRequestSchema.parse(body); -```tsx title="src/web/App.tsx" -import { useState, useRef, useEffect } from 'react'; + const history = await readHistory(input.conversationId); + const userMessage: ChatMessage = { + role: 'user', + content: input.message, + }; -interface Message { - role: 'user' | 'assistant'; - content: string; -} + const messages = [...history, userMessage]; -export function App() { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); - const [isStreaming, setIsStreaming] = useState(false); - const messagesEndRef = useRef(null); + const completion = await openai.chat.completions.create({ + model: 'gpt-5.4-mini', + messages: [ + { role: 'system', content: 'You are a concise product support assistant.' }, + ...messages, + ], + }); - useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); + const assistantMessage: ChatMessage = { + role: 'assistant', + content: completion.choices[0]?.message?.content ?? '', + }; - const sendMessage = async () => { - if (!input.trim() || isStreaming) return; + const nextHistory = await saveHistory(input.conversationId, [ + ...messages, + assistantMessage, + ]); + + return c.json({ + conversationId: input.conversationId, + message: assistantMessage, + messageCount: nextHistory.length, + model: completion.model, + tokens: completion.usage?.total_tokens ?? 0, + }); +}); - const userMessage = input.trim(); - setInput(''); - setMessages((prev) => [...prev, { role: 'user', content: userMessage }]); - setIsStreaming(true); +app.get('/api/chat/:conversationId', async (c) => { + const conversationId = c.req.param('conversationId'); + const messages = await readHistory(conversationId); - // Reserve a bubble for the streaming assistant response - setMessages((prev) => [...prev, { role: 'assistant', content: '' }]); + return c.json({ conversationId, messages }); +}); - const response = await fetch('/api/chat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: userMessage }), - }); +app.delete('/api/chat/:conversationId', async (c) => { + const conversationId = c.req.param('conversationId'); + await kv.delete(HISTORY_NAMESPACE, conversationId); - const reader = response.body?.getReader(); - const decoder = new TextDecoder(); + return c.json({ conversationId, messages: [] }); +}); - while (reader) { - const { done, value } = await reader.read(); - if (done) break; +export default app; +``` - const chunk = decoder.decode(value); - setMessages((prev) => { - const updated = [...prev]; - const lastMessage = updated.at(-1); +`KeyValueClient` reads `AGENTUITY_SDK_KEY` from the environment. Run this through `agentuity dev` or a linked Agentuity project when you want managed key-value storage and AI Gateway routing. - if (lastMessage) { - lastMessage.content += chunk; - } +## Call the Route - return updated; - }); - } +Generate a conversation ID in your app, cookie, or authenticated user session. Pass that ID on each request. - setIsStreaming(false); - }; +```typescript +const conversationId = crypto.randomUUID(); + +const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + conversationId, + message: 'What changed in the v3 Agentuity starter?', + }), +}); - return ( -
-
- {messages.map((msg, i) => ( -
- {msg.content || '...'} -
- ))} -
-
- -
- setInput(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && sendMessage()} - placeholder="Type a message..." - disabled={isStreaming} - style={{ flex: 1, padding: '0.75rem' }} - /> - -
-
- ); +if (!response.ok) { + throw new Error(`Chat failed with ${response.status}`); } -``` -The frontend reads the streaming response chunk by chunk and updates the UI as text arrives. +const result = await response.json(); +``` -## Key Points +The response includes the assistant message, model name, token count when the provider returns usage, and the stored message count. -- **Thread state** (`ctx.thread.state`) expires after 1 hour of inactivity -- **Async API**: All thread state methods are async (`await ctx.thread.state.get()`) -- **Messages array** stores the full conversation history -- **`waitUntil`** saves the response after streaming completes -- **Thread ID** (`ctx.thread.id`) identifies the conversation +## Reset a Conversation - -For append-only patterns like chat history, use `push()` with `maxRecords` for automatic sliding window behavior: +Delete the key when the user starts over: ```typescript -await ctx.thread.state.push('messages', newMessage, 100); // Keep last 100 +await fetch(`/api/chat/${conversationId}`, { method: 'DELETE' }); ``` + +## Notes + +- use one namespace for chat history and make the key explicit +- store a rolling window or summary instead of unlimited messages +- keep provider token usage in the response when the UI should show model details +- use key-value storage or a database for v3 app state, not v2 runtime thread state + + +Older v2 apps that use `ctx.thread.state` can keep that model while you plan a migration. New v3 examples should use framework-owned state boundaries: cookies, database rows, key-value records, or authenticated user IDs. -## See Also +## Next Steps -- [State Management](/agents/state-management) for all state scopes -- [Streaming Responses](/agents/streaming-responses) for streaming patterns +- [Chat and Streaming](/patterns/chat-and-streaming): stream assistant text while storing the final transcript +- [Key-Value Storage](/services/storage/key-value): configure TTLs, search keys, and manage namespaces +- [Migrating Runtime Apps to Frameworks](/migration/runtime-to-frameworks): move v2 agent runtime state into v3 app-owned state diff --git a/docs/src/web/content/cookbook/patterns/cron-with-storage.mdx b/docs/src/web/content/cookbook/patterns/cron-with-storage.mdx index 3b388d139..ee5033d50 100644 --- a/docs/src/web/content/cookbook/patterns/cron-with-storage.mdx +++ b/docs/src/web/content/cookbook/patterns/cron-with-storage.mdx @@ -175,5 +175,5 @@ See the [Scheduled Digest](https://github.com/agentuity/examples/tree/main/featu ## See Also -- [Cron Routes](/routes/cron) for schedule expressions and patterns +- [Schedules](/services/schedules) for recurring delivery and schedule expressions - [Key-Value Storage](/services/storage/key-value) for KV operations and TTL options diff --git a/docs/src/web/content/cookbook/patterns/llm-as-a-judge.mdx b/docs/src/web/content/cookbook/patterns/llm-as-a-judge.mdx index 1873781b2..d9bf3a58e 100644 --- a/docs/src/web/content/cookbook/patterns/llm-as-a-judge.mdx +++ b/docs/src/web/content/cookbook/patterns/llm-as-a-judge.mdx @@ -383,5 +383,5 @@ See the [Code Runner](https://github.com/agentuity/examples/tree/main/features/c ## Next Steps -- [Using the AI SDK](/agents/ai-sdk-integration): Model selection and configuration +- [Using the AI SDK](/patterns/agents-as-a-pattern): Model selection and configuration - [Vector Storage](/services/storage/vector): Build RAG systems to ground responses diff --git a/docs/src/web/content/cookbook/patterns/product-search.mdx b/docs/src/web/content/cookbook/patterns/product-search.mdx index 049e9bbb0..7803fcb89 100644 --- a/docs/src/web/content/cookbook/patterns/product-search.mdx +++ b/docs/src/web/content/cookbook/patterns/product-search.mdx @@ -447,4 +447,4 @@ curl -X POST "http://localhost:3500/api/products/advisor" \ - [Vector Storage](/services/storage/vector) for all vector operations - [Build a RAG Agent](/cookbook/tutorials/rag-agent) for question-answering -- [AI SDK Integration](/agents/ai-sdk-integration) for `generateObject` patterns +- [AI SDK Integration](/patterns/agents-as-a-pattern) for `generateObject` patterns diff --git a/docs/src/web/content/cookbook/patterns/server-utilities.mdx b/docs/src/web/content/cookbook/patterns/server-utilities.mdx index afc6a1c90..5733b42cb 100644 --- a/docs/src/web/content/cookbook/patterns/server-utilities.mdx +++ b/docs/src/web/content/cookbook/patterns/server-utilities.mdx @@ -333,7 +333,7 @@ For one-off queue management, use the CLI instead: `agentuity cloud queue create ## Alternative: HTTP Routes -If you want to centralize storage logic in your Agentuity project (for [middleware](/routes/middleware), sharing across multiple apps, or avoiding SDK key distribution), use [HTTP routes](/routes/http) instead. +If you want to centralize storage logic in your Agentuity project (for [Hono middleware](/frameworks/hono), sharing across multiple apps, or avoiding SDK key distribution), use [Hono routes](/frameworks/hono) instead. ```typescript title="src/api/sessions/route.ts" import { Hono } from 'hono'; @@ -565,6 +565,5 @@ const jsonSchema = toJSONSchema(schema); - [Standalone Packages](/reference/standalone-packages): Full list of service packages and their APIs - [Queues](/services/queues): Queue concepts and CLI commands -- [HTTP Routes](/routes/http): Route creation with Hono -- [Route Middleware](/routes/middleware): Authentication patterns +- [Using Hono with Agentuity](/frameworks/hono): Route creation and middleware patterns - [RPC Client](/frontend/rpc-client): Typed client generation diff --git a/docs/src/web/content/cookbook/patterns/webhook-handler.mdx b/docs/src/web/content/cookbook/patterns/webhook-handler.mdx index ab162dc8a..fa97394da 100644 --- a/docs/src/web/content/cookbook/patterns/webhook-handler.mdx +++ b/docs/src/web/content/cookbook/patterns/webhook-handler.mdx @@ -149,5 +149,5 @@ function isSlackUrlVerification( ## See Also -- [HTTP Routes](/routes/http) for route patterns -- [Calling Agents from Routes](/routes/calling-agents) for `c.waitUntil()` handoff patterns +- [Using Hono with Agentuity](/frameworks/hono) for route patterns +- [Agents as a Pattern](/patterns/agents-as-a-pattern) for `c.waitUntil()` handoff patterns diff --git a/docs/src/web/content/cookbook/tutorials/rag-agent.mdx b/docs/src/web/content/cookbook/tutorials/rag-agent.mdx index 57343e7e4..090cadfbf 100644 --- a/docs/src/web/content/cookbook/tutorials/rag-agent.mdx +++ b/docs/src/web/content/cookbook/tutorials/rag-agent.mdx @@ -428,5 +428,5 @@ For RAG apps, use [LLM as a Judge](/cookbook/patterns/llm-as-a-judge) to check w ## Next Steps - [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Review answer quality with model-based checks -- [Streaming Responses](/agents/streaming-responses): Stream longer answers as they are generated +- [Streaming Responses](/patterns/chat-and-streaming): Stream longer answers as they are generated - [Vector Storage](/services/storage/vector): Add metadata filters and advanced search options diff --git a/docs/src/web/content/cookbook/tutorials/understanding-agents.mdx b/docs/src/web/content/cookbook/tutorials/understanding-agents.mdx index 3dd4ec6b9..1843aadc3 100644 --- a/docs/src/web/content/cookbook/tutorials/understanding-agents.mdx +++ b/docs/src/web/content/cookbook/tutorials/understanding-agents.mdx @@ -30,7 +30,7 @@ A research agent that: ## Prerequisites - An Agentuity project ([Quickstart](/get-started/quickstart) if you need one) -- Basic familiarity with [AI SDK Integration](/agents/ai-sdk-integration) +- Basic familiarity with [AI SDK Integration](/patterns/agents-as-a-pattern) ## Project Structure @@ -388,5 +388,5 @@ Check the logs to see the agent loop in action: the search tool being called, re ## Next Steps -- [AI SDK Integration](/agents/ai-sdk-integration): More patterns for tool use and streaming -- [Calling Other Agents](/agents/calling-other-agents): Build multi-agent systems +- [AI SDK Integration](/patterns/agents-as-a-pattern): More patterns for tool use and streaming +- [Calling Other Agents](/patterns/agents-as-a-pattern): Build multi-agent systems diff --git a/docs/src/web/content/deploy-operate/deploy-framework-apps.mdx b/docs/src/web/content/deploy-operate/deploy-framework-apps.mdx new file mode 100644 index 000000000..c347bb8da --- /dev/null +++ b/docs/src/web/content/deploy-operate/deploy-framework-apps.mdx @@ -0,0 +1,133 @@ +--- +title: Deploying Framework Apps +short_title: Deploy Framework Apps +description: Build and deploy framework projects with the v3 Agentuity CLI. +--- + +Deploy a `package.json`-backed framework app after it is registered with Agentuity, has `AGENTUITY_SDK_KEY` in `.env`, and passes `agentuity build`. + +```bash +agentuity project import --validate-only +agentuity project import +agentuity build +agentuity deploy +``` + +## Validate the Project + +Check the local project shape before registration: + +```bash +agentuity project import --validate-only +``` + +This verifies that the directory looks like a JavaScript or TypeScript project without creating cloud resources or writing files. When you run `agentuity project import` without `--validate-only`, the CLI can register the project and write `agentuity.json` plus the project SDK key. + +Run `agentuity project import` before the first deploy for any existing app that does not already have `agentuity.json`. `agentuity deploy` expects registered project metadata before its deploy handler runs, so it is not the import step for fresh directories. + +## Build Locally + +Run your framework's native build first, then run the Agentuity packaging check: + +```bash +bun run build +agentuity build +``` + +The CLI detects supported JavaScript frameworks from `package.json` and framework config files. Some frameworks have dedicated handling; other app shapes use the generic web-process adapter. + +`agentuity build`: + +- detects the framework from `package.json` and framework config files +- installs dependencies with the detected package manager +- runs the framework build command +- writes deployable output to `.agentuity` unless you pass `--outdir` +- packages launch metadata so Agentuity knows how to start the app +- runs TypeScript type checking unless you pass `--dev` or `--skip-type-check` + +Useful checks: + +```bash +agentuity build --dev +agentuity build --outdir .agentuity-check +agentuity build --skip-type-check +agentuity build --report-file build-report.json +``` + +`--dev` and `--skip-type-check` are useful for fast packaging checks because they skip the final typecheck step. Use the normal `agentuity build` path before deploy when you want the same verification deploy uses. + +See [Building Deployment Bundles](/reference/cli/build-configuration): read the detected framework, adapter name, and packaging output before you deploy. + + +If `agentuity build` starts from a shell where package binaries are not on `PATH`, prefix the command with your project's local bin directory: + +```bash +PATH="$(pwd)/node_modules/.bin:$PATH" agentuity build +``` + +Run this from the app directory after dependencies are installed. + + + +Generated starters can be useful local starting points, but only deploy app shapes that pass `agentuity build` in your project. If your framework's own build passes and `agentuity build` does not, keep working locally until the Agentuity packaging step passes. + + +## Deploy + +Deploy from the project directory: + +```bash +agentuity deploy +``` + +The deploy command requires an authenticated CLI session and a registered project. For an existing app, run `agentuity project import` before the first deploy so the CLI can resolve the project and local SDK key. + +During deploy, the CLI: + +1. checks that `AGENTUITY_SDK_KEY` exists in the local project environment +2. validates deployment resource settings and configured domains +3. attempts to sync non-reserved values from local `.env` +4. typechecks, builds, and packages the framework output +5. uploads the encrypted deployment bundle and static assets + + +If deploy cannot find `AGENTUITY_SDK_KEY`, it exits before building. Run `agentuity cloud env pull` from a registered project when you need to restore the local `.env` file. + + +The env sync step is best effort. If syncing fails, deploy continues and reports that step as skipped. + +## What Gets Uploaded + +The deploy flow zips `.agentuity`, excludes local `.env` files from the bundle, encrypts the archive, and uploads client assets separately when the build produced them. + +That upload is based on the `.agentuity` output and the generated metadata file. Keep framework-specific assumptions conservative unless you have verified them in the exact project shape you are deploying. + +## Package Scripts + +Calling `agentuity deploy` directly runs the deploy command only. If you rely on package manager lifecycle scripts, call your package script instead: + +```json title="package.json" +{ + "scripts": { + "predeploy": "bun run typecheck", + "deploy": "agentuity deploy", + "postdeploy": "bun run notify" + } +} +``` + +```bash +bun run deploy +``` + +See [Automating Deployments with the GitHub App](/reference/github-app): linked repositories run the deploy flow from Agentuity's build environment after dependency installation. + +## Existing v2 Projects + +For v2 projects, inspect the migration CLI before changing the deploy flow: + +```bash +npx @agentuity/migrate@alpha --help +``` + +The alpha tag includes the v2 to v3 migration command. Keep v2 projects on their existing deploy path until you have migrated and validated the v3 build. diff --git a/docs/src/web/content/deploy-operate/environment-variables.mdx b/docs/src/web/content/deploy-operate/environment-variables.mdx new file mode 100644 index 000000000..b2d669bfa --- /dev/null +++ b/docs/src/web/content/deploy-operate/environment-variables.mdx @@ -0,0 +1,117 @@ +--- +title: Managing Environment Variables +short_title: Environment Variables +description: Manage local .env files, cloud environment variables, public variables, and secrets. +--- + +Use this page when you want to know exactly which env values the current CLI reads from local files and which values it syncs to Agentuity Cloud. + +```bash +agentuity cloud env list +agentuity cloud env set OPENAI_API_KEY "sk_..." --secret +agentuity cloud env pull +``` + +## Local Development + +`agentuity dev` looks up `AGENTUITY_SDK_KEY` for the local dev process. It does not try to fully load every local env value itself. + +```bash +agentuity dev +``` + +The CLI searches project env files for that key using the active profile. For a local profile, it prefers `.env.`, then `.env.development`, then `.env`. For non-local or production-style profiles, it prefers `.env.`, then `.env`, then `.env.production`. + +When the key is available, the CLI can patch OpenAI, Anthropic, and Groq env vars for the dev process. It only does that when the provider key is missing or already matches the SDK key. + + +The CLI looks up `AGENTUITY_SDK_KEY` itself. Other local variables are usually loaded by the runtime, your shell, or your own app tooling. + + +## Deploy Sync + +During `agentuity deploy`, the CLI reads local `.env`, removes reserved Agentuity system keys, splits the remaining values into environment variables and secrets, and attempts to sync them before the build step. + +```bash +agentuity deploy +``` + +The sync step is non-fatal. If it cannot sync variables, deploy continues and reports that step as skipped. + +## Public Variables and Secrets + +Public prefixes are always treated as environment variables, never secrets: + +| Prefix | Use for | +|--------|---------| +| `VITE_` | Vite browser-exposed values | +| `PUBLIC_` | Frameworks that expose public values with `PUBLIC_` | +| `AGENTUITY_PUBLIC_` | Agentuity public values | + +Values are treated as secrets when the key is `DATABASE_URL` or ends with one of these suffixes: + +```text +_SECRET +_KEY +_TOKEN +_PASSWORD +_PRIVATE +``` + +Use `--secret` when setting a value that should be encrypted and masked: + +```bash +agentuity cloud env set API_KEY "sk_..." --secret +``` + +## List Current Cloud Values + +List current variables from the cloud when you want to inspect what a deploy will see: + +```bash +agentuity cloud env list +agentuity cloud env list --org +``` + +Project-scoped `list` shows a merged project and organization view. When the same key exists in both places, the project value takes precedence. + +## Pull Cloud Values + +Pull cloud values into local `.env`: + +```bash +agentuity cloud env pull +``` + +Without `--force`, local values win when a key already exists. With `--force`, cloud values overwrite local values after confirmation: + +```bash +agentuity cloud env pull --force +``` + +Project pulls also write the project `AGENTUITY_SDK_KEY` when the cloud project returns one. + +## Push Local Values + +Push local `.env` values to the project or organization: + +```bash +agentuity cloud env push +agentuity cloud env push --org +``` + +The CLI reads `.env`, filters reserved `AGENTUITY_*` keys, shows a diff, and asks for confirmation before overwriting existing cloud values unless you pass `--force`: + +```bash +agentuity cloud env push --force +``` + +## Organization Scope + +Use `--org` for variables shared across projects in the same organization: + +```bash +agentuity cloud env set OPENAI_API_KEY "sk_..." --secret --org +agentuity cloud env pull --org +agentuity cloud env push --org +``` diff --git a/docs/src/web/content/deploy-operate/index.mdx b/docs/src/web/content/deploy-operate/index.mdx new file mode 100644 index 000000000..7d7f75849 --- /dev/null +++ b/docs/src/web/content/deploy-operate/index.mdx @@ -0,0 +1,40 @@ +--- +title: Build & Deploy +description: Run local development, builds, deploys, and environment management with the v3 CLI. +--- + +import { KeyRound, Play, Rocket } from 'lucide-react'; + +Use these pages when you want to run a framework app locally, package it for Agentuity, deploy it, and manage the environment values it needs. + + + } + /> + } + /> + } + /> + + +## Common Flow + +```bash +agentuity project import +agentuity dev +agentuity build +agentuity deploy +``` + +Use [Local Development](/deploy-operate/local-development) when you want to run your framework server with Agentuity values injected. Use [Deploy Framework Apps](/deploy-operate/deploy-framework-apps) when the project is registered and the local build step succeeds. diff --git a/docs/src/web/content/deploy-operate/local-development.mdx b/docs/src/web/content/deploy-operate/local-development.mdx new file mode 100644 index 000000000..708383d63 --- /dev/null +++ b/docs/src/web/content/deploy-operate/local-development.mdx @@ -0,0 +1,67 @@ +--- +title: Running Local Development +short_title: Local Development +description: Run your framework dev script with Agentuity environment wiring. +--- + +`agentuity dev` runs your project's package-manager `dev` script and passes Agentuity environment values into that process. Use it when you want your normal framework dev server plus Agentuity local wiring. + +```bash +agentuity dev +``` + +## What Runs + +The CLI reads `package.json`, detects the package manager, and runs the selected script: + +```json title="package.json" +{ + "scripts": { + "dev": "next dev", + "dev:web": "vite --host 0.0.0.0" + } +} +``` + +```bash +agentuity dev +agentuity dev --script dev:web +``` + +If the script is missing, the command exits and prints the available scripts. + +## Port + +`agentuity dev` sets `PORT` for the child process. The default is `3000`; pass `--port` when your framework should listen elsewhere: + +```bash +agentuity dev --port 4321 +``` + +Frameworks differ in how they read `PORT`. If your framework ignores it, configure the port in the framework's own dev script. + +## AI Gateway Environment + +When `AGENTUITY_SDK_KEY` is available, `agentuity dev` injects provider-compatible environment variables for OpenAI, Anthropic, and Groq unless you already set your own provider key. + +```bash +agentuity dev +``` + +This lets provider SDK calls route through Agentuity during local development. If no project SDK key is found, the CLI can fall back to your CLI auth key for gateway routing and prints a reminder to run `agentuity project import`. + +Service clients should still use a project SDK key. Link the app when local routes need key-value storage, vector search, object storage, or other Agentuity services. + + +`agentuity dev` does not replace Next.js, Vite, Hono, or another framework dev server. It starts the script you already defined and passes Agentuity environment values into that process. + + +## Validate Before Running + +Use import validation when you want to check project shape without creating or deploying anything: + +```bash +agentuity project import --validate-only +``` + +For local environment behavior, see [Environment Variables](/deploy-operate/environment-variables): it explains which values are read from `.env` and which values are synced to Agentuity Cloud. diff --git a/docs/src/web/content/deploy-operate/meta.json b/docs/src/web/content/deploy-operate/meta.json new file mode 100644 index 000000000..fe11e3219 --- /dev/null +++ b/docs/src/web/content/deploy-operate/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Build & Deploy", + "pages": ["local-development", "deploy-framework-apps", "environment-variables"] +} diff --git a/docs/src/web/content/frameworks/astro.mdx b/docs/src/web/content/frameworks/astro.mdx new file mode 100644 index 000000000..72cb2c65a --- /dev/null +++ b/docs/src/web/content/frameworks/astro.mdx @@ -0,0 +1,42 @@ +--- +title: Using Astro with Agentuity +short_title: Astro +description: Add Agentuity CLI wiring and deployment validation to an Astro app +--- + +Start with a normal [Astro](https://docs.astro.build) app. Agentuity adds local development and deploy wiring around Astro's own pages, API routes, and build output. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-astro-app --framework astro +cd my-astro-app +``` + +The starter runs `create-astro`, adds `@agentuity/cli`, adds a `deploy` script, and can overlay an AI example at `src/pages/api/translate.ts` plus `src/pages/index.astro`. + +For an existing Astro app, add the CLI and link the project: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity dev` then runs your Astro `dev` script with Agentuity environment values, including `AGENTUITY_SDK_KEY` when the project is linked. + +## Validate Before Deploy + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +Astro is detected as its own framework, but the current build path still uses the generic adapter. Check that `.agentuity/launch.json` matches the Astro output you expect, especially if the app is no longer using the starter's default static shape. + +## Next Steps + +- [App Configuration](/get-started/app-configuration): configure `agentuity.json`, env vars, and scripts +- [Build Configuration](/reference/cli/build-configuration): inspect launch metadata and packaging output +- [Standalone Packages](/reference/standalone-packages): add Agentuity services from Astro server code diff --git a/docs/src/web/content/frameworks/hono.mdx b/docs/src/web/content/frameworks/hono.mdx new file mode 100644 index 000000000..34fece9eb --- /dev/null +++ b/docs/src/web/content/frameworks/hono.mdx @@ -0,0 +1,112 @@ +--- +title: Using Hono with Agentuity +short_title: Hono +description: Add Agentuity services and deployment context to a Hono app +--- + +Start with a normal [Hono](https://hono.dev/docs/) app. Use Agentuity service clients inside Hono handlers, then validate the packaged build before you deploy. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-hono-api --framework hono +cd my-hono-api +``` + +The starter creates a Hono app, adds `@agentuity/cli`, package scripts for local development, build, start, and deploy, plus an optional AI route example in `src/index.ts`. + +## Add a Service Client + +Install a service client: + +```bash +bun add @agentuity/keyvalue@alpha +``` + +Then call it from your route handlers. + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { KeyValueClient } from '@agentuity/keyvalue'; + +const app = new Hono(); +const kv = new KeyValueClient(); + +function readTitle(value: unknown): string | undefined { + if (typeof value !== 'object' || value === null || !('title' in value)) { + return undefined; + } + + const { title } = value; + return typeof title === 'string' && title.trim() ? title.trim() : undefined; +} + +app.post('/api/events', async (c) => { + const body: unknown = await c.req.json(); + const title = readTitle(body); + + if (!title) { + return c.json({ error: 'title is required' }, 400); + } + + const id = crypto.randomUUID(); + const event = { + id, + title, + createdAt: new Date().toISOString(), + }; + + await kv.set('events', id, event, { ttl: null }); + + return c.json(event, 201); +}); + +export default app; +``` + + +Keep request parsing, validation, and responses in normal Hono handlers. Add Agentuity clients only where the route needs platform services. + + +## Local Development + +`agentuity dev` runs your package manager's `dev` script and injects Agentuity environment variables into that child process. + +```bash +agentuity dev +``` + +Pass a custom script when your Hono app uses a different dev entry: + +```bash +agentuity dev --script dev:api +``` + +## Validate Before Deploy + +Treat `agentuity build` as the packaging check before deploy. For some Hono app shapes, you may need an explicit `start` script and entrypoint so the packaged app has a clear process to run: + +```json title="package.json" +{ + "scripts": { + "dev": "bun run --hot src/index.ts", + "build": "bun build src/index.ts --target=bun --outdir=dist", + "start": "bun dist/index.js", + "deploy": "agentuity deploy" + } +} +``` + +Then validate before deploying: + +```bash +agentuity build +``` + +Deploy only after the build writes `.agentuity/launch.json` with the expected web process. + +## Next Steps + +- [Hono Routing](https://hono.dev/docs/api/routing): use standard Hono request and response helpers +- [Logging](/services/observability/logging): add structured logging in routes +- [Standalone Packages](/reference/standalone-packages): use service clients without Hono middleware diff --git a/docs/src/web/content/frameworks/index.mdx b/docs/src/web/content/frameworks/index.mdx new file mode 100644 index 000000000..1e8357387 --- /dev/null +++ b/docs/src/web/content/frameworks/index.mdx @@ -0,0 +1,113 @@ +--- +title: Frameworks +description: Start with your framework and add Agentuity services +--- + +Start with the framework's own app shape, then add Agentuity services, local development, and deploy metadata where they fit. + + + Next.js} + /> + Nuxt} + /> + React Router} + /> + Svelte} + /> + Astro} + /> + Hono} + /> + Vite} + /> + TanStack} + /> + + +## How the v3 Shape Works + +Agentuity does not ask every app to become an Agentuity runtime app. Keep the framework's normal file conventions: + +| Framework | Use the framework's route shape | Add Agentuity through | +| --- | --- | --- | +| Next.js | `app/api/**/route.ts` | standalone service clients | +| Hono | `new Hono()` routes | standalone service clients | +| TanStack Start | server routes and server functions | standalone service clients | + +`agentuity dev` runs your package manager's `dev` script with the environment your app needs. `agentuity build` packages the app and writes `.agentuity/launch.json` and related deployment files when packaging succeeds. + +## Framework Starters + +`bun create agentuity@alpha --framework ` currently scaffolds these starters: + +| Slug | Framework | +| --- | --- | +| `nextjs` | Next.js | +| `nuxt` | Nuxt | +| `remix` | React Router | +| `sveltekit` | SvelteKit | +| `astro` | Astro | +| `hono` | Hono | +| `vite-react` | Vite + React | + +Some framework app shapes work through the build path even when they do not have a `project create` starter yet. TanStack Start falls into that group, so start with TanStack's own CLI and validate the build output before you deploy. + + +Run `bun run build`, then `agentuity build`, and inspect `.agentuity/launch.json` before your first deploy. Use `--skip-type-check` only for a faster packaging check while iterating. + + +## Add Services + +Use standalone service clients as the default path: + +```bash +bun add @agentuity/keyvalue@alpha +``` + +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; + +const kv = new KeyValueClient(); + +await kv.set('sessions', 'latest', { updatedAt: new Date().toISOString() }); +``` + +Use direct clients when you want the same code to run across frameworks, scripts, and server routes. + +## Next Steps + +- [Services](/services): choose storage, messaging, identity, observability, and execution clients +- [Deployment](/reference/cli/deployment): review deploy flags and lifecycle behavior +- [Build Configuration](/reference/cli/build-configuration): inspect framework detection and launch metadata diff --git a/docs/src/web/content/frameworks/meta.json b/docs/src/web/content/frameworks/meta.json new file mode 100644 index 000000000..3db4959e5 --- /dev/null +++ b/docs/src/web/content/frameworks/meta.json @@ -0,0 +1,14 @@ +{ + "title": "Frameworks", + "pages": [ + "index", + "nextjs", + "nuxt", + "react-router", + "sveltekit", + "astro", + "hono", + "vite-react", + "tanstack-start" + ] +} diff --git a/docs/src/web/content/frameworks/nextjs.mdx b/docs/src/web/content/frameworks/nextjs.mdx new file mode 100644 index 000000000..1773c0441 --- /dev/null +++ b/docs/src/web/content/frameworks/nextjs.mdx @@ -0,0 +1,106 @@ +--- +title: Using Next.js with Agentuity +short_title: Next.js +description: Add Agentuity service clients, local development, and deployment metadata to a Next.js app +--- + +Start with a normal [Next.js](https://nextjs.org/docs) App Router project. Agentuity adds service clients, local development support, and deploy validation around the framework's own routes and build output. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-next-app --framework nextjs +cd my-next-app +``` + +The starter creates a Next.js app, adds `@agentuity/cli`, a `deploy` script, and an optional AI route example under `src/app/api`. + +For an existing Next.js app, add the CLI and import the project instead: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity project import` registers the app and writes the local project metadata that `agentuity deploy` expects. Run it once before the first deploy for existing apps. + +## Add a Service Client + +Use standalone service clients from route handlers. This keeps the code portable and does not require an Agentuity-specific router. + +```bash +bun add @agentuity/keyvalue@alpha +``` + +```typescript title="src/app/api/notes/route.ts" +import { KeyValueClient } from '@agentuity/keyvalue'; +import { NextResponse } from 'next/server'; + +interface Note { + readonly id: string; + readonly text: string; + readonly createdAt: string; +} + +const kv = new KeyValueClient(); + +function readText(value: unknown): string | undefined { + if (typeof value !== 'object' || value === null || !('text' in value)) { + return undefined; + } + + const { text } = value; + return typeof text === 'string' && text.trim() ? text.trim() : undefined; +} + +export async function POST(request: Request): Promise { + const body: unknown = await request.json(); + const text = readText(body); + + if (!text) { + return NextResponse.json({ error: 'text is required' }, { status: 400 }); + } + + const note: Note = { + id: crypto.randomUUID(), + text, + createdAt: new Date().toISOString(), + }; + + await kv.set('notes', note.id, note, { ttl: 60 * 60 * 24 * 7 }); + + return NextResponse.json({ note }); +} +``` + +`KeyValueClient` reads `AGENTUITY_SDK_KEY` from the environment. Link the app before local service calls or deploys that need it. + +## Validate Before Deploy + +Build the app with Next.js first, then run the Agentuity build before deploying. + +Validate in this order: + +```bash +bun run build +agentuity build +``` + +If the Agentuity build cannot find the local `next` binary, run it with the app's package binaries on `PATH`: + +```bash +PATH="$(pwd)/node_modules/.bin:$PATH" agentuity build +``` + +Deploy only after `agentuity build` succeeds and `.agentuity/launch.json` points at the process you expect. If your app uses Next.js standalone output, inspect the packaged server entry before deploying. + + +Keep API handlers in `app/api/**/route.ts`. Agentuity does not require `createApp()` or an Agentuity-owned router for v3 Next.js apps. + + +## Next Steps + +- [Key-Value Storage](/services/storage/key-value): store short-lived state from route handlers +- [Deployment](/reference/cli/deployment): see CLI deploy flags and lifecycle script behavior +- [App Configuration](/get-started/app-configuration): configure project IDs, domains, and environment files diff --git a/docs/src/web/content/frameworks/nuxt.mdx b/docs/src/web/content/frameworks/nuxt.mdx new file mode 100644 index 000000000..c8e28efe0 --- /dev/null +++ b/docs/src/web/content/frameworks/nuxt.mdx @@ -0,0 +1,42 @@ +--- +title: Using Nuxt with Agentuity +short_title: Nuxt +description: Add Agentuity CLI wiring and deployment validation to a Nuxt app +--- + +Start with a normal [Nuxt](https://nuxt.com/docs) app. Agentuity keeps Nuxt's route and Nitro layout, then adds local development, project linking, and deploy validation around that shape. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-nuxt-app --framework nuxt +cd my-nuxt-app +``` + +The starter runs `nuxi init`, adds `@agentuity/cli`, adds a `deploy` script, and can overlay an AI example at `server/api/translate.ts` plus `app.vue`. + +For an existing Nuxt app, add the CLI and link the project: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity dev` runs the Nuxt `dev` script and passes Agentuity environment values into that process. + +## Validate Before Deploy + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +Nuxt is detected as its own framework, and the current build path packages it through the generic adapter. Check that launch metadata points at the Nitro server you expect and that static assets are coming from `.output/public`. + +## Next Steps + +- [App Configuration](/get-started/app-configuration): configure `agentuity.json`, env vars, and scripts +- [Build Configuration](/reference/cli/build-configuration): inspect launch metadata and packaging output +- [Standalone Packages](/reference/standalone-packages): add Agentuity services from server routes and server utilities diff --git a/docs/src/web/content/frameworks/react-router.mdx b/docs/src/web/content/frameworks/react-router.mdx new file mode 100644 index 000000000..4c4f79dec --- /dev/null +++ b/docs/src/web/content/frameworks/react-router.mdx @@ -0,0 +1,55 @@ +--- +title: Using React Router with Agentuity +short_title: React Router +description: Add Agentuity CLI wiring and deployment validation to a React Router app +--- + +Use this page for current [React Router](https://reactrouter.com/home) apps. The create flow still uses the slug `remix`, but it scaffolds a React Router app and the build detector also recognizes React Router projects separately. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-react-router-app --framework remix +cd my-react-router-app +``` + +The starter runs `create-react-router`, adds `@agentuity/cli`, adds a `deploy` script, and can overlay an AI example at `app/routes/api.translate.ts` plus `app/routes/home.tsx`. + +React Router framework mode uses `app/routes.ts` as the route map. If you add a resource route by hand, register it there too: + +```ts title="app/routes.ts" +import { type RouteConfig, index, route } from '@react-router/dev/routes'; + +export default [ + index('routes/home.tsx'), + route('api/translate', 'routes/api.translate.ts'), +] satisfies RouteConfig; +``` + +For an existing React Router app, add the CLI and link the project: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + + +Use `--framework remix` for project creation today. Build detection can still recognize React Router apps from `react-router.config.*` or the React Router Vite plugin. + + +## Validate Before Deploy + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +React Router currently packages through the generic build adapter. Check that the launch metadata and static asset paths match the app's `build/` output before the first deploy. + +## Next Steps + +- [App Configuration](/get-started/app-configuration): configure `agentuity.json`, env vars, and scripts +- [Build Configuration](/reference/cli/build-configuration): inspect launch metadata and packaging output +- [Standalone Packages](/reference/standalone-packages): add Agentuity services from route handlers and server code diff --git a/docs/src/web/content/frameworks/sveltekit.mdx b/docs/src/web/content/frameworks/sveltekit.mdx new file mode 100644 index 000000000..c5e5064d8 --- /dev/null +++ b/docs/src/web/content/frameworks/sveltekit.mdx @@ -0,0 +1,42 @@ +--- +title: Using SvelteKit with Agentuity +short_title: SvelteKit +description: Add Agentuity CLI wiring and deployment validation to a SvelteKit app +--- + +Start with a normal [SvelteKit](https://svelte.dev/docs/kit/introduction) app. Agentuity keeps the SvelteKit file layout and adds project linking, local development, and deploy validation around it. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-sveltekit-app --framework sveltekit +cd my-sveltekit-app +``` + +The starter runs `sv create`, adds `@agentuity/cli`, adds a `deploy` script, and can overlay an AI example at `src/routes/+page.server.ts` plus `src/routes/+page.svelte`. + +For an existing SvelteKit app, add the CLI and link the project: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity dev` runs the SvelteKit `dev` script with the Agentuity environment values your app needs. + +## Validate Before Deploy + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +SvelteKit is detected as its own framework, but the current build path still uses the generic adapter. In practice, validate the packaged server entry and static assets before deploy, especially if your app depends on a specific SvelteKit adapter. + +## Next Steps + +- [App Configuration](/get-started/app-configuration): configure `agentuity.json`, env vars, and scripts +- [Build Configuration](/reference/cli/build-configuration): inspect launch metadata and packaging output +- [Standalone Packages](/reference/standalone-packages): add Agentuity services from SvelteKit server code diff --git a/docs/src/web/content/frameworks/tanstack-start.mdx b/docs/src/web/content/frameworks/tanstack-start.mdx new file mode 100644 index 000000000..7e755c331 --- /dev/null +++ b/docs/src/web/content/frameworks/tanstack-start.mdx @@ -0,0 +1,102 @@ +--- +title: Using TanStack Start with Agentuity +short_title: TanStack Start +description: Add Agentuity service clients to a TanStack Start app and validate the detected build path +--- + +Use this page for an existing [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/overview) app. TanStack Start is not currently a `project create --framework` starter, so start with TanStack's CLI and validate the Agentuity build output before deploying. + +## Create the Framework App + +Use TanStack's own CLI first: + +```bash +bunx @tanstack/cli@latest create my-start-app --framework React --package-manager bun +cd my-start-app +``` + +Then add Agentuity: + +```bash +bun add @agentuity/keyvalue@alpha +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity project import` registers the app and writes the local project metadata that `agentuity deploy` expects. Run it once before the first deploy for existing apps. + +## Add a Server Route + +TanStack Start server routes can use Agentuity service clients directly. + +```tsx title="src/routes/api.visits.tsx" +import { KeyValueClient } from '@agentuity/keyvalue'; +import { createFileRoute } from '@tanstack/react-router'; + +interface VisitCounter { + readonly count: number; +} + +const kv = new KeyValueClient(); + +export const Route = createFileRoute('/api/visits')({ + server: { + handlers: { + GET: async () => { + const current = await kv.get('metrics', 'visits'); + const count = current.exists ? current.data.count + 1 : 1; + + await kv.set('metrics', 'visits', { count }, { ttl: null }); + + return Response.json({ count }); + }, + }, + }, +}); +``` + +`KeyValueClient` reads `AGENTUITY_SDK_KEY` from the runtime environment. During local development, `agentuity dev` runs your app's `dev` script and injects the same Agentuity environment variables that the framework process would otherwise need. + +## Run Locally + +```bash +agentuity dev +``` + +If you need Bun's runtime flag for your app, keep it in the package script: + +```json title="package.json" +{ + "scripts": { + "dev": "bun --bun vite dev --port 3000", + "build": "vite build", + "start": "node .output/server/index.mjs", + "deploy": "agentuity deploy" + } +} +``` + +## Validate Before Deploy + +Use `agentuity build` as the packaging check before relying on `agentuity deploy`. + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +If the Agentuity build cannot find the local `vite` binary, run it with the app's package binaries on `PATH`: + +```bash +PATH="$(pwd)/node_modules/.bin:$PATH" agentuity build +``` + +For an SSR app, the launch metadata should point at your server entry, not only an injected static file server. If the package only contains static assets, keep the framework app on its current host and use Agentuity service clients from server routes until your build packaging is explicit. + +## Next Steps + +- [Key-Value Storage](/services/storage/key-value): use the same client API from TanStack server routes +- [Deployment](/reference/cli/deployment): review deploy behavior before wiring CI +- [Build Configuration](/reference/cli/build-configuration): inspect detected framework output and launch metadata diff --git a/docs/src/web/content/frameworks/vite-react.mdx b/docs/src/web/content/frameworks/vite-react.mdx new file mode 100644 index 000000000..49fca9836 --- /dev/null +++ b/docs/src/web/content/frameworks/vite-react.mdx @@ -0,0 +1,42 @@ +--- +title: Using Vite React with Agentuity +short_title: Vite + React +description: Add Agentuity CLI wiring and deployment validation to a Vite React app +--- + +Start with a normal [Vite](https://vite.dev/guide/) React app. Agentuity adds local development and deploy wiring, plus an optional example server, without replacing the usual Vite project shape. + +## Create a Starter + +```bash +bun create agentuity@alpha --name my-vite-app --framework vite-react +cd my-vite-app +``` + +The starter runs `create-vite`, adds `@agentuity/cli`, adds a `deploy` script, and can overlay an example `server.ts`, `src/App.tsx`, and `vite.config.ts`. + +For an existing Vite React app, add the CLI and link the project: + +```bash +bun add -d @agentuity/cli@alpha +agentuity project import --validate-only +agentuity project import +``` + +`agentuity dev` runs your Vite `dev` script with Agentuity environment values. Use that when the app needs `AGENTUITY_SDK_KEY` locally. + +## Validate Before Deploy + +```bash +bun run build +agentuity build +cat .agentuity/launch.json +``` + +`vite-react` is a create slug. The build path detects the app as generic `vite`, so validate the packaged output before deploy. For a static SPA, confirm the bundle is treated as static output. If you keep the optional `server.ts` or add your own server, confirm the launch metadata points at that process instead. + +## Next Steps + +- [App Configuration](/get-started/app-configuration): configure `agentuity.json`, env vars, and scripts +- [Build Configuration](/reference/cli/build-configuration): inspect launch metadata and packaging output +- [Standalone Packages](/reference/standalone-packages): add Agentuity services from server code or utilities diff --git a/docs/src/web/content/frontend/advanced-hooks.mdx b/docs/src/web/content/frontend/advanced-hooks.mdx index 3af944539..24cd2adc8 100644 --- a/docs/src/web/content/frontend/advanced-hooks.mdx +++ b/docs/src/web/content/frontend/advanced-hooks.mdx @@ -56,7 +56,7 @@ export function ModeratedRoom({ roomId }: { roomId: string }) { } ``` -Use this pattern when you need richer call telemetry, custom data channel flows, or room-specific UX beyond the basic examples on [WebRTC](/routes/webrtc). +Use this pattern when you need richer call telemetry, custom data channel flows, or room-specific UX beyond the basic examples on [WebRTC](/patterns/chat-and-streaming). ## WebSocketManager @@ -204,5 +204,4 @@ If you are not in React, import the same low-level managers directly from `@agen - [React Hooks](/frontend/react-hooks): Auth, analytics, and WebRTC hooks from `@agentuity/react` - [RPC Client](/frontend/rpc-client): Type-safe route calls with `hc()` -- [WebSockets](/routes/websockets): Server-side WebSocket routes -- [Server-Sent Events](/routes/sse): Server-side SSE routes +- [Chat and Streaming](/patterns/chat-and-streaming): server-side WebSocket and SSE patterns diff --git a/docs/src/web/content/frontend/authentication.mdx b/docs/src/web/content/frontend/authentication.mdx index 12075ff12..0cabe8a82 100644 --- a/docs/src/web/content/frontend/authentication.mdx +++ b/docs/src/web/content/frontend/authentication.mdx @@ -1,508 +1,188 @@ --- -title: Adding Authentication +title: Frontend Authentication short_title: Authentication -description: Add user authentication with Agentuity Auth +description: Connect browser UI to framework-owned sessions and Agentuity OIDC routes --- -Protect your agents and routes with user authentication. Agentuity provides a first-party auth solution powered by [BetterAuth](https://better-auth.com). +In v3, frontend auth state belongs to your app. The browser signs in through your framework routes, then calls your own API routes with cookies or headers. Those server routes validate the user and call Agentuity services with server-only credentials. - -This page covers React frontend auth setup. For a platform overview covering API keys, bearer tokens, and session auth configuration, see [Authentication Services](/services/authentication). - - -## Full-Stack Auth in Seconds - - - ```tsx -const { user, isAuthenticated } = useAuth(); -if (!isAuthenticated) return ; -return
Welcome, {user.name}
; -``` -
- -```typescript -router.get('/me', authMiddleware, async (c) => { - const user = await c.var.auth.getUser(); - return c.json(user); -}); -``` - - -```typescript -handler: async (ctx, input) => { - const user = await ctx.auth?.getUser(); - return `Hello, ${user?.name ?? 'anonymous'}!`; -} -``` - -
+import { useEffect, useState } from 'react'; -## What You Get - -- **Email/password authentication** out of the box -- **Session and API key middleware** for routes -- **Native `ctx.auth` support** in agents -- **Organizations, teams, and roles** via BetterAuth plugins -- **JWT tokens** for external service integration - -## Quick Start - -Use the `agentuity-auth` template, or add auth to any project manually: - -```bash -# Start from the auth template -bunx agentuity create my-app --template agentuity-auth - -# Or add auth to an existing project -bun add @agentuity/auth better-auth drizzle-orm -``` - -Then create your auth configuration (see [Server Setup](#server-setup) below), set `DATABASE_URL` and `AGENTUITY_AUTH_SECRET` in your environment, and run the BetterAuth CLI to generate and apply the schema (use `bunx` or `npx`): - -```bash -# With Bun -bunx @better-auth/cli generate -bunx @better-auth/cli migrate +interface AppUser { + readonly id: string; + readonly email: string | null; + readonly name: string | null; +} -# Or with npm -npx @better-auth/cli generate -npx @better-auth/cli migrate -``` +interface SessionResponse { + readonly authenticated: boolean; + readonly user: AppUser | null; +} -## Server Setup +function readAppUser(value: unknown): AppUser | null { + if (typeof value !== 'object' || value === null) { + return null; + } -### The Basics + if (!('id' in value) || typeof value.id !== 'string') { + return null; + } -Create an auth instance with just a connection string: + if (!('email' in value) || (typeof value.email !== 'string' && value.email !== null)) { + return null; + } -```typescript title="src/auth.ts" -import { createAuth } from '@agentuity/auth'; + if (!('name' in value) || (typeof value.name !== 'string' && value.name !== null)) { + return null; + } -export const auth = createAuth({ - connectionString: process.env.DATABASE_URL, -}); -``` + return { + id: value.id, + email: value.email, + name: value.name, + }; +} -That's it! This gives you: -- Email/password authentication -- Session management -- All default plugins (see below) +function readSessionResponse(value: unknown): SessionResponse { + if (typeof value !== 'object' || value === null) { + return { authenticated: false, user: null }; + } -### Default Plugins + if (!('authenticated' in value) || typeof value.authenticated !== 'boolean') { + return { authenticated: false, user: null }; + } -Agentuity Auth includes these plugins automatically: + if (!value.authenticated || !('user' in value)) { + return { authenticated: false, user: null }; + } -| Plugin | Purpose | -|--------|---------| -| `organization` | Multi-tenancy with teams, roles, and invitations | -| `jwt` | JWT token generation with JWKS endpoint | -| `bearer` | Bearer token auth via `Authorization` header | -| `apiKey` | API key authentication for programmatic access | + const user = readAppUser(value.user); - -If you need full control over plugins, use `skipDefaultPlugins: true` and add only what you need. - + if (!user) { + return { authenticated: false, user: null }; + } -### Mounting Auth Routes + return { + authenticated: true, + user, + }; +} -Mount the auth handler to expose sign-in, sign-up, session, and other endpoints: +export function AccountMenu() { + const [session, setSession] = useState(null); -```typescript title="src/api/index.ts" -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import { mountAuthRoutes } from '@agentuity/auth'; -import { auth } from '../auth'; + useEffect(() => { + let active = true; -const router = new Hono(); + async function loadSession(): Promise { + const response = await fetch('/auth/me', { credentials: 'include' }); + const nextSession = readSessionResponse(await response.json()); -// If app.ts mounts this router at /api, these are served at /api/auth/* -router.on(['GET', 'POST'], '/auth/*', mountAuthRoutes(auth)); + if (active) { + setSession(nextSession); + } + } -export default router; -``` + void loadSession(); -### Advanced Configuration + return () => { + active = false; + }; + }, []); -```typescript title="src/auth.ts" -import { createAuth } from '@agentuity/auth'; + if (!session) { + return ; + } -export const auth = createAuth({ - connectionString: process.env.DATABASE_URL, - // Or: database: drizzleAdapter(db, { provider: 'pg', schema: authSchema }), + if (!session.authenticated) { + return Sign in with Agentuity; + } - skipDefaultPlugins: false, // Set true for full control over plugins - apiKey: { enabled: true, defaultPrefix: 'ag_', defaultKeyLength: 64 }, - trustedOrigins: ['https://your-domain.com'], // Auto-resolved from env by default - plugins: [], // Add custom BetterAuth plugins -}); + return ( +
+ {session.user?.email} + +
+ ); +} ``` -## Middleware +The component above does not import an Agentuity auth provider. It reads your app's session endpoint and sends the user to your app's login route. -### Session Middleware +## Server Routes Own the Session -Protect routes with session-based authentication: +Keep the auth provider, session lookup, and Agentuity service calls on the server side. ```typescript -import { createSessionMiddleware } from '@agentuity/auth'; -import { auth } from '../auth'; - -// Required authentication (returns 401 if not authenticated) -const authMiddleware = createSessionMiddleware(auth); - -// Optional authentication (continues without auth context if not authenticated) -const optionalAuth = createSessionMiddleware(auth, { optional: true }); +import { KeyValueClient } from '@agentuity/keyvalue'; -// Role-based access (returns 403 if user lacks required role) -const adminOnly = createSessionMiddleware(auth, { hasOrgRole: ['admin', 'owner'] }); -``` - -**Usage examples:** - -```typescript -// Protect one route -router.get('/me', authMiddleware, async (c) => { - const user = await c.var.auth.getUser(); - return c.json({ id: user.id, email: user.email, name: user.name }); -}); - -// Allow both authenticated and anonymous access -router.get('/content', optionalAuth, async (c) => { - const user = await c.var.auth.getUser().catch(() => null); - return c.json({ premium: !!user }); -}); - -// Admin-only route -router.get('/admin', adminOnly, async (c) => { - return c.json({ message: 'Welcome, admin!' }); -}); -``` - -### API Key Middleware - -For programmatic access via API keys: +interface AppUser { + readonly id: string; + readonly email: string; +} -```typescript -import { createApiKeyMiddleware } from '@agentuity/auth'; -import { auth } from '../auth'; +interface AppSession { + readonly user: AppUser; +} -const apiKeyAuth = createApiKeyMiddleware(auth); -const optionalApiKey = createApiKeyMiddleware(auth, { optional: true }); -const writeAccess = createApiKeyMiddleware(auth, { hasPermission: { project: 'write' } }); -const fullAccess = createApiKeyMiddleware(auth, { hasPermission: { project: ['read', 'write'], admin: '*' } }); -``` +interface DashboardState { + readonly lastOpenedAt: string; +} -**API keys are sent via headers:** `x-agentuity-auth-api-key: your_key` or `Authorization: ApiKey your_key` +declare function getSession(request: Request): Promise; -### The Auth Context +const kv = new KeyValueClient(); -When middleware authenticates a request, `c.var.auth` provides these methods: +export async function GET(request: Request): Promise { + const session = await getSession(request); -| Method | Returns | Description | -|--------|---------|-------------| -| `getUser()` | `Promise` | Get the authenticated user | -| `getOrg()` | `Promise` | Get active organization with full details | -| `getOrgRole()` | `Promise` | Get user's role in active org | -| `hasOrgRole(...roles)` | `Promise` | Check if user has one of the specified roles | -| `hasPermission(resource, ...actions)` | `boolean` | Check API key permissions | -| `getToken()` | `Promise` | Get the bearer token from request | -| `authMethod` | `'session' \| 'api-key' \| 'bearer'` | How the request was authenticated | -| `apiKey` | `AuthApiKeyContext \| null` | API key details (if authenticated via API key) | + if (!session) { + return Response.json({ authenticated: false }, { status: 401 }); + } -**Example:** + const result = await kv.get('dashboard-state', session.user.id); -```typescript -router.get('/profile', authMiddleware, async (c) => { - const user = await c.var.auth.getUser(); - const org = await c.var.auth.getOrg(); - const isAdmin = await c.var.auth.hasOrgRole('admin', 'owner'); - - return c.json({ - user: { id: user.id, email: user.email, name: user.name }, - organization: org ? { id: org.id, name: org.name, role: org.role } : null, - isAdmin, + return Response.json({ + authenticated: true, + user: session.user, + state: result.exists ? result.data : null, }); -}); -``` - - -See the complete type definitions in the [auth package types](https://github.com/agentuity/sdk/tree/main/packages/auth/src/agentuity/types.ts). - - -## Client Setup - -### Creating the Auth Client - -```typescript title="src/web/auth-client.ts" -import { createAuthClient } from '@agentuity/auth/react'; - -export const authClient = createAuthClient(); - -// Export methods for use in components -export const { signIn, signUp, signOut, useSession, getSession } = authClient; -``` - -### AuthProvider - -Wrap your app with `AuthProvider`. Add `AgentuityProvider` only if you also use `@agentuity/react` transport hooks and need `AuthProvider` to mirror the bearer token into them: - -```tsx title="src/web/frontend.tsx" -import { useState } from 'react'; -import { AgentuityProvider } from '@agentuity/react'; -import { AuthProvider, createAuthClient } from '@agentuity/auth/react'; -import { App } from './App'; - -const authClient = createAuthClient(); - -export function Root() { - const [authHeader, setAuthHeader] = useState(null); - - return ( - - - - - - ); } ``` -### useAuth Hook +Use your framework's normal route shape for this code. In Next.js this is a route handler. In TanStack Start, Remix, SvelteKit, Nuxt, Astro, or Hono, the same work happens in the server route for that framework. -Access auth state in your components: +## Add Sign in with Agentuity -```tsx -import { useAuth } from '@agentuity/auth/react'; - -function Profile() { - const { user, isAuthenticated, isPending, error } = useAuth(); - - if (isPending) return
Loading...
; - if (error) return
Error: {error.message}
; - if (!isAuthenticated) return
Please sign in
; +When Agentuity is the identity provider, the frontend only needs links or forms to your auth routes: +```tsx +export function SignedOutActions() { return (
-

Welcome, {user.name}!

-

Email: {user.email}

+ Sign in with Agentuity
); } ``` -### Sign In / Sign Up / Sign Out - -```tsx -import { signIn, signUp, signOut } from './auth-client'; - -// Email/password sign in -await signIn.email({ email, password }); - -// Email/password sign up -await signUp.email({ email, password, name }); - -// Sign out -await signOut(); -``` - -## Using Auth in Agents - -### The ctx.auth Interface - -Auth is available natively on `ctx.auth` in agent handlers: - -```typescript title="src/agent/protected/agent.ts" -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -export default createAgent('protected', { - schema: { - input: s.object({ query: s.string() }), - output: s.object({ result: s.string(), userId: s.string() }), - }, - handler: async (ctx, { query }) => { - if (!ctx.auth) { - return { result: 'Please sign in', userId: '' }; - } - - const user = await ctx.auth.getUser(); - const org = await ctx.auth.getOrg(); - - // Check organization roles - if (org && await ctx.auth.hasOrgRole('admin')) { - // Admin-only logic - } - - return { result: `Hello ${user.name}`, userId: user.id }; - }, -}); -``` - -### Agent-to-Agent Auth Propagation - -When one agent calls another, auth context propagates automatically: - -```typescript title="src/agent/hello/agent.ts" -import { createAgent } from '@agentuity/runtime'; -import poemAgent from '../poem/agent'; - -export default createAgent('hello', { - handler: async (ctx, { name }) => { - const user = ctx.auth ? await ctx.auth.getUser() : null; - - // Auth context passes through to poem agent automatically - const poem = await poemAgent.run({ - userEmail: user?.email, - userName: name, - }); - - return `Hello ${name}!\n\n${poem}`; - }, -}); -``` - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `DATABASE_URL` | PostgreSQL connection string | **Required** | -| `AGENTUITY_AUTH_SECRET` | Secret for signing tokens | Falls back to `BETTER_AUTH_SECRET` | -| `AGENTUITY_CLOUD_BASE_URL` | Platform-provided base URL for auth callbacks | Checked before `AGENTUITY_BASE_URL` | -| `AGENTUITY_BASE_URL` | Base URL for auth callbacks | Falls back to `BETTER_AUTH_URL` | - -**Auto-resolved trusted origins:** -- `AGENTUITY_CLOUD_DOMAINS` - Platform-set domains (deployment URLs, custom domains) -- `AUTH_TRUSTED_DOMAINS` - Developer-set additional trusted domains (comma-separated) - -**Generate a secure secret:** - -```bash -openssl rand -hex 32 -``` - - -If you're using a development secret, generate a new one before deploying to production. Store it securely in your environment variables. - - -## Database Configuration - -### Connection String (Simplest) - -Just provide the connection string and Agentuity handles the rest: - -```typescript -import { createAuth } from '@agentuity/auth'; - -export const auth = createAuth({ - connectionString: process.env.DATABASE_URL, -}); -``` - -### Bring Your Own Drizzle - -If you have an existing Drizzle setup: - -```typescript -import { drizzle } from 'drizzle-orm/bun-sql'; -import { drizzleAdapter } from '@agentuity/drizzle'; -import * as authSchema from '@agentuity/auth/schema'; -import * as appSchema from './schema'; - -const schema = { ...authSchema, ...appSchema }; -const databaseUrl = process.env.DATABASE_URL; -if (!databaseUrl) throw new Error('DATABASE_URL is required'); - -const db = drizzle(databaseUrl, { schema }); - -export const auth = createAuth({ - database: drizzleAdapter(db, { provider: 'pg', schema: authSchema }), -}); -``` - -The `@agentuity/auth/schema` export provides all auth-related Drizzle tables (`user`, `session`, `account`, `verification`, `organization`, `member`, `invitation`, `apiKey`). - -## Built-in Features - -### Organizations & Teams - -Create and manage organizations: - -```typescript -// Create an organization -const org = await auth.api.createOrganization({ - body: { name: 'My Team', slug: 'my-team' }, - headers: c.req.raw.headers, -}); - -// Get user's role in active org -const role = await c.var.auth.getOrgRole(); - -// Check role -if (await c.var.auth.hasOrgRole('admin', 'owner')) { - // Admin actions -} -``` - - -See the [BetterAuth Organization Plugin docs](https://www.better-auth.com/docs/plugins/organization) for the complete API including invitations, member management, and role configuration. - +The `/auth/login`, `/auth/callback`, `/auth/me`, and `/auth/logout` routes live on the server. See [Sign in with Agentuity](/services/oidc-provider): it shows the OAuth redirect, callback state check, token exchange, and local app session. -### API Keys +## Protect Browser Calls -Create API keys for programmatic access: +- Call your own API routes from the browser. +- Validate the session in each route that needs a signed-in user. +- Keep `AGENTUITY_SDK_KEY`, `OAUTH_CLIENT_SECRET`, and refresh tokens out of browser bundles. +- Store OAuth tokens server-side when you request scoped Agentuity access. -```typescript -const result = await auth.api.createApiKey({ - body: { - name: 'my-integration', - userId: user.id, - expiresIn: 60 * 60 * 24 * 30, // 30 days - permissions: { project: ['read', 'write'] }, - }, -}); -console.log('API Key:', result.key); // Only shown once! -``` - - -See the [BetterAuth API Key Plugin docs](https://www.better-auth.com/docs/plugins/api-key) for listing, revoking, and permission schemas. + +The v2 `@agentuity/auth/react` provider and hooks are not the default v3 frontend path. Existing v2 apps can keep using the v2 docs while new v3 apps use framework-owned auth or Agentuity OIDC routes. -### JWT & Bearer Tokens - -```typescript -// Get token in route handler -const token = await c.var.auth.getToken(); - -// JWKS endpoint: GET /api/auth/.well-known/jwks.json -``` - - -See the [BetterAuth JWT Plugin docs](https://www.better-auth.com/docs/plugins/jwt) for token customization and verification. - - -## Schema & Migrations - -Use the BetterAuth CLI to generate and apply the auth schema (use `bunx` or `npx`): - -```bash -# Generate SQL for all auth tables (useful for review or manual migrations) -bunx @better-auth/cli generate -# or: npx @better-auth/cli generate - -# Apply migrations directly -bunx @better-auth/cli migrate -# or: npx @better-auth/cli migrate -``` - -If you use Drizzle or Prisma, prefer your ORM's own migration tooling and generate the schema with `@better-auth/cli generate`, then incorporate it into your migration files. - ## Next Steps -- [Middleware & Routes](/routes/middleware): Authentication and validation patterns -- [Provider Setup](/frontend/provider-setup): `AgentuityProvider` configuration for `@agentuity/react` apps -- [React Hooks](/frontend/react-hooks): Building custom UIs -- [Authentication Services](/services/authentication): Choose between OIDC and app-owned auth +- [Sign in with Agentuity](/services/oidc-provider): add OIDC routes for Agentuity account sign-in +- [Choosing Authentication](/services/authentication): decide where identity and sessions should live +- [Using Hono with Agentuity](/frameworks/hono): expose Agentuity service clients in Hono routes +- [App Configuration](/get-started/app-configuration#environment-variables): configure server-only Agentuity keys diff --git a/docs/src/web/content/frontend/provider-setup.mdx b/docs/src/web/content/frontend/provider-setup.mdx index c3cd3ccf7..5d748f3b2 100644 --- a/docs/src/web/content/frontend/provider-setup.mdx +++ b/docs/src/web/content/frontend/provider-setup.mdx @@ -142,7 +142,7 @@ const res = await client.chat.$post({ json: { message: 'Hello' } }); // Type-saf ``` -The typed client comes from your exported router type. If route inference looks wrong, check that your root router is exported as a single chained expression, like the current testing apps. +The typed client comes from your exported router type. If route inference looks wrong, check that your root router is exported as a single chained expression. ## Next Steps diff --git a/docs/src/web/content/frontend/react-hooks.mdx b/docs/src/web/content/frontend/react-hooks.mdx index a6fbcd664..ec8fb3ec1 100644 --- a/docs/src/web/content/frontend/react-hooks.mdx +++ b/docs/src/web/content/frontend/react-hooks.mdx @@ -163,7 +163,7 @@ function CallPanel({ roomId }: { roomId: string }) { } ``` -For signaling routes, rooms, data channels, and media options, see [WebRTC](/routes/webrtc). +For signaling routes, rooms, data channels, and media options, see [WebRTC](/patterns/chat-and-streaming). ## Analytics Hooks @@ -202,7 +202,7 @@ Use the transport that matches the route you are calling: | Peer-to-peer | `useWebRTCCall` | Video/audio calls, data channels | | Request/response | `hc()` or `fetch` | API calls, form submissions | -See [WebSockets](/routes/websockets), [SSE](/routes/sse), and [WebRTC](/routes/webrtc) for server-side setup. +See [WebSockets](/patterns/chat-and-streaming), [SSE](/patterns/chat-and-streaming), and [WebRTC](/patterns/chat-and-streaming) for server-side setup. ## Next Steps diff --git a/docs/src/web/content/frontend/rpc-client.mdx b/docs/src/web/content/frontend/rpc-client.mdx index e9dcbc085..abe956fac 100644 --- a/docs/src/web/content/frontend/rpc-client.mdx +++ b/docs/src/web/content/frontend/rpc-client.mdx @@ -249,5 +249,4 @@ External backends like Next.js or Express can use HTTP to call Agentuity routes - [React Hooks](/frontend/react-hooks): Provider, auth, analytics, and WebRTC hooks from `@agentuity/react` - [Provider Setup](/frontend/provider-setup): `AgentuityProvider` configuration for `@agentuity/react` apps - [Adding Authentication](/frontend/authentication): Protect routes with Agentuity Auth -- [WebSockets](/routes/websockets): Create WebSocket routes -- [Server-Sent Events](/routes/sse): Create SSE routes +- [Chat and Streaming](/patterns/chat-and-streaming): Create WebSocket and SSE routes diff --git a/docs/src/web/content/get-started/app-configuration.mdx b/docs/src/web/content/get-started/app-configuration.mdx index e2835d8a8..9c77c9025 100644 --- a/docs/src/web/content/get-started/app-configuration.mdx +++ b/docs/src/web/content/get-started/app-configuration.mdx @@ -1,20 +1,44 @@ --- title: App Configuration -description: Configure your Agentuity project +description: Configure a v3 Agentuity framework project --- -Agentuity projects keep runtime behavior in code and deployment metadata in `agentuity.json`. +In v3, your framework owns routing and startup. Agentuity configuration lives in `agentuity.json`, package scripts, environment variables, and the service clients you import. + +## package.json + +The framework's create command owns your `dev` and `build` scripts. Agentuity adds a `deploy` script: + +```json title="package.json" +{ + "scripts": { + "dev": "...", + "build": "...", + "deploy": "agentuity deploy" + } +} +``` + +Use `agentuity dev` when you want the CLI to run your framework's `dev` script with Agentuity development behavior: + +```bash +agentuity dev +agentuity dev --port 8080 +agentuity dev --script dev:web +``` + +`agentuity dev` reads `package.json` and runs the selected script. The default script is `dev`. ## agentuity.json -The project configuration file: +Registered projects use `agentuity.json` for cloud project metadata and deploy resources: -```json +```json title="agentuity.json" { "$schema": "https://agentuity.dev/schema/cli/v1/agentuity.json", "projectId": "proj_...", "orgId": "org_...", - "region": "use", + "region": "usc", "deployment": { "resources": { "memory": "500Mi", @@ -26,166 +50,29 @@ The project configuration file: } ``` -No agent definitions, no trigger configurations. Those live in your code. - -## app.ts - -The app entry point wires agents, routes, and configuration together: - -```typescript -import { createApp } from '@agentuity/runtime'; -import agents from './src/agent'; -import api from './src/api'; - -const app = await createApp({ - router: { path: '/api', router: api }, - agents, - analytics: true, -}); - -app.logger.debug('Running %s', app.server.url); - -export default app; -``` - -### With Startup and Cleanup Logic - -Initialize shared resources in modules or startup code, then register cleanup with `registerShutdownHook()`: - -```typescript -import { createApp, registerShutdownHook } from '@agentuity/runtime'; - -const db = await connectDatabase(); - -registerShutdownHook(async () => { - await db.close(); -}); - -const app = await createApp({ - router: { path: '/api', router: api }, - agents, -}); - -app.logger.debug('Running %s', app.server.url); - -export default app; -``` - -### With Additional Options - -```typescript -import { createApp } from '@agentuity/runtime'; -import agents from './src/agent'; -import api from './src/api'; - -const app = await createApp({ - router: { path: '/api', router: api }, - agents, - - // CORS configuration - cors: { - sameOrigin: true, - allowedOrigins: ['https://myapp.com'], - }, - - // Response compression (gzip/deflate) - compression: { - threshold: 1024, // Compress responses larger than 1KB (default) - }, - - // Custom storage implementations (optional) - services: { - keyvalue: myCustomKV, - vector: myCustomVector, - }, -}); - -export default app; -``` - -#### Compression Options - -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `enabled` | `boolean` | `true` | Enable/disable compression | -| `threshold` | `number` | `1024` | Minimum response size (bytes) to compress | -| `filter` | `(c) => boolean` | - | Custom filter function | - -Compression automatically bypasses WebSocket upgrades and requests without `Accept-Encoding` headers. - -### Event Listeners - -Listen for agent lifecycle events using the top-level `addEventListener`: - -```typescript -import { addEventListener } from '@agentuity/runtime'; - -addEventListener('agent.started', (eventName, agent, ctx) => { - ctx.logger.info('Agent started', { name: agent.metadata.name }); -}); - -addEventListener('agent.completed', (eventName, agent, ctx) => { - ctx.logger.info('Agent completed', { session: ctx.sessionId }); -}); -``` - -## Build Configuration - -Use `vite.config.ts` for build settings (plugins, entry point, rollup options). Runtime settings like analytics and service configuration belong in `createApp()`. - -```typescript title="vite.config.ts" -import react from '@vitejs/plugin-react'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vite'; -import { join } from 'node:path'; - -export default defineConfig({ - plugins: [react(), tailwindcss()], - root: '.', - build: { - rollupOptions: { - input: join(import.meta.dirname, 'src/web/index.html'), - }, - }, -}); -``` +The CLI writes this file when a project is registered during `agentuity project create` or linked later with `agentuity project import`. - -Most projects only need to change `plugins` in `vite.config.ts`. The default template includes React and Tailwind CSS. Replace them with your framework of choice (e.g., `@sveltejs/vite-plugin-svelte`). + +`agentuity.json` stores deployment metadata. Define pages, API routes, middleware, and server handlers in your framework files. -See the [reference page](/reference/cli/build-configuration) for all options and examples. +## Deployment Resources -## Configuring Deployment and Build Resources +Use `deployment` for the running app and `build` for the cloud build sandbox: -Configure how your project runs and builds in the cloud using the `deployment` and `build` sections of `agentuity.json`. - -### deployment - -The `deployment` section controls resources allocated when your project is handling requests, and any custom domains: - -- `deployment.resources`: CPU, memory, and disk allocated to the running service -- `deployment.domains`: custom domains attached to the project - -### build - -The `build` section controls the sandbox where your project compiles during cloud deployment: - -- `build.timeout`: maximum time allowed for the build (e.g., `"30m"`) -- `build.resources`: memory, CPU, and disk for the build sandbox - -```json +```json title="agentuity.json" { + "$schema": "https://agentuity.dev/schema/cli/v1/agentuity.json", "projectId": "proj_...", "orgId": "org_...", - "region": "use", + "region": "usc", "deployment": { "resources": { "memory": "500Mi", "cpu": "500m", "disk": "500Mi" }, - "domains": [] + "domains": ["app.example.com"] }, "build": { "timeout": "30m", @@ -198,146 +85,76 @@ The `build` section controls the sandbox where your project compiles during clou } ``` -`deployment.resources` controls what your service gets when handling requests. `build.resources` controls the isolated sandbox where your project compiles. Increase build resources if you have large dependencies or complex compilation steps. - -For detailed options, see [Deploying to the Cloud](/reference/cli/deployment#resource-configuration). - -### Run Deploy Lifecycle Scripts - -Run custom steps before or after deployment using `predeploy` and `postdeploy` scripts in `package.json`: - -```json title="package.json" -{ - "scripts": { - "predeploy": "bun run typecheck", - "deploy": "agentuity deploy", - "postdeploy": "echo 'Deploy complete'" - } -} -``` - -Package manager lifecycle hooks run when you call `bun run deploy`. Running `agentuity deploy` directly skips `predeploy` and `postdeploy`. +Increase `build.resources` when dependency installation or framework compilation needs more room. Increase `deployment.resources` when the running app needs more CPU, memory, or disk. -When deploying via the [Agentuity GitHub App](/reference/github-app#build-process), the platform always runs `bun install` first. If `predeploy` is defined, it runs after dependency installation and before `agentuity deploy`. See [Running Deploy Lifecycle Scripts](/reference/cli/deployment#running-deploy-lifecycle-scripts) for the full reference. +See [Deploying to the Cloud](/reference/cli/deployment#resource-configuration): resource options and deploy behavior. ## Environment Variables -```bash -# Required -AGENTUITY_SDK_KEY=... # API key for Agentuity services - -# Optional -AGENTUITY_LOG_LEVEL=info # trace, debug, info, warn, error -PORT=3500 # Dev server port (default: 3500) +Use `.env` files for local development and cloud environment sync: -# Resource Credentials (auto-added by CLI when creating resources) -DATABASE_URL=postgresql://... # Added by: agentuity cloud db create -# S3 credentials # Added by: agentuity cloud s3 create -# REDIS_URL=redis://... # From: agentuity cloud redis show - -# LLM Provider Keys (optional; if using your own API keys instead of the AI Gateway) +```bash title=".env" +AGENTUITY_SDK_KEY=sdk_... OPENAI_API_KEY=sk-... -ANTHROPIC_API_KEY=sk-ant-... - -# Frontend-accessible (exposed to browser) -AGENTUITY_PUBLIC_API_URL=... # Any of these prefixes work: -VITE_MY_VAR=... # - AGENTUITY_PUBLIC_* -PUBLIC_MY_VAR=... # - VITE_* - # - PUBLIC_* +AGENTUITY_PUBLIC_API_URL=https://your-project.agentuity.run ``` - -If you don't set provider API keys, LLM requests are routed through the Agentuity AI Gateway using your SDK key. This provides unified billing and monitoring. - - - -Environment variables prefixed with `AGENTUITY_PUBLIC_`, `VITE_`, or `PUBLIC_` are exposed to the frontend bundle and visible in the browser. Never put secrets or API keys in these variables. - - -### Agentuity Keys +Common variables: -Use `AGENTUITY_SDK_KEY` in project code. The CLI writes this key to `.env` when you create, import, reconcile, or pull a project environment. Runtime services, AI Gateway calls, standalone packages, and deployed app code read it when they call Agentuity services. +| Variable | Purpose | +|----------|---------| +| `AGENTUITY_SDK_KEY` | Authenticates service clients and AI Gateway requests from app code | +| `PORT` | Set by `agentuity dev --port` for framework dev servers that honor `PORT` | +| `OPENAI_API_KEY` | Optional provider key. If omitted, `agentuity dev` can route OpenAI through AI Gateway when `AGENTUITY_SDK_KEY` is available | +| `ANTHROPIC_API_KEY` | Optional provider key. If omitted, `agentuity dev` can route Anthropic through AI Gateway when `AGENTUITY_SDK_KEY` is available | +| `GROQ_API_KEY` | Optional provider key. If omitted, `agentuity dev` can route Groq through AI Gateway when `AGENTUITY_SDK_KEY` is available | -CLI authentication is separate. `agentuity auth login` stores the CLI credential in your OS keychain or Agentuity config. Automation can pass `AGENTUITY_API_KEY`, or `AGENTUITY_CLI_API_KEY` with `AGENTUITY_USER_ID`, so the CLI can run project and deployment commands without an interactive login. Do not read those variables from app code. - -`AGENTUITY_CLI_KEY` is not the CLI login variable. Some standalone clients accept it as a compatibility fallback API key env var, but new examples and projects should use `AGENTUITY_SDK_KEY` or pass `apiKey` directly. - -### Environment-Specific Files + +Frameworks have their own public env var prefixes. The Agentuity CLI treats `AGENTUITY_PUBLIC_`, `VITE_`, and `PUBLIC_` as public when syncing environment variables. Never put API keys or tokens in public variables. + -Use separate `.env` files instead of commenting/uncommenting variables when switching between local development and deployed contexts. +Keep `AGENTUITY_SDK_KEY` in the same local env files your framework already uses. Commands such as `agentuity dev` and `agentuity deploy` need that key when your app calls Agentuity services or AI Gateway. -**File loading order today:** +## Service Clients -| Mode | Files checked | -|------|---------------| -| `agentuity dev` / local profile | `.env.{profile}` → `.env.development` → `.env` | -| Production build or non-local profile | `.env.{profile}` → `.env` → `.env.production` | +Add Agentuity services to the framework code that needs them. Standalone clients read `AGENTUITY_SDK_KEY` by default: -Files loaded later in the chain override earlier ones. For `agentuity dev`, the order is `.env.{profile}` then `.env.development` then `.env`, so `.env` wins when keys conflict. For deployed or non-local profiles, the order is `.env.{profile}` then `.env` then `.env.production`, so `.env.production` wins. +```bash +bun add @agentuity/keyvalue@alpha +``` -Profile-specific files use the active CLI profile. See [CLI Profiles](/reference/cli/profiles) for switching profiles and profile-specific defaults. +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; -**Example setup:** +const kv = new KeyValueClient(); -```bash title=".env" -# Shared defaults loaded in development and production -AGENTUITY_SDK_KEY=sdk_... -OPENAI_API_KEY=sk-... -AGENTUITY_PUBLIC_API_URL=https://your-project.agentuity.run +await kv.set('settings', 'theme', { value: 'dark' }); ``` -```bash title=".env.development" -# Development-only values -DEBUG=true -TRUSTED_ORIGINS=http://localhost:3000 -``` +Use the same pattern for queues, vector storage, email, schedules, tasks, sandbox, and other standalone packages. See [Using Standalone Packages](/reference/standalone-packages): package names and constructor options. - -Instead of commenting out lines when switching contexts, put shared defaults in `.env` and values that only exist in local development in `.env.development`. Be careful with duplicate keys: the CLI loads `.env.development` before `.env`, so `.env` (loaded last) wins when both define the same variable. - - -**Additional files:** +## Deploy Lifecycle Scripts -- **`.env.local`**: Bun may use this for app runtime behavior, but the Agentuity CLI does not currently include `.env.local` when it searches for `AGENTUITY_SDK_KEY` during `agentuity dev` or deploy preflight. Keep required CLI credentials in one of the files listed above. +Package manager lifecycle hooks run when you call the package script: -**Git behavior:** The default `.gitignore` ignores `.env` and `.env.*`. If you add differently named secret files, add them to `.gitignore` too. - -## Infrastructure as Code +```json title="package.json" +{ + "scripts": { + "predeploy": "bun run typecheck", + "deploy": "agentuity deploy", + "postdeploy": "echo 'Deploy complete'" + } +} +``` -Agentuity defines route infrastructure in your source files: +Run `bun run deploy` to include `predeploy` and `postdeploy`. Run `agentuity deploy` directly when you want only the CLI deploy command. -```typescript -// src/api/index.ts -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; -import { websocket } from '@agentuity/runtime'; -import chatHandler from '@agent/chat/agent'; - -const api = new Hono() - // Standard HTTP endpoint - .post('/cleanup', async (c) => { - return c.text('OK'); - }) - // WebSocket endpoint - .get('/chat', websocket((c, ws) => { - ws.onMessage(async (event) => { - if (typeof event.data !== 'string') { - ws.send(JSON.stringify({ error: 'Only text frames are supported' })); - return; - } - - const result = await chatHandler.run({ message: event.data }); - ws.send(JSON.stringify(result)); - }); - })); - -export default api; -``` +## Existing v2 Apps -This keeps routes and agent registration in version control with the code that handles them. +Existing v2 apps can keep using runtime configuration in `app.ts`, including `createApp()` options and runtime-managed agents or routes. New v3 framework projects should configure the framework directly and use `agentuity.json` for Agentuity deployment metadata. ## Next Steps -- [HTTP Routes](/routes/http): Define HTTP endpoints -- [Schedules](/services/schedules): Run recurring tasks -- [AI Gateway](/agents/ai-gateway): Configure LLM providers +- [Project Structure](/get-started/project-structure): See where files live across frameworks +- [Using Standalone Packages](/reference/standalone-packages): Add Agentuity services to framework routes and scripts +- [Deployment](/reference/cli/deployment): Configure cloud deploy behavior diff --git a/docs/src/web/content/get-started/index.mdx b/docs/src/web/content/get-started/index.mdx index 57500247c..88442aabc 100644 --- a/docs/src/web/content/get-started/index.mdx +++ b/docs/src/web/content/get-started/index.mdx @@ -1,41 +1,41 @@ --- title: Get Started -description: Start building AI agents with Agentuity +description: Start with your framework, add Agentuity services, and deploy with the CLI --- import { Rocket, Download, Zap, FolderTree, Settings } from 'lucide-react'; -Welcome to Agentuity. Follow these guides to set up your environment and build your first agent. +New Agentuity apps start with the framework you already use. Use the CLI to create the project, run it locally, and add Agentuity services when your app needs them. } /> } /> } /> } /> } /> diff --git a/docs/src/web/content/get-started/installation.mdx b/docs/src/web/content/get-started/installation.mdx index e79ca2d51..844874151 100644 --- a/docs/src/web/content/get-started/installation.mdx +++ b/docs/src/web/content/get-started/installation.mdx @@ -1,55 +1,101 @@ --- title: Installation -description: Set up your development environment +description: Install the Agentuity CLI and create a framework project --- +Install the CLI, sign in, then create a project from a supported framework starter. + ## Install the CLI ```bash curl -fsSL https://agentuity.sh | sh ``` -This installs the `agentuity` CLI globally on your system. +Verify the install: + +```bash +agentuity --help +``` + + +The v3 create flow depends on Bun for scaffolding and dependency installation. Install [Bun](https://bun.sh) first if it is not already available on your machine. + + + +While v3 is in alpha, start new projects with `bun create agentuity@alpha`. A global `agentuity` command uses whatever version your package manager installed on PATH, so it can be older than the project you are testing. + - -If you prefer package managers: `bun install -g @agentuity/cli` + +If you prefer installing the CLI through Bun, run `bun install -g @agentuity/cli`. +## Sign In + +```bash +agentuity auth login +``` + +If you register a project during setup, the CLI can write `agentuity.json` and add `AGENTUITY_SDK_KEY` to `.env`. + ## Create a Project ```bash -agentuity project create --name my-app -# or -agentuity create --name my-app +bun create agentuity@alpha --name my-app +``` + +In an interactive terminal, the CLI asks which framework starter to use, then sets up the project and adds Agentuity integration. + +If your CLI version supports `--framework`, you can skip the framework prompt: + +```bash +bun create agentuity@alpha --name my-app --framework nextjs ``` -This scaffolds a project using the [default template](/reference/cli/getting-started#default-template), which includes a translation agent, API routes, and a React frontend with Tailwind. +Supported framework slugs: + +| Slug | Framework | +|------|-----------| +| `nextjs` | Next.js | +| `nuxt` | Nuxt | +| `remix` | React Router | +| `sveltekit` | SvelteKit | +| `astro` | Astro | +| `hono` | Hono | +| `vite-react` | Vite + React | -## Start the Dev Server +## Run Locally ```bash +cd my-app agentuity dev -# or -bun run dev ``` -Your project is now running at `http://localhost:3500`. The dev server uses Vite for frontend updates and Bun for server rebuilds. +`agentuity dev` runs your project's own `dev` script and adds Agentuity development behavior. When `AGENTUITY_SDK_KEY` is available, your app can also use Agentuity AI Gateway in local development. The framework prints the local URL. - -Agentuity projects run on [Bun](https://bun.sh). If you don't have Bun installed, the CLI will prompt you to install it when you create your first project. - +Use a different port when the framework honors `PORT`: -### CLI Shortcuts +```bash +agentuity dev --port 8080 +``` -While the dev server is running, use these shortcuts: +## Deploy -| Key | Action | -|-----|--------| -| `h` | Show help | -| `c` | Clear console | -| `q` | Quit | +```bash +agentuity deploy +``` + +New projects also get a package script: + +```bash +bun run deploy +``` + + +Existing v2 apps can keep using the v2 runtime model. For new v3 apps, start with a framework project and add Agentuity services, CLI dev support, and deploy. + ## Next Steps -- [Quickstart](/get-started/quickstart): Build your first agent -- [Project Structure](/get-started/project-structure): Understand the file layout +- [Quickstart](/get-started/quickstart): Create, edit, run, and deploy a small app +- [Project Structure](/get-started/project-structure): See where framework files and Agentuity files live +- [App Configuration](/get-started/app-configuration): Configure scripts, `agentuity.json`, services, and env vars diff --git a/docs/src/web/content/get-started/meta.json b/docs/src/web/content/get-started/meta.json index 67c02f2bc..9901e5b76 100644 --- a/docs/src/web/content/get-started/meta.json +++ b/docs/src/web/content/get-started/meta.json @@ -1,10 +1,10 @@ { "title": "Get Started", "pages": [ - "what-is-agentuity", - "installation", "quickstart", + "installation", "project-structure", - "app-configuration" + "app-configuration", + "what-is-agentuity" ] } diff --git a/docs/src/web/content/get-started/project-structure.mdx b/docs/src/web/content/get-started/project-structure.mdx index 0e6f7dd79..33f4ffa14 100644 --- a/docs/src/web/content/get-started/project-structure.mdx +++ b/docs/src/web/content/get-started/project-structure.mdx @@ -1,163 +1,81 @@ --- title: Project Structure -description: Understand how Agentuity projects are organized +description: Understand how Agentuity fits into framework projects --- -Agentuity projects use explicit wiring: you define agents, routes, and configuration in code. +Agentuity v3 keeps your framework's file layout. The CLI adds project metadata, a deploy script, and optional example files without moving your app into an Agentuity-owned runtime structure. - -Creating a new agent is as simple as adding a folder to `src/agent/` with an `agent.ts` file and registering it in `src/agent/index.ts`. - +## What the CLI Adds -## Directory Layout +After `bun create agentuity@alpha`, expect the framework's normal files plus a few Agentuity pieces: -``` -my-project/ -├── agentuity.json # Project configuration -├── vite.config.ts # Build configuration (Vite plugins, entry point) -├── app.ts # App entry point (wires agents + router) -├── .env # Environment variables -├── package.json # Dependencies -└── src/ - ├── agent/ # Agent code - │ ├── index.ts # Agent registry (exports array of agents) - │ └── translate/ - │ └── index.ts # Default translation agent - ├── api/ # HTTP routes - │ └── index.ts # Router, imports agents - └── web/ # Frontend code (scaffolded by default) - ├── index.html # HTML entry point - ├── frontend.tsx # React entry point - └── App.tsx # Main component -``` +| File or field | Added when | Purpose | +|---------------|------------|---------| +| `@agentuity/cli` | Every v3 create flow | Runs `agentuity dev`, `agentuity build`, and `agentuity deploy` from the project | +| `scripts.deploy` | Every v3 create flow | Lets `bun run deploy` call `agentuity deploy` | +| AI example files | When you include the AI example | Adds a route and UI that call the OpenAI SDK through Agentuity's gateway | +| `agentuity.json` | Registered or imported projects | Stores project id, org id, region, domains, and resource settings | +| `.env` | Registered or imported projects | Stores `AGENTUITY_SDK_KEY` and linked resource credentials | +| Local ignore entries | Every v3 create flow | Ignores common local files such as `.agentuity/` and `.env` | -Agent names come from the first argument to `createAgent()`. Examples often use `agent.ts`; the default project template uses `index.ts` inside each agent folder. Register each exported agent in `src/agent/index.ts`. - - -You can add helper files (like `types.ts`, `helpers.ts`, `utils.ts`) alongside your agents and routes. - +If you create a project with `--no-register`, add cloud metadata later with `agentuity project import`. -## Agents and Routes +## Framework File Layouts -Agents and routes are separate concerns in Agentuity: +The exact files come from the framework's own create command. Agentuity's generated AI example follows each framework's conventions: -| Concept | Location | Purpose | -|---------|----------|---------| -| **Agents** | `src/agent/` | Core logic, schemas, validation | -| **Routes** | `src/api/` | HTTP endpoints, streams, WebSockets, and cron handlers | +| Framework | API route location | App UI location | +|-----------|--------------------|-----------------| +| Next.js | `src/app/api/translate/route.ts` | `src/app/page.tsx` | +| Nuxt | `server/api/translate.ts` | `app.vue` | +| React Router | `app/routes/api.translate.ts` | `app/routes/home.tsx` | +| SvelteKit | `src/routes/+page.server.ts` | `src/routes/+page.svelte` | +| Astro | `src/pages/api/translate.ts` | `src/pages/index.astro` | +| Hono | `src/index.ts` | `src/index.ts` | +| Vite + React | `server.ts` | `src/App.tsx` | -Agents contain your core logic with schema validation and lifecycle hooks. Routes define HTTP endpoints and handlers for streaming, WebSockets, and cron. This separation keeps concerns clear: agents don't know how they're called, and routes handle the protocol details. +## Routes and Services -### app.ts - -`app.ts` is the entry point that wires agents and routes together via `createApp()`: +In v3, route handlers usually live where the framework expects them. Add Agentuity services with standalone clients when the route needs platform storage, queues, email, schedules, sandbox execution, or other services. ```typescript -import { createApp } from '@agentuity/runtime'; -import agents from './src/agent'; -import api from './src/api'; - -export default await createApp({ - router: { path: '/api', router: api }, - agents, -}); -``` +import { KeyValueClient } from '@agentuity/keyvalue'; -### agent.ts +const kv = new KeyValueClient(); -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('chat', { - schema: { - input: s.object({ message: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, input) => { - return { response: `Echo: ${input.message}` }; - }, +await kv.set('sessions', 'user-123', { + lastSeenAt: new Date().toISOString(), }); - -export default agent; ``` -### src/agent/index.ts - -The agent registry imports all agents and exports them as an array: - -```typescript -import chat from './chat/agent'; - -export default [chat]; -``` - -### src/api/index.ts - -Routes import agents directly and call them: +This pattern works in framework server code, background scripts, and local tools as long as `AGENTUITY_SDK_KEY` is available. -```typescript -import { createRouter } from '@agentuity/runtime'; -import chat from '@agent/chat/agent'; - -const router = createRouter(); - -router.post('/chat', chat.validator(), async (c) => { - const data = c.req.valid('json'); - return c.json(await chat.run(data)); -}); - -export type ApiRouter = typeof router; - -export default router; -``` - -`chat.validator()` validates the request body against the agent's input schema and provides type-safe access via `c.req.valid('json')`. - -## When to Use Agents vs Direct Routes - -| | Agents | Direct Routes (no agent) | -|---|---|---| -| **Location** | `src/agent/` | `src/api/` | -| **Schema validation** | Built-in with `agent.validator()` | Manual | -| **Events & Post-response Hooks** | Yes | No | -| **Storage access** | Full | Full | -| **Best for** | Validated workflows, LLM processing | Health checks, webhooks, simple endpoints | - -Use simple routes (without calling an agent) for lightweight endpoints like health checks. For structured processing with validation, create an agent and call it from your route. - -## Frontend (`src/web/`) - -Place frontend code in `src/web/` to deploy alongside your agents. The CLI bundles and serves it automatically. - -React is supported out of the box. Use Hono's `hc()` client for type-safe API calls. For browser utilities, prefer `@agentuity/frontend` and `@agentuity/auth/react`; the older `@agentuity/react` package remains available as a compatibility layer. + +In Hono projects, Agentuity integrations can expose services on `c.var.*`. Use standalone clients as the portable default, and use Hono context when you want Hono-specific route ergonomics. + -Point `hc()` at the same `/api` mount path you configured in `app.ts`, then import your router type from `src/api/index.ts`. +## App Entrypoints -```tsx -// src/web/App.tsx -import { hc } from 'hono/client'; -import type { ApiRouter } from '../api'; +There is no required v3 `app.ts` entrypoint for framework projects. Your framework owns startup and routing: -const client = hc('/api'); +| Framework | Typical entrypoint | +|-----------|--------------------| +| Next.js | `src/app/` and `next.config.*` | +| Nuxt | `app.vue`, `server/`, and `nuxt.config.*` | +| React Router | `app/` and `react-router.config.*` | +| SvelteKit | `src/routes/` and `svelte.config.*` | +| Astro | `src/pages/` and `astro.config.*` | +| Hono | `src/index.ts` | +| Vite + React | `src/` for the client, `server.ts` for the example API | -function App() { - const handleClick = async () => { - const res = await client.chat.$post({ json: { message: 'Hello' } }); - const data = await res.json(); - console.log(data.response); - }; +During build, Agentuity packages the framework output for deployment. - return ; -} -``` +## Existing v2 Apps - -You can also deploy your frontend elsewhere (e.g., Vercel, Netlify) and call your Agentuity API routes using the Hono `hc()` client with a configured base URL. - +Existing v2 apps can continue using `src/agent`, `src/api`, `createApp()`, `createAgent()`, and runtime-managed routes. For new v3 apps, keep the framework layout and add Agentuity services where the framework code needs them. ## Next Steps -- [App Configuration](/get-started/app-configuration): Configure `app.ts` and `agentuity.json` -- [Creating Agents](/agents/creating-agents): Deep dive into agent creation -- [Routes](/routes): Learn about HTTP and other triggers +- [App Configuration](/get-started/app-configuration): Configure `agentuity.json`, scripts, env vars, and service clients +- [Using Standalone Packages](/reference/standalone-packages): Use Agentuity services from framework routes and scripts +- [Deployment](/reference/cli/deployment): Configure deploy behavior and resources diff --git a/docs/src/web/content/get-started/quickstart.mdx b/docs/src/web/content/get-started/quickstart.mdx index 59ac1c779..aa503a940 100644 --- a/docs/src/web/content/get-started/quickstart.mdx +++ b/docs/src/web/content/get-started/quickstart.mdx @@ -1,269 +1,139 @@ --- title: Quickstart -description: Build your first agent in 5 minutes +description: Create a framework app, add an AI route, and deploy it to Agentuity --- -import { Lightbulb } from 'lucide-react'; +Use Next.js here as the concrete path. You will create a framework app, add a server route, run it through `agentuity dev`, and deploy it. -Start from a new Agentuity project and replace the default translation example with a small chat agent using OpenAI. +## 1. Create the Project - -When you create a project with `agentuity project create`, you get a translation agent demonstrating: - -- **AI Gateway**: OpenAI SDK routed through Agentuity (unified billing, no separate API keys) -- **Thread state**: Persistent translation history across requests -- **Structured logging**: Observability via `ctx.logger` -- **Tailwind CSS**: Pre-configured styling - +```bash +bun create agentuity@alpha --name my-app --framework nextjs +cd my-app +``` -} - description="Learn how agents use tools and run in loops to complete tasks autonomously." -/> +The starter creates a Next.js app, adds the Agentuity CLI, and includes a small AI example route. -## 1. Add Chat Dependencies + +You can use `nuxt`, `remix`, `sveltekit`, `astro`, `hono`, or `vite-react` instead. See [Installation](/get-started/installation): supported framework slugs and create options. + -The default template includes the OpenAI SDK. This quickstart uses Vercel AI SDK helpers, so add them before replacing the default agent: +## 2. Run the App ```bash -bun add ai @ai-sdk/openai +agentuity dev ``` -## 2. Create the Agent +Open the URL printed by Next.js. For a default Next.js app, that is usually `http://localhost:3000`. -Create `src/agent/chat/agent.ts`: +Test the generated translation route: -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('Chat', { - schema: { - input: s.object({ message: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, input) => { - ctx.logger.info('Received message', { message: input.message }); - - const { text } = await generateText({ - model: openai('gpt-5.4-nano'), - prompt: input.message, - }); - - return { response: text }; - }, -}); - -export default agent; +```bash +curl -X POST http://localhost:3000/api/translate \ + -H "Content-Type: application/json" \ + -d '{"text":"Hello from Agentuity","toLanguage":"French"}' ``` -## 3. Register the Agent +## 3. Add a Chat Route -Add the agent to the registry in `src/agent/index.ts`: +Add validation for the request body: -```typescript -import chat from './chat/agent'; - -export default [chat]; +```bash +bun add zod +mkdir -p src/app/api/chat ``` -## 4. Add a Route - -Create `src/api/index.ts`: +Create `src/app/api/chat/route.ts`: -```typescript -import { createRouter } from '@agentuity/runtime'; -import chat from '@agent/chat/agent'; - -const router = createRouter(); +```typescript title="src/app/api/chat/route.ts" +import OpenAI from 'openai'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; -router.post('/chat', chat.validator(), async (c) => { - const data = c.req.valid('json'); - return c.json(await chat.run(data)); +const ChatRequest = z.object({ + message: z.string().min(1), }); -export type ApiRouter = typeof router; +const openai = new OpenAI(); -export default router; -``` +export async function POST(request: Request): Promise { + const body: unknown = await request.json(); + const parsed = ChatRequest.safeParse(body); -Wire the route and your agent into the project-root `app.ts`: + if (!parsed.success) { + return NextResponse.json({ error: 'Invalid chat request' }, { status: 400 }); + } -```typescript -import { createApp } from '@agentuity/runtime'; -import api from './src/api/index'; -import agents from './src/agent'; + const completion = await openai.chat.completions.create({ + model: 'gpt-5.4-mini', + messages: [{ role: 'user', content: parsed.data.message }], + }); -export default await createApp({ - router: { path: '/api', router: api }, - agents, -}); + return NextResponse.json({ + response: completion.choices[0]?.message?.content ?? '', + }); +} ``` -## 5. Run Your Agent +With `AGENTUITY_SDK_KEY` set, `agentuity dev` can route OpenAI SDK calls through Agentuity during local development. If your app already uses `OPENAI_API_KEY`, it can keep using it. -Start the dev server: +## 4. Call the Route -```bash -agentuity dev -# or: bun run dev -``` - -Test your agent via curl: +Keep `agentuity dev` running, then call the new route: ```bash -curl -X POST http://localhost:3500/api/chat \ +curl -X POST http://localhost:3000/api/chat \ -H "Content-Type: application/json" \ - -d '{"message": "What is the capital of France?"}' + -d '{"message":"Give me one sentence about Agentuity."}' ``` Response: ```json { - "response": "The capital of France is Paris." -} -``` - -## 6. Add a Frontend - -Add a React UI in `src/web/` to call your agent. Use Hono's `hc()` client for type-safe API calls: - -```tsx -import { hc } from 'hono/client'; -import type { ApiRouter } from '../api'; -import { useState } from 'react'; - -const client = hc('/api'); - -export function App() { - const [message, setMessage] = useState(''); - const [response, setResponse] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleSend = async () => { - setIsLoading(true); - try { - const res = await client.chat.$post({ json: { message } }); - const data = await res.json(); - setResponse(data.response); - } finally { - setIsLoading(false); - } - }; - - return ( -
- setMessage(e.target.value)} - placeholder="Ask something..." - disabled={isLoading} - /> - - {response &&

{response}

} -
- ); + "response": "Agentuity helps you run AI apps and services from your TypeScript framework." } ``` -The `hc()` client infers types from your router, so `data.response` is fully typed. - -## 7. Add Streaming +## 5. Add Services When You Need Them -For real-time responses, return a stream instead: +Framework routes can use Agentuity services through standalone clients. For example, add key-value storage to any server route or script: -```typescript -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('chat', { - schema: { - input: s.object({ message: s.string() }), - stream: true, - }, - handler: async (ctx, input) => { - ctx.logger.info('Streaming chat response'); - - const { textStream } = streamText({ - model: openai('gpt-5.4-nano'), - prompt: input.message, - }); - - return textStream; - }, -}); - -export default agent; +```bash +bun add @agentuity/keyvalue@alpha ``` -Update the route to return the agent stream: - ```typescript -import { createRouter } from '@agentuity/runtime'; -import chat from '@agent/chat/agent'; +import { KeyValueClient } from '@agentuity/keyvalue'; -const router = createRouter(); +const kv = new KeyValueClient(); -router.post('/chat', chat.validator(), async (c) => { - const data = c.req.valid('json'); - return c.body(await chat.run(data)); +await kv.set('chat', 'latest-message', { + message: 'Give me one sentence about Agentuity.', }); - -export type ApiRouter = typeof router; - -export default router; ``` -## 8. Deploy + +Service clients read `AGENTUITY_SDK_KEY` from the environment by default. `agentuity project create` writes it for registered projects, and `agentuity project import` can link an existing local app. + -When the project is ready, deploy to Agentuity: +## 6. Deploy ```bash agentuity deploy -# or: bun run deploy ``` -Your agent is live with a public URL. View deployments, logs, and sessions in the [Web App](https://app.agentuity.com). - - -After your first deployment, the App populates with: -- **Agents**: Your deployed agents with their endpoints -- **Routes**: Registered HTTP, streaming, and other routes -- **Sessions**: Request history and logs as traffic flows -- **Deployments**: Version history with rollback options - - -## What's Next? - - -Install the [OpenCode plugin](/reference/cli/opencode-plugin) for AI-assisted agent development. Get help writing agents, debugging, and deploying directly from your editor: +or use the generated package script: ```bash -agentuity ai opencode install +bun run deploy ``` - - -**Learn the concepts:** - -- [Understanding How Agents Work](/cookbook/tutorials/understanding-agents): Tools, loops, and autonomous behavior - -**Build something more:** -- [Build a multi-agent system](/agents/calling-other-agents): Routing, RAG, workflows -- [Persist data](/agents/state-management): Use thread and session state -- [RPC Client](/frontend/rpc-client): Type-safe `hc()` patterns for browser and server code +View deployments, logs, and project settings in the [Web App](https://app.agentuity.com). -**Understand the platform:** +## Next Steps -- [Project Structure](/get-started/project-structure): Agents, routes, and frontend -- [App Configuration](/get-started/app-configuration): Configure your project -- [Local Development](/reference/cli/development): Dev server, hot reload, and debugging +- [Project Structure](/get-started/project-structure): See what changes across frameworks +- [App Configuration](/get-started/app-configuration): Configure `agentuity.json`, scripts, env vars, and services +- [Using Standalone Packages](/reference/standalone-packages): Add Agentuity services to framework routes and scripts +- [Local Development](/deploy-operate/local-development#ai-gateway-environment): use provider SDKs through Agentuity in local development diff --git a/docs/src/web/content/get-started/what-is-agentuity.mdx b/docs/src/web/content/get-started/what-is-agentuity.mdx index 1da59ddcd..ee49ca98c 100644 --- a/docs/src/web/content/get-started/what-is-agentuity.mdx +++ b/docs/src/web/content/get-started/what-is-agentuity.mdx @@ -1,64 +1,87 @@ --- title: What is Agentuity? -description: The full-stack platform for building, deploying, and operating AI agents +description: Build, deploy, and operate TypeScript framework apps with Agentuity services --- -Write TypeScript, define routes, and deploy with a single command. We handle the infrastructure, from automatic scaling to built-in observability and more. +Agentuity v3 starts from your framework. Keep your framework's routes and server code, add Agentuity services where your app needs them, then deploy the app with the CLI. -Here's a basic example: +```bash +bun create agentuity@alpha --name my-app --framework nextjs +cd my-app +agentuity dev +``` -```typescript -import { createAgent } from '@agentuity/runtime'; +## The Default Shape + +Your app stays a framework app. With `zod` installed, a Next.js route can validate input and call a model: + +```typescript title="src/app/api/chat/route.ts" import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('Chat', { - schema: { - input: s.object({ message: s.string() }), - output: s.object({ response: s.string() }), - }, - handler: async (ctx, input) => { - const { text } = await generateText({ - model: openai('gpt-5.4-nano'), - prompt: input.message, - }); - return { response: text }; - }, +import { NextResponse } from 'next/server'; +import { z } from 'zod'; + +const ChatRequest = z.object({ + message: z.string().min(1), }); -export default agent; +export async function POST(request: Request): Promise { + const body: unknown = await request.json(); + const parsed = ChatRequest.safeParse(body); + + if (!parsed.success) { + return NextResponse.json({ error: 'Invalid chat request' }, { status: 400 }); + } + + const { text } = await generateText({ + model: openai('gpt-5.4-mini'), + prompt: parsed.data.message, + }); + + return NextResponse.json({ response: text }); +} ``` -This agent uses the AI SDK to call OpenAI and respond to messages. Deploy with `agentuity deploy` and it scales automatically. Call it from web apps, other agents, or any HTTP client. +`agentuity dev` runs your framework's dev script and can route provider SDK calls through Agentuity AI Gateway when `AGENTUITY_SDK_KEY` is available. If your app already uses provider keys, it can keep using them. -## What You Get +## What Agentuity Adds -- **Routes & Triggers**: HTTP, WebSocket, SSE, and scheduled work -- **Storage**: Key-value, vector, object storage, durable streams -- **[Sandbox](/services/sandbox)**: Execute code in isolated containers with controlled resources and network access -- **[AI Gateway](/agents/ai-gateway)**: Route LLM calls through OpenAI, Anthropic, Google, and more -- **Type Safety**: Built-in schema validation with `@agentuity/schema`, Zod, or Valibot -- **Observability**: Logging, tracing, real-time analytics -- **[Web App](https://app.agentuity.com)**: Visual dashboard for deployments, logs, and more -- **Frontend**: Deploy React apps alongside your agents +| Area | What you use | +|------|--------------| +| Local development | `agentuity dev` around your framework dev server | +| Deployment | `agentuity build` and `agentuity deploy` | +| Project metadata | `agentuity.json` for project id, org id, region, domains, and resources | +| Services | standalone clients such as `@agentuity/keyvalue`, `@agentuity/queue`, `@agentuity/sandbox`, and `@agentuity/email` | +| Operations | deployments, logs, resource settings, and project management in the [web app](https://app.agentuity.com) | -## How It Works +Use service clients inside the route, worker, script, or server function that owns the work: -Each agent lives in its own file under `src/agent/`. Routes are defined separately in `src/api/index.ts`: +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; -| File | Purpose | -| -------------- | ------------------------------------ | -| `agent.ts` | Agent logic with schema validation | -| `api/index.ts` | HTTP endpoints that call your agents | +const kv = new KeyValueClient(); -Infrastructure like routes and scheduled work is defined in code, not config. Rolling back a deployment restores the exact configuration from that version. +await kv.set('sessions', 'user-123', { + updatedAt: new Date().toISOString(), +}); +``` ---- +## Where Agents Fit + +You can still call a model-backed workflow an agent when it classifies, summarizes, routes, drafts, or makes a bounded decision. In v3, that agent is usually an application pattern: a plain function called from a framework route, queue consumer, schedule, or script. + +It is not the default runtime layer for new apps. New v3 projects do not need a required `src/agent` folder or `createAgent()` wrapper. + +See [Agents as a Pattern](/patterns/agents-as-a-pattern): structure model-backed workflows without returning to the old runtime layer. -

We're building for a future where agents are the primary way to build and operate software, and where infrastructure is purpose-built for this new paradigm.

+ +If an app already uses `@agentuity/runtime`, `createApp()`, or `createAgent()`, you can keep that app working while you plan a migration. See [Migration](/migration): move an existing runtime app toward the v3 framework-first model when you are ready. + ## Next Steps -- [Installation](/get-started/installation): Set up your environment -- [Quickstart](/get-started/quickstart): Build your first agent in 5 minutes +- [Installation](/get-started/installation): install the CLI and choose a framework starter +- [Quickstart](/get-started/quickstart): create a Next.js app, run it locally, and deploy it +- [Project Structure](/get-started/project-structure): see what Agentuity adds to each framework +- [Frameworks](/frameworks): use framework-specific setup guidance +- [Services](/services): choose storage, messaging, identity, observability, and execution clients diff --git a/docs/src/web/content/meta.json b/docs/src/web/content/meta.json index 55b625062..3ae2633b5 100644 --- a/docs/src/web/content/meta.json +++ b/docs/src/web/content/meta.json @@ -1,12 +1,13 @@ { "sections": [ "get-started", - "agents", - "routes", - "frontend", + "frameworks", "services", + "deploy-operate", + "patterns", "cookbook", "community", - "reference" + "reference", + "migration" ] } diff --git a/docs/src/web/content/migration/from-v2.mdx b/docs/src/web/content/migration/from-v2.mdx new file mode 100644 index 000000000..d25b73c3f --- /dev/null +++ b/docs/src/web/content/migration/from-v2.mdx @@ -0,0 +1,95 @@ +--- +title: Migrating from v2 to v3 +short_title: From v2 +description: Move a v2 runtime app toward the v3 framework-first app shape. +--- + +Start with a dry run so you can see which parts of your v2 app are automatic and which parts need review. + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 --dry-run +``` + +## Before You Start + +Work from a branch with a clean git worktree. The migration tool checks this before writing files, because it rewrites source files and package metadata. + + +You can keep a v2 app on the v2 runtime model until you are ready to migrate. Use v3 when you want the framework-first app shape for new work or for an existing app you are actively moving. + + +## What Changes + +| v2 runtime app | v3 framework app | +| --- | --- | +| `app.ts` calls `createApp()` | your framework owns the app entry point | +| `createAgent()` wraps handlers | handlers become framework code, server functions, or plain functions | +| services arrive through `ctx.*` or `c.var.*` | standalone clients are imported directly, with Hono middleware as an option | +| `ctx.thread`, `ctx.session`, `ctx.sessionId`, or `c.var.thread` | explicit app state, cookies, database rows, KV records, or platform session inspection | +| `agentuity.config.ts` carries runtime settings | framework config and `agentuity.json` carry their own concerns | +| `src/agent/index.ts` registers agents with the runtime | functions are imported where they are called | + +The [Runtime to Frameworks](/migration/runtime-to-frameworks) page shows the same shift with code. + +## Run the Migration + +Use the alpha tag for v3 migrations so the `--v2-to-v3` flag is available. + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 +``` + +The migrator can: + +- replace `createApp()` with a Hono starting point in `src/index.ts` +- add `hono` and `@agentuity/hono` +- generate `src/services.ts` for detected service clients +- convert simple `createAgent()` files to plain exported functions +- rewrite `ctx.kv`, `ctx.vector`, and other detected service access to direct imports +- remove `@agentuity/runtime` and v2-only packages from `package.json` +- run `bun install` and a TypeScript check after it writes changes + +## Review Manual Work + +Some v2 patterns need a person to choose the replacement: + +| Pattern | What to do | +| --- | --- | +| `setup()` or `shutdown()` lifecycle hooks | move initialization and cleanup into normal framework or module-level code | +| `ctx.thread.state`, `ctx.session.state`, or `c.var.thread` | choose the app-owned state boundary: cookie, database, KV, durable stream, or platform session lookup | +| `ctx.config` or `ctx.app` | replace shared runtime state with explicit imports or framework state | +| event listeners on agents | move the behavior into your app's own event flow | +| `agentuity.config.ts` runtime options | move build options to framework config and project metadata to `agentuity.json` | +| files importing `@agentuity/evals` | rebuild the eval flow separately if you still need it | + +The migration tool also strips v2 validator middleware from routes. Validate request bodies inside the framework route, usually with Zod or your existing validation library. + +The migrator may leave stubs for thread and session APIs. Treat those as review markers, not as working conversation memory. + +## Register Before Deploy + +Migrated apps usually need to be linked to Agentuity Cloud before their first v3 deploy. Validate the local project shape, then import it: + +```bash +agentuity project import --validate-only +agentuity project import +``` + +`agentuity project import` can write `agentuity.json`, register the project, and write `AGENTUITY_SDK_KEY` to the local env file. After that, validate the framework build when the package has one, then run the Agentuity build: + +```bash +# If your package has a build script: +bun run build +agentuity build +agentuity deploy +``` + + +Framework projects should pass `agentuity build` before deploy. For Hono migrations, check the generated `package.json` scripts and `.agentuity/launch.json` before treating the project as deploy-ready. + + +## Next Steps + +- [Migration CLI](/migration/migrate-cli): see the full command flow and flags +- [Deploying Framework Apps](/deploy-operate/deploy-framework-apps): register, build, and deploy framework projects +- [Services](/services): choose standalone service clients after the runtime migration diff --git a/docs/src/web/content/migration/index.mdx b/docs/src/web/content/migration/index.mdx new file mode 100644 index 000000000..ceef0c5da --- /dev/null +++ b/docs/src/web/content/migration/index.mdx @@ -0,0 +1,57 @@ +--- +title: Migration +description: Move existing Agentuity projects toward the v3 framework-first model +--- + +import { ArrowRightLeft, Route, Terminal } from 'lucide-react'; + +Use these pages when you are moving an existing Agentuity app toward the v3 framework-first model. v2 remains supported for existing apps. v3 is the default path for new work. + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 --dry-run +``` + + + } + /> + } + /> + } + /> + + +## Which Guide to Use + +| Starting point | Use | +| --- | --- | +| v2 app using `@agentuity/runtime`, `createApp()`, or `createAgent()` | [Migrating from v2](/migration/from-v2): move toward a framework app with Agentuity services | +| v2 runtime concepts are still unclear | [Runtime to Frameworks](/migration/runtime-to-frameworks): translate the app model before editing code | +| You want the exact migration command and flags | [Migration CLI](/migration/migrate-cli): run `npx @agentuity/migrate@alpha` safely | +| v1 app moving to v2 first | [Migrating from v1 to v2](/reference/migration-guide): use the existing v1 to v2 guide | + + +You do not need to rewrite a working v2 app just because v3 is the default docs path. Migrate when you want the framework-first app shape, standalone service clients, or the v3 deploy flow. + + +## Migration Shape + +The v3 migration is not a package bump only. It changes where Agentuity sits in your app: + +- your framework owns the routes, entry point, and build shape +- Agentuity service clients are imported where the code needs them +- `@agentuity/hono` is available when Hono `c.var.*` access is the better fit +- `agentuity project import`, `agentuity build`, and `agentuity deploy` connect the app to Agentuity Cloud + +Start with a dry run, review the report, then migrate in a branch with a clean git worktree. diff --git a/docs/src/web/content/migration/meta.json b/docs/src/web/content/migration/meta.json new file mode 100644 index 000000000..2534a2fe9 --- /dev/null +++ b/docs/src/web/content/migration/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Migration", + "pages": ["from-v2", "runtime-to-frameworks", "migrate-cli"] +} diff --git a/docs/src/web/content/migration/migrate-cli.mdx b/docs/src/web/content/migration/migrate-cli.mdx new file mode 100644 index 000000000..04f5cbd2a --- /dev/null +++ b/docs/src/web/content/migration/migrate-cli.mdx @@ -0,0 +1,104 @@ +--- +title: Using the Migration CLI +short_title: Migration CLI +description: Run the Agentuity migration tool for v2 to v3 projects. +--- + +Use the alpha tag for v3 migrations so the CLI includes `--v2-to-v3`. + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 --dry-run +``` + +## Command + +Run from the project root, or pass the project directory as the first argument: + +```bash +npx @agentuity/migrate@alpha [project-dir] --v2-to-v3 +``` + +Useful flags: + +| Flag | Use | +| --- | --- | +| `--v2-to-v3` | force the v2 to v3 migration mode | +| `--dry-run` | print the migration report without changing files | +| `--yes`, `-y` | skip confirmation prompts | +| `--help`, `-h` | print available modes and flags | + +The tool can also auto-detect migration mode from `@agentuity/runtime` in `package.json`, but pass `--v2-to-v3` when you are following the v3 migration docs. + +## Safe Workflow + +Preview first: + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 --dry-run +``` + +Apply after the report looks right: + +```bash +npx @agentuity/migrate@alpha --v2-to-v3 +``` + +The tool refuses to write changes when it detects a dirty git worktree. Commit, stash, or otherwise clear unrelated local changes before applying the migration. + +## What the Tool Changes + +The v2 to v3 migrator looks for runtime-era patterns and applies the changes it can make mechanically: + +| Area | Change | +| --- | --- | +| entry point | replaces `app.ts` and `createApp()` with a Hono starting point in `src/index.ts` | +| packages | removes `@agentuity/runtime`, adds `hono`, `@agentuity/hono`, and detected service packages | +| services | generates `src/services.ts` with singleton service clients | +| simple agents | converts simple `createAgent()` handlers into plain exported functions | +| routes | rewrites detected `ctx.*` and `c.var.*` service access to direct service imports | +| thread and session APIs | leaves review markers where runtime thread/session state needs an app-owned replacement | +| removed packages | removes v2-only packages such as `@agentuity/evals`, `@agentuity/frontend`, and `@agentuity/workbench` | +| schema usage | ports detected `@agentuity/schema` usage to Zod when that transform applies | + +After writing changes, the tool runs `bun install` and a TypeScript check. If TypeScript reports errors, keep the diff and fix the remaining migration work manually. + +## What Still Needs Review + +Manual work is normal for apps that used runtime container features: + +- lifecycle hooks such as `setup()` or `shutdown()` +- agent event listeners +- thread or session state used for chat memory, request grouping, or browser sessions +- shared app state through `ctx.app` +- setup output read through `ctx.config` +- Hono route validation previously handled by Agentuity validator middleware +- files importing `@agentuity/evals` and custom Workbench/frontend flows + + +The migrator gives you a starting point, not a deploy guarantee. Review `src/index.ts`, `src/services.ts`, package scripts, and the TypeScript output before deploying. + + +## After Migration + +Link the project to Agentuity Cloud before the first v3 deploy: + +```bash +agentuity project import --validate-only +agentuity project import +``` + +Then run the framework build if the package has one, followed by the Agentuity build: + +```bash +# If your package has a build script: +bun run build +agentuity build +``` + +Deploy only after the build succeeds for your app shape. + +## Next Steps + +- [Migrating from v2](/migration/from-v2): put the CLI into the full migration flow +- [Runtime to Frameworks](/migration/runtime-to-frameworks): map runtime APIs to framework code +- [Deploying Framework Apps](/deploy-operate/deploy-framework-apps): register, build, and deploy after migration diff --git a/docs/src/web/content/migration/runtime-to-frameworks.mdx b/docs/src/web/content/migration/runtime-to-frameworks.mdx new file mode 100644 index 000000000..3b5cb0196 --- /dev/null +++ b/docs/src/web/content/migration/runtime-to-frameworks.mdx @@ -0,0 +1,128 @@ +--- +title: Moving from Runtime Apps to Framework Apps +short_title: Runtime to Frameworks +description: Translate v2 runtime concepts into the v3 framework-first model. +--- + +In v2, Agentuity owns the app shape. In v3, the framework owns the app shape and Agentuity adds services, local development, build metadata, and deploy. + +```typescript title="app.ts" +import { createApp } from '@agentuity/runtime'; +import api from './src/api'; +import agents from './src/agent'; + +export default await createApp({ + router: { path: '/api', router: api }, + agents, +}); +``` + +## Keep the Framework Shape + +A v3 app keeps the framework's normal entry point. In a Hono app, that means `new Hono()` and normal route mounting: + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import api from './api'; + +const app = new Hono(); + +app.use('*', agentuity()); +app.route('/api', api); + +export default app; +``` + +For Next.js, TanStack Start, Astro, SvelteKit, Remix, or Nuxt, keep that framework's route and server conventions instead of recreating the v2 runtime structure. + +## Replace Runtime Concepts + +| v2 concept | v3 replacement | +| --- | --- | +| `createApp()` | framework entry point, such as `src/index.ts` for Hono or route handlers for Next.js | +| `createRouter()` | the framework router directly, such as `new Hono()` | +| `createAgent()` | plain functions, AI SDK calls, server functions, or route handlers | +| `ctx.kv`, `ctx.vector`, `ctx.queue` | standalone service clients imported where needed | +| `c.var.kv` in Hono routes | standalone clients, or `@agentuity/hono` middleware when `c.var.*` fits the route | +| `ctx.thread`, `ctx.session`, `ctx.sessionId` | explicit app state, cookies, databases, KV records, or platform session inspection | +| `agentuity.config.ts` | framework config plus `agentuity.json` | +| app-level `setup()` and `shutdown()` | module initialization, framework lifecycle hooks, or process cleanup | + +The `@agentuity/runtime` package in v3 is a deprecation shim for old imports. It is there to fail clearly if runtime APIs are installed under v3, not as the current app model. + +## Prefer Service Clients First + +Direct service clients are the portable default because the same code can run in routes, server functions, scripts, and workers. + +```typescript title="src/services.ts" +import { KeyValueClient } from '@agentuity/keyvalue'; + +export const kv = new KeyValueClient(); +``` + +```typescript title="src/api/events.ts" +import { Hono } from 'hono'; +import { kv } from '../services'; + +const router = new Hono(); + +router.post('/', async (c) => { + const body: unknown = await c.req.json(); + const id = crypto.randomUUID(); + + await kv.set('events', id, { + id, + body, + createdAt: new Date().toISOString(), + }); + + return c.json({ id }, 201); +}); + +export default router; +``` + +Use `@agentuity/hono` when a Hono route reads better with injected services: + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; + +const app = new Hono(); + +app.use('*', agentuity()); + +app.get('/api/latest', async (c) => { + const latest = await c.var.kv.get('events', 'latest'); + + return c.json({ latest }); +}); + +export default app; +``` + +## Move Agents Into App Code + +Simple v2 agents usually become plain functions: + +```typescript title="src/agent/summarize.ts" +export async function summarize(input: { + readonly text: string; +}): Promise<{ readonly summary: string }> { + // Call your model provider or AI SDK here. + return { summary: input.text.slice(0, 120) }; +} +``` + +Then call that function from the framework route, queue worker, task handler, or server function that owns the request. + +Complex agents need review when they use lifecycle hooks, event listeners, `ctx.config`, or `ctx.app`. Those concepts were tied to the v2 runtime container, so the right v3 replacement depends on where that state belongs in your framework app. + +Thread and session state also need a design pass. A chat app might move message history to KV or a database keyed by an app session cookie. A debugging tool might use platform session and thread records only for inspection. + +## Next Steps + +- [Migrating from v2](/migration/from-v2): run the migration in order +- [Migration CLI](/migration/migrate-cli): inspect what the tool changes automatically +- [Frameworks](/frameworks): choose framework-specific setup guidance diff --git a/docs/src/web/content/patterns/agents-as-a-pattern.mdx b/docs/src/web/content/patterns/agents-as-a-pattern.mdx new file mode 100644 index 000000000..d733ecb0b --- /dev/null +++ b/docs/src/web/content/patterns/agents-as-a-pattern.mdx @@ -0,0 +1,151 @@ +--- +title: Agents as a Pattern +description: Build model-backed workflows with framework routes and Agentuity service clients +--- + +Use an agent when a request needs a bounded decision loop: validate input, gather context, call a model, store useful state, and return a result. In v3, that is ordinary framework code. There is no `createAgent()` step in the default path. + +```bash +bun add ai @ai-sdk/openai @agentuity/keyvalue@alpha @agentuity/schema@alpha +``` + +```typescript title="app/api/triage/route.ts" +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { KeyValueClient } from '@agentuity/keyvalue'; +import { s } from '@agentuity/schema'; + +const inputSchema = s.object({ + customerId: s.string(), + message: s.string(), +}); + +const outputSchema = s.object({ + priority: s.enum(['low', 'medium', 'high']), + summary: s.string(), + nextAction: s.string(), +}); + +type TriageInput = s.infer; +type TriageResult = s.infer; + +const kv = new KeyValueClient(); + +async function runSupportTriage(input: TriageInput): Promise { + const previous = await kv.get('support-triage', input.customerId); + + const { text } = await generateText({ + model: openai('gpt-5.4-mini'), + system: + 'Return JSON with priority, summary, and nextAction. Do not include markdown.', + prompt: [ + `Customer message: ${input.message}`, + `Previous summary: ${previous.exists ? previous.data.summary : 'None'}`, + ].join('\n\n'), + }); + + const raw: unknown = JSON.parse(text); + const result = outputSchema.parse(raw); + + await kv.set('support-triage', input.customerId, result, { + ttl: 60 * 60 * 24 * 30, + }); + + return result; +} + +export async function POST(request: Request): Promise { + const body: unknown = await request.json(); + const input = inputSchema.parse(body); + const result = await runSupportTriage(input); + + return Response.json(result); +} +``` + +This is the v3 agent shape: the route belongs to your framework, the model call belongs to your AI SDK or provider SDK, and Agentuity stores the durable context. + + +If an existing project uses `createAgent()` from `@agentuity/runtime`, keep using the v2 docs for that app or follow the migration guide when you are ready to move it. New v3 pages use framework routes and service clients. + + +## Split the Route from the Work + +Keep the model-backed work in a plain function. That makes the same behavior usable from an HTTP route, a schedule, a queue destination, or a script. + +```typescript +export async function triageMessage(body: unknown): Promise { + const input = inputSchema.parse(body); + return runSupportTriage(input); +} +``` + +The route becomes a thin adapter: + +```typescript +export async function POST(request: Request): Promise { + const result = await triageMessage(await request.json()); + return Response.json(result); +} +``` + +## Use Hono Middleware When It Helps + +Direct clients are the portable default. In Hono apps, `@agentuity/hono` can initialize the same clients once and expose them on `c.var.*`. + +```bash +bun add hono @agentuity/hono@alpha +``` + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import { s } from '@agentuity/schema'; +import type { Logger } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; + +const inputSchema = s.object({ + customerId: s.string(), + message: s.string(), +}); + +interface TriageSnapshot { + readonly summary: string; +} + +type Variables = Pick & { + logger: Logger; +}; + +const app = new Hono<{ Variables: Variables }>(); + +app.use('*', agentuity()); + +app.post('/api/triage', async (c) => { + const body: unknown = await c.req.json(); + const input = inputSchema.parse(body); + + const previous = await c.var.kv.get('support-triage', input.customerId); + c.var.logger.info('triage requested', { customerId: input.customerId }); + + return c.json({ + previousSummary: previous.exists ? previous.data.summary : null, + }); +}); + +export default app; +``` + +## When to Use This Pattern + +| Use this pattern for | Reach for something else when | +|----------------------|-------------------------------| +| classifying, routing, summarizing, drafting, or reviewing input | the work is pure CRUD with no model decision | +| workflows that need durable context between requests | the state belongs in a database transaction | +| small orchestration functions that may later run from queues or schedules | you need a long-running sandbox or Coder session | + +## Next Steps + +- [Chat and Streaming](/patterns/chat-and-streaming): return model output as it is generated +- [Background Work](/patterns/background-work): move slow model or export work out of request handlers +- [Key-Value Storage](/services/storage/key-value): store compact state by namespace and key diff --git a/docs/src/web/content/patterns/background-work.mdx b/docs/src/web/content/patterns/background-work.mdx new file mode 100644 index 000000000..ef8612a87 --- /dev/null +++ b/docs/src/web/content/patterns/background-work.mdx @@ -0,0 +1,157 @@ +--- +title: Background Work +description: Use queues, status records, and durable streams for work that should outlive a request +--- + +Use background work when a request starts something slow: report generation, webhook fan-out, email preparation, model evaluation, or file processing. The route should enqueue the job and return a handle the client can poll. + +```bash +bun add hono @agentuity/queue@alpha @agentuity/keyvalue@alpha @agentuity/stream@alpha @agentuity/schema@alpha +``` + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { QueueClient } from '@agentuity/queue'; +import { KeyValueClient } from '@agentuity/keyvalue'; +import { StreamClient } from '@agentuity/stream'; +import { s } from '@agentuity/schema'; + +const requestSchema = s.object({ + accountId: s.string(), + reportMonth: s.string(), +}); + +const jobSchema = s.object({ + jobId: s.string(), + accountId: s.string(), + reportMonth: s.string(), +}); + +type ReportJob = s.infer; + +type ReportStatus = + | { readonly kind: 'queued'; readonly messageId: string } + | { readonly kind: 'processing' } + | { readonly kind: 'done'; readonly streamUrl: string } + | { readonly kind: 'failed'; readonly message: string }; + +const queue = new QueueClient(); +const kv = new KeyValueClient(); +const streams = new StreamClient(); +const app = new Hono(); + +// Create this once from setup code, a script, or the Console. +await queue.createQueue('report-jobs', { + queueType: 'worker', + description: 'Monthly report generation jobs', +}); + +app.post('/api/reports', async (c) => { + const body: unknown = await c.req.json(); + const input = requestSchema.parse(body); + const jobId = crypto.randomUUID(); + + const job: ReportJob = { + jobId, + accountId: input.accountId, + reportMonth: input.reportMonth, + }; + + const message = await queue.publish('report-jobs', job, { + idempotencyKey: jobId, + }); + + await kv.set('report-status', jobId, { + kind: 'queued', + messageId: message.id, + }); + + return c.json({ jobId, statusUrl: `/api/reports/${jobId}` }, 202); +}); + +app.get('/api/reports/:jobId', async (c) => { + const result = await kv.get('report-status', c.req.param('jobId')); + + if (!result.exists) { + return c.json({ error: 'Report job not found' }, 404); + } + + return c.json(result.data); +}); + +export default app; +``` + +The request route stays short. It validates the payload, publishes a queue message, records status in KV, and returns `202 Accepted`. + +## Process the Job + +Use the same job function from a queue destination, schedule destination, webhook route, or local script. The route below assumes the request body is the job payload. + +```typescript title="src/index.ts" +async function writeReport(job: ReportJob): Promise { + const stream = await streams.create('monthly-reports', { + contentType: 'text/csv', + metadata: { + accountId: job.accountId, + reportMonth: job.reportMonth, + }, + ttl: 60 * 60 * 24 * 90, + }); + + try { + await stream.write('account_id,report_month,total\n'); + await stream.write(`${job.accountId},${job.reportMonth},42817\n`); + } finally { + await stream.close(); + } + + return stream.url; +} + +app.post('/api/workers/report-jobs', async (c) => { + const body: unknown = await c.req.json(); + const job = jobSchema.parse(body); + + await kv.set('report-status', job.jobId, { kind: 'processing' }); + + try { + const streamUrl = await writeReport(job); + await kv.set('report-status', job.jobId, { + kind: 'done', + streamUrl, + }); + + return c.json({ ok: true }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Report generation failed'; + + await kv.set('report-status', job.jobId, { + kind: 'failed', + message, + }); + + return c.json({ ok: false }, 500); + } +}); +``` + + +The queue message is the handoff. KV stores the user-visible status, and durable streams store output that is too large or too useful to keep in the response body. + + +## Pick the Right Service + +| Need | Use | +|------|-----| +| async handoff between routes or workers | [Queues](/services/queues) | +| user-visible job status | [Key-Value Storage](/services/storage/key-value) | +| large generated output | [Durable Streams](/services/storage/durable-streams) | +| cron-based starts | [Schedules](/services/schedules) | +| external event intake | [Webhooks](/services/webhooks) | + +## Next Steps + +- [Queues](/services/queues): publish work with idempotency and metadata +- [Durable Streams](/services/storage/durable-streams): write generated files and logs +- [Schedules](/services/schedules): start recurring work without an incoming user request diff --git a/docs/src/web/content/patterns/chat-and-streaming.mdx b/docs/src/web/content/patterns/chat-and-streaming.mdx new file mode 100644 index 000000000..5c77873a4 --- /dev/null +++ b/docs/src/web/content/patterns/chat-and-streaming.mdx @@ -0,0 +1,125 @@ +--- +title: Chat and Streaming +description: Stream model output from framework routes and persist chat state with Agentuity clients +--- + +Use streaming when the user should see model output as it is generated. Keep the HTTP response ephemeral, then store the transcript in a service client when the stream finishes. + +```bash +bun add hono ai @ai-sdk/openai @agentuity/keyvalue@alpha @agentuity/schema@alpha +``` + +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { KeyValueClient } from '@agentuity/keyvalue'; +import { s } from '@agentuity/schema'; + +const inputSchema = s.object({ + conversationId: s.string(), + message: s.string(), +}); + +const messageSchema = s.object({ + role: s.enum(['user', 'assistant']), + content: s.string(), +}); + +const historySchema = s.array(messageSchema); + +type ChatMessage = s.infer; + +const kv = new KeyValueClient(); +const app = new Hono(); + +app.post('/api/chat', async (c) => { + const body: unknown = await c.req.json(); + const input = inputSchema.parse(body); + + const stored = await kv.get('chat-history', input.conversationId); + const history = stored.exists ? historySchema.parse(stored.data) : []; + + const userMessage: ChatMessage = { + role: 'user', + content: input.message, + }; + const nextHistory = [...history, userMessage]; + + const result = streamText({ + model: openai('gpt-5.4-mini'), + system: 'You are a concise product support assistant.', + prompt: nextHistory + .map((message) => `${message.role}: ${message.content}`) + .join('\n'), + async onFinish({ text }) { + const assistantMessage: ChatMessage = { + role: 'assistant', + content: text, + }; + + await kv.set('chat-history', input.conversationId, [...nextHistory, assistantMessage], { + ttl: 60 * 60 * 24 * 30, + }); + }, + }); + + return result.toTextStreamResponse(); +}); + +export default app; +``` + +`streamText()` returns a standard `Response`, so the same shape works in Hono and other frameworks that return Web `Response` objects. Agentuity deploys the framework app; the service client reads its API key from the project environment. + +## Read the Stream in the Browser + +For a custom UI, read the response body and append chunks as they arrive. + +```typescript +async function sendMessage(conversationId: string, message: string): Promise { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ conversationId, message }), + }); + + if (!response.ok) { + throw new Error(`Chat request failed with ${response.status}`); + } + + if (!response.body) { + throw new Error('Chat response did not include a body'); + } + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + let assistantMessage = ''; + + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + assistantMessage += value; + } + + return assistantMessage; +} +``` + +## Choose the Stream Shape + +| Stream shape | Use it when | +|--------------|-------------| +| `toTextStreamResponse()` | your client only needs assistant text chunks | +| `toUIMessageStreamResponse()` | your frontend uses AI SDK UI message streams | +| [Durable Streams](/services/storage/durable-streams) | the generated output must be downloadable or replayed later | +| Server-sent events | you need named events such as `status`, `chunk`, and `done` | + + +For long conversations, store a rolling summary and the latest turns instead of sending the full transcript on every request. + + +## Next Steps + +- [Agents as a Pattern](/patterns/agents-as-a-pattern): wrap model calls in reusable app functions +- [Durable Streams](/services/storage/durable-streams): store large generated output outside the response +- [Key-Value Storage](/services/storage/key-value): keep compact conversation state by ID diff --git a/docs/src/web/content/patterns/index.mdx b/docs/src/web/content/patterns/index.mdx new file mode 100644 index 000000000..b0ce20b76 --- /dev/null +++ b/docs/src/web/content/patterns/index.mdx @@ -0,0 +1,41 @@ +--- +title: Patterns +description: Framework-first examples for common Agentuity application flows +--- + +import { Bot, MessageSquareText, Rows3 } from 'lucide-react'; + +Patterns show how to assemble Agentuity services inside your framework routes, functions, and workers. In v3, start from your framework, call model providers directly, and add Agentuity service clients where you need durable state, queues, streams, telemetry, or deploy support. + + + } + /> + } + /> + } + /> + + +## How to Read These + +Use these pages when you know the app behavior you want but need the v3 shape. The examples use direct service clients first because they work in any server framework. Hono apps can also install `@agentuity/hono` and read the same clients from `c.var.*`. + +Existing v2 runtime projects can keep using the supported v2 agent APIs. These v3 patterns use "agent" as an application design pattern, not as `createAgent()`. + +## Related Docs + +- [Frameworks](/frameworks): start from Next.js, Hono, TanStack Start, or another supported framework +- [Services](/services): choose the Agentuity client for storage, queues, tasks, schedules, sandbox, and observability +- [Deploying Apps](/deploy-operate): run the same framework project with `agentuity dev` and `agentuity deploy` diff --git a/docs/src/web/content/patterns/meta.json b/docs/src/web/content/patterns/meta.json new file mode 100644 index 000000000..01de3081f --- /dev/null +++ b/docs/src/web/content/patterns/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Patterns", + "pages": ["agents-as-a-pattern", "chat-and-streaming", "background-work"] +} diff --git a/docs/src/web/content/reference/cli/ai-commands.mdx b/docs/src/web/content/reference/cli/ai-commands.mdx index ab66f8487..131069874 100644 --- a/docs/src/web/content/reference/cli/ai-commands.mdx +++ b/docs/src/web/content/reference/cli/ai-commands.mdx @@ -175,5 +175,5 @@ See [OpenCode Plugin: Cadence Mode](/reference/cli/opencode-plugin#cadence-mode) ## Next Steps - [Getting Started with the CLI](/reference/cli/getting-started): Install and authenticate -- [Local Development](/reference/cli/development): Run agents locally -- [Creating Agents](/agents/creating-agents): Build your first agent +- [Local Development](/reference/cli/development): Run Agentuity projects locally +- [Agents as a Pattern](/patterns/agents-as-a-pattern): Structure model-backed workflows in v3 apps diff --git a/docs/src/web/content/reference/cli/build-configuration.mdx b/docs/src/web/content/reference/cli/build-configuration.mdx index f96e0cd87..aad1735d8 100644 --- a/docs/src/web/content/reference/cli/build-configuration.mdx +++ b/docs/src/web/content/reference/cli/build-configuration.mdx @@ -1,109 +1,123 @@ --- -title: Build Configuration -description: Customize the build process with Vite plugins and build-time constants +title: Building Deployment Bundles +short_title: Build Configuration +description: Build framework apps into Agentuity deployment bundles with launch metadata and static assets. --- -Customize how your project is built using `vite.config.ts` in your project root. Add Vite plugins for frontend builds or define build-time constants. +Use `agentuity build` to turn a framework app into the `.agentuity` deployment bundle that `agentuity deploy` uploads. -## Basic Configuration +## Basic Build -```typescript -import { defineConfig } from 'vite'; +Build the current project: -export default defineConfig({ - // Configuration options here -}); +```bash +agentuity build ``` -## Configuration Options +Write the bundle somewhere else: -### plugins +```bash +agentuity build --outdir ./dist-agentuity +``` -Add Vite plugins for frontend builds (`src/web/`). The CLI passes your `vite.config.ts` to Vite directly. If the file is missing, the CLI writes a React fallback config for older projects. +Skip the post-build type check: -```typescript -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import tailwindcss from '@tailwindcss/vite'; - -export default defineConfig({ - plugins: [react(), tailwindcss()], -}); +```bash +agentuity build --skip-type-check ``` -See the [Vite plugin documentation](https://vitejs.dev/plugins/) for available plugins. +The command detects the framework from `package.json`, runs the matching adapter, writes launch metadata, enumerates static assets, and type checks the project unless you skip that step. - -Vite plugins apply to frontend builds only. Server code is bundled separately with Bun. - +## Read the Build Output -### define +The first few lines tell you what the detector found and which adapter packaged the app: -Replace identifiers with constant values at build time. Values must be JSON-stringified. +```text +Detecting framework... +Detected nextjs (node) +Building with nextjs adapter to .agentuity +✓ Dependencies installed in 510ms +✓ Next.js build completed in 3400ms +✓ Standalone output packaged +Build complete (nextjs, 4012ms) +``` -```typescript -import { defineConfig } from 'vite'; +Detection checks known framework packages and config files first, then falls back to a generic `package.json` shape. `BUILD010` usually means the CLI did not find a known framework or a generic build/start entry it can package. -export default defineConfig({ - define: { - 'API_VERSION': JSON.stringify('v2'), - '__DEV__': JSON.stringify(false), - }, -}); -``` +For frameworks that produce static assets or standalone servers, the adapter logs the important packaging step. For example, Next.js should report `Standalone output packaged`; static-oriented builds may report that a static file server was injected or assets were copied into the bundle. - -The development asset server sets these keys for Agentuity internals. Avoid redefining them in your Vite config: -- specific `import.meta.env.AGENTUITY_PUBLIC_*` keys that Agentuity injects internally -- `process.env.NODE_ENV` - + +If `agentuity build` cannot find a framework CLI that exists in `node_modules/.bin`, run it with local binaries on `PATH`: -## Environment Variables +```bash +PATH="$PWD/node_modules/.bin:$PATH" agentuity build +``` + -Vite exposes `VITE_*` variables to frontend code by default. Agentuity's dev server and generated environment types also recognize these public prefixes: +## Build Options -| Prefix | Description | +| Option | Description | |--------|-------------| -| `VITE_*` | Standard Vite convention | -| `AGENTUITY_PUBLIC_*` | Agentuity convention | -| `PUBLIC_*` | Short form | +| `--dir ` | Project directory, defaults to the current directory | +| `--outdir ` | Output directory for build artifacts, defaults to `.agentuity` | +| `--dev` | Enable development build mode | +| `--skip-type-check` | Skip type checking after the framework build | +| `--report-file ` | Write build diagnostics as JSON | +| `--project-id ` | Project ID, alternative to resolving from `--dir` | -For deployed frontend builds, add `envPrefix` if you want to expose `AGENTUITY_PUBLIC_*` or `PUBLIC_*` variables: +### CI Build Options -```typescript -import { defineConfig } from 'vite'; +| Option | Description | +|--------|-------------| +| `--ci` | Run CI build mode from a source archive | +| `--url ` | Source archive URL, required with `--ci` | +| `--directory ` | Subdirectory within the extracted source | +| `--logs-url ` | URL to CI build logs | +| `--trigger ` | Build trigger: `cli`, `workflow`, or `webhook` | +| `--event ` | Build event: `manual`, `push`, `pull_request`, or `workflow` | +| `--pull-request-number ` | Pull request number | +| `--pull-request-url ` | Pull request URL | + +### Git Metadata Options + +| Option | Description | +|--------|-------------| +| `--message ` | Message to associate with the build | +| `--commit ` | Git commit SHA | +| `--branch ` | Git branch | +| `--repo ` | Git repository URL | +| `--provider ` | Git provider, such as `github`, `gitlab`, or `bitbucket` | +| `--commit-url ` | URL to the commit | -export default defineConfig({ - envPrefix: ['VITE_', 'AGENTUITY_PUBLIC_', 'PUBLIC_'], -}); -``` +## Deployment Bundle Contents -```typescript -// .env.local -AGENTUITY_PUBLIC_API_URL=https://api.example.com +The build output is a self-contained deployment bundle. It includes framework output plus Agentuity metadata: -// src/web/App.tsx -const apiUrl = import.meta.env.AGENTUITY_PUBLIC_API_URL; -``` +| File or data | Purpose | +|--------------|---------| +| `.agentuity/` | Default deployment bundle directory | +| `launch.json` | Machine-readable launch metadata for starting the app | +| `Procfile` | Process command compatibility file | +| `.agentuity-build` | Build marker with framework, runtime, and build date | +| static asset metadata | Asset filenames, content types, sizes, and gzip hints for CDN upload | - -Public environment variables are bundled into frontend code and visible in the browser. Never put secrets or API keys in public variables. - +`agentuity deploy` uses this bundle, the launch metadata, and the static asset list when it creates a deployment. -## Build Architecture +## Framework Notes -Agentuity uses a hybrid build system: +Next.js generated projects can build with the framework command: -| Component | Tool | Output | -|-----------|------|--------| -| Frontend (`src/web/`) | Vite | `.agentuity/client/` | -| Server (agents, routes) | Bun | `.agentuity/app.js` | +```bash +bun run build +``` -This separation allows Vite's optimizations for frontend (HMR, tree-shaking, CSS processing) while using Bun's fast bundling for server code. +If `agentuity build` cannot resolve `next` or another local framework binary, use the `PATH` form shown above. -For details on how these components interact during development, see [Dev Server Architecture](/reference/cli/development#dev-server-architecture). +Generated Hono projects include `build` and `start` scripts so the generic detector has a process to package. If `agentuity build` reports `BUILD010`, check that `package.json` still has both scripts and that `start` points at the built server entry. -## Full Example +## Vite Projects + +For Vite-based apps, use `vite.config.ts` the same way you would in a normal Vite project. ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; @@ -111,20 +125,19 @@ import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ - // Vite plugins for frontend plugins: [react(), tailwindcss()], - envPrefix: ['VITE_', 'AGENTUITY_PUBLIC_', 'PUBLIC_'], - - // Build-time constants - define: { - 'APP_VERSION': JSON.stringify('1.0.0'), - }, }); ``` + +Values exposed through Vite public prefixes are bundled into frontend code. Do not put secrets or API keys in public variables. + + +Use framework-native config files for non-Vite frameworks such as Next.js, Nuxt, SvelteKit, Astro, and React Router. + ## Next Steps -- [Tailwind CSS Setup](/cookbook/patterns/tailwind-setup): Add Tailwind styling to your frontend -- [Development Server](/reference/cli/development): Run the dev server with your configuration -- [Deployment](/reference/cli/deployment): Deploy with build configuration +- [Local Development](/reference/cli/development): Run the framework dev server with Agentuity environment wiring +- [Deployment](/reference/cli/deployment): Upload and provision the deployment bundle +- [App Configuration](/get-started/app-configuration): Configure regions, resources, and environment files diff --git a/docs/src/web/content/reference/cli/claude-code-plugin.mdx b/docs/src/web/content/reference/cli/claude-code-plugin.mdx index a87ebf0f4..c43277a70 100644 --- a/docs/src/web/content/reference/cli/claude-code-plugin.mdx +++ b/docs/src/web/content/reference/cli/claude-code-plugin.mdx @@ -81,4 +81,4 @@ claude plugin validate /path/to/sdk/packages/claude-code - [OpenCode Plugin](/reference/cli/opencode-plugin): Alternative plugin for OpenCode with a multi-agent team - [AI Commands](/reference/cli/ai-commands): Other AI-related CLI commands -- [Creating Agents](/agents/creating-agents): Build your first agent +- [Agents as a Pattern](/patterns/agents-as-a-pattern): Structure model-backed workflows in v3 apps diff --git a/docs/src/web/content/reference/cli/debugging.mdx b/docs/src/web/content/reference/cli/debugging.mdx index 1f33271e0..709acba4a 100644 --- a/docs/src/web/content/reference/cli/debugging.mdx +++ b/docs/src/web/content/reference/cli/debugging.mdx @@ -197,7 +197,7 @@ Session logs show output for a single request. Deployment logs show all output f ## Thread Inspection -List and manage conversation threads. For details on how threads work in agents, see [State Management](/agents/state-management). +List and manage conversation threads. For details on how threads work in agents, see [State Management](/services/storage/key-value). ```bash # List recent threads diff --git a/docs/src/web/content/reference/cli/deployment.mdx b/docs/src/web/content/reference/cli/deployment.mdx index 94c0a7f49..3612a850d 100644 --- a/docs/src/web/content/reference/cli/deployment.mdx +++ b/docs/src/web/content/reference/cli/deployment.mdx @@ -1,303 +1,212 @@ --- title: Deploying to the Cloud short_title: Deployment -description: Deploy your agents to Agentuity Cloud with automatic infrastructure provisioning. +description: Deploy registered framework apps to Agentuity Cloud. --- -Deploy your Agentuity project to the cloud with a single command. The platform handles infrastructure, scaling, and monitoring automatically. +Use `agentuity deploy` from a registered project when you are ready to build, upload, and activate a cloud deployment. -## Why Deploy to Agentuity? +## Before You Deploy -Deploying agents *should be* as easy as deploying a web app. But agents need long-running processes, persistent storage, sandbox environments, and observability built in. Setting this up yourself means stitching together containers, databases, secret management, and monitoring. +Deploy expects a local project linked to Agentuity Cloud. A linked project has `agentuity.json` and an Agentuity SDK key available through `.env` or your environment. -Agentuity handles all of this automatically. You push code, and the platform provisions everything your agents need: compute, storage, networking, and observability. No infrastructure configuration, no Docker files, no Kubernetes. +If you already have a framework app, or you created one with `--no-register`, import it first: -**What this gives you:** +```bash +agentuity project import --name my-app +``` + +Then deploy: -- **Single-command deployment**: `agentuity deploy` handles build, upload, and provisioning -- **Automatic HTTPS**: Every deployment gets secure URLs with SSL certificates -- **Built-in rollback**: Revert to any previous deployment instantly -- **Zero infrastructure management**: No containers, orchestration, or scaling to configure +```bash +agentuity deploy +``` + +The deploy command can reconcile missing project configuration interactively, but importing first gives you a clear setup step before the build starts. ## Deploy Your Project +Deploy the current project: + ```bash agentuity deploy -# or +``` + +If your `package.json` has a deploy script, this is also valid: + +```bash bun run deploy ``` -The deploy command: -1. Syncs environment variables from `.env` -2. Builds and packages your project -3. Encrypts and uploads the deployment bundle securely -4. Provisions your deployment on Agentuity's infrastructure -5. Activates your deployment +The deploy flow: -After deployment, view your environment variables and secrets in the [Web App](https://app.agentuity.com) under **Project > Settings**. +1. Checks project registration and region. +2. Syncs non-Agentuity values from `.env`. +3. Creates a deployment record. +4. Builds, verifies, and packages the deployment bundle. +5. Sends launch metadata and static asset metadata to Agentuity Cloud. +6. Encrypts and uploads the deployment bundle. +7. Uploads static assets when the build emitted them. +8. Provisions and activates the deployment. -**Example output:** +Example output: -``` +```text ✓ Sync Env & Secrets -✓ Create Deployment ✓ Build, Verify and Package ✓ Encrypt and Upload Deployment ✓ Provision Deployment Your project was deployed! -→ Deployment ID: deploy_abc123xyz -→ Deployment: https:// -→ Project: https:// -→ Dashboard: https://app.agentuity.com/projects/proj_456def/deployments/deploy_abc123xyz +Deployment ID: deploy_abc123xyz +Deployment: https:// +Project: https:// +Dashboard: https://app.agentuity.com/r/deploy_abc123xyz ``` -The Dashboard URL links directly to the deployment details in the Agentuity App. +## Deployment Bundle -## Deployment URLs +During deploy, the CLI runs the same framework detection and adapter pipeline used by `agentuity build`. The result is a `.agentuity` deployment bundle with: -Each deployment gets two URLs: +| Data | How it is used | +|------|----------------| +| framework output | Code and runtime files uploaded as the encrypted deployment bundle | +| `launch.json` | Launch metadata that tells Agentuity how to start the app | +| `Procfile` | Process command compatibility file | +| `.agentuity-build` | Build marker with framework, runtime, and build date | +| static asset metadata | Asset filenames, content types, sizes, and gzip hints for CDN upload | -**Deployment URL**: -- Unique URL for this specific deployment -- Persists even after new deployments -- Use for testing a specific version +For non-Agentuity framework apps, deploy metadata contains empty Agentuity route and agent lists, plus the launch metadata and any static assets discovered in the framework build output. -**Project URL** (`proj_xxx.agentuity.cloud`): -- Always points to the active deployment -- Updates automatically when you deploy -- Use for stable endpoints and webhooks +## Deploy Options -```bash -# Deploy creates both URLs -agentuity deploy +| Option | Description | +|--------|-------------| +| `--dir ` | Project directory, defaults to the current directory | +| `--project-id ` | Project ID, alternative to resolving from `--dir` | +| `--report-file ` | Write build and deploy diagnostics as JSON | +| `-y, --confirm` | Confirm region changes in non-interactive environments | +| `--message ` | Message to associate with this deployment | +| `--commit ` | Git commit SHA | +| `--branch ` | Git branch | +| `--repo ` | Git repository URL | +| `--provider ` | Git provider, such as `github`, `gitlab`, or `bitbucket` | +| `--commit-url ` | URL to the commit | +| `--logs-url ` | URL to CI build logs | +| `--trigger ` | Deployment trigger: `cli`, `workflow`, or `webhook` | +| `--event ` | Deployment event: `manual`, `push`, `pull_request`, or `workflow` | +| `--pull-request-number ` | Pull request number | +| `--pull-request-url ` | Pull request URL | + +Global CLI options can be placed before the command: -# Deployment URL: https:// -# Project URL: https:// +```bash +agentuity --log-level debug deploy +agentuity --dry-run deploy +agentuity --json deploy ``` -Both URLs are automatically provisioned with HTTPS. The project URL always routes to the active deployment. +## Deployment URLs + +A successful deploy can return a deployment URL, a project URL, and configured custom domains. + +| URL | Use it for | +|-----|------------| +| Deployment URL | Testing one specific deployment | +| Project URL | Stable endpoints and webhooks that should follow the active deployment | +| Custom domain | Public application or API traffic on your own domain | + +The project URL points to the active deployment and updates after each successful deploy. ## Viewing Deployments List recent deployments: ```bash -# Show 10 most recent agentuity cloud deployment list - -# Custom count agentuity cloud deployment list --count=25 - -# For a specific project agentuity cloud deployment list --project-id=proj_abc123xyz ``` -**Example output:** - -| ID | State | Active | Created | Message | Tags | -|----|-------|--------|---------|---------|------| -| deploy_abc123 | completed | Yes | 12/1/25, 3:45 PM | | staging | -| deploy_def456 | completed | | 12/1/25, 2:30 PM | | v1.2.0 | -| deploy_ghi789 | completed | | 11/30/25, 5:00 PM | | | - -## Deployment Details - -View detailed information about a deployment: +Show one deployment: ```bash -# Show deployment details agentuity cloud deployment show deploy_abc123xyz - -# For specific project -agentuity cloud deployment show deploy_abc123xyz --project-id=proj_abc123 -``` - -**Output includes:** -- Deployment ID, state, and creation time -- Active status -- Tags and custom domains -- Cloud region -- DNS records (for custom domain configuration) -- Git metadata (repo, commit, branch, PR) -- Build information (SDK version, runtime, platform) - -``` -ID: deploy_abc123xyz -Project: proj_456def -State: completed -Active: Yes -Created: 12/1/25, 3:45 PM -Tags: staging, hotfix-123 -Domains: api.example.com -DNS Records: api.example.com CNAME p1234567890.agentuity.run - -Git Information - Repo: myorg/myapp - Branch: main - Commit: a1b2c3d - Message: Fix authentication bug - -Build Information - Agentuity: 1.0.0 - Bun: 1.1.40 - Platform: linux - Arch: x64 +agentuity cloud deployment show deploy_abc123xyz --project-id=proj_abc123xyz ``` - -When custom domains are configured, the `show` command displays the required DNS records for each domain. Use this to verify your CNAME configuration. - - -## Viewing Logs - -Fetch logs for a deployment: +View deployment logs: ```bash -# View logs agentuity cloud deployment logs deploy_abc123xyz - -# Limit log entries agentuity cloud deployment logs deploy_abc123xyz --limit=50 - -# Hide timestamps agentuity cloud deployment logs deploy_abc123xyz --no-timestamps - -# For specific project -agentuity cloud deployment logs deploy_abc123xyz --project-id=proj_abc123 +agentuity cloud deployment logs deploy_abc123xyz --project-id=proj_abc123xyz ``` -Logs show severity (INFO, WARN, ERROR) and message body with color-coded output. - - -For live streaming logs, use SSH access. See [Debugging](/reference/cli/debugging) for SSH setup. - - -## Rolling Back +## Rollback, Undeploy, and Delete -Revert to a previous deployment: +Roll back to the previous completed deployment: ```bash agentuity cloud deployment rollback -``` - -The command: -1. Finds the previous completed deployment -2. Prompts for confirmation -3. Activates the previous deployment -4. Updates the project URL - -``` -Rollback to deployment deploy_def456? (y/N): y -✓ Rolled back to deployment deploy_def456 -``` - -Use for specific projects: - -```bash agentuity cloud deployment rollback --project-id=proj_abc123xyz ``` - -Rollback activates the most recent completed deployment before the current one. The current deployment remains available at its deployment URL but is no longer active. - - -## Undeploying - Stop the active deployment: ```bash -# With confirmation prompt agentuity cloud deployment undeploy - -# Skip confirmation agentuity cloud deployment undeploy --force - -# For specific project agentuity cloud deployment undeploy --project-id=proj_abc123xyz ``` -After undeploying: -- The project URL becomes unavailable -- All deployment-specific URLs remain accessible -- Previous deployments are not deleted -- You can redeploy or rollback at any time - - -Undeploying stops the active deployment immediately. Your project URL will return 404 until you deploy or rollback. - - -## Deleting Deployments - -Permanently delete a deployment: +Delete one deployment: ```bash -# Delete a specific deployment agentuity cloud deployment delete deploy_abc123xyz - -# For specific project -agentuity cloud deployment delete deploy_abc123xyz --project-id=proj_abc123xyz - -# Skip confirmation agentuity cloud deployment delete deploy_abc123xyz --force +agentuity cloud deployment delete deploy_abc123xyz --project-id=proj_abc123xyz ``` - -Deleting a deployment permanently deletes it. You cannot rollback to a deleted deployment. + +Deleting a deployment removes it permanently. Undeploying only stops the active deployment, and rollback keeps previous completed deployments available. -## Preview Environments +## Environment Variables -Deploy pull requests to isolated preview environments for testing before merging. Enable previews when linking your repository: +Deploy syncs values from `.env` before the build step. Keys that start with `AGENTUITY_` are filtered out because the platform manages them. ```bash -agentuity git link --preview true +# .env +DATABASE_URL=postgres://... +WEBHOOK_SECRET=secret123 +MY_CUSTOM_API_KEY=xxx ``` -Or toggle it in the Web App under your project's **Settings > GitHub > Preview Environments**. - -Once enabled, every pull request to your connected repository automatically deploys to a unique preview URL. This allows you to: -- Test changes in an isolated environment before merging -- Share preview links with reviewers -- Run integration tests against the preview deployment - -Preview environments are automatically cleaned up when the pull request is closed or merged. +Variables with secret-looking suffixes such as `_SECRET`, `_KEY`, `_TOKEN`, `_PASSWORD`, or `_PRIVATE` are stored as secrets. Other values are stored as regular environment variables. - -Preview environments require a connected GitHub repository. See [Agentuity GitHub App](/reference/github-app) for how automated deployments work, or [Setting Up Git Integration](/reference/cli/git-integration) for CLI setup. + +If your code uses the Agentuity AI Gateway, you do not need to deploy provider keys such as `OPENAI_API_KEY`. Include provider keys only when you want to bypass the gateway. ## Regions -Agentuity Cloud deploys to multiple geographic regions. Choose a region based on latency requirements and data residency needs. - -### Available Regions - -- `use` - US East -- `usc` - US Central -- `usw` - US West - -More regions coming soon. - -### Setting a Default Region - -Set a default region to skip the selection prompt on every deployment: +Set a default region: ```bash -# Set default region agentuity cloud region select usw +``` -# View current default -agentuity cloud region current +Show or clear it: -# Clear default (prompts on each deploy) +```bash +agentuity cloud region current agentuity cloud region unselect ``` -### Region in Configuration - -The region is stored in `agentuity.json` after your first deployment: +The selected project region is stored in `agentuity.json`: ```json { @@ -307,17 +216,11 @@ The region is stored in `agentuity.json` after your first deployment: } ``` -### Changing Regions - -When you deploy to a different region than the one in `agentuity.json`, the CLI prompts for confirmation. This prevents accidental region changes that could affect latency or data residency. - -### Cross-Region Resources - -The SDK automatically handles cross-region resource access. If you create a sandbox or storage resource in one region and access it from another, the platform routes requests transparently. No configuration needed. +If the local region differs from the server region, interactive deploys prompt before updating it. In non-interactive runs, pass `--confirm` when you intend to accept the region change. ## Custom Domains -Configure custom domains in `agentuity.json`: +Add custom domains in `agentuity.json`: ```json { @@ -330,63 +233,22 @@ Configure custom domains in `agentuity.json`: } ``` -### DNS Configuration +Deploy validates DNS before activating the domains. The required CNAME value is shown by the CLI and in deployment details. -Add a CNAME record for each custom domain. A CNAME record tells DNS to resolve your domain to Agentuity's servers: - -``` +```text Type: CNAME Name: api.example.com Value: p.agentuity.run TTL: 600 ``` -The `p` value is your project's unique identifier, shown when you run `agentuity deploy` or in the App. - -The CLI validates DNS records during deployment: - -```bash -agentuity deploy -``` - -``` -✓ Validate Custom Domains: api.example.com, app.example.com -✓ Sync Env & Secrets -... -``` - - -Deployment fails if DNS records are missing or incorrect. The CLI shows the required CNAME value in the error message. - - - -After adding a custom domain to your configuration, you must deploy to activate it. The domain becomes active only after a successful deployment. + +Custom domain changes take effect after a successful deploy. -### Multiple Domains - -Custom domains replace the default URLs: - -```json -{ - "deployment": { - "domains": ["api.example.com"] - } -} -``` - -After deployment with custom domains, the CLI only shows custom URLs: - -``` -→ Deployment ID: deploy_abc123xyz -→ Deployment URL: https://api.example.com -``` - -The project URL and deployment-specific URLs still exist but custom domains take precedence in the output. +## Runtime and Build Resources -## Resource Configuration - -Configure CPU, memory, and disk limits in `agentuity.json`: +Configure runtime resources in `agentuity.json`: ```json { @@ -395,29 +257,15 @@ Configure CPU, memory, and disk limits in `agentuity.json`: "cpu": "500m", "memory": "500Mi", "disk": "500Mi" - }, - "domains": [] + } } } ``` -**Defaults:** `cpu: "500m"`, `memory: "500Mi"`, `disk: "500Mi"`. Increase as needed for your workload. - -The `domains` array is empty by default. Add custom domains when needed (see [Custom Domains](#custom-domains)). - -## Configuring Build Resources - -`deployment.resources` controls how much CPU, memory, and disk your agent gets at runtime. `build` controls the sandbox resources available during the build step, when your project is compiled and bundled. +Configure build resources separately when framework compilation needs more capacity: ```json { - "deployment": { - "resources": { - "cpu": "500m", - "memory": "500Mi", - "disk": "500Mi" - } - }, "build": { "timeout": "30m", "resources": { @@ -429,107 +277,26 @@ The `domains` array is empty by default. Add custom domains when needed (see [Cu } ``` -Build resource defaults are platform-managed. Override them when builds require more capacity: large dependency trees, heavy compilation steps, or complex bundling. +Runtime resources affect the deployed app. Build resources affect the build sandbox used to compile and package it. -**Build fields:** +## Machines -| Field | Description | Example | -|-------|-------------|---------| -| `timeout` | Maximum time allowed for the build step | `"30m"` | -| `resources.memory` | Memory available in the build sandbox | `"4Gi"` | -| `resources.cpu` | CPU cores available in the build sandbox | `"2"` | -| `resources.disk` | Disk space available in the build sandbox | `"4Gi"` | - -All fields are optional. Omit `build` entirely to use platform defaults. - - -If your build frequently times out or runs out of memory, increase `build.resources.memory` or `build.timeout` before adjusting runtime `deployment.resources`. Build and runtime environments are separate. - - -## Environment Variables - -The deploy command syncs variables from your `.env` file. +Machines are the compute instances running deployments. Use these commands when you need to inspect or manage them: ```bash -# .env -DATABASE_URL=postgres://... -WEBHOOK_SECRET=secret123 -MY_CUSTOM_API_KEY=xxx -``` - - -If you're using the [AI Gateway](/agents/ai-gateway), you don't need to include provider API keys (like `OPENAI_API_KEY`). The gateway handles authentication automatically. Only include provider keys if you're bypassing the gateway. - - -Variables are automatically: -- Filtered (removes `AGENTUITY_` prefixed keys) -- Categorized (regular env vs secrets based on naming) -- Encrypted (secrets only) -- Synced to cloud before the build step - - -Variables with suffixes like `_SECRET`, `_KEY`, `_TOKEN`, `_PASSWORD`, or `_PRIVATE` are automatically encrypted as secrets. All other variables are stored as regular environment variables. - - -## Machine Management - -Machines are the compute instances running your deployments. View and manage them when debugging infrastructure issues. - -View and manage the machines running your deployments: - -```bash -# List all machines for current project agentuity cloud machine list - -# Get details for a specific machine agentuity cloud machine get machine_abc123xyz - -# Delete a machine -agentuity cloud machine delete machine_abc123xyz - -# List deployments running on a machine agentuity cloud machine deployments machine_abc123xyz +agentuity cloud machine delete machine_abc123xyz ``` -Machine details include status, region, resource allocation, and uptime. Use machine management to: -- Monitor infrastructure health -- Debug performance issues -- Clean up unused machines - - -Deleting a machine immediately terminates any deployments running on it. Only delete machines that are unused or when intentionally removing capacity. + +Deleting a machine terminates deployments running on it. Use it only when you intend to remove that capacity. -## Deploy Options +## Deploy Lifecycle Scripts -| Option | Description | -|--------|-------------| -| `-y, --confirm` | Skip all confirmation prompts (useful for CI/CD) | -| `--dry-run` | Simulate deployment without executing (place before `deploy`) | -| `--log-level debug` | Show verbose output | -| `--project-id ` | Deploy specific project | -| `--message ` | Message to associate with this build | - -```bash -# Verbose deployment -agentuity deploy --log-level=debug - -# Specific project -agentuity deploy --project-id=proj_abc123xyz - -# With a message -agentuity deploy --message "Fix authentication bug" - -# Simulate deployment (dry run) -agentuity --dry-run deploy - -# Skip confirmation prompts (for CI/CD) -agentuity deploy --confirm -``` - -## Running Deploy Lifecycle Scripts - -`bun run deploy` follows standard npm/bun lifecycle script conventions. Add `predeploy` and `postdeploy` scripts to `package.json` to run steps automatically before and after the deploy command. +`bun run deploy` follows normal package manager lifecycle hooks. Add `predeploy` and `postdeploy` scripts when you want local work to run before or after the CLI command. ```json title="package.json" { @@ -541,51 +308,33 @@ agentuity deploy --confirm } ``` -`predeploy` runs before the deploy command and `postdeploy` runs after it completes. - -**Common use cases:** +`predeploy` and `postdeploy` run when you call `bun run deploy`. They do not run when you call `agentuity deploy` directly. -- `predeploy`: Install workspace dependencies, compile shared packages, run custom validation -- `postdeploy`: Send a notification, update a changelog, trigger downstream systems +## CI Deployments - -When you run `bun run deploy` locally, `predeploy` and `postdeploy` are standard npm lifecycle hooks. They do **not** run when calling `agentuity deploy` directly. +CI systems can pass git and pull request metadata directly to `agentuity deploy`: -When deploying via the [Agentuity GitHub App](/reference/github-app#build-process), the platform installs dependencies first, then runs `predeploy` if it exists. `postdeploy` runs after a successful `agentuity deploy`. See [Build Process](/reference/github-app#build-process) for details. - - -## CI/CD Pipelines - -For automated deployments in CI/CD systems (GitHub Actions, GitLab CI, etc.), you can use a pre-created deployment. - -### Using Pre-Created Deployments +```bash +agentuity deploy \ + --trigger workflow \ + --event pull_request \ + --branch "$GITHUB_HEAD_REF" \ + --commit "$GITHUB_SHA" \ + --repo "$GITHUB_REPOSITORY" \ + --provider github +``` -Set the `AGENTUITY_DEPLOYMENT` environment variable with deployment details: +When a CI system pre-creates the deployment, it can pass `AGENTUITY_DEPLOYMENT`: ```bash export AGENTUITY_DEPLOYMENT='{"id":"deploy_xxx","orgId":"org_xxx","publicKey":"..."}' agentuity deploy ``` -The JSON object requires: - -| Field | Description | -|-------|-------------| -| `id` | The deployment ID | -| `orgId` | Your organization ID | -| `publicKey` | The deployment's public key | - -When this variable is set, the CLI: -- Skips deployment creation and uses the existing deployment -- Uses plain output mode (no TUI) for cleaner CI logs -- Validates the JSON schema before proceeding - - -Pre-created deployments are typically provisioned by external systems like GitHub Actions workflows or custom CI pipelines that integrate with the Agentuity API. - +The JSON object requires `id`, `orgId`, and `publicKey`. ## Next Steps -- [Debugging Deployments](/reference/cli/debugging): SSH access and debugging tools -- [Logging](/services/observability/logging): Configure logging in your agents -- [Key-Value Storage](/services/storage/key-value): Store persistent data +- [Build Configuration](/reference/cli/build-configuration): Inspect the local deployment bundle before deploy +- [Debugging Deployments](/reference/cli/debugging): Connect with SSH and inspect deployed behavior +- [App Configuration](/get-started/app-configuration): Configure project metadata and environment files diff --git a/docs/src/web/content/reference/cli/development.mdx b/docs/src/web/content/reference/cli/development.mdx index ab3c8e799..fab44a122 100644 --- a/docs/src/web/content/reference/cli/development.mdx +++ b/docs/src/web/content/reference/cli/development.mdx @@ -1,225 +1,95 @@ --- title: Local Development short_title: Development -description: Run the development server with hot reload, type checking, and public URL support. +description: Run your framework development server with Agentuity environment wiring. --- -Run your Agentuity project locally with automatic hot reload and type checking. If your project is connected to Agentuity Cloud, `agentuity dev` also enables a public URL by default outside CI for sharing or webhook testing. +`agentuity dev` runs your project's package script and passes the Agentuity environment it can resolve for the current project. ## Starting the Dev Server +Run the default `dev` script: + ```bash agentuity dev -# or -bun run dev ``` -The server starts on port 3500 by default with: -- Hot reload on file changes -- TypeScript type checking -- Public URL tunneling when cloud-connected (disable with `--no-public`) -- Interactive keyboard shortcuts - -## Dev Server Options - -| Option | Default | Description | -|--------|---------|-------------| -| `--dir ` | current directory | Project directory | -| `--local` | false | Use local services instead of cloud services | -| `--port` | 3500 (or `PORT` env) | TCP port for the dev server | -| `--no-public` | - | Disable public URL tunneling | -| `--no-interactive` | - | Disable interactive keyboard shortcuts | -| `--inspect` | - | Enable Bun debugger for debugging | -| `--inspect-wait` | - | Enable debugger and wait for connection before starting | -| `--inspect-brk` | - | Enable debugger and break on first line | -| `--no-typecheck` | - | Skip TypeScript type checking on startup and restarts | -| `--resume ` | - | Resume a paused Hub session by ID | -| `--project-id ` | - | Use a specific project instead of resolving from `--dir` | - - -The dev server respects the `PORT` environment variable. The `--port` flag takes precedence if both are set. - +Pass a port through the `PORT` environment variable: ```bash -# Custom port agentuity dev --port 8080 - -# Disable public URL -agentuity dev --no-public - -# Non-interactive mode (useful for CI/CD) -agentuity dev --no-interactive - -# Local services mode -agentuity dev --local ``` -## Debugging with Inspector - -Use the inspector flags to debug your agents with Chrome DevTools or VS Code: +Run a different package script: ```bash -# Enable inspector (attach debugger anytime) -agentuity dev --inspect - -# Wait for debugger before starting the server -agentuity dev --inspect-wait - -# Break on first line of executed code -agentuity dev --inspect-brk +agentuity dev --script dev:web ``` -Bun dynamically selects an available port and prints it to the console. Check the output for the debugger URL and port number. +The command reads `package.json`, detects the package manager, and runs that package manager's `run` command for the selected script. The framework still owns hot reload, routing, terminal output, and local server behavior. -After starting, open `chrome://inspect` in Chrome or use VS Code's debugger to attach. +## Dev Options - -Create a launch configuration that attaches to the running process. Update the port to match the one shown in the console output: +| Option | Default | Description | +|--------|---------|-------------| +| `--dir ` | current directory | Project directory | +| `--port ` | `3000` | Port to pass to the child process as `PORT` | +| `--script + ``` -## Custom Event Tracking - -### Data Attributes - -Add `data-analytics` to any element to track clicks without writing JavaScript. The beacon records these as `click:` custom events: - -```html - -View Pricing +```typescript title="analytics-entry.ts" +import '@agentuity/analytics/beacon'; ``` -### JavaScript API + +Static ESM imports run before the rest of the module body. Use `init()` or inject `window.__AGENTUITY_ANALYTICS__` before loading a separate beacon entry. + + +## Custom Events -Track events programmatically using the `@agentuity/frontend` package: +Track named events from framework components, route transitions, or plain browser handlers. ```typescript -import { track, getAnalytics } from '@agentuity/frontend'; +import { flush, identify, track } from '@agentuity/analytics'; + +identify('user_456', { plan: 'pro' }); -// Track a custom event track('purchase_completed', { productId: 'prod_123', - amount: 99.99, + amountCents: 9999, currency: 'USD', }); -// Access the full analytics client for identify and flush -const analytics = getAnalytics(); -analytics?.identify('user_456', { plan: 'pro' }); -analytics?.flush(); +flush(); ``` -The same client is also available globally as `window.agentuityAnalytics` for non-module contexts (inline scripts, third-party integrations): +`track()` queues the event with the current page view. `identify()` stores a user ID and stringified traits for the payload. `flush()` forces a send. -```typescript -window.agentuityAnalytics?.track('cta_clicked', { location: 'hero' }); -``` - - -The analytics client initializes when the beacon script loads. In module code, `getAnalytics()` returns `null` until the beacon is ready. In React, check the `ready` value from `useAnalytics()` before relying on immediate `identify()` or `flush()` calls. - +## Data Attributes -## React Integration +Add `data-analytics` to elements when you want click events without a custom handler. The beacon records them as `click:` events. -If you already use `@agentuity/react`, its compatibility hooks can track analytics from React components. For new apps, prefer `@agentuity/frontend` for browser-level analytics calls. - -### useAnalytics +```html + +View Pricing +``` -```tsx -import { useAnalytics } from '@agentuity/react'; +## Access the Client -function ProductPage({ productId }: { productId: string }) { - const { track, trackClick, identify, ready } = useAnalytics(); +Use `getAnalytics()` when non-module code needs the global client after the beacon is ready. - const handlePurchase = () => { - track('purchase_started', { productId }); - }; +```typescript +import { getAnalytics } from '@agentuity/analytics'; - return ( -
- - -
- ); -} +const analytics = getAnalytics(); +analytics?.identify('user_456', { plan: 'pro' }); +analytics?.track('cta_clicked', { location: 'hero' }); +analytics?.flush(); ``` -| Property | Type | Description | -|----------|------|-------------| -| `track` | `(eventName, properties?) => void` | Track a custom event | -| `trackClick` | `(eventName, properties?) => (event) => void` | Returns a click handler that tracks an event | -| `identify` | `(userId, traits?) => void` | Identify the current user | -| `flush` | `() => void` | Force-send pending events | -| `ready` | `boolean` | `true` when the analytics client is available | +The same client is available globally: -### useTrackOnMount +```typescript +window.agentuityAnalytics?.track('cta_clicked', { location: 'hero' }); +``` -Track an event when a component mounts: +`getAnalytics()` returns `null` until the beacon has initialized. -```tsx -import { useTrackOnMount } from '@agentuity/react'; +## What Gets Tracked -function ProductPage({ productId }: { productId: string }) { - // Fires once per component instance by default - useTrackOnMount({ - eventName: 'product_viewed', - properties: { productId }, - }); +| Category | Data | +|----------|------| +| page context | URL, path, referrer, title, with query strings stripped | +| device | screen size, viewport size, pixel ratio, user agent, language | +| geolocation | country, region, city, timezone, and coarse coordinates from the location service | +| performance | load time, DOM ready, TTFB | +| Web Vitals | FCP, LCP, CLS, INP | +| engagement | scroll depth, time on page, visibility sessions | +| UTM params | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` | +| SPA navigation | virtual page views on `pushState`, `replaceState`, and `popstate` | +| errors | JavaScript errors and unhandled promise rejections | + +Events are sent when the page becomes hidden, unloads, or changes SPA routes. The client uses `navigator.sendBeacon()` when available and falls back to `fetch()` with `keepalive`. - return
Product {productId}
; -} -``` +## Configuration | Option | Type | Default | Description | |--------|------|---------|-------------| -| `eventName` | `string` | - | Event name to track | -| `properties` | `object` | - | Event properties | -| `once` | `boolean` | `true` | Only track once per component instance | - -### withPageTracking - -Higher-order component that tracks a `page_view` event on mount: - -```tsx -import { withPageTracking } from '@agentuity/react'; - -function HomePage() { - return
Welcome!
; -} - -export default withPageTracking(HomePage, 'home'); -``` +| `enabled` | `boolean` | required | Enable tracking | +| `orgId` | `string` | required | Agentuity organization ID | +| `projectId` | `string` | required | Agentuity project ID | +| `isDevmode` | `boolean` | `false` | Log payloads to the browser console instead of sending | +| `trackClicks` | `boolean` | `true` | Track clicks on `[data-analytics]` elements | +| `trackScroll` | `boolean` | `true` | Track scroll depth milestones | +| `trackWebVitals` | `boolean` | `true` | Track FCP, LCP, CLS, and INP | +| `trackErrors` | `boolean` | `true` | Track JavaScript errors and unhandled rejections | +| `trackSPANavigation` | `boolean` | `true` | Track client-side route changes | +| `sampleRate` | `number` | `1` | Sample rate from 0 to 1 | +| `endpoint` | `string` | `/_agentuity/webanalytics/collect` | Custom collection endpoint | ## Privacy -The analytics beacon keeps request data separate from agent sessions: +The analytics package is browser-scoped and keeps the payload focused on page-view data. -- **No agent session for page views**: Web analytics uses a signed `atid_a` thread cookie and does not create `sess_...` records or `x-session-id` headers -- **Visitor IDs use localStorage**: Users can clear `agentuity_visitor_id` from browser storage -- **Query strings stripped**: URLs are sanitized before sending to prevent leaking tokens or PII -- **Sampling**: Use `sampleRate` to reduce data collection on high-traffic pages +- Query strings are stripped before URLs are sent. +- Visitor IDs use `localStorage` under `agentuity_visitor_id`. +- Geo data is cached in `sessionStorage` under `agentuity_geo`. +- `sampleRate` can reduce collection on high-traffic pages. +- `isDevmode: true` logs payloads to the browser console instead of sending them. Users can opt out of tracking programmatically: -```tsx -import { isOptedOut, setOptOut } from '@agentuity/frontend'; +```typescript +import { isOptedOut, setOptOut } from '@agentuity/analytics'; -function PrivacyToggle() { +export function toggleAnalyticsOptOut(): boolean { const optedOut = isOptedOut(); - - return ( - - ); + setOptOut(!optedOut); + return !optedOut; } ``` -To disable the beacon for an app, set `analytics: false` in `createApp()`. - - -In development, analytics data is logged to the browser console instead of being sent to the collection endpoint. This lets you verify tracking behavior without generating real data. - +To disable analytics for a page or environment, initialize with `enabled: false` or do not initialize the package. ## Next Steps -- [React Hooks](/frontend/react-hooks): `useAnalytics` and other hooks for React integration -- [Logging](/services/observability/logging): Collected logs for agents and routes -- [Tracing](/services/observability/tracing): OpenTelemetry spans for performance debugging +- [Logging](/services/observability/logging): collect logs from server routes and scripts +- [Tracing](/services/observability/tracing): add OpenTelemetry spans for performance debugging diff --git a/docs/src/web/content/services/oidc-provider.mdx b/docs/src/web/content/services/oidc-provider.mdx index 1711828f4..6fc9fb0f9 100644 --- a/docs/src/web/content/services/oidc-provider.mdx +++ b/docs/src/web/content/services/oidc-provider.mdx @@ -4,77 +4,28 @@ short_title: OIDC Provider description: Add Agentuity account sign-in and scoped access to your app with OAuth 2.0 and OIDC --- -Use Sign in with Agentuity when users should authenticate with their Agentuity account and grant scoped access to Agentuity resources. Your app owns its local session. Agentuity handles identity, consent, OAuth apps, scopes, and tokens. - -## Choose the Right Auth Path - -| Need | Use | -|------|-----| -| Let users sign in with their Agentuity account | Agentuity OIDC provider | -| Request scoped access to Agentuity resources | Agentuity OIDC provider with OAuth scopes | -| Manage your own users, sessions, API keys, or bearer tokens | [`@agentuity/auth`](/services/authentication#quick-start-with-agentuityauth) | -| Register or rotate OAuth app credentials | [CLI OAuth commands](/reference/cli/oauth) | - -## Create an OAuth App - -The easiest way to create an OAuth app is in the [Agentuity Console](https://app.agentuity.com/settings/oauth-apps). The Console walks you through the app name, homepage URL, client type, redirect URIs, and scopes, then shows the client details and connected users. - -For server-backed apps, choose a confidential client so the client secret stays on the server. Do not ship the client secret to browser-only code. - -Use the CLI when you want to script the same setup: +Use Sign in with Agentuity when users should authenticate with their Agentuity account or grant scoped access to Agentuity resources. Your app still owns its local session after the callback. ```bash -agentuity cloud oidc create \ - --name "My App" \ - --description "Sign in with Agentuity" \ - --homepage-url "https://example.com" \ - --type confidential \ - --redirect-uris "https://example.com/api/oauth/callback" \ - --scopes "openid,profile,email" -``` - - -The client secret is shown once when you create the OAuth app. Store it in your environment variables before leaving the screen. - - -## Configure Environment Variables - -The `@agentuity/core/oauth` helpers read `OAUTH_*` variables by default: - -```env -OAUTH_ISSUER=https://auth.agentuity.cloud -OAUTH_CLIENT_ID=your-client-id -OAUTH_CLIENT_SECRET=your-client-secret -OAUTH_SCOPES=openid profile email -``` - -| Variable | Description | -|----------|-------------| -| `OAUTH_ISSUER` | Agentuity's OIDC issuer. The SDK derives `/authorize`, `/oauth/token`, `/userinfo`, `/revoke`, and `/end_session` from this value unless you pass explicit endpoint URLs. | -| `OAUTH_CLIENT_ID` | Client ID from your OAuth app. | -| `OAUTH_CLIENT_SECRET` | Client secret for confidential clients. | -| `OAUTH_SCOPES` | Space-separated scopes requested during sign-in. | - -The Console can show copyable environment variables for the app. The SDK helpers below read `OAUTH_*` names, so map any Agentuity-prefixed values to these names or pass them explicitly to the helpers. - -The public discovery document is available at: - -```txt -https://auth.agentuity.cloud/.well-known/openid-configuration +bun add @agentuity/core@alpha hono ``` -Use the discovery document when you need endpoint metadata or the current JWKS URI for token verification. - -## Add Sign-In Routes +```typescript title="src/index.ts" +import { Hono } from 'hono'; +import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; +import { buildAuthorizeUrl, exchangeToken, fetchUserInfo } from '@agentuity/core/oauth'; +import type { OAuthFlowConfig } from '@agentuity/core/oauth'; -This example redirects users to Agentuity, validates the callback state, exchanges the code for tokens, fetches the user profile, and creates a local app session. +interface AppUser { + readonly id: string; + readonly email: string | null; + readonly name: string | null; +} -```typescript title="src/api/index.ts" -import { createRouter } from '@agentuity/runtime'; -import { buildAuthorizeUrl, exchangeToken, fetchUserInfo } from '@agentuity/core/oauth'; -import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; +const app = new Hono(); -const router = createRouter(); +const STATE_COOKIE = 'agentuity_oauth_state'; +const SESSION_COOKIE = 'app_session'; function requireEnv(name: string): string { const value = process.env[name]; @@ -84,7 +35,7 @@ function requireEnv(name: string): string { return value; } -const oauthConfig = { +const oauthConfig: OAuthFlowConfig = { issuer: requireEnv('OAUTH_ISSUER'), clientId: requireEnv('OAUTH_CLIENT_ID'), clientSecret: requireEnv('OAUTH_CLIENT_SECRET'), @@ -92,17 +43,49 @@ const oauthConfig = { }; function getRedirectUri(requestUrl: string): string { - return `${new URL(requestUrl).origin}/api/oauth/callback`; + return `${new URL(requestUrl).origin}/auth/callback`; } -router.get('/oauth/login', (c) => { +function readAppUser(value: unknown): AppUser | null { + if (typeof value !== 'object' || value === null) { + return null; + } + + if (!('id' in value) || typeof value.id !== 'string') { + return null; + } + + if (!('email' in value) || (typeof value.email !== 'string' && value.email !== null)) { + return null; + } + + if (!('name' in value) || (typeof value.name !== 'string' && value.name !== null)) { + return null; + } + + return { + id: value.id, + email: value.email, + name: value.name, + }; +} + +function readSessionCookie(value: string): AppUser | null { + try { + return readAppUser(JSON.parse(decodeURIComponent(value))); + } catch { + return null; + } +} + +app.get('/auth/login', (c) => { const state = crypto.randomUUID(); const redirectUri = getRedirectUri(c.req.url); const loginUrl = new URL(buildAuthorizeUrl(redirectUri, oauthConfig)); - // The callback must return the same state to prevent CSRF attacks loginUrl.searchParams.set('state', state); - setCookie(c, 'agentuity_oauth_state', state, { + + setCookie(c, STATE_COOKIE, state, { path: '/', httpOnly: true, secure: new URL(c.req.url).protocol === 'https:', @@ -113,95 +96,191 @@ router.get('/oauth/login', (c) => { return c.redirect(loginUrl.toString()); }); -router.get('/oauth/callback', async (c) => { +app.get('/auth/callback', async (c) => { const code = c.req.query('code'); const returnedState = c.req.query('state'); - const storedState = getCookie(c, 'agentuity_oauth_state'); + const storedState = getCookie(c, STATE_COOKIE); - deleteCookie(c, 'agentuity_oauth_state', { path: '/' }); + deleteCookie(c, STATE_COOKIE, { path: '/' }); - if (!code || !returnedState || returnedState !== storedState) { + if (!code || !returnedState || !storedState || returnedState !== storedState) { return c.json({ error: 'Invalid OAuth callback' }, 400); } try { const redirectUri = getRedirectUri(c.req.url); const token = await exchangeToken(code, redirectUri, oauthConfig); - const user = await fetchUserInfo(token.access_token, oauthConfig); - - // Store your own app session, avoid putting access tokens in browser cookies - setCookie( - c, - 'app_session', - encodeURIComponent( - JSON.stringify({ - userId: user.sub, - email: user.email, - name: user.name, - }) - ), - { - path: '/', - httpOnly: true, - secure: new URL(c.req.url).protocol === 'https:', - sameSite: 'Lax', - maxAge: 60 * 60 * 24, - } - ); + const profile = await fetchUserInfo(token.access_token, oauthConfig); + + const user = { + id: profile.sub, + email: profile.email ?? null, + name: profile.name ?? null, + } satisfies AppUser; + + setCookie(c, SESSION_COOKIE, encodeURIComponent(JSON.stringify(user)), { + path: '/', + httpOnly: true, + secure: new URL(c.req.url).protocol === 'https:', + sameSite: 'Lax', + maxAge: 60 * 60 * 24, + }); return c.redirect('/'); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - c.var.logger.error('OAuth sign-in failed', { message }); + } catch { return c.json({ error: 'OAuth sign-in failed' }, 500); } }); -router.post('/oauth/logout', (c) => { - deleteCookie(c, 'app_session', { path: '/' }); +app.get('/auth/me', (c) => { + const session = getCookie(c, SESSION_COOKIE); + + if (!session) { + return c.json({ authenticated: false }); + } + + const user = readSessionCookie(session); + + if (!user) { + deleteCookie(c, SESSION_COOKIE, { path: '/' }); + return c.json({ authenticated: false }, 401); + } + + return c.json({ + authenticated: true, + user, + }); +}); + +app.post('/auth/logout', (c) => { + deleteCookie(c, SESSION_COOKIE, { path: '/' }); return c.redirect('/'); }); -export default router; +export default app; ``` - -The redirect URI in your OAuth app must match the callback URL your app sends to `buildAuthorizeUrl()`. For local testing, register a local URL such as `http://localhost:3500/api/oauth/callback`. +This example uses Hono because the flow is easy to see in one file. The same boundary applies in Next.js, TanStack Start, Remix, SvelteKit, Nuxt, Astro, or any framework route: redirect from the server, exchange the code on the server, then create your app session. + +## Create an OAuth App + +Create the OAuth app in the [Agentuity Console](https://app.agentuity.com/settings/oauth-apps) or with the CLI. For server-backed framework apps, use a confidential client so the client secret stays on the server. + +```bash +agentuity cloud oidc create \ + --name "My App" \ + --description "Sign in with Agentuity" \ + --homepage-url "https://example.com" \ + --type confidential \ + --redirect-uris "https://example.com/auth/callback" \ + --scopes "openid,profile,email" +``` + + +The client secret is returned when you create the OAuth app or rotate its secret. Store it in your environment variables before leaving that output. + + +## Configure Environment Variables + +The `@agentuity/core/oauth` helpers read `OAUTH_*` variables by default: + +```env +OAUTH_ISSUER=https://auth.agentuity.cloud +OAUTH_CLIENT_ID=your-client-id +OAUTH_CLIENT_SECRET=your-client-secret +OAUTH_SCOPES=openid profile email +``` + +| Variable | Description | +|----------|-------------| +| `OAUTH_ISSUER` | OIDC issuer base URL. The helpers derive `/authorize`, `/oauth/token`, `/userinfo`, `/revoke`, and `/end_session` from this value. | +| `OAUTH_CLIENT_ID` | Client ID from your OAuth app. | +| `OAUTH_CLIENT_SECRET` | Client secret for confidential clients. `exchangeToken()` requires it. | +| `OAUTH_SCOPES` | Space-separated scopes requested during sign-in. Defaults to `openid profile email`. | + +Pass explicit endpoint URLs only when you need to override issuer-derived URLs: + +| Variable | Default from `OAUTH_ISSUER` | +|----------|-----------------------------| +| `OAUTH_AUTHORIZE_URL` | `{issuer}/authorize` | +| `OAUTH_TOKEN_URL` | `{issuer}/oauth/token` | +| `OAUTH_USERINFO_URL` | `{issuer}/userinfo` | +| `OAUTH_REVOKE_URL` | `{issuer}/revoke` | +| `OAUTH_END_SESSION_URL` | `{issuer}/end_session` | + +The public discovery document is available at: + +```txt +https://auth.agentuity.cloud/.well-known/openid-configuration +``` + +Use the discovery document when you need endpoint metadata or the current JWKS URI for token verification. + + +The CLI `--scopes` flag is comma-separated, for example `openid,profile,email`. `OAUTH_SCOPES` is space-separated because it is sent as the OAuth `scope` parameter. ## Store Tokens for Scoped Access -If your app needs to call Agentuity APIs after sign-in, store tokens server-side. `KeyValueTokenStorage` stores tokens in Agentuity KV and refreshes expired access tokens when a refresh token is available. +If your app needs to call Agentuity APIs on behalf of the signed-in user, store OAuth tokens server-side. `KeyValueTokenStorage` stores tokens in Agentuity KV and refreshes expired access tokens when a refresh token is available. + +```bash +bun add @agentuity/keyvalue@alpha +``` ```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; import { KeyValueTokenStorage, isTokenExpired } from '@agentuity/core/oauth'; +import type { OAuthFlowConfig, OAuthTokenResponse } from '@agentuity/core/oauth'; -router.get('/oauth/callback', async (c) => { - // Validate callback state, exchange the code, and fetch the user - const tokenStore = new KeyValueTokenStorage(c.var.kv, { - config: oauthConfig, - prefix: 'agentuity:', - }); +const kv = new KeyValueClient(); + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is required`); + } + return value; +} + +const oauthConfig: OAuthFlowConfig = { + issuer: requireEnv('OAUTH_ISSUER'), + clientId: requireEnv('OAUTH_CLIENT_ID'), + clientSecret: requireEnv('OAUTH_CLIENT_SECRET'), + scopes: process.env.OAUTH_SCOPES ?? 'openid profile email', +}; - await tokenStore.set(user.sub, token); +const tokenStore = new KeyValueTokenStorage(kv, { + config: oauthConfig, + namespace: 'oauth-tokens', + prefix: 'agentuity:', +}); - const storedToken = await tokenStore.get(user.sub); +export async function persistTokens( + userId: string, + token: OAuthTokenResponse +): Promise { + await tokenStore.set(userId, token); +} + +export async function getUsableAccessToken(userId: string): Promise { + const token = await tokenStore.get(userId); - if (!storedToken || isTokenExpired(storedToken)) { - return c.redirect('/api/oauth/login'); + if (!token || isTokenExpired(token)) { + return null; } - return c.redirect('/'); -}); + return token.access_token; +} ``` -Request `offline_access` only when your app needs a refresh token: +Request `offline_access` only when your app needs refresh tokens: ```env OAUTH_SCOPES=openid profile email offline_access ``` -When a user disconnects the app, call `tokenStore.invalidate(user.sub)` to remove the token from KV and revoke it with the provider when possible. +When a user disconnects the app, call `tokenStore.invalidate(userId)` to remove the stored token and revoke it with the provider when possible. ## Scopes and Consent @@ -213,11 +292,20 @@ To browse available scopes, use the interactive scope picker: agentuity cloud oidc create ``` -If you already know the scopes, pass them with `--scopes` when you create the OAuth app. +If you already know the scopes, pass them with `--scopes`: + +```bash +agentuity cloud oidc create \ + --name "My App" \ + --homepage-url "https://example.com" \ + --type confidential \ + --redirect-uris "https://example.com/auth/callback" \ + --scopes "openid,profile,email,offline_access" +``` ## Next Steps -- [CLI OAuth Commands](/reference/cli/oauth): Create clients, rotate secrets, and inspect connected users -- [REST API OAuth Reference](/reference/api/oauth): Manage OAuth applications and consent grants over HTTP -- [Authentication Services](/services/authentication): Choose between Sign in with Agentuity and app-owned auth -- [ctx.auth Reference](/reference/sdk-reference/context-api#authentication): Use app-owned auth details inside Agentuity handlers +- [Choosing Authentication](/services/authentication): decide between OIDC and framework-owned sessions +- [CLI OAuth Commands](/reference/cli/oauth): create clients, rotate secrets, and inspect connected users +- [REST API OAuth Reference](/reference/api/oauth): manage OAuth applications and consent grants over HTTP +- [Framework-Owned Sessions](/services/authentication#framework-owned-sessions): connect browser UI to your framework routes diff --git a/docs/src/web/content/services/queues.mdx b/docs/src/web/content/services/queues.mdx index 4ffca5985..0fada804d 100644 --- a/docs/src/web/content/services/queues.mdx +++ b/docs/src/web/content/services/queues.mdx @@ -3,563 +3,226 @@ title: Queues description: Publish messages for async processing, webhooks, and event-driven workflows --- -Queues enable asynchronous message processing for background tasks, webhooks, and event-driven workflows. Publish messages from agents and consume them with workers or webhook destinations. +Use queues when a request should hand work to another process and return quickly. Start with `QueueClient`; Hono apps can use `c.var.queue` after installing the Agentuity middleware. - -Use the [`@agentuity/queue`](/reference/standalone-packages#message-queues) standalone package to access this service from any Node.js or Bun app without the runtime. - - -## When to Use Queues - -| Pattern | Best For | -|---------|----------| -| **Queues** | Background jobs, webhooks, event-driven processing, decoupled services | -| [Durable Streams](/services/storage/durable-streams) | Large exports, audit logs, streaming data | -| [Key-Value](/services/storage/key-value) | Fast lookups, caching, configuration | - -**Use queues when you need to:** -- Process tasks asynchronously (email sending, report generation) -- Decouple services with reliable message delivery -- Deliver webhooks to external endpoints -- Handle bursty workloads with rate limiting -- Retry failed operations with exponential backoff - -## Access Patterns - -| Context | Access | Details | -|---------|--------|---------| -| Agents | `ctx.queue` | See examples below | -| Routes | `c.var.queue` | See [Using in Routes](#using-in-routes) | -| Standalone package | `QueueClient` | See [Standalone Usage](#standalone-usage) | - - -The Queue API is identical in all contexts. `ctx.queue.publish()` and `c.var.queue.publish()` work the same way. - - -## Queue Types +```bash +bun add @agentuity/queue@alpha +``` -| Type | Behavior | -|------|----------| -| `worker` | Point-to-point delivery. Each message is consumed by exactly one consumer and requires acknowledgment. Use for background jobs and task processing. | -| `pubsub` | Broadcast delivery. Every subscriber receives every message. Use for event notifications and fan-out patterns. | +```typescript +import { QueueClient } from '@agentuity/queue'; -## Creating Queues +interface ReportRequest { + readonly userId: string; + readonly report: 'daily' | 'weekly'; +} -Create a queue from an agent or route using `ctx.queue.createQueue()`: +const queue = new QueueClient(); -```typescript -// Inside an agent handler or route -const result = await ctx.queue.createQueue('notifications', { // [!code highlight] - queueType: 'worker', - description: 'Order notifications', +await queue.createQueue('email-reports', { + description: 'Report emails generated outside the request path', settings: { - defaultTtlSeconds: 86400, // messages expire after 24 hours + defaultTtlSeconds: 60 * 60 * 24, defaultMaxRetries: 3, }, -}); // [!code highlight] -``` +}); -```typescript -interface QueueCreateResult { - name: string; // Queue name - queueType: string; // 'worker' or 'pubsub' +export async function queueReport(userId: string): Promise { + const result = await queue.publish( + 'email-reports', + { userId, report: 'daily' } satisfies ReportRequest, + { + partitionKey: userId, + idempotencyKey: `daily-report:${userId}`, + } + ); + + return result.id; } ``` -All parameters are optional. Omitting `queueType` defaults to `worker`. +`QueueClient` reads `AGENTUITY_SDK_KEY` by default. In Agentuity projects, keep the key in `.env` for local `agentuity dev` and configure the same environment variable for deployed apps. -| Option | Type | Description | -|--------|------|-------------| -| `queueType` | `'worker' \| 'pubsub'` | Worker for point-to-point, pubsub for broadcast (optional, defaults to `worker`) | -| `description` | `string` | Human-readable description (optional) | -| `settings.defaultTtlSeconds` | `number` | Message expiration window in seconds (optional) | -| `settings.defaultVisibilityTimeoutSeconds` | `number` | Visibility timeout after a message is received, before it returns to the queue (optional) | -| `settings.defaultMaxRetries` | `number` | Delivery retry limit before moving to DLQ (optional) | -| `settings.maxInFlightPerClient` | `number` | Concurrent message limit per consumer (optional) | -| `settings.retentionSeconds` | `number` | Retention period for acknowledged messages (optional) | +## When to Use Queues - -`createQueue` is safe to call multiple times. If the queue already exists, it returns successfully without error. The SDK caches creation within the current request context, so duplicate calls in the same handler return immediately without additional API requests. - +| Need | Use | +|------|-----| +| background jobs or async handoff | [Queues](/services/queues) | +| exact key lookup or counters | [Key-Value Storage](/services/storage/key-value) | +| append-only ordered data | [Durable Streams](/services/storage/durable-streams) | +| managed ingest from external services | [Webhooks](/services/webhooks) | +| recurring delivery on a timer | [Schedules](/services/schedules) | -## Deleting Queues +## Client Setup -Delete a queue and all its messages with `ctx.queue.deleteQueue()`: +Construct the client once at module scope and reuse it from handlers, routes, or scripts. ```typescript -await ctx.queue.deleteQueue('old-notifications'); +import { QueueClient } from '@agentuity/queue'; + +const queue = new QueueClient({ + orgId: process.env.AGENTUITY_ORG_ID, +}); ``` - -Deleting a queue is irreversible. All pending messages are lost immediately. If the queue does not exist, a `QueueNotFoundError` is thrown. - +| Option | Description | +|--------|-------------| +| `apiKey` | Optional API key. Defaults to `AGENTUITY_SDK_KEY`, then `AGENTUITY_CLI_KEY`. | +| `orgId` | Optional organization ID for org-scoped requests. | +| `url` | Optional Queue API URL. Defaults to `AGENTUITY_QUEUE_URL`, then the regional Agentuity service URL. | +| `logger` | Optional logger instance. | -## Publishing Messages +## Create a Queue -Publish messages from agents using `ctx.queue.publish()`: +`createQueue()` is idempotent. If the queue already exists, the call returns successfully. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('OrderProcessor', { - handler: async (ctx, input) => { - // Queue an email to be sent asynchronously - const result = await ctx.queue.publish('email-queue', { - to: input.customerEmail, - subject: 'Order Confirmed', - orderId: input.orderId, - }); - - ctx.logger.info('Email queued', { messageId: result.id }); - - return { success: true, messageId: result.id }; +await queue.createQueue('order-events', { + queueType: 'worker', + description: 'Orders waiting for fulfillment', + settings: { + defaultVisibilityTimeoutSeconds: 60, + maxInFlightPerClient: 10, + retentionSeconds: 60 * 60 * 24 * 30, }, }); ``` -### Publish Options - -```typescript -const agent = createAgent('TaskScheduler', { - handler: async (ctx, input) => { - await ctx.queue.publish('task-queue', input.task, { - // Attach metadata for filtering or routing - metadata: { priority: 'high', region: 'us-west' }, - - // Guarantee ordering for messages with the same key - partitionKey: input.customerId, +| Field | Description | +|-------|-------------| +| `queueType` | Optional. `worker` sends each message to one consumer. `pubsub` broadcasts to subscribers. Defaults to `worker`. | +| `description` | Optional human-readable purpose for the queue. | +| `settings.defaultTtlSeconds` | Optional message expiration in seconds. `null` means no expiration. | +| `settings.defaultVisibilityTimeoutSeconds` | Optional invisibility window after a worker receives a message. | +| `settings.defaultMaxRetries` | Optional retry limit before a failed message moves to the dead letter queue. | +| `settings.maxInFlightPerClient` | Optional concurrent message limit per consumer. | +| `settings.retentionSeconds` | Optional retention period for acknowledged messages. | - // Prevent duplicate messages - idempotencyKey: `task-${input.taskId}-v1`, +## Publish Messages - // Auto-expire after 1 hour - ttl: 3600, - }); +Payloads can be strings or JSON-serializable objects. - return { queued: true }; +```typescript +const result = await queue.publish( + 'order-events', + { + orderId: 'ord_123', + event: 'paid', }, -}); + { + metadata: { source: 'checkout' }, + partitionKey: 'ord_123', + idempotencyKey: 'order-paid:ord_123', + ttl: 60 * 60, + } +); ``` | Option | Description | |--------|-------------| -| `metadata` | Key-value pairs for routing or filtering | -| `partitionKey` | Messages with the same key are processed in order | -| `idempotencyKey` | Prevents duplicate messages if the same key is published again | -| `ttl` | Time-to-live in seconds before the message expires | +| `metadata` | Optional JSON metadata for routing or inspection. | +| `partitionKey` | Optional key for ordering related messages. | +| `idempotencyKey` | Optional deduplication key for retrying publishers. | +| `ttl` | Optional time-to-live in seconds. | +| `projectId` | Optional project ID for cross-project publishing. | +| `agentId` | Optional agent ID for attribution. | +| `sync` | Optional. When `true`, waits until the message is persisted before returning. | -### Publish Result +`publish()` returns the message ID, queue offset, and publish timestamp: ```typescript interface QueuePublishResult { - id: string; // Unique message ID (msg_...) - offset: number; // Sequential position in the queue - publishedAt: string; // ISO 8601 timestamp + readonly id: string; + readonly offset: number; + readonly publishedAt: string; } ``` -### Synchronous Publishing - -Use `sync: true` when you need to wait for the message to be persisted before returning: +Use `sync: true` only when the caller must know the message was persisted before continuing. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('CriticalProcessor', { - handler: async (ctx, input) => { - // Wait for message to be persisted - const result = await ctx.queue.publish('critical-tasks', { - taskId: input.taskId, - payload: input.data, - }, { - sync: true, - }); - - ctx.logger.info('Task queued synchronously', { taskId: input.taskId }); - return { status: 'queued', messageId: result.id }; - }, -}); -``` - - -Synchronous publishing blocks until the message is persisted. Use it only when you need confirmation that the message was accepted. For most use cases, async publishing is faster and more resilient. - - -### CLI Publishing - -```bash -# Publish a message via CLI -agentuity cloud queue publish order-processing '{"orderId": "123"}' - -# With options -agentuity cloud queue publish order-processing '{"orderId": "123"}' \ - --partition-key customer-456 \ - --idempotency-key order-123 \ - --ttl 3600 +await queue.publish('billing-events', { invoiceId: 'inv_123' }, { sync: true }); ``` -## Using in Routes +## Delete a Queue -Routes have the same queue access via `c.var.queue`: +Deleting a queue also deletes its messages. ```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; - -const router = new Hono(); - -router.post('/webhook/stripe', async (c) => { - const event = await c.req.json(); - - // Queue webhook for async processing - await c.var.queue.publish('stripe-webhooks', { - type: event.type, - data: event.data, - }); - - // Return 200 immediately (Stripe expects fast responses) - return c.json({ received: true }); -}); - -export default router; +await queue.deleteQueue('old-order-events'); ``` - -Use queues for webhooks that need quick acknowledgment. Return 200 immediately, then process the payload asynchronously. + +`deleteQueue()` permanently removes the queue and any pending, delivered, or failed messages associated with it. -## Standalone Usage +## Handle Errors -Use queues from external workers or background jobs with `QueueClient`: +The queue package exports typed errors for common client-side failures. ```typescript -import { QueueClient } from '@agentuity/queue'; +import { QueueClient, QueueNotFoundError, QueueValidationError } from '@agentuity/queue'; const queue = new QueueClient(); -export async function queueDailyReports(users: readonly { id: string }[]) { - for (const user of users) { - await queue.publish('email-reports', { - userId: user.id, - reportType: 'daily', - }); - } -} -``` - -Use [Running Agents Without HTTP](/agents/standalone-execution) when you need a full runtime context instead of the standalone queue client. - -## Error Handling - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { QueueNotFoundError, QueueValidationError } from '@agentuity/core'; - -const agent = createAgent('SafePublisher', { - handler: async (ctx, input) => { - try { - await ctx.queue.publish('notifications', input.notification); - return { success: true }; - } catch (error) { - if (error instanceof QueueNotFoundError) { - ctx.logger.error('Queue does not exist', { queue: 'notifications' }); - return { success: false, error: 'Queue not found' }; - } - if (error instanceof QueueValidationError) { - ctx.logger.error('Invalid message', { field: error.field }); - return { success: false, error: 'Validation failed' }; - } - throw error; +export async function publishNotification(userId: string): Promise { + try { + await queue.publish('notifications', { userId }); + return true; + } catch (error) { + if (error instanceof QueueNotFoundError) { + return false; } - }, -}); -``` - -## Queue Management - -Create and manage queues using the CLI or `@agentuity/server` package. -### CLI Commands - -```bash -# Create a worker queue (--name is optional, auto-generated if omitted) -agentuity cloud queue create worker --name order-processing - -# Create a pubsub queue for broadcasting -agentuity cloud queue create pubsub --name events -``` - -| Option | Description | -|--------|-------------| -| `--name ` | Queue name (optional, auto-generated if omitted) | -| `--description ` | Queue description | -| `--ttl ` | Default message TTL in seconds | -| `--visibility-timeout ` | Default visibility timeout (worker queues) | -| `--max-retries ` | Maximum retry attempts before moving to DLQ | - -```bash -# List all queues -agentuity cloud queue list - -# Filter by type and status -agentuity cloud queue list --queue-type worker --status active - -# Filter by name -agentuity cloud queue list --name order-processing - -# Sort by message count, descending -agentuity cloud queue list --sort message_count --direction desc - -# Paginate results -agentuity cloud queue list --limit 10 --offset 0 -``` - -| Option | Description | -|--------|-------------| -| `--name ` | Filter by queue name | -| `--queue-type ` | Filter by type: `worker` or `pubsub` | -| `--status ` | Filter by status: `active` or `paused` | -| `--org-id ` | Filter by organization | -| `--sort ` | Sort by `name`, `created`, `updated`, `message_count`, or `dlq_count` (default: `created`) | -| `--direction ` | Sort direction: `asc` or `desc` (default: `desc`) | -| `--limit ` | Maximum number of results | -| `--offset ` | Pagination offset | - -```bash -# Get queue details and stats -agentuity cloud queue get order-processing - -# Pause/resume processing -agentuity cloud queue pause order-processing -agentuity cloud queue resume order-processing - -# Delete a queue -agentuity cloud queue delete order-processing --confirm -``` - -For programmatic queue management, see [SDK Utilities for External Apps](/cookbook/patterns/server-utilities#queue-management). - -## Consuming Messages - -### Webhook Destinations - -Configure webhook destinations to automatically deliver messages to HTTP endpoints. Set these up in the [Web App](https://app.agentuity.com/services/queue) or [programmatically](/cookbook/patterns/server-utilities#webhook-destinations). - -Webhook destinations support: -- Custom headers and authentication -- Configurable timeouts (up to 30 seconds) -- Retry policies with exponential backoff - -```bash -# Add a webhook destination -agentuity cloud queue destinations create order-processing --url https://example.com/webhook - -# List destinations -agentuity cloud queue destinations list order-processing + if (error instanceof QueueValidationError) { + return false; + } -# Delete a destination -agentuity cloud queue destinations delete order-processing + throw error; + } +} ``` -### Pull-Based Consumption +## Hono -For workers that pull and process messages, see [Pull-Based Consumption](/cookbook/patterns/server-utilities#pull-based-consumption). This pattern is useful for long-running workers that need fine-grained control over message processing. +In Hono apps, `@agentuity/hono` initializes `QueueClient` once and exposes it on `c.var.queue`. ```bash -# Receive a message from a worker queue -agentuity cloud queue receive order-processing - -# Acknowledge a processed message -agentuity cloud queue ack order-processing - -# Return a message to the queue for retry -agentuity cloud queue nack order-processing +bun add @agentuity/hono@alpha hono ``` -### Real-Time Subscriptions - -When you need to process messages as they arrive rather than polling, use the WebSocket subscription API from `@agentuity/server`. - -#### Callback-Based API - ```typescript -import { createLogger, createQueueWebSocket, getServiceUrls } from '@agentuity/server'; - -const logger = createLogger(); -const baseUrl = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc').catalyst; - -const connection = createQueueWebSocket({ - queueName: 'order-processing', - baseUrl, - onMessage: (message) => { - logger.info('Received: %s %o', message.id, message.payload); - }, - onOpen: () => logger.info('Connected'), - onClose: (code, reason) => logger.info('Closed: %d %s', code, reason), - onError: (error) => logger.error('Error: %s', error.message), -}); - -// Later: close the connection -connection.close(); -``` +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; -The connection handle exposes `state`, `clientId`, and `lastOffset` properties for monitoring and session resumption. +type Variables = Pick; -#### Async Iterator API +const app = new Hono<{ Variables: Variables }>(); -For `for await...of` consumption, use `subscribeToQueue`: +app.use('*', agentuity()); -```typescript -import { createLogger, getServiceUrls, subscribeToQueue } from '@agentuity/server'; - -const logger = createLogger(); -const baseUrl = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc').catalyst; -const controller = new AbortController(); - -for await (const message of subscribeToQueue({ - queueName: 'order-processing', - baseUrl, - signal: controller.signal, -})) { - logger.info('Received: %s %o', message.id, message.payload); -} +app.post('/stripe/events', async (c) => { + const eventId = c.req.header('stripe-event-id'); -// To stop: controller.abort() -``` + if (!eventId) { + return c.json({ error: 'Missing stripe-event-id header' }, 400); + } -#### Session Resumption + const payload = await c.req.text(); -Save `clientId` and `lastOffset` from a connection and pass them back on reconnect to avoid reprocessing messages: + await c.var.queue.publish('stripe-events', payload, { + idempotencyKey: eventId, + }); -```typescript -import { createLogger, createQueueWebSocket, getServiceUrls } from '@agentuity/server'; - -const logger = createLogger(); -const baseUrl = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc').catalyst; - -// Save state from a previous connection -const savedClientId = previousConnection.clientId; -const savedOffset = previousConnection.lastOffset; - -// Resume from where you left off -const connection = createQueueWebSocket({ - queueName: 'order-processing', - baseUrl, - clientId: savedClientId, // Resume this subscription - lastOffset: savedOffset, // Skip already-processed messages - onMessage: (message) => { - logger.info('Received: %s %o', message.id, message.payload); - }, + return c.json({ received: true }); }); -``` - -#### Connection Options - -| Option | Default | Description | -|--------|---------|-------------| -| `autoReconnect` | `true` | Automatically reconnect on disconnection | -| `maxReconnectAttempts` | `Infinity` | Maximum reconnection attempts before giving up | -| `reconnectDelayMs` | `1000` | Initial reconnection delay (uses exponential backoff with jitter) | -| `maxReconnectDelayMs` | `30000` | Maximum reconnection delay cap | - -#### Connection States - -The connection transitions through these states: - -`connecting` → `authenticating` → `connected` - -On disconnection with `autoReconnect` enabled, it cycles through `reconnecting` → `connecting` → `authenticating` → `connected`. Authentication failures are terminal and do not trigger reconnection. - -## Dead Letter Queue - -Messages that exceed the retry limit are moved to the dead letter queue (DLQ). Inspect and replay failed messages: - -```bash -# List failed messages -agentuity cloud queue dlq list order-processing - -# Replay a message back to the queue -agentuity cloud queue dlq replay order-processing msg_abc123 - -# Purge all DLQ messages -agentuity cloud queue dlq purge order-processing --confirm -``` -DLQ list supports `--limit` and `--offset` for pagination. - -For programmatic DLQ access, see [Dead Letter Queue Operations](/cookbook/patterns/server-utilities#dead-letter-queue-operations). - -## HTTP Ingestion Sources - -Create public HTTP endpoints to ingest data into queues from external services. Configure these in the [Web App](https://app.agentuity.com/services/queue) or [programmatically](/cookbook/patterns/server-utilities#http-ingestion-sources). - -| Auth Type | Description | -|-----------|-------------| -| `none` | No authentication | -| `basic` | HTTP Basic Auth (`username:password`) | -| `header` | Custom header value (`Bearer token`) | - -```bash -# Create an HTTP ingestion source -agentuity cloud queue sources create order-processing \ - --name stripe-ingest \ - --auth-type basic \ - --auth-value user:pass - -# List sources -agentuity cloud queue sources list order-processing +export default app; ``` -## Monitoring - -```bash -# View queue statistics -agentuity cloud queue stats order-processing - -# Live stats with auto-refresh -agentuity cloud queue stats order-processing --live - -# List messages in a queue -agentuity cloud queue messages order-processing -``` - -## Queue Settings - -Configure queue behavior when creating or updating: - -| Setting | Default | Description | -|---------|---------|-------------| -| `default_ttl_seconds` | null | Message expiration (null = never) | -| `default_visibility_timeout_seconds` | 30 | Processing timeout before message returns to queue | -| `default_max_retries` | 5 | Attempts before moving to DLQ | -| `default_retry_backoff_ms` | 1000 | Initial retry delay | -| `default_retry_max_backoff_ms` | 60000 | Maximum retry delay | -| `default_retry_multiplier` | 2.0 | Exponential backoff multiplier | -| `max_in_flight_per_client` | 10 | Concurrent messages per consumer | -| `retention_seconds` | 2592000 | How long to keep acknowledged messages (30 days) | - -## Validation Limits - -| Limit | Value | -|-------|-------| -| Queue name length | 1-256 characters | -| Queue name format | Lowercase letters, digits, underscores, hyphens. Must start with letter or underscore. | -| Payload size | 1 MB max | -| Partition key length | 256 characters max | -| Idempotency key length | 256 characters max | -| Batch size | 1000 messages max | - -## Best Practices - -- **Use idempotency keys** for operations that shouldn't be duplicated (payments, emails) -- **Set appropriate TTLs** for time-sensitive messages -- **Use partition keys** when message ordering matters within a group -- **Monitor DLQ** regularly to catch and fix processing failures -- **Configure webhook retry policies** to handle transient failures gracefully - ## Next Steps -- [Durable Streams](/services/storage/durable-streams): Streaming storage for large exports -- [Key-Value Storage](/services/storage/key-value): Fast caching and configuration -- [Background Tasks](/cookbook/patterns/background-tasks): Patterns for async processing -- [Webhook Handler](/cookbook/patterns/webhook-handler): Receiving external webhooks +- [Webhooks](/services/webhooks): create managed ingest URLs that forward payloads to your app +- [Schedules](/services/schedules): run recurring work on a cron expression +- [Using Standalone Packages](/reference/standalone-packages): configure service clients outside Agentuity projects diff --git a/docs/src/web/content/services/sandbox/index.mdx b/docs/src/web/content/services/sandbox/index.mdx index 7a021bb53..457eeb6f3 100644 --- a/docs/src/web/content/services/sandbox/index.mdx +++ b/docs/src/web/content/services/sandbox/index.mdx @@ -1,330 +1,251 @@ --- title: Running Code in Sandboxes -description: Run code in isolated, secure containers with configurable resources +description: Run code in isolated containers with configurable runtimes, resources, and file access --- -Execute code in isolated Linux containers with configurable resource limits, network controls, and execution timeouts. +Use sandboxes when your app needs to execute code without running it on your own host. The v3 path is the standalone `@agentuity/sandbox` package, which you can use from any Bun or Node.js process. -## Why Sandboxes? - -Agents that reason about code need somewhere safe to execute it. Whether generating Python scripts, validating builds, or running user-provided code, you can't let arbitrary execution happen on your infrastructure. +```bash +bun add @agentuity/sandbox@alpha +``` -The pattern keeps repeating: spin up a secure environment, run code, tear it down. Without proper isolation, a single bad script could access sensitive data, exhaust resources, or compromise your systems. +```typescript +import { SandboxClient } from '@agentuity/sandbox'; -Agentuity sandboxes handle the isolation layer. One-shot runs create a sandbox, execute a command, and destroy it. Interactive sandboxes keep their filesystem until you destroy them or the idle timeout reaps them. +const client = new SandboxClient({ + // Optional. Defaults to AGENTUITY_SDK_KEY, then AGENTUITY_CLI_KEY. + apiKey: process.env.AGENTUITY_SDK_KEY, +}); -**What this gives you:** +const result = await client.run({ + runtime: 'python:3.14', + command: { exec: ['python3', '-c', 'print("hello from a sandbox")'] }, + resources: { memory: '256Mi', cpu: '500m' }, + timeout: { execution: '30s' }, +}); -- **Security by default**: Network disabled, filesystem isolated, resource limits enforced -- **No infrastructure management**: Containers spin up and tear down automatically -- **Multi-language support**: Run Python, Node.js, shell scripts, and more -- **Consistent environments**: Use snapshots to get the same setup every time, with dependencies pre-installed +console.log(result.stdout); +``` -## Three Ways to Use Sandboxes + +Older v2 runtime examples may show `ctx.sandbox`, `c.var.sandbox`, or `createAgent()`. These pages use `SandboxClient` directly because that is the v3 service client. + -| Method | Best For | -|--------|----------| -| **[Web App](https://app.agentuity.com)** | Visual management, browsing runtimes and snapshots | -| **[SDK](/services/sandbox/sdk-usage)** | Programmatic use in agents and routes (`ctx.sandbox`) | -| **[CLI](/reference/cli/sandbox)** | Local development, scripting, CI/CD | +## When to Use Sandboxes - -Your agents are written in TypeScript, but the sandbox can run any language safely. Use `ctx.sandbox.run()` to execute Python, Node.js, shell scripts, or anything available via `apt install` in isolated containers. - +| Use case | Example | +|----------|---------| +| Code execution agents | Run user-provided Python or JavaScript without sharing your host filesystem | +| Code validation | Check whether generated code compiles, tests, or produces expected output | +| Build jobs | Install dependencies and run builds in a disposable environment | +| AI coding assistants | Give an assistant a workspace where it can edit and execute files | +| Data processing | Run short-lived scripts with explicit CPU, memory, and timeout limits | ## Key Concepts | Concept | Description | |---------|-------------| -| **Runtime** | A pre-configured base environment (OS + language tools) provided by Agentuity | -| **Sandbox** | A running container created from a runtime where you execute commands | -| **Snapshot** | A saved sandbox state that can be used to create new sandboxes | -| **Checkpoint** | A saved filesystem state for one sandbox, used by pause/resume and restore workflows | +| **Runtime** | A base environment such as `bun:1`, `node:lts`, or `python:3.14` | +| **Sandbox** | A running container created from a runtime or snapshot | +| **Snapshot** | A reusable filesystem state for starting future sandboxes faster | +| **Checkpoint** | A filesystem state for one sandbox, used by pause, resume, restore, and delete workflows | -Runtimes, sandboxes, and snapshots build on each other: **Runtime → Sandbox → Snapshot**. Checkpoints are sandbox-scoped: you restore the same sandbox back to a saved filesystem state instead of creating a reusable base image. +Runtimes, sandboxes, and snapshots build on each other: **Runtime -> Sandbox -> Snapshot**. Checkpoints stay scoped to one sandbox. -1. Pick a **runtime** (e.g., `bun:1` or `node:latest`) -2. Create a **sandbox** from that runtime -3. Optionally save a **snapshot** to reuse your configured environment +## Execution Modes -## Runtimes - -Runtimes are pre-configured base environments that Agentuity provides. Each includes an operating system, language toolchain, and common utilities. +Use `client.run()` for one command. It creates a sandbox, runs the command, streams/captures output, and cleans up the sandbox. -### Language Runtimes - -Use these for general code execution: +```typescript +import { SandboxClient } from '@agentuity/sandbox'; -| Runtime | Description | -|---------|-------------| -| `base:latest` | Minimal base runtime with essential tools (default) | -| `bun:1` | Bun 1.x with JavaScript/TypeScript support | -| `node:latest` | Node.js latest version | -| `node:lts` | Node.js LTS version | -| `python:3.13` | Python 3.13 with uv package manager | -| `python:3.14` | Python 3.14 with uv package manager | +const client = new SandboxClient(); -### Agent Runtimes +const result = await client.run({ + runtime: 'bun:1', + command: { + exec: ['bun', '-e', 'console.log(JSON.stringify({ ok: true }))'], + }, + timeout: { execution: '15s' }, +}); -Pre-configured AI coding assistants: +console.log({ + exitCode: result.exitCode, + stdout: result.stdout, +}); +``` -| Runtime | Description | -|---------|-------------| -| `claude-code:latest` | Claude Code AI assistant | -| `amp:latest` | Amp AI coding assistant | -| `opencode:latest` | OpenCode AI coding assistant | +Use `client.create()` when the workflow needs multiple commands or persistent files. Destroy interactive sandboxes when you are done with them. - -Run `agentuity cloud sandbox runtime list` to see all available runtimes, or view them in the [Web App](https://app.agentuity.com) under **Services > Sandbox > Runtimes**. - +```typescript +import { Buffer } from 'node:buffer'; +import { SandboxClient } from '@agentuity/sandbox'; + +const client = new SandboxClient(); +const sandbox = await client.create({ + runtime: 'bun:1', + resources: { memory: '1Gi', cpu: '1000m' }, + network: { enabled: true }, + timeout: { idle: '10m', execution: '2m' }, +}); -### Runtime Metadata +try { + await sandbox.writeFiles([ + { + path: 'index.ts', + content: Buffer.from('console.log("ready")'), + }, + ]); + + const execution = await sandbox.execute({ + command: ['bun', 'run', 'index.ts'], + }); + + console.log({ exitCode: execution.exitCode }); +} finally { + await sandbox.destroy(); +} +``` -Each runtime includes metadata for identification and resource planning: +See [Using the Sandbox API](/services/sandbox/sdk-usage) for file I/O, background jobs, checkpoints, snapshots, and method reference tables. -| Field | Description | -|-------|-------------| -| `description` | What the runtime provides | -| `iconUrl` | URL to runtime icon | -| `brandColor` | Hex color for UI display | -| `url` | Link to runtime documentation or homepage | -| `tags` | Categories like `language`, `testing`, `agent` | -| `requirements` | Minimum memory, CPU, disk, and `networkEnabled` requirements | +## Runtimes -View runtime details with `agentuity cloud sandbox runtime list --json`. +Runtimes are preconfigured base environments. List the runtimes available to your org from code or from the CLI: -## Snapshots +```typescript +import { SandboxClient } from '@agentuity/sandbox'; -A snapshot captures the filesystem state of a sandbox. You create new sandboxes *from* a snapshot rather than running it directly. +const client = new SandboxClient(); +const { runtimes } = await client.listRuntimes({ limit: 20 }); -Snapshots build on top of runtimes. When you create a snapshot, it includes everything from the base runtime plus your installed dependencies and files. +for (const runtime of runtimes) { + console.log(runtime.name, runtime.description ?? ''); +} +``` -**Workflow:** -1. Create a sandbox from a runtime -2. Install dependencies and configure the environment -3. Save a snapshot -4. Create new sandboxes from that snapshot (fast, no reinstallation needed) +```bash +agentuity cloud sandbox runtime list +``` -See [Creating and Using Snapshots](/services/sandbox/snapshots) for details. +Common runtime names include: -## Two Execution Modes +| Runtime | Description | +|---------|-------------| +| `base:latest` | Minimal base runtime with common tools | +| `bun:1` | Bun 1.x with JavaScript and TypeScript support | +| `node:latest` | Latest Node.js runtime | +| `node:lts` | Node.js LTS runtime | +| `python:3.13` | Python 3.13 with uv package manager | +| `python:3.14` | Python 3.14 with uv package manager | +| `claude-code:latest` | Claude Code coding assistant runtime | +| `amp:latest` | Amp coding assistant runtime | +| `opencode:latest` | OpenCode coding assistant runtime | -Choose based on your use case: +Runtime responses include metadata such as `description`, `iconUrl`, `brandColor`, `url`, `tags`, and `requirements`. -### One-shot (`sandbox.run()`) +## Snapshots -Creates a sandbox, runs a single command, then destroys the sandbox. Best for stateless code execution. +A snapshot captures a sandbox filesystem so new sandboxes can start with dependencies and files already present. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('CodeRunner', { - handler: async (ctx, input) => { - const result = await ctx.sandbox.run({ - command: { exec: ['python3', '-c', 'print("Hello!")'] }, - resources: { memory: '256Mi', cpu: '500m' }, - }); +import { SandboxClient } from '@agentuity/sandbox'; - ctx.logger.info('Output', { stdout: result.stdout, exitCode: result.exitCode }); - return { output: result.stdout, exitCode: result.exitCode }; - }, +const client = new SandboxClient(); +const sandbox = await client.create({ + runtime: 'bun:1', + network: { enabled: true }, }); -``` -### Interactive (`sandbox.create()`) +try { + await sandbox.execute({ command: ['bun', 'init', '-y'] }); + await sandbox.execute({ command: ['bun', 'add', 'zod'] }); -Creates a persistent sandbox for multiple commands. Best for stateful workflows like dependency installation. + const snapshot = await client.createSnapshot(sandbox.id, { + name: 'bun-zod', + tag: 'bun-zod', + description: 'Bun project with Zod installed', + }); -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('ProjectBuilder', { - handler: async (ctx, input) => { - const sandbox = await ctx.sandbox.create({ - runtime: 'node:lts', - resources: { memory: '1Gi' }, - network: { enabled: true }, // Required for package installation - }); - - try { - await sandbox.execute({ command: ['npm', 'init', '-y'] }); - await sandbox.execute({ command: ['npm', 'install', 'zod'] }); - const result = await sandbox.execute({ - command: ['node', '-e', 'console.log("ready")'], - }); - - return { exitCode: result.exitCode }; - } finally { - await sandbox.destroy(); - } - }, -}); + console.log(snapshot.snapshotId); +} finally { + await sandbox.destroy(); +} ``` -## Background Jobs - -Jobs let you run long-running commands in a sandbox without blocking. Unlike regular execution, jobs: - -- **Run in parallel**: Multiple jobs can execute simultaneously -- **Don't block**: Control returns immediately after creation -- **Persist**: Jobs continue even after the creating request completes -- **Capture output**: Stdout/stderr are captured to streams for later retrieval - -### Creating Jobs +Create future sandboxes from the snapshot ID or tag: ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('JobRunner', { - handler: async (ctx, input) => { - const sandbox = await ctx.sandbox.create({ - runtime: 'node:lts', - resources: { memory: '2Gi' }, - network: { enabled: true }, - }); - - // Create a background job - const job = await sandbox.createJob({ - command: ['sh', '-c', 'sleep 30 && echo done'], - }); - - ctx.logger.info('Job started', { jobId: job.jobId }); - - // Check status later - const status = await sandbox.getJob(job.jobId); - if (status.status === 'completed') { - ctx.logger.info('Job completed', { exitCode: status.exitCode }); - } - - return { jobId: job.jobId }; - }, +const sandbox = await client.create({ + snapshot: 'bun-zod', + resources: { memory: '512Mi' }, }); ``` -### Job Lifecycle +See [Creating and Using Snapshots](/services/sandbox/snapshots) for declarative snapshot files, CLI commands, and snapshot management APIs. -| Status | Description | -|--------|-------------| -| `pending` | Job created, waiting to start | -| `running` | Job actively executing | -| `completed` | Finished with exit code 0 | -| `failed` | Finished with non-zero exit code | -| `cancelled` | Terminated by user request | +## Background Jobs -### Stopping Jobs +Use jobs for commands that should keep running while your process does other work. Jobs are managed from `SandboxClient` with a sandbox ID. ```typescript -// Graceful stop (SIGTERM, then SIGKILL after grace period) -await sandbox.stopJob(job.jobId); +import { SandboxClient } from '@agentuity/sandbox'; -// Force kill immediately -await sandbox.stopJob(job.jobId, true); -``` +const client = new SandboxClient(); +const sandbox = await client.create({ runtime: 'bun:1' }); -### Use Cases +try { + const job = await client.createJob(sandbox.id, { + command: ['sh', '-c', 'sleep 30 && echo done'], + }); -| Use Case | Example | -|----------|---------| -| Build processes | Run `npm run build` in background | -| Long-running tests | Execute test suites without blocking | -| Data processing | Process large files asynchronously | -| Service daemons | Run background services in sandbox | - -## SDK Access - -| Context | Access | -|---------|--------| -| Agents | `ctx.sandbox` | -| Routes | `c.var.sandbox` | - -The API is identical in both contexts. - -## Configuration Options - -| Option | Description | Example | -|--------|-------------|---------| -| `runtime` | Runtime environment | `'bun:1'`, `'python:3.14'` | -| `resources.memory` | Memory limit (Kubernetes-style) | `'512Mi'`, `'1Gi'` | -| `resources.cpu` | CPU limit in millicores | `'500m'`, `'1000m'` | -| `resources.disk` | Disk space limit | `'1Gi'` | -| `network.enabled` | Allow outbound network | `true` (default: `false`) | -| `network.port` | Port to expose to internet (1024-65535) | `3000` | -| `projectId` | Associate sandbox with a project | `'proj_abc123'` | -| `timeout.idle` | Idle timeout before cleanup | `'10m'`, `'1h'` | -| `timeout.execution` | Max execution time per command | `'5m'`, `'30s'` | -| `dependencies` | Apt packages to install | `['python3', 'git']` | -| `packages` | npm/bun packages to install globally | `['typescript', 'tsx']` | -| `env` | Environment variables | `{ NODE_ENV: 'test' }` | -| `snapshot` | Create from existing snapshot | `'my-env'` or `snp_abc123` | - -## Sandbox Events - -Every sandbox records lifecycle events as it transitions through states. Use `sandboxEventList` to retrieve these events for auditing or debugging. - - -`sandboxEventList` is a server-side function from `@agentuity/server`. It requires an `APIClient` instance, typically used in CLI tools or standalone scripts rather than agent handlers. - - -```typescript -import { sandboxEventList } from '@agentuity/server'; + const current = await job.get(); + console.log({ jobId: job.id, status: current.status }); -const { events } = await sandboxEventList(client, { - sandboxId: 'sbx_abc123', - limit: 50, // optional, default 50 - direction: 'asc', // optional: 'asc' (oldest first, default) or 'desc' -}); + await job.stop(); +} finally { + await sandbox.destroy(); +} ``` -Each event includes: +## Events and Lifecycle -| Field | Description | -|-------|-------------| -| `eventId` | Unique identifier for the event | -| `sandboxId` | ID of the sandbox | -| `type` | Event type (e.g., `create`, `destroy`, `lifecycle:started`) | -| `event` | Arbitrary payload data for the event | -| `createdAt` | ISO timestamp when the event was recorded | +Every sandbox records lifecycle events. Use `client.listEvents()` when you need to inspect what happened to a sandbox. -From the CLI, use `agentuity cloud sandbox events ` to list events. See [CLI Commands](/reference/cli/sandbox) for options. +```typescript +const { events } = await client.listEvents('sbx_abc123', { + limit: 50, + direction: 'asc', +}); -## Resume Paused Sandboxes +for (const event of events) { + console.log(event.type, event.createdAt); +} +``` -`sandbox.execute()` automatically resumes a suspended sandbox and returns `autoResumed: true` on the execution result. Call `sandbox.resume()` when you want the sandbox awake before issuing a batch of commands. +Use `sandbox.pause()` and `sandbox.resume()` when you want to checkpoint and later restart an interactive sandbox. `sandbox.execute()` can also auto-resume a suspended sandbox before running a command and may return `autoResumed: true`. ```typescript await sandbox.resume(); -const execution = await sandbox.execute({ command: ['bun', 'run', 'test'] }); -ctx.logger - .child({ executionId: execution.executionId, autoResumed: execution.autoResumed }) - .info('Sandbox command completed'); -``` - - -Use explicit `sandbox.resume()` (or `agentuity cloud sandbox resume`) when startup latency matters before the first command. For single commands, `execute()` can wake the sandbox for you. - - -## When to Use Sandbox +const execution = await sandbox.execute({ + command: ['bun', 'run', 'test'], +}); -| Use Case | Example | -|----------|---------| -| Code execution agents | Run user-provided Python/JavaScript safely | -| Code validation | Verify generated code compiles and runs | -| AI coding assistants | Execute code suggested by LLMs | -| Automated testing | Run tests in clean environments | -| Build systems | Compile projects in isolated containers | +console.log({ + executionId: execution.executionId, + autoResumed: execution.autoResumed, +}); +``` -## Security +## Security Defaults -Sandboxes provide isolation through: +Sandboxes give each run an isolated workspace and explicit resource limits. Network access is disabled unless you enable it with `network.enabled` or expose a port with `network.port`. -- **Network disabled by default**: Enable explicitly when needed -- **Resource limits**: Prevent resource exhaustion -- **Execution timeouts**: Prevent runaway processes -- **Filesystem isolation**: Each sandbox has its own workspace +Set timeouts for untrusted commands, keep resource limits tight, and use snapshots for shared dependencies instead of reinstalling packages on every run. ## Next Steps -- [SDK Usage](/services/sandbox/sdk-usage): Detailed API for file I/O, streaming, and advanced configuration -- [Snapshots](/services/sandbox/snapshots): Skip dependency installation with pre-configured environments -- [CLI Commands](/reference/cli/sandbox): Debug sandboxes and create snapshots manually +- [Using the Sandbox API](/services/sandbox/sdk-usage): method examples for `SandboxClient` +- [Creating and Using Snapshots](/services/sandbox/snapshots): declarative and manual snapshot workflows +- [CLI Sandbox Commands](/reference/cli/sandbox): inspect and manage sandboxes from the terminal diff --git a/docs/src/web/content/services/sandbox/sdk-usage.mdx b/docs/src/web/content/services/sandbox/sdk-usage.mdx index bee1bc900..f23cd3247 100644 --- a/docs/src/web/content/services/sandbox/sdk-usage.mdx +++ b/docs/src/web/content/services/sandbox/sdk-usage.mdx @@ -1,62 +1,66 @@ --- title: Using the Sandbox API short_title: SDK Usage -description: Programmatic API for creating and managing sandboxes +description: Create, execute, inspect, and clean up sandboxes with SandboxClient --- -Access sandbox functionality through `ctx.sandbox` in agents or `c.var.sandbox` in routes. Choose between one-shot execution for single commands or interactive sandboxes for multi-step workflows. +Use `SandboxClient` when you want to create sandboxes from a script, server route, worker, or agent process. Set `AGENTUITY_SDK_KEY` in the environment, or pass `apiKey` to the constructor. - -Use the [`@agentuity/sandbox`](/reference/standalone-packages#sandbox) standalone package to access this service from any Node.js or Bun app without the runtime. +```typescript +import { SandboxClient } from '@agentuity/sandbox'; + +const client = new SandboxClient({ + apiKey: process.env.AGENTUITY_SDK_KEY, +}); +``` + + +If you are maintaining v2 runtime code, you may still see `ctx.sandbox` or `c.var.sandbox`. New v3 code should use `SandboxClient` from `@agentuity/sandbox`. ## One-shot Execution -Use `sandbox.run()` when you need to execute a single command. The sandbox is automatically created and destroyed. +Use `client.run()` for a disposable sandbox. The client creates the sandbox, runs the command, captures output, and destroys the sandbox. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { z } from 'zod'; - -const agent = createAgent('CodeRunner', { - schema: { - input: z.object({ code: z.string() }), - output: z.object({ - success: z.boolean(), - output: z.string(), - exitCode: z.number(), - }), - }, - handler: async (ctx, input) => { - const result = await ctx.sandbox.run({ - command: { - exec: ['python3', '-c', input.code], - }, - resources: { memory: '256Mi', cpu: '500m' }, - timeout: { execution: '30s' }, - }); - - return { - success: result.exitCode === 0, - output: result.stdout || result.stderr || '', - exitCode: result.exitCode, - }; +import { SandboxClient } from '@agentuity/sandbox'; + +const client = new SandboxClient(); + +const result = await client.run({ + runtime: 'python:3.14', + command: { + exec: ['python3', '-c', 'print(sum([1, 2, 3]))'], }, + resources: { memory: '256Mi', cpu: '500m' }, + timeout: { execution: '30s' }, +}); + +console.log({ + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr, }); ``` -### With File Input +### Write Files Before Running -Write files to the sandbox before execution: +Pass `command.files` to write files into the sandbox before the one-shot command starts. ```typescript -const result = await ctx.sandbox.run({ +import { Buffer } from 'node:buffer'; +import { SandboxClient } from '@agentuity/sandbox'; + +const client = new SandboxClient(); + +const result = await client.run({ + runtime: 'bun:1', command: { exec: ['bun', 'run', 'index.ts'], files: [ { path: 'index.ts', - content: Buffer.from('console.log("Hello from TypeScript!")'), + content: Buffer.from('console.log("hello from TypeScript")'), }, { path: 'data.json', @@ -64,174 +68,152 @@ const result = await ctx.sandbox.run({ }, ], }, - resources: { memory: '512Mi' }, }); + +console.log(result.stdout); ``` -## Interactive Sandbox +### Stream Output -Use `sandbox.create()` for multi-step workflows. The sandbox persists until you explicitly destroy it. +The optional second argument to `run()` accepts Node streams and an `AbortSignal`. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('ProjectBuilder', { - handler: async (ctx, input) => { - // Create a persistent sandbox - const sandbox = await ctx.sandbox.create({ - runtime: 'node:lts', - resources: { memory: '1Gi', cpu: '1000m' }, - network: { enabled: true }, // Allow package downloads - dependencies: ['git'], // Pre-install apt packages - }); - - try { - // Run multiple commands in sequence - await sandbox.execute({ command: ['npm', 'init', '-y'] }); - await sandbox.execute({ command: ['npm', 'install', 'zod'] }); - - // Write project files - await sandbox.writeFiles([ - { - path: 'index.ts', - content: Buffer.from(` - import { z } from 'zod'; - const schema = z.object({ name: z.string() }); - console.log(schema.parse({ name: 'test' })); - `), - }, - ]); - - // Build and run - const result = await sandbox.execute({ - command: ['npx', 'tsx', 'index.ts'], - }); - - return { - exitCode: result.exitCode, - stdoutStreamUrl: result.stdoutStreamUrl, - }; - } finally { - // Always clean up - await sandbox.destroy(); - } - }, -}); -``` +import { SandboxClient } from '@agentuity/sandbox'; -### Writing Files +const client = new SandboxClient(); -Write files to the sandbox workspace before or during execution: +const result = await client.run( + { + runtime: 'bun:1', + command: { exec: ['bun', '-e', 'console.error("err"); console.log("out")'] }, + }, + { + stdout: process.stdout, + stderr: process.stderr, + } +); -```typescript -await sandbox.writeFiles([ - { path: 'src/main.py', content: Buffer.from('print("Hello")') }, - { path: 'config.json', content: Buffer.from('{"debug": true}') }, -]); +console.log('exit code:', result.exitCode); ``` -### Exposing Ports +## Interactive Sandboxes -Expose a port from the sandbox to make it accessible via a public URL: +Use `client.create()` when the workflow needs persistent files or multiple commands. ```typescript -const sandbox = await ctx.sandbox.create({ - network: { - enabled: true, - port: 3000, // Expose port 3000 (valid range: 1024-65535) - }, - resources: { memory: '512Mi' }, +import { Buffer } from 'node:buffer'; +import { SandboxClient } from '@agentuity/sandbox'; + +const client = new SandboxClient(); +const sandbox = await client.create({ + runtime: 'bun:1', + resources: { memory: '1Gi', cpu: '1000m' }, + network: { enabled: true }, + timeout: { idle: '10m', execution: '2m' }, }); -// Start a web server inside the sandbox -await sandbox.execute({ command: ['npm', 'run', 'serve'] }); +try { + await sandbox.execute({ command: ['bun', 'init', '-y'] }); + await sandbox.execute({ command: ['bun', 'add', 'zod'] }); + + await sandbox.writeFiles([ + { + path: 'index.ts', + content: Buffer.from(` + import { z } from 'zod'; + + const schema = z.object({ name: z.string() }); + console.log(schema.parse({ name: 'Ada' }).name); + `), + }, + ]); + + const execution = await sandbox.execute({ + command: ['bun', 'run', 'index.ts'], + }); -// Get the public URL -const info = await ctx.sandbox.get(sandbox.id); -if (info.url) { - ctx.logger.info('Server accessible at', { url: info.url, port: info.networkPort }); + console.log({ + executionId: execution.executionId, + exitCode: execution.exitCode, + }); +} finally { + await sandbox.destroy(); } ``` - -Setting `network.port` enables networking for the exposed port. Include `network.enabled: true` when the sandbox also needs outbound network access. - +## File Operations - -When using the CLI, `--port` enables networking for you and returns the public URL directly in the create response. See [CLI Sandbox Commands](/reference/cli/sandbox#port-mapping) for details. - +Interactive sandbox instances support file reads, writes, directory creation, listing, and removal. -### Project Association +```typescript +import { Buffer } from 'node:buffer'; +import { SandboxClient } from '@agentuity/sandbox'; -Associate sandboxes with a project for organization and filtering: +const client = new SandboxClient(); +const sandbox = await client.create({ runtime: 'bun:1' }); -```typescript -const sandbox = await ctx.sandbox.create({ - projectId: 'proj_abc123', // Associate with project - resources: { memory: '512Mi' }, -}); +try { + await sandbox.mkDir('src', true); -// List sandboxes by project -const { sandboxes } = await ctx.sandbox.list({ - projectId: 'proj_abc123', -}); -``` + const filesWritten = await sandbox.writeFiles([ + { path: 'src/index.ts', content: Buffer.from('console.log("ok")') }, + ]); -### Reading Files + const files = await sandbox.listFiles('src'); + const stream = await sandbox.readFile('src/index.ts'); + const source = await readText(stream); -Read files from the sandbox as streams: + console.log({ filesWritten, files, source }); +} finally { + await sandbox.destroy(); +} -```typescript -const stream = await sandbox.readFile('output/results.json'); -const reader = stream.getReader(); -const chunks: Uint8Array[] = []; - -while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); +async function readText(stream: ReadableStream): Promise { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let output = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + output += decoder.decode(value, { stream: true }); + } + + return output + decoder.decode(); } +``` + +Remove files and directories with `rmFile()` and `rmDir()`: -const content = new TextDecoder().decode(Buffer.concat(chunks)); -const data = JSON.parse(content); +```typescript +const removedFile = await sandbox.rmFile('src/index.ts'); +const removedDir = await sandbox.rmDir('src', true); + +console.log({ + fileExisted: removedFile.found, + dirExisted: removedDir.found, +}); ``` -### Reading Execution Output +## Reading Execution Output -`sandbox.execute()` waits for the command to finish, then returns stream URLs for stdout and stderr when output was captured. Fetch those URLs when you need the command output without loading it into the execution response. +`sandbox.execute()` waits for the command to finish and returns stream URLs when output was captured. Fetch those URLs when you need stdout or stderr after the command completes. ```typescript -const sandbox = await ctx.sandbox.create(); +const execution = await sandbox.execute({ + command: ['bun', '-e', 'console.log("build output")'], +}); -try { - // Run the command and wait for its final status - const execution = await sandbox.execute({ command: ['npm', 'run', 'build'] }); - - // Read captured output only when the service returned stream URLs - const stdout = execution.stdoutStreamUrl - ? await readTextStream(execution.stdoutStreamUrl) - : ''; - const stderr = execution.stderrStreamUrl - ? await readTextStream(execution.stderrStreamUrl) - : ''; - - if (execution.exitCode !== 0) { - ctx.logger.error('Build failed', { - exitCode: execution.exitCode, - stderr, - }); - } +const stdout = execution.stdoutStreamUrl + ? await readOutputUrl(execution.stdoutStreamUrl) + : ''; - return { - exitCode: execution.exitCode, - stdout, - stderr, - }; -} finally { - await sandbox.destroy(); -} +console.log({ + exitCode: execution.exitCode, + stdout, +}); -async function readTextStream(url: string): Promise { +async function readOutputUrl(url: string): Promise { const response = await fetch(url); if (!response.ok || !response.body) { throw new Error(`Unable to read sandbox output: ${response.status}`); @@ -247,400 +229,354 @@ async function readTextStream(url: string): Promise { output += decoder.decode(value, { stream: true }); } - output += decoder.decode(); - return output; + return output + decoder.decode(); } ``` -## Creating from Snapshot - -Start sandboxes from pre-configured snapshots for faster cold starts: - - -A snapshot is a saved filesystem state. You create sandboxes *from* snapshots rather than running them directly. See [Snapshots](/services/sandbox/snapshots) for how to create them. - - -```typescript -const sandbox = await ctx.sandbox.create({ - snapshot: 'node-project-base', // Use tag or snapshot ID - resources: { memory: '512Mi' }, -}); - -// Sandbox already has node_modules and dependencies installed -await sandbox.execute({ command: ['npm', 'run', 'build'] }); -``` - ## Environment Variables -Pass environment variables when creating or running sandboxes: +Pass environment variables when creating or running a sandbox. ```typescript -const result = await ctx.sandbox.run({ - command: { exec: ['node', '-e', 'console.log(process.env.API_KEY)'] }, +const result = await client.run({ + runtime: 'bun:1', + command: { exec: ['bun', '-e', 'console.log(process.env.API_URL)'] }, env: { - API_KEY: 'secret-key', - NODE_ENV: 'test', - DEBUG: 'true', + API_URL: 'https://api.example.com', }, - resources: { memory: '256Mi' }, }); + +console.log(result.stdout); ``` -Update an existing interactive sandbox with `setEnv()`. Set a value to `null` to delete that variable. +Update an interactive sandbox with `setEnv()`. Set a value to `null` to delete that variable. ```typescript -const sandbox = await ctx.sandbox.create(); - -await sandbox.setEnv({ - API_KEY: 'secret-key', +const updated = await sandbox.setEnv({ + API_URL: 'https://api.example.com', DEBUG: 'true', }); await sandbox.setEnv({ - DEBUG: null, // Delete DEBUG + DEBUG: null, }); + +console.log(updated.API_URL); ``` -Code running inside the sandbox is not running in your agent process. Use standard logging (`console.log`, `print`, etc.) inside the sandboxed command, and pass any values it needs explicitly with `env` or `setEnv()`. +Code inside the sandbox is a separate process. Pass values explicitly with `env`, `setEnv()`, or files. -## Cancelling Execution +## Exposing Ports -Use an `AbortSignal` to cancel long-running commands: +Set `network.port` to expose one port from the sandbox. Set `network.enabled: true` too when commands inside the sandbox need outbound network access. ```typescript -const controller = new AbortController(); - -// Set a 5-second timeout -setTimeout(() => controller.abort(), 5000); +const sandbox = await client.create({ + runtime: 'bun:1', + network: { + enabled: true, + port: 3000, + }, +}); try { - const result = await sandbox.execute({ - command: ['npm', 'run', 'long-task'], - signal: controller.signal, + await sandbox.writeFiles([ + { + path: 'server.ts', + content: Buffer.from(` + Bun.serve({ + port: 3000, + fetch: () => new Response('ok'), + }); + `), + }, + ]); + + await client.createJob(sandbox.id, { + command: ['bun', 'run', 'server.ts'], }); - return { exitCode: result.exitCode }; -} catch (error) { - if (error instanceof DOMException && error.name === 'AbortError') { - ctx.logger.warn('Execution cancelled'); - return { exitCode: null, cancelled: true }; - } - throw error; + + const info = await sandbox.get(); + console.log(info.url); +} finally { + await sandbox.destroy(); } ``` -## Using in Routes +## Cancelling Commands -Routes access sandbox through `c.var.sandbox`: +Pass an `AbortSignal` to `execute()` or as the second argument to `run()`. ```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; - -const router = new Hono(); - -router.post('/execute', async (c) => { - const { language, code } = await c.req.json(); - - const commands: Record = { - python: ['python3', '-c', code], - javascript: ['node', '-e', code], - typescript: ['bun', '-e', code], - }; - - const command = commands[language]; - if (!command) { - return c.json({ error: 'Unsupported language' }, 400); - } - - const result = await c.var.sandbox.run({ - command: { exec: command }, - timeout: { execution: '10s' }, - resources: { memory: '128Mi', cpu: '250m' }, - }); +const controller = new AbortController(); +const timeout = setTimeout(() => controller.abort(), 5_000); - return c.json({ - success: result.exitCode === 0, - output: result.stdout || result.stderr, - exitCode: result.exitCode, - durationMs: result.durationMs, +try { + const execution = await sandbox.execute({ + command: ['sh', '-c', 'sleep 60'], + signal: controller.signal, }); -}); -export default router; + console.log(execution.status); +} catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + console.log('execution cancelled'); + } else { + throw error; + } +} finally { + clearTimeout(timeout); +} ``` ## Sandbox Management -### Listing Sandboxes +Use `list()`, `get()`, `connect()`, and `destroy()` for existing sandboxes. ```typescript -const { sandboxes, total } = await ctx.sandbox.list({ - status: 'idle', // Filter by status - projectId: 'proj_x', // Filter by project - snapshotId: 'snp_y', // Filter by snapshot +const { sandboxes, total } = await client.list({ + status: 'idle', limit: 10, offset: 0, }); +console.log('total:', total); + for (const info of sandboxes) { - ctx.logger.info('Sandbox', { - id: info.sandboxId, - status: info.status, - executions: info.executions, - }); + console.log(info.sandboxId, info.status, info.executions); } ``` -### Getting Sandbox Info +`get()` returns metadata. `connect()` returns a full interactive instance with methods like `execute()` and `writeFiles()`. ```typescript -const info = await ctx.sandbox.get('sbx_abc123'); -ctx.logger.info('Sandbox details', { - status: info.status, - createdAt: info.createdAt, - snapshot: info.snapshot?.id, -}); -``` +const info = await client.get('sbx_abc123'); +console.log(info.status, info.createdAt); -### Destroying Sandboxes - -```typescript -// Via sandbox instance -await sandbox.destroy(); +const existing = await client.connect('sbx_abc123'); +const files = await existing.listFiles(); +console.log(files.map((file) => file.path)); -// Via service (by ID) -await ctx.sandbox.destroy('sbx_abc123'); +await client.destroy('sbx_abc123'); ``` -## Configuration Reference +## Background Jobs -### SandboxCreateOptions +Use jobs for commands that should keep running after the request that started them returns. -| Option | Type | Description | -|--------|------|-------------| -| `runtime` | `string` | Runtime environment: `'bun:1'`, `'python:3.14'` | -| `runtimeId` | `string` | Runtime ID, such as `'srt_xxx'` | -| `name` | `string` | Optional sandbox name | -| `description` | `string` | Optional description | -| `resources.memory` | `string` | Memory limit: `'256Mi'`, `'1Gi'` | -| `resources.cpu` | `string` | CPU in millicores: `'500m'`, `'1000m'` | -| `resources.disk` | `string` | Disk limit: `'512Mi'`, `'2Gi'` | -| `network.enabled` | `boolean` | Enable outbound network (default: `false`) | -| `network.port` | `number` | Port to expose to internet (1024-65535) | -| `projectId` | `string` | Associate sandbox with a project | -| `timeout.idle` | `string` | Auto-destroy after idle: `'10m'`, `'1h'` | -| `timeout.execution` | `string` | Max command duration: `'30s'`, `'5m'` | -| `dependencies` | `string[]` | Apt packages: `['python3', 'git']` | -| `packages` | `string[]` | npm/bun packages to install globally | -| `snapshot` | `string` | Snapshot ID or tag to restore from | -| `env` | `Record` | Environment variables | -| `metadata` | `Record` | User-defined metadata for tracking | -| `scopes` | `string[]` | Permission scopes for automatic service access | +```typescript +const sandbox = await client.create({ runtime: 'bun:1' }); -When `snapshot` is set, do not also set `runtime` or `runtimeId`. The snapshot already includes its base runtime. +try { + const job = await client.createJob(sandbox.id, { + command: ['sh', '-c', 'sleep 30 && echo done'], + }); -### ExecuteOptions + const current = await job.get(); + console.log(current.status); -| Option | Type | Description | -|--------|------|-------------| -| `command` | `string[]` | Command and arguments | -| `files` | `FileToWrite[]` | Files to create before execution | -| `timeout` | `string` | Override execution timeout | -| `stream` | `object` | Optional stdout, stderr, and timestamp stream configuration | -| `signal` | `AbortSignal` | Cancel the execution | + const { jobs } = await client.listJobs(sandbox.id, 10); + console.log(jobs.map((item) => item.jobId)); -### Execution + await job.stop(); +} finally { + await sandbox.destroy(); +} +``` -Returned by `sandbox.execute()`: +Job statuses are `pending`, `running`, `completed`, `failed`, or `cancelled`. -| Field | Type | Description | -|-------|------|-------------| -| `executionId` | `string` | Unique execution ID for debugging | -| `status` | `string` | `'queued'`, `'running'`, `'completed'`, `'failed'`, `'timeout'`, `'cancelled'` | -| `exitCode` | `number \| undefined` | Process exit code, when available | -| `durationMs` | `number \| undefined` | Execution duration in milliseconds, when available | -| `stdoutStreamUrl` | `string \| undefined` | URL to fetch stdout stream | -| `stderrStreamUrl` | `string \| undefined` | URL to fetch stderr stream | +## Pause, Resume, and Checkpoints -### SandboxRunResult +Pause a sandbox when you want to preserve its filesystem but stop the runtime. Resume it before issuing a batch of commands. -| Field | Type | Description | -|-------|------|-------------| -| `sandboxId` | `string` | Sandbox ID (for debugging) | -| `exitCode` | `number` | Process exit code | -| `durationMs` | `number` | Execution duration | -| `stdout` | `string` | Captured stdout (if available) | -| `stderr` | `string` | Captured stderr (if available) | +```typescript +await sandbox.pause(); +await sandbox.resume(); -### SandboxInfo +const execution = await sandbox.execute({ + command: ['bun', 'run', 'test'], +}); -Returned by `ctx.sandbox.get()` and in list results: +console.log(execution.autoResumed); +``` -| Field | Type | Description | -|-------|------|-------------| -| `sandboxId` | `string` | Unique sandbox identifier | -| `status` | `SandboxStatus` | `'creating'`, `'idle'`, `'running'`, `'paused'`, `'stopping'`, `'suspended'`, `'terminated'`, `'failed'`, `'deleted'` | -| `createdAt` | `string` | ISO timestamp | -| `runtime` | `SandboxRuntimeInfo` | Runtime details, when available | -| `snapshot` | `SandboxSnapshotInfo` | Source snapshot, when available | -| `networkPort` | `number` | Port exposed from sandbox (if configured) | -| `url` | `string` | Public URL (when port is configured) | -| `user` | `SandboxUserInfo` | User who created the sandbox | -| `agent` | `SandboxAgentInfo` | Agent that created the sandbox | -| `project` | `SandboxProjectInfo` | Associated project | -| `org` | `SandboxOrgInfo` | Organization (always present) | - -Access context information from sandbox info: +Disk checkpoints are named restore points for one sandbox. ```typescript -const info = await ctx.sandbox.get('sbx_abc123'); +const checkpoint = await client.createDiskCheckpoint(sandbox.id, 'before-upgrade'); -// Organization is always present -ctx.logger.info('Organization', { id: info.org.id, name: info.org.name }); +await sandbox.execute({ command: ['bun', 'add', 'typescript'] }); +await checkpoint.restore(); -// User info (when created by a user) -if (info.user) { - ctx.logger.info('Created by', { - userId: info.user.id, - name: `${info.user.firstName} ${info.user.lastName}`, - }); -} - -// Agent info (when created by an agent) -if (info.agent) { - ctx.logger.info('Agent', { id: info.agent.id, name: info.agent.name }); -} +const checkpoints = await client.listDiskCheckpoints(sandbox.id); +console.log(checkpoints.map((item) => item.name)); -// Project info (when associated with a project) -if (info.project) { - ctx.logger.info('Project', { id: info.project.id, name: info.project.name }); -} +await checkpoint.delete(); ``` -## Snapshot Management API - -Manage snapshots programmatically through `ctx.sandbox.snapshot`. This lets agents create, list, and manage snapshots without CLI access. +## Snapshots -### Creating Snapshots - -Save the current state of a sandbox as a snapshot: +Snapshots are reusable bases for future sandboxes. Create them from a configured sandbox, then create new sandboxes with `snapshot`. ```typescript -const sandbox = await ctx.sandbox.create({ - runtime: 'node:lts', +const sandbox = await client.create({ + runtime: 'bun:1', network: { enabled: true }, - resources: { memory: '1Gi' }, }); -// Set up the environment -await sandbox.execute({ command: ['npm', 'init', '-y'] }); -await sandbox.execute({ command: ['npm', 'install', 'typescript', 'zod'] }); +try { + await sandbox.execute({ command: ['bun', 'init', '-y'] }); + await sandbox.execute({ command: ['bun', 'add', 'zod'] }); -// Save as snapshot -const snapshot = await ctx.sandbox.snapshot.create(sandbox.id, { - name: 'typescript-zod-env', - description: 'TypeScript environment with Zod validation', - tag: 'latest', - public: false, // Keep private to your org -}); + const snapshot = await client.createSnapshot(sandbox.id, { + name: 'bun-zod', + tag: 'bun-zod', + description: 'Bun project with Zod installed', + }); + + console.log(snapshot.snapshotId); +} finally { + await sandbox.destroy(); +} -ctx.logger.info('Snapshot created', { - snapshotId: snapshot.snapshotId, - sizeBytes: snapshot.sizeBytes, - fileCount: snapshot.fileCount, +const next = await client.create({ + snapshot: 'bun-zod', + resources: { memory: '512Mi' }, }); -await sandbox.destroy(); +await next.destroy(); ``` -### Listing Snapshots +Manage snapshots with the same client: ```typescript -const { snapshots, total } = await ctx.sandbox.snapshot.list({ - sandboxId: 'sbx_abc123', // Filter by source sandbox - limit: 50, - offset: 0, -}); +const { snapshots } = await client.listSnapshots({ limit: 20 }); +const snapshot = await client.getSnapshot('snp_xyz789'); -for (const snap of snapshots) { - ctx.logger.info('Snapshot', { - id: snap.snapshotId, - name: snap.name, - tag: snap.tag, - createdAt: snap.createdAt, - }); -} +await client.tagSnapshot(snapshot.snapshotId, 'v1.0'); +await client.tagSnapshot(snapshot.snapshotId, null); +await client.deleteSnapshot(snapshot.snapshotId); + +console.log(snapshots.length); ``` -### Getting Snapshot Details +When `snapshot` is set, do not also set `runtime` or `runtimeId`. The snapshot already includes its base runtime. -```typescript -const snapshot = await ctx.sandbox.snapshot.get('snp_xyz789'); +## Events -ctx.logger.info('Snapshot details', { - name: snapshot.name, - sizeBytes: snapshot.sizeBytes, - fileCount: snapshot.fileCount, - files: snapshot.files, // Array of file info -}); -``` +List lifecycle events when you need to inspect sandbox history. -### Tagging Snapshots +```typescript +const { events } = await client.listEvents(sandbox.id, { + limit: 50, + direction: 'asc', +}); -Update or remove a snapshot's tag: +for (const event of events) { + console.log(event.type, event.createdAt); +} +``` -```typescript -// Add or update tag -await ctx.sandbox.snapshot.tag('snp_xyz789', 'v1.0'); +## Configuration Reference -// Point "latest" to a new snapshot -await ctx.sandbox.snapshot.tag('snp_newversion', 'latest'); +### SandboxClientOptions -// Remove tag -await ctx.sandbox.snapshot.tag('snp_xyz789', null); -``` +| Option | Type | Description | +|--------|------|-------------| +| `apiKey` | `string` | API key. Defaults to `AGENTUITY_SDK_KEY`, then `AGENTUITY_CLI_KEY` | +| `url` | `string` | Sandbox API URL override | +| `orgId` | `string` | Organization ID for multi-tenant operations | +| `logger` | `Logger` | Custom logger | -### Deleting Snapshots +### SandboxCreateOptions -```typescript -await ctx.sandbox.snapshot.delete('snp_xyz789'); -``` +| Option | Type | Description | +|--------|------|-------------| +| `runtime` | `string` | Runtime name, such as `'bun:1'` or `'python:3.14'` | +| `runtimeId` | `string` | Runtime ID, such as `'srt_xxx'` | +| `name` | `string` | Optional sandbox name | +| `description` | `string` | Optional sandbox description | +| `resources.memory` | `string` | Memory limit, such as `'256Mi'` or `'1Gi'` | +| `resources.cpu` | `string` | CPU limit in millicores, such as `'500m'` or `'1000m'` | +| `resources.disk` | `string` | Disk limit, such as `'512Mi'` or `'2Gi'` | +| `network.enabled` | `boolean` | Enables outbound network access | +| `network.port` | `number` | Port to expose to the internet, 1024-65535 | +| `projectId` | `string` | Project ID to associate with the sandbox | +| `timeout.idle` | `string` | Idle timeout before cleanup, such as `'10m'` | +| `timeout.execution` | `string` | Max command duration, such as `'30s'` | +| `dependencies` | `string[]` | Apt packages to install | +| `packages` | `string[]` | npm/bun packages to install globally | +| `env` | `Record` | Environment variables | +| `files` | `FileToWrite[]` | Files to write on creation | +| `snapshot` | `string` | Snapshot ID or tag | +| `metadata` | `Record` | User-defined metadata | +| `scopes` | `string[]` | Permission scopes for automatic service access | -### SnapshotCreateOptions +### ExecuteOptions | Option | Type | Description | |--------|------|-------------| -| `name` | `string` | Display name (URL-safe: letters, numbers, underscores, dashes) | -| `description` | `string` | Description of the snapshot | -| `tag` | `string` | Tag name (defaults to "latest") | -| `public` | `boolean` | Make snapshot publicly accessible (default: `false`) | +| `command` | `string[]` | Command and arguments | +| `files` | `FileToWrite[]` | Files to write before execution | +| `timeout` | `string` | Execution timeout override | +| `stream` | `object` | Optional stdout, stderr, and timestamp stream configuration | +| `signal` | `AbortSignal` | Cancel the execution | -### SnapshotInfo +### SandboxInstance + +| Property or method | Type | +|--------------------|------| +| `id` | `string` | +| `status` | `SandboxStatus` | +| `execute(options)` | `Promise` | +| `writeFiles(files)` | `Promise` | +| `readFile(path)` | `Promise>` | +| `listFiles(path?)` | `Promise` | +| `mkDir(path, recursive?)` | `Promise` | +| `rmFile(path)` | `Promise<{ found: boolean }>` | +| `rmDir(path, recursive?)` | `Promise<{ found: boolean }>` | +| `setEnv(env)` | `Promise>` | +| `get()` | `Promise` | +| `pause()` | `Promise` | +| `resume()` | `Promise` | +| `destroy()` | `Promise` | -Returned by create, get, and list operations: +### Execution | Field | Type | Description | |-------|------|-------------| -| `snapshotId` | `string` | Unique identifier | -| `name` | `string` | Display name | -| `tag` | `string \| null` | Current tag | -| `sizeBytes` | `number` | Total size in bytes | -| `fileCount` | `number` | Number of files | -| `files` | `SnapshotFileInfo[]` | File details (path, size, mime type) | -| `createdAt` | `string` | ISO timestamp | -| `downloadUrl` | `string` | URL to download snapshot archive | -| `public` | `boolean` | Whether publicly accessible | - -## Best Practices - -- **Set [resource limits](#sandboxcreateoptions)**: Control memory and CPU usage for predictable performance -- **Use timeouts**: Always set execution timeouts for untrusted code -- **Enable network when needed**: Required for package installation, API calls, and external requests -- **Clean up interactive sandboxes**: Use try/finally to ensure `destroy()` is called -- **Use snapshots for common environments**: Pre-install dependencies to reduce cold start time -- **Tag important snapshots**: Use semantic versioning tags (`v1.0`, `latest`) for reproducibility +| `executionId` | `string` | Unique execution ID | +| `status` | `'queued' \| 'running' \| 'completed' \| 'failed' \| 'timeout' \| 'cancelled'` | Execution status | +| `exitCode` | `number \| undefined` | Process exit code, when available | +| `durationMs` | `number \| undefined` | Execution duration in milliseconds | +| `stdoutStreamUrl` | `string \| undefined` | URL for stdout | +| `stderrStreamUrl` | `string \| undefined` | URL for stderr | +| `outputTruncated` | `boolean \| undefined` | Whether captured output was truncated | +| `autoResumed` | `boolean \| undefined` | Whether the sandbox was resumed automatically | + +### SandboxRunResult + +| Field | Type | Description | +|-------|------|-------------| +| `sandboxId` | `string` | Sandbox ID used for the run | +| `exitCode` | `number` | Process exit code | +| `durationMs` | `number` | Execution duration in milliseconds | +| `stdout` | `string \| undefined` | Captured stdout | +| `stderr` | `string \| undefined` | Captured stderr | + +### Snapshot Methods + +| Method | Returns | +|--------|---------| +| `createSnapshot(sandboxId, options?)` | `Promise` | +| `getSnapshot(snapshotId)` | `Promise` | +| `listSnapshots(params?)` | `Promise` | +| `tagSnapshot(snapshotId, tag)` | `Promise` | +| `deleteSnapshot(snapshotId)` | `Promise` | +| `getSnapshotLineage(params?)` | `Promise` | ## Next Steps -- [Snapshots](/services/sandbox/snapshots): CLI commands and declarative snapshot definitions -- [CLI Commands](/reference/cli/sandbox): Debug sandboxes from the terminal +- [Creating and Using Snapshots](/services/sandbox/snapshots): declarative snapshot files and CLI snapshot workflows +- [CLI Sandbox Commands](/reference/cli/sandbox): inspect sandboxes, jobs, files, and snapshots from the terminal diff --git a/docs/src/web/content/services/sandbox/snapshots.mdx b/docs/src/web/content/services/sandbox/snapshots.mdx index c0f642745..c44599ee6 100644 --- a/docs/src/web/content/services/sandbox/snapshots.mdx +++ b/docs/src/web/content/services/sandbox/snapshots.mdx @@ -1,320 +1,256 @@ --- title: Creating and Using Snapshots short_title: Snapshots -description: Save and restore sandbox filesystem states for faster cold starts +description: Save sandbox filesystem states and reuse them as bases for new sandboxes --- -Skip slow dependency installation on every run by saving sandbox filesystem states as snapshots. +Use snapshots when a sandbox needs the same dependencies or files on many runs. Create the environment once, save it, then start future sandboxes from that snapshot. -Manage snapshots via the [Web App](https://app.agentuity.com) under **Services > Sandbox > Snapshots**, the [SDK](/services/sandbox/sdk-usage), or [CLI](/reference/cli/sandbox). +```typescript +import { SandboxClient } from '@agentuity/sandbox'; - -A snapshot captures the filesystem state of a sandbox at a point in time. You create new sandboxes *from* snapshots, not run them directly. - +const client = new SandboxClient(); +const sandbox = await client.create({ + runtime: 'bun:1', + network: { enabled: true }, +}); - -Snapshots are reusable bases for new sandboxes. Checkpoints belong to one sandbox and support pause, resume, restore, and delete workflows. - +try { + await sandbox.execute({ command: ['bun', 'init', '-y'] }); + await sandbox.execute({ command: ['bun', 'add', 'zod'] }); -## How Snapshots Work + const snapshot = await client.createSnapshot(sandbox.id, { + name: 'bun-zod', + tag: 'bun-zod', + description: 'Bun project with Zod installed', + }); -Snapshots build on top of [runtimes](/services/sandbox#runtimes). When you create a snapshot: + console.log(snapshot.snapshotId); +} finally { + await sandbox.destroy(); +} +``` -1. Start with a **runtime** (e.g., `bun:1`) as the base layer -2. Add your **dependencies** (apt packages, npm modules, etc.) -3. Include your **files** (source code, config) -4. The snapshot captures everything: runtime + dependencies + files + +A snapshot is not executable by itself. Use the snapshot ID or tag in `client.create({ snapshot })`. + -When you create a sandbox from a snapshot, it starts with all of this pre-configured. +## How Snapshots Work -## The Snapshot Workflow +Snapshots build on top of [runtimes](/services/sandbox#runtimes). A snapshot captures the runtime layer plus the files and dependencies present in the sandbox workspace. -1. Create a sandbox -2. Install dependencies and configure the environment -3. Save a snapshot (captures the filesystem state) -4. Later, create new sandboxes from that snapshot -5. New sandboxes start with everything pre-configured +Create a new sandbox from a snapshot: -## Declarative Snapshots (Recommended) +```typescript +const sandbox = await client.create({ + snapshot: 'bun-zod', + resources: { memory: '512Mi' }, +}); -Define snapshots in a YAML file for reproducible, version-controlled environments. This approach is ideal for CI/CD pipelines and team collaboration. +try { + const execution = await sandbox.execute({ + command: ['bun', 'run', 'index.ts'], + }); -### Quick Start + console.log(execution.exitCode); +} finally { + await sandbox.destroy(); +} +``` -Generate a template with helpful comments explaining each field: +When `snapshot` is set, do not also set `runtime` or `runtimeId`. The snapshot already includes its base runtime. -```bash -agentuity cloud sandbox snapshot generate > agentuity-snapshot.yaml -``` +## Declarative Snapshots -The generated file includes documentation for runtime selection, apt dependencies, npm/bun packages, file patterns, environment variables, and metadata. Customize it for your needs, then build: +Use a snapshot file when you want the environment definition in version control. ```bash -agentuity cloud sandbox snapshot build . +agentuity cloud sandbox snapshot generate > agentuity-snapshot.yaml ``` -### Build File Example +The generated file includes commented fields for the runtime, dependencies, packages, included files, environment variables, and metadata. A minimal file looks like this: ```yaml version: 1 -runtime: bun:1 # Base runtime - snapshot layers on top of this -description: Node.js environment with common dependencies - -dependencies: # Apt packages to install - - curl - - jq +runtime: bun:1 +name: bun-zod +description: Bun project with Zod installed -packages: # npm/bun packages to install globally - - typescript +packages: + - zod -files: # Files to include from your project +files: - src/** - package.json - - "!**/*.test.ts" # Exclude test files - - "!node_modules/**" # Exclude node_modules + - "!**/*.test.ts" + - "!node_modules/**" env: - NODE_ENV: production + NODE_ENV: test ``` -### Build Options +Build it from the directory that contains the files you want to include: ```bash -# Build with a custom build file +agentuity cloud sandbox snapshot build . --tag bun-zod +``` + +Common build options: + +```bash +# Use a custom build file agentuity cloud sandbox snapshot build ./project --file custom-build.yaml -# Build with a custom tag -agentuity cloud sandbox snapshot build . --tag production +# Override the tag +agentuity cloud sandbox snapshot build . --tag staging -# Build with environment variable substitution +# Substitute values used by the snapshot file agentuity cloud sandbox snapshot build . --env API_KEY=secret -# Add metadata substitution +# Attach metadata agentuity cloud sandbox snapshot build . --metadata VERSION=1.0.0 -# Force rebuild even if content unchanged +# Upload even when the content hash matches an existing snapshot agentuity cloud sandbox snapshot build . --force ``` -### When to Use Declarative vs Manual +## Manual Snapshots -| Approach | Best For | -|----------|----------| -| **Declarative** | CI/CD pipelines, reproducible environments, team collaboration, version control | -| **Manual** | Quick experimentation, one-off debugging, exploring new configurations | - -## Creating a Snapshot Manually - -Create a sandbox, configure it, then save a snapshot: +Manual snapshots are useful while you are exploring an environment from the CLI. ```bash -# Create a sandbox with a runtime agentuity cloud sandbox create --runtime bun:1 --memory 1Gi --network # Returns: sbx_abc123 -# Install dependencies -agentuity cloud sandbox exec sbx_abc123 -- npm init -y -agentuity cloud sandbox exec sbx_abc123 -- npm install typescript zod +agentuity cloud sandbox exec sbx_abc123 -- bun init -y +agentuity cloud sandbox exec sbx_abc123 -- bun add zod -# Create snapshot with a tag -agentuity cloud sandbox snapshot create sbx_abc123 --tag node-typescript - -# Clean up the original sandbox +agentuity cloud sandbox snapshot create sbx_abc123 --name bun-zod --tag bun-zod agentuity cloud sandbox delete sbx_abc123 ``` -## Using Snapshots in Code - -Create sandboxes from snapshots using the `snapshot` option: +You can do the same from code: ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TypeScriptRunner', { - handler: async (ctx, input) => { - // Create sandbox from snapshot - dependencies already installed - const sandbox = await ctx.sandbox.create({ - snapshot: 'node-typescript', // Use tag or snapshot ID - resources: { memory: '512Mi' }, - }); - - try { - // Write the user's code - await sandbox.writeFiles([ - { path: 'index.ts', content: Buffer.from(input.code) }, - ]); - - // Run immediately - no npm install needed - const result = await sandbox.execute({ - command: ['npx', 'tsx', 'index.ts'], - }); - - return { success: true, exitCode: result.exitCode }; - } catch (error) { - ctx.logger.error('Sandbox execution failed', { error }); - return { success: false, exitCode: -1, error: String(error) }; - } finally { - await sandbox.destroy(); - } - }, +const sandbox = await client.create({ + runtime: 'bun:1', + network: { enabled: true }, }); -``` - -## CLI Commands -### Create Snapshot +try { + await sandbox.execute({ command: ['bun', 'init', '-y'] }); + await sandbox.execute({ command: ['bun', 'add', 'zod'] }); -```bash -agentuity cloud sandbox snapshot create [options] + const snapshot = await client.createSnapshot(sandbox.id, { + name: 'bun-zod', + tag: 'bun-zod', + }); -# Examples -agentuity cloud sandbox snapshot create sbx_abc123 -agentuity cloud sandbox snapshot create sbx_abc123 --tag python-ml -agentuity cloud sandbox snapshot create sbx_abc123 --name python-ml --description "Python data tools" -agentuity cloud sandbox snapshot create sbx_abc123 --public + console.log(snapshot.snapshotId); +} finally { + await sandbox.destroy(); +} ``` -### List Snapshots +## Managing Snapshots -```bash -agentuity cloud sandbox snapshot list [--sandbox ] [--limit ] +List and inspect snapshots with `SandboxClient`: -# Examples -agentuity cloud sandbox snapshot list -agentuity cloud sandbox snapshot list --sandbox sbx_abc123 -``` +```typescript +const { snapshots, total } = await client.listSnapshots({ + limit: 20, + offset: 0, + sort: 'created', + direction: 'desc', +}); -### Get Snapshot Details +console.log('total snapshots:', total); -```bash -agentuity cloud sandbox snapshot get +for (const snapshot of snapshots) { + console.log(snapshot.snapshotId, snapshot.name, snapshot.tag ?? ''); +} -# Shows file listing, size, and sandboxes using this snapshot +const snapshot = await client.getSnapshot('snp_xyz789'); +console.log({ + name: snapshot.name, + sizeBytes: snapshot.sizeBytes, + fileCount: snapshot.fileCount, +}); ``` -### Tag a Snapshot - -```bash -agentuity cloud sandbox snapshot tag -agentuity cloud sandbox snapshot tag --clear +Update or remove a tag: -# Examples -agentuity cloud sandbox snapshot tag snp_xyz789 v1.0 -agentuity cloud sandbox snapshot tag snp_xyz789 latest -agentuity cloud sandbox snapshot tag snp_xyz789 --clear # Remove tag +```typescript +await client.tagSnapshot('snp_xyz789', 'v1.0'); +await client.tagSnapshot('snp_xyz789', null); ``` -### Delete Snapshot - -```bash -agentuity cloud sandbox snapshot delete [--confirm] +Delete a snapshot when you no longer need it: -# Example -agentuity cloud sandbox snapshot delete snp_xyz789 --confirm +```typescript +await client.deleteSnapshot('snp_xyz789'); ``` -### Create Sandbox from Snapshot +From the CLI: ```bash -agentuity cloud sandbox create --snapshot [options] - -# Examples -agentuity cloud sandbox create --snapshot node-typescript -agentuity cloud sandbox create --snapshot snp_xyz789 --memory 1Gi +agentuity cloud sandbox snapshot list +agentuity cloud sandbox snapshot list --sandbox sbx_abc123 +agentuity cloud sandbox snapshot get snp_xyz789 +agentuity cloud sandbox snapshot tag snp_xyz789 v1.0 +agentuity cloud sandbox snapshot tag snp_xyz789 --clear +agentuity cloud sandbox snapshot delete snp_xyz789 --confirm ``` -## Use Cases +## Versioned Environments -### Pre-configured Language Environments - -Create snapshots for each language you support: +Use tags when callers should not need to know snapshot IDs. ```bash -# Python with common packages -agentuity cloud sandbox create --runtime base:latest --network -agentuity cloud sandbox exec sbx_... -- apt-get update -agentuity cloud sandbox exec sbx_... -- apt-get install -y python3 python3-pip -agentuity cloud sandbox exec sbx_... -- pip install numpy pandas requests -agentuity cloud sandbox snapshot create sbx_... --tag python-data - -# Node.js with TypeScript -agentuity cloud sandbox create --runtime bun:1 --network -agentuity cloud sandbox exec sbx_... -- npm install -g typescript tsx -agentuity cloud sandbox snapshot create sbx_... --tag node-typescript +agentuity cloud sandbox snapshot create sbx_abc123 --name project-env --tag project-env-v1 +agentuity cloud sandbox snapshot create sbx_def456 --name project-env --tag project-env-v2 +agentuity cloud sandbox snapshot tag snp_newid latest ``` -Then use in your agent: +Then choose the tag from code: ```typescript -const languageSnapshots: Record = { - python: 'python-data', - typescript: 'node-typescript', - javascript: 'node-typescript', -}; - -const sandbox = await ctx.sandbox.create({ - snapshot: languageSnapshots[input.language], +const sandbox = await client.create({ + snapshot: 'latest', }); ``` -### Version-controlled Environments +## What Snapshots Include -Use tags for versioning: - -```bash -# Create v1.0 -agentuity cloud sandbox snapshot create sbx_... --tag project-env-v1.0 +Snapshots include: -# Later, create v2.0 with updated dependencies -agentuity cloud sandbox snapshot create sbx_... --tag project-env-v2.0 +- files in the sandbox workspace +- installed dependencies and packages +- file permissions and directory structure +- environment variables from a declarative snapshot build file -# Keep "latest" pointing to current version -agentuity cloud sandbox snapshot tag snp_newid latest -``` +Snapshots do not include running processes or active network connections. For manual snapshots, pass environment variables again when you create or run the next sandbox. -### Faster Cold Starts +## Snapshots vs Checkpoints -Without snapshots, every sandbox creation requires dependency installation: +Snapshots are reusable bases for new sandboxes. Checkpoints belong to one sandbox and let you restore that same sandbox to a previous filesystem state. ```typescript -// Slow: Install dependencies every time -const sandbox = await ctx.sandbox.create({ network: { enabled: true } }); -await sandbox.execute({ command: ['npm', 'install'] }); // 30+ seconds -await sandbox.execute({ command: ['npm', 'run', 'build'] }); -``` - -With snapshots: +const checkpoint = await client.createDiskCheckpoint(sandbox.id, 'before-upgrade'); -```typescript -// Fast: Dependencies already installed -const sandbox = await ctx.sandbox.create({ snapshot: 'project-deps' }); -await sandbox.execute({ command: ['npm', 'run', 'build'] }); // Immediate +await sandbox.execute({ command: ['bun', 'add', 'typescript'] }); +await checkpoint.restore(); +await checkpoint.delete(); ``` -## Snapshot Data - -When you create a snapshot, it captures: - -- All files in the sandbox workspace -- Installed packages and dependencies -- File permissions and structure -- Environment variables from a declarative snapshot build file - -Snapshots don't include: - -- Running processes -- Network connections - -For manual snapshots created from a sandbox, set runtime environment variables when you create or run the next sandbox. Declarative snapshot builds can include `env` values in the build file, or substitute them at build time with `--env KEY=VALUE`. - ## Best Practices -- **Tag important snapshots**: Use descriptive tags like `python-ml-v2` instead of relying on IDs -- **Keep snapshots lean**: Only include necessary dependencies to minimize size -- **Version your snapshots**: Use semantic versioning tags for reproducibility -- **Clean up unused snapshots**: Delete snapshots you no longer need +- Use snapshots for dependencies that are expensive to install repeatedly. +- Keep snapshot contents small. Include only files and packages needed by the run. +- Tag snapshots that application code will reference, such as `latest`, `v1.0`, or `python-data-v2`. +- Delete old snapshots once no active workflow depends on them. ## Next Steps -- [SDK Usage](/services/sandbox/sdk-usage): File I/O, streaming, and advanced configuration options -- [CLI Commands](/reference/cli/sandbox): Debug sandboxes and manage snapshots from the terminal +- [Using the Sandbox API](/services/sandbox/sdk-usage): file I/O, jobs, checkpoints, and method reference tables +- [CLI Sandbox Commands](/reference/cli/sandbox): manage sandbox resources from the terminal diff --git a/docs/src/web/content/services/schedules.mdx b/docs/src/web/content/services/schedules.mdx index 4c1bcaf78..4951339ef 100644 --- a/docs/src/web/content/services/schedules.mdx +++ b/docs/src/web/content/services/schedules.mdx @@ -3,290 +3,244 @@ title: Schedules description: Create platform-managed cron jobs with HTTP and sandbox destinations --- -When a job needs to run on a timer, like data syncs, nightly cleanup, or report generation, Schedules let you configure timing and destinations outside your app code. The platform calls your endpoints on a cron schedule, tracks each delivery, and retries on failure. +Use schedules when timing should live in Agentuity instead of inside your app process. Start with `ScheduleClient`; Hono apps can use `c.var.schedule` after installing the Agentuity middleware. - -Use Schedules when you want platform-managed recurring jobs with delivery tracking, retries, and destinations managed outside your app code. - - - -Use the [`@agentuity/schedule`](/reference/standalone-packages#schedules) standalone package to access this service from any Node.js or Bun app without the runtime. - +```bash +bun add @agentuity/schedule@alpha +``` - -There are two ways to run scheduled work: +```typescript +import { ScheduleClient } from '@agentuity/schedule'; + +const schedules = new ScheduleClient(); + +export async function createDailySync(url: string): Promise { + const { schedule } = await schedules.create({ + name: 'Daily customer sync', + description: 'Calls the sync endpoint every weekday morning', + expression: '0 9 * * 1-5', + destinations: [ + { + type: 'url', + config: { + url, + method: 'POST', + }, + }, + ], + }); -- [Cron routes](/routes/cron) are route handlers in your app, triggered on a schedule via HTTP POST. The schedule is defined in code and deployed with your project. -- **Schedules** are platform-managed resources. The platform calls your destinations (HTTP URLs or sandboxes) on schedule, with per-delivery tracking and automatic retries. + return schedule.id; +} +``` -Use cron routes when you want in-process scheduling with route services on `c.var.*`. Use Schedules when you need delivery tracking, multiple destination types, or want to manage job timing independently of deployments. - +`ScheduleClient` reads `AGENTUITY_SDK_KEY` by default. In Agentuity projects, keep the key in `.env` for local `agentuity dev` and configure the same environment variable for deployed apps. ## When to Use Schedules -| Approach | Best For | -|----------|----------| -| **Schedules** | Platform-managed recurring jobs with delivery tracking and multiple destination types | -| [Cron Routes](/routes/cron) | In-process scheduled handlers with access to route services on `c.var.*` | -| [Queues](/services/queues) | Event-driven async processing, not time-based | - -## Managing Schedules - -| Context | Access | Details | -|---------|--------|---------| -| Agents | `ctx.schedule` | See examples below | -| Routes | `c.var.schedule` | See [Using in Routes](#using-in-routes) | -| CLI | `agentuity cloud schedule` | Manage schedules from the command line | -| Web App | [Web App](https://app.agentuity.com/services/schedule) | Create and inspect schedules in the UI | +| Need | Use | +|------|-----| +| recurring delivery with history and retries | [Schedules](/services/schedules) | +| event-driven async work | [Queues](/services/queues) | +| managed callback ingest | [Webhooks](/services/webhooks) | +| isolated command execution | [Sandbox](/services/sandbox) | -## Creating Schedules +## Client Setup -Pass a `name`, an `expression` (standard five-field cron), and an optional array of `destinations`. The schedule and its destinations are created atomically: if any destination fails validation, the entire operation is rolled back. +Construct the client once at module scope and reuse it from handlers, routes, or scripts. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('ScheduleSetup', { - handler: async (ctx, input) => { - const result = await ctx.schedule.create({ // [!code highlight] - name: 'Daily Report', - description: 'Generate and email daily reports', // optional - expression: '0 9 * * 1-5', // weekdays at 9am // [!code highlight] - destinations: [ - { - type: 'url', - config: { - url: 'https://example.com/reports/generate', - method: 'POST', - headers: { 'Authorization': 'Bearer my-token' }, - }, - }, - ], - }); - - ctx.logger.info('Schedule created', { - id: result.schedule.id, - nextRun: result.schedule.due_date, - }); +import { ScheduleClient } from '@agentuity/schedule'; - return { scheduleId: result.schedule.id }; - }, +const schedules = new ScheduleClient({ + orgId: process.env.AGENTUITY_ORG_ID, }); ``` -### Common Cron Expressions - -| Expression | Fires | -|------------|-------| -| `* * * * *` | Every minute | -| `*/5 * * * *` | Every 5 minutes | -| `0 * * * *` | Every hour | -| `0 0 * * *` | Daily at midnight | -| `0 9 * * 1-5` | Weekdays at 9am | -| `0 0 * * 1` | Weekly on Monday at midnight | -| `0 0 1 * *` | Monthly on the 1st at midnight | - -The server validates cron expressions on creation and update. `due_date` on the schedule reflects the next computed execution time. - -## Destination Types - -Each schedule can have multiple destinations. When the schedule fires, the platform sends a request to each destination. +| Option | Description | +|--------|-------------| +| `apiKey` | Optional API key. Defaults to `AGENTUITY_SDK_KEY`, then `AGENTUITY_CLI_KEY`. | +| `orgId` | Optional organization ID for org-scoped requests. | +| `url` | Optional Schedule API URL. Defaults to `AGENTUITY_SCHEDULE_URL`, then the regional Agentuity service URL. | +| `logger` | Optional logger instance. | -| Type | Config | Description | -|------|--------|-------------| -| `url` | `{ url, headers?, method? }` | Sends an HTTP request to the URL on schedule | -| `sandbox` | `{ sandbox_id, command? }` | Runs a command in a sandbox on schedule | +## Create Schedules -### URL Destination +`create()` requires a `name` and a five-field cron `expression`. Destinations are optional, but most schedules create at least one destination with the schedule. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('WebhookScheduler', { - handler: async (ctx, input) => { - const { destination } = await ctx.schedule.createDestination(input.scheduleId, { // [!code highlight] - type: 'url', // [!code highlight] +const result = await schedules.create({ + name: 'Hourly inventory refresh', + expression: '0 * * * *', + destinations: [ + { + type: 'url', config: { - url: 'https://example.com/webhook/sync', + url: 'https://api.example.com/inventory/refresh', method: 'POST', headers: { - 'Authorization': `Bearer ${input.apiToken}`, - 'Content-Type': 'application/json', + 'X-Schedule-Source': 'agentuity', }, }, - }); - - ctx.logger.info('URL destination added', { destinationId: destination.id }); - return { destinationId: destination.id }; - }, + }, + ], }); + +const nextRun = result.schedule.due_date; ``` -### Sandbox Destination +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Human-readable schedule name. | +| `expression` | Yes | Standard five-field cron expression. | +| `description` | No | Optional purpose or owner note. | +| `destinations` | No | Optional URL or sandbox destinations created with the schedule. | -```typescript -import { createAgent } from '@agentuity/runtime'; +Destinations are created atomically with the schedule. If a destination fails validation, the schedule is not created. -const agent = createAgent('SandboxScheduler', { - handler: async (ctx, input) => { - const { destination } = await ctx.schedule.createDestination(input.scheduleId, { // [!code highlight] - type: 'sandbox', // [!code highlight] - config: { - sandbox_id: input.sandboxId, - command: 'bun run src/run/sync.ts', - }, - }); +## Cron Expressions - ctx.logger.info('Sandbox destination added', { destinationId: destination.id }); - return { destinationId: destination.id }; - }, -}); -``` +| Expression | Fires | +|------------|-------| +| `* * * * *` | Every minute. | +| `*/5 * * * *` | Every 5 minutes. | +| `0 * * * *` | Every hour. | +| `0 0 * * *` | Daily at midnight. | +| `0 9 * * 1-5` | Weekdays at 9am. | +| `0 0 * * 1` | Weekly on Monday at midnight. | +| `0 0 1 * *` | Monthly on the 1st at midnight. | -## Listing and Updating Schedules +The service validates cron expressions on create and update, then computes `due_date` for the next run. -```typescript -import { createAgent } from '@agentuity/runtime'; +## Destination Types -const agent = createAgent('ScheduleManager', { - handler: async (ctx, input) => { - // List schedules with optional pagination - const { schedules, total } = await ctx.schedule.list({ limit: 20, offset: 0 }); - ctx.logger.info('Schedules', { count: schedules.length, total }); +Each schedule can have multiple destinations. When the schedule fires, Agentuity creates one delivery record per destination. - // Get a specific schedule with its destinations - const { schedule, destinations } = await ctx.schedule.get(input.scheduleId); - ctx.logger.info('Next run', { dueDate: schedule.due_date }); +| Type | Config | +|------|--------| +| `url` | `{ url, headers?, method? }` | +| `sandbox` | `{ sandbox_id, command? }` | - // Update the cron expression: due_date is automatically recomputed - const { schedule: updated } = await ctx.schedule.update(input.scheduleId, { // [!code highlight] - expression: '0 0 * * *', // change to daily at midnight - }); +Add a destination after the schedule exists: - return { nextRun: updated.due_date, destinationCount: destinations.length }; +```typescript +const { destination } = await schedules.createDestination(result.schedule.id, { + type: 'url', + config: { + url: 'https://api.example.com/reports/daily', + method: 'POST', }, }); ``` -## Delivery Tracking - -Each time a schedule fires, one delivery record is created per destination. Use `listDeliveries` to inspect delivery history and check for failures. +Use a sandbox destination when the scheduled work should run as a command in an Agentuity sandbox. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('DeliveryInspector', { - handler: async (ctx, input) => { - const { deliveries } = await ctx.schedule.listDeliveries(input.scheduleId); // [!code highlight] - - for (const delivery of deliveries) { - ctx.logger.info('Delivery record', { - date: delivery.date, - status: delivery.status, // 'pending' | 'success' | 'failed' - retries: delivery.retries, // number of retry attempts - error: delivery.error, // null on success - }); - } - - const failed = deliveries.filter(d => d.status === 'failed'); - return { total: deliveries.length, failed: failed.length }; +await schedules.createDestination(result.schedule.id, { + type: 'sandbox', + config: { + sandbox_id: 'sbx_123', + command: 'bun run scripts/sync.ts', }, }); ``` -### Delivery Statuses +## List and Update -| Status | Description | -|--------|-------------| -| `pending` | Delivery is queued and has not been attempted yet | -| `success` | The destination received the request and responded successfully | -| `failed` | The delivery encountered an error; `error` field contains the reason | +Use `list()` for dashboards or admin tools, `get()` for a schedule and its destinations, and `update()` to change the name, description, or cron expression. -The `retries` field on each delivery record tracks how many retry attempts were made before the final status was recorded. +```typescript +const { schedules: page, total } = await schedules.list({ limit: 20, offset: 0 }); -## Fetching a Specific Destination or Delivery +const { schedule, destinations } = await schedules.get(result.schedule.id); -Two convenience methods let you look up a single record by ID without iterating manually. +const { schedule: updated } = await schedules.update(schedule.id, { + expression: '0 6 * * 1-5', +}); +``` -`getDestination(scheduleId, destinationId)` fetches the schedule via `get()` and returns the matching destination: +Changing `expression` recomputes `due_date`. -```typescript -const dest = await ctx.schedule.getDestination(input.scheduleId, input.destinationId); -ctx.logger.info('Destination type', { type: dest.type }); -``` +## Delivery Tracking -`getDelivery(scheduleId, deliveryId, params?)` calls `listDeliveries()` and returns the matching record. Pass `params` to control the pagination window if the delivery may not be in the first page: +Use `listDeliveries()` to inspect recent delivery attempts for a schedule. ```typescript -const delivery = await ctx.schedule.getDelivery(input.scheduleId, input.deliveryId); -ctx.logger.info('Delivery status', { status: delivery.status, retries: delivery.retries }); -``` +const { deliveries } = await schedules.listDeliveries(result.schedule.id, { + limit: 50, + offset: 0, +}); -Both methods throw if the ID is not found in the result set. +const failed = deliveries.filter((delivery) => delivery.status === 'failed'); +``` -## Removing Destinations and Schedules +| Status | Meaning | +|--------|---------| +| `pending` | Delivery has been queued. | +| `success` | The destination received the scheduled request. | +| `failed` | Delivery failed. The `error` field contains the reason when available. | -```typescript -import { createAgent } from '@agentuity/runtime'; +Each delivery includes the schedule ID, destination ID, status, retry count, optional error, and optional response data. -const agent = createAgent('ScheduleCleaner', { - handler: async (ctx, input) => { - // Remove a single destination (keeps the schedule) - await ctx.schedule.deleteDestination(input.destinationId); +## Delete - // Delete a schedule and all its destinations and delivery history - await ctx.schedule.delete(input.scheduleId); // [!code highlight] +Delete destinations when a schedule should keep running but stop calling one target. Delete the schedule when the job is no longer needed. - return { deleted: true }; - }, -}); +```typescript +await schedules.deleteDestination(destination.id); +await schedules.delete(result.schedule.id); ``` - -`delete()` permanently removes the schedule, all its destinations, and all delivery history. This operation cannot be undone. + +`delete()` permanently removes the schedule, its destinations, and its delivery records. -## Using in Routes +## Hono -Routes access the schedule service via `c.var.schedule`: +In Hono apps, `@agentuity/hono` initializes `ScheduleClient` once and exposes it on `c.var.schedule`. + +```bash +bun add @agentuity/hono@alpha hono +``` ```typescript import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; + +type Variables = Pick; + +const app = new Hono<{ Variables: Variables }>(); -const router = new Hono(); +app.use('*', agentuity()); -router.post('/schedules', async (c) => { - const body = await c.req.json(); +app.post('/schedules/daily-sync', async (c) => { + const url = c.req.query('url'); - const result = await c.var.schedule.create({ // [!code highlight] - name: body.name, - expression: body.expression, - destinations: body.destinations, + if (!url) { + return c.json({ error: 'url is required' }, 400); + } + + const { schedule } = await c.var.schedule.create({ + name: 'Daily sync', + expression: '0 9 * * *', + destinations: [{ type: 'url', config: { url, method: 'POST' } }], }); - return c.json({ scheduleId: result.schedule.id, nextRun: result.schedule.due_date }); + return c.json({ scheduleId: schedule.id, nextRun: schedule.due_date }, 201); }); -router.get('/schedules/:id/deliveries', async (c) => { - const scheduleId = c.req.param('id'); - const { deliveries } = await c.var.schedule.listDeliveries(scheduleId); +app.get('/schedules/:id/deliveries', async (c) => { + const { deliveries } = await c.var.schedule.listDeliveries(c.req.param('id')); return c.json({ deliveries }); }); -export default router; +export default app; ``` -## Best Practices - -- **Use descriptive names**: names like `"Daily User Report - EU"` are easier to manage than `"job-1"` when listing schedules in the dashboard -- **Keep destinations idempotent**: the platform may retry failed deliveries, so ensure your endpoints handle duplicate invocations safely -- **Monitor delivery history**: poll `listDeliveries` or check the dashboard to catch recurring failures before they accumulate -- **Remove unused destinations before changing schedules**: deleting a destination does not affect other destinations on the same schedule, making partial updates safe - ## Next Steps -- [Cron Routes](/routes/cron): In-process scheduled handlers that run inside your deployed app -- [Queues](/services/queues): Async, event-driven processing for work that doesn't need a fixed time trigger -- [Sandbox](/services/sandbox): Isolated execution environments used as schedule destinations +- [Queues](/services/queues): hand scheduled work to a background queue +- [Webhooks](/services/webhooks): receive external callbacks with receipts and delivery tracking +- [Sandbox](/services/sandbox): run scheduled commands in isolated environments diff --git a/docs/src/web/content/services/storage/custom.mdx b/docs/src/web/content/services/storage/custom.mdx index e95e1cc56..ea6afd636 100644 --- a/docs/src/web/content/services/storage/custom.mdx +++ b/docs/src/web/content/services/storage/custom.mdx @@ -1,142 +1,155 @@ --- title: Custom Storage short_title: Custom -description: Local development storage and custom runtime storage implementations +description: Swap storage backends behind small app-owned interfaces --- -Agentuity uses local runtime storage when you are unauthenticated or when local mode is enabled, and managed runtime storage when your app runs with cloud credentials. You can override the runtime implementations for key-value, vector, and stream storage. +Most v3 apps should use the dedicated service clients directly: `KeyValueClient`, `VectorClient`, `StreamClient`, and Bun's `s3` APIs. When you need a different backend, put that choice behind a small interface in your app instead of depending on runtime storage overrides. -## Local Development +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; -During local development, KV, vector, and stream storage are backed by a local SQLite database and project-scoped by the project path. This happens automatically when you're not authenticated, or when you explicitly enable local mode: +interface Session { + readonly userId: string; + readonly email: string; +} -```typescript -import { createApp } from '@agentuity/runtime'; +interface SessionStore { + get(id: string): Promise; + set(id: string, session: Session): Promise; +} -const app = await createApp({ - services: { - useLocal: true, // Force local storage even when authenticated - }, -}); +class AgentuitySessionStore implements SessionStore { + readonly #kv = new KeyValueClient(); + + async get(id: string): Promise { + const result = await this.#kv.get('sessions', id); + return result.exists ? result.data : undefined; + } -export default app; + async set(id: string, session: Session): Promise { + await this.#kv.set('sessions', id, session, { + ttl: 60 * 60 * 24 * 30, + }); + } +} ``` -Local data is stored in `~/.config/agentuity/local.db`, with stream buffers under `~/.config/agentuity/streams`. It persists between runs without affecting cloud data. +Keep the app interface as small as the feature needs. A session store usually does not need the full key-value namespace API, search, stats, and namespace deletion methods. -## Production +## When to Customize -When deployed to Agentuity, your agents use managed KV, vector, and stream storage unless you pass custom implementations in `createApp()`. +| Need | Pattern | +|------|---------| +| use Agentuity managed storage | instantiate the dedicated client directly | +| swap Redis, Postgres, or another backend | define an app-owned interface and provide another implementation | +| share the complete Agentuity method shape | implement the exported storage interface | +| use a different S3-compatible object store | create a Bun `S3Client` with explicit credentials | +| test without managed credentials | use fakes, mocks, or Bun-only local implementations | -## Custom Storage Implementations +## Dedicated Clients -You can replace runtime storage services with your own implementations. This is useful when you need to: +Direct clients are the default v3 path. They read `AGENTUITY_SDK_KEY` from the environment and also accept explicit `apiKey`, `url`, `orgId`, and `logger` options. -- **Use existing infrastructure**: Connect to your company's Redis, Postgres, or vector database -- **Meet compliance requirements**: Keep data in specific regions or systems -- **Self-host**: Run entirely on your own infrastructure +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; +import { StreamClient } from '@agentuity/stream'; +import { VectorClient } from '@agentuity/vector'; -Pass your implementation to `createApp()`: +const kv = new KeyValueClient(); +const streams = new StreamClient(); +const vector = new VectorClient(); -```typescript -import { createApp } from '@agentuity/runtime'; -import { MyRedisKV, MyPineconeVector } from './my-storage'; - -const app = await createApp({ - services: { - keyvalue: new MyRedisKV(), - vector: new MyPineconeVector(), - // stream uses the Agentuity default - }, +const kvForAnotherOrg = new KeyValueClient({ + orgId: 'org_123', }); - -export default app; ``` - -Object storage is not configured through `createApp({ services })`. Use Bun's `s3`/`S3Client` APIs with the credentials written by `agentuity project add storage` or `agentuity cloud storage create`. - +Use constructor options when you are writing admin scripts, tests, or multi-org tools. Most application code should let the client read project environment variables. + +## Full Storage Interfaces -### Storage Interfaces +The dedicated packages re-export the complete service interfaces from `@agentuity/core`. Use these when you want a drop-in adapter with the same method names as Agentuity storage. + +```typescript +import type { KeyValueStorage } from '@agentuity/keyvalue'; +import type { StreamStorage } from '@agentuity/stream'; +import type { VectorStorage } from '@agentuity/vector'; +``` -Your implementation must satisfy the interface for the storage type you override. When your agent calls `ctx.kv.get()`, the SDK calls your implementation's `get()` method. +| Interface | Core methods | +|-----------|--------------| +| `KeyValueStorage` | `get`, `set`, `delete`, `search`, `getKeys`, `getStats`, namespace methods | +| `VectorStorage` | `upsert`, `get`, `getMany`, `search`, `delete`, `exists`, stats and namespace methods | +| `StreamStorage` | `create`, `get`, `download`, `list`, `delete` | -These interfaces are exported from `@agentuity/core`: +Only implement these full interfaces when callers need full compatibility. For most apps, a smaller interface is easier to test and harder to misuse. -#### KeyValueStorage +## Object Storage + +Object storage is not part of the key-value, vector, or stream interface set. Use Bun's `S3Client` when you need another bucket or S3-compatible provider. ```typescript -interface KeyValueStorage { - get(name: string, key: string): Promise>; - set( - name: string, - key: string, - value: T, - params?: { ttl?: number | null; contentType?: string } - ): Promise; - delete(name: string, key: string): Promise; - getStats(name: string): Promise; - getAllStats( - params?: GetAllStatsParams - ): Promise | KeyValueStatsPaginated>; - getNamespaces(): Promise; - search( - name: string, - keyword: string - ): Promise>>; - getKeys(name: string): Promise; - deleteNamespace(name: string): Promise; - createNamespace(name: string, params?: CreateNamespaceParams): Promise; +import { S3Client } from 'bun'; + +function requireEnv(name: string): string { + const value = process.env[name]; + + if (!value) { + throw new Error(`${name} is required`); + } + + return value; } + +const assets = new S3Client({ + accessKeyId: requireEnv('ASSETS_ACCESS_KEY'), + secretAccessKey: requireEnv('ASSETS_SECRET_KEY'), + bucket: 'assets', + endpoint: 'https://example.r2.cloudflarestorage.com', +}); + +await assets.write('healthcheck.txt', 'ok', { type: 'text/plain' }); ``` -#### VectorStorage +## Local Tests + +For unit tests, prefer a small fake that matches your app-owned interface. That keeps tests independent from managed credentials and avoids pulling in storage behavior your feature does not use. ```typescript -interface VectorStorage { - upsert(name: string, ...documents: VectorUpsertParams[]): Promise; - get = Record>( - name: string, - key: string - ): Promise>; - getMany = Record>( - name: string, - ...keys: string[] - ): Promise>>; - search = Record>( - name: string, - params: VectorSearchParams - ): Promise[]>; - delete(name: string, ...keys: string[]): Promise; - exists(name: string): Promise; - getStats(name: string): Promise; - getAllStats( - params?: VectorGetAllStatsParams - ): Promise | VectorStatsPaginated>; - getNamespaces(): Promise; - deleteNamespace(name: string): Promise; +class MemorySessionStore implements SessionStore { + readonly #sessions = new Map(); + + async get(id: string): Promise { + return this.#sessions.get(id); + } + + async set(id: string, session: Session): Promise { + this.#sessions.set(id, session); + } } ``` -#### StreamStorage +For Bun-only local storage checks, `@agentuity/local` exports implementations backed by `~/.config/agentuity/local.db`. ```typescript -interface StreamStorage { - create(namespace: string, props?: CreateStreamProps): Promise; - get(id: string): Promise; - download(id: string): Promise>; - list(params?: ListStreamsParams): Promise; - delete(id: string): Promise; -} +import { getLocalDB, LocalKeyValueStorage } from '@agentuity/local'; + +const localKv = new LocalKeyValueStorage(getLocalDB(), process.cwd()); + +await localKv.set('sessions', 'session_123', { + userId: 'user_123', + email: 'ada@example.com', +}); ``` - -Custom storage must be accessible from Agentuity's infrastructure when deployed. Ensure your storage endpoints are reachable and properly authenticated. + +Local storage is useful for constructor and API-shape tests. Validate managed-service behavior, search relevance, TTLs, and permissions against the managed service before relying on those details. ## Next Steps -- [Key-Value Storage](/services/storage/key-value): Fast caching and configuration -- [Vector Storage](/services/storage/vector): Semantic search and embeddings -- [Object Storage](/services/storage/object): File and media storage -- [Durable Streams](/services/storage/durable-streams): Streaming large data exports +- [Key-Value Storage](/services/storage/key-value): store exact-key cache and state +- [Vector Storage](/services/storage/vector): use semantic search when exact keys are not enough +- [Durable Streams](/services/storage/durable-streams): write generated output that remains readable by URL +- [Object Storage](/services/storage/object): store files and binary blobs with Bun S3 diff --git a/docs/src/web/content/services/storage/durable-streams.mdx b/docs/src/web/content/services/storage/durable-streams.mdx index ffd9dc3c9..3d7266935 100644 --- a/docs/src/web/content/services/storage/durable-streams.mdx +++ b/docs/src/web/content/services/storage/durable-streams.mdx @@ -1,525 +1,206 @@ --- title: Durable Streams -description: Streaming storage for large exports, audit logs, and real-time data +description: Write generated output incrementally and keep it available by URL --- -Durable streams provide streaming storage for large data exports, audit logs, and real-time processing. Streams follow a **write-once, read-many** pattern: once data is written and the stream is closed, the content is immutable and accessible via URL until deleted. +Use durable streams when output is produced over time and should remain readable after the request or worker finishes: CSV exports, logs, generated reports, transcripts, and long model outputs. Start with `StreamClient`; Hono apps can use `c.var.stream` after installing the Agentuity middleware. -## Why Durable Streams? - -WebSocket and SSE connections are straightforward to set up, but they're fragile in practice. Tabs get suspended, networks disconnect, pages get refreshed. When the connection drops, any in-flight data is lost, unless you build custom reconnection logic on top. - -This becomes a real problem when you're streaming LLM responses. Token streaming is often the primary UI for chat applications, and agentic apps stream tool outputs and progress events over long-running sessions. If someone refreshes mid-generation, you're faced with two bad options: re-run an expensive inference call, or lose the response entirely. - -Durable streams solve this by decoupling the data from the connection. Instead of streaming directly to the client, you write to persistent storage and return a URL. The stream continues writing in the background regardless of what happens on the client side. +```bash +bun add @agentuity/stream@alpha +``` -**What this gives you:** +```typescript +import { StreamClient } from '@agentuity/stream'; -- **Refresh-safe**: If someone refreshes the page mid-stream, the URL still works and the content is preserved. The expensive work you already did isn't wasted. -- **Background processing**: Return the stream URL immediately and write data asynchronously with `ctx.waitUntil()`. Your handler responds fast while the stream continues writing. -- **Shareable URLs**: A stream is just a URL. You can share it, open it on another device, or have multiple viewers access the same output. -- **Durable artifacts**: Once closed, streams are immutable and remain accessible via their public URL. Useful for audit logs, exports, and generated reports. +const streams = new StreamClient(); -## When to Use Durable Streams - -| Storage Type | Best For | -|--------------|----------| -| **Durable Streams** | Large exports, audit logs, streaming data, batch processing | -| [Key-Value](/services/storage/key-value) | Fast lookups, caching, configuration | -| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG | -| [Object (S3)](/services/storage/object) | Files, images, documents, media | -| [Database](/services/database) | Structured data, complex queries, transactions | - -**Use streams when you need to:** -- Export large datasets incrementally -- Create audit logs while streaming to clients -- Process data in chunks without holding everything in memory -- Return a URL immediately while data writes in the background - -## Access Patterns - -| Context | Access | Details | -|---------|--------|---------| -| Agents | `ctx.stream` | See examples below | -| Routes | `c.var.stream` | See [Using in Routes](#using-in-routes) | -| Standalone | `createAgentContext()` | See [Standalone Usage](#standalone-usage) | -| External backends | HTTP routes | [SDK Utilities for External Apps](/cookbook/patterns/server-utilities) | -| Frontend | Via routes | [React Hooks](/frontend/react-hooks) | - - -The Stream API is identical in all contexts. `ctx.stream.create()` and `c.var.stream.create()` work the same way. See [Accessing Services](/reference/sdk-reference/router#accessing-services) for the full reference. - +const stream = await streams.create('monthly-reports', { + contentType: 'text/csv', + metadata: { accountId: 'acct_123', month: '2026-04' }, + ttl: 60 * 60 * 24 * 90, +}); -## Creating Streams +try { + await stream.write('account_id,month,total\n'); + await stream.write('acct_123,2026-04,42817\n'); +} finally { + await stream.close(); +} -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('StreamCreator', { - schema: { - input: s.object({ userId: s.string() }), - }, - handler: async (ctx, input) => { - const stream = await ctx.stream.create('export', { - contentType: 'text/csv', - compress: true, // optional gzip compression - metadata: { userId: input.userId }, - ttl: 86400 * 7, // expires in 7 days - }); - - // Stream is ready immediately - ctx.logger.info('Stream created', { - id: stream.id, - url: stream.url, - }); - - return { streamId: stream.id, streamUrl: stream.url }; - }, -}); +const url = stream.url; ``` -**Options:** -- `contentType`: MIME type for the stream content (e.g., `text/csv`, `application/json`) -- `compress`: Enable gzip compression for smaller storage and faster transfers -- `metadata`: Key-value pairs for tracking stream context (user IDs, timestamps, etc.) -- `ttl`: Time-to-live in seconds (see TTL semantics below) +`StreamClient` reads `AGENTUITY_SDK_KEY` by default. Agentuity project code can keep that key in `.env` for local development and deployed environments can receive it through project environment configuration. -**TTL semantics:** +## When to Use Durable Streams -| Value | Behavior | -|-------|----------| -| `undefined` | Streams expire after 30 days (default) | -| `null` or `0` | Streams never expire | -| `>= 60` | Custom TTL in seconds (minimum 60 seconds, maximum 90 days) | +| Need | Use | +|------|-----| +| append-only generated output | [Durable Streams](/services/storage/durable-streams) | +| files or binary data | [Object Storage](/services/storage/object) | +| exact key lookup | [Key-Value](/services/storage/key-value) | +| semantic search or RAG retrieval | [Vector](/services/storage/vector) | +| relational joins or transactions | [Database](/services/database) | - -TTL is enforced only in cloud deployments. During local development, streams persist indefinitely regardless of the TTL value. The `expiresAt` field will not be populated in local stream metadata. - +Use streams when you need to write chunks as they are produced, hand a URL to another service, or keep a finished artifact around without first assembling the full file in memory. -### Compression +## Create and Write -Enable `compress: true` to reduce storage size and transfer time for text-heavy streams: +`create()` returns a writable stream with an `id`, public `url`, `bytesWritten`, and `compressed` flag. ```typescript -const stream = await ctx.stream.create('export', { - contentType: 'text/csv', - compress: true, -}); -``` +import { StreamClient } from '@agentuity/stream'; -Compression uses server-side gzip. Individual `write()` calls send raw data; compression is applied when the stream is closed. The resulting URL serves the data with `Content-Encoding: gzip`, so browsers and HTTP clients decompress it automatically. +const streams = new StreamClient(); - -Compressed streams require the client to accept gzip encoding. Browsers handle this natively. If you're downloading streams programmatically with a client that doesn't support gzip, you'll need to decompress the data manually. - - -The `stream.compressed` property indicates whether compression is active: - -```typescript -const stream = await ctx.stream.create('data', { compress: true }); -ctx.logger.info('Compressed:', { compressed: stream.compressed }); -``` - -## Writing Data - -Write data incrementally, then close the stream: +async function writeAuditLog(userId: string, events: readonly string[]): Promise { + const stream = await streams.create('audit-logs', { + contentType: 'text/plain', + metadata: { userId }, + }); -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('CSVExporter', { - schema: { - input: s.object({ - users: s.array(s.object({ - name: s.string(), - email: s.string(), - created: s.string(), - })), - }), - }, - handler: async (ctx, input) => { - const stream = await ctx.stream.create('export', { - contentType: 'text/csv', - }); - - // Write header - await stream.write('Name,Email,Created\n'); - - // Write rows - for (const user of input.users) { - await stream.write(`${user.name},${user.email},${user.created}\n`); + try { + for (const event of events) { + await stream.write(`${new Date().toISOString()} ${event}\n`); } - - // Close when done + } finally { await stream.close(); + } - ctx.logger.info('Export complete', { - bytesWritten: stream.bytesWritten, - }); - - return { url: stream.url }; - }, -}); + return stream.url; +} ``` - -Streams must be closed manually with `stream.close()`. They do not auto-close. Failing to close a stream leaves it in an incomplete state. +`write()` accepts strings, `Uint8Array`, `ArrayBuffer`, and objects. Objects are serialized as JSON. Split large payloads across multiple writes instead of sending one oversized chunk. + + +Streams do not auto-close. Use `finally` around writes so the stream is finalized even when generation fails partway through. -## Background Processing +## TTL -Use `ctx.waitUntil()` to write data in the background while returning immediately: +Streams expire after 30 days unless you pass `ttl` when creating the stream. -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('BackgroundExporter', { - schema: { - input: s.object({ - reportId: s.string(), - rows: s.array(s.object({ - name: s.string(), - value: s.number(), - })), - }), - }, - handler: async (ctx, input) => { - const stream = await ctx.stream.create('report', { - contentType: 'application/json', - }); - - // Return URL immediately - const response = { streamUrl: stream.url }; - - // Process in background - ctx.waitUntil(async () => { - await stream.write(JSON.stringify({ - reportId: input.reportId, - rows: input.rows, - }, null, 2)); - await stream.close(); - - ctx.logger.info('Background export complete'); - }); - - return response; - }, -}); -``` +| Value | Behavior | +|-------|----------| +| omitted | expire after 30 days | +| `null` or `0` | never expire | +| `>= 60` | expire after that many seconds | -## Managing Streams +Managed stream storage clamps TTL values below 60 seconds to 60 seconds and values above 90 days to 90 days. -### Listing Streams +## Compression -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('StreamManager', { - schema: { - input: s.object({ userId: s.string() }), - }, - handler: async (ctx, input) => { - // List streams with optional filtering - const result = await ctx.stream.list({ - namespace: 'export', // filter by stream namespace - metadata: { userId: input.userId }, // filter by metadata - limit: 100, // max 1000, default 100 - offset: 0, // pagination offset - }); - - ctx.logger.info('Found streams', { - total: result.total, - count: result.streams.length, - }); - - return { streams: result.streams }; - }, -}); -``` - -### Reading Stream Content +Set `compress: true` for text-heavy output such as CSV, JSON, and logs. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('StreamReader', { - schema: { - input: s.object({ streamId: s.string() }), - }, - handler: async (ctx, input) => { - // Get stream metadata by ID - const info = await ctx.stream.get(input.streamId); - ctx.logger.info('Stream info', { - namespace: info.namespace, - sizeBytes: info.sizeBytes, - expiresAt: info.expiresAt, // ISO timestamp when stream expires - }); - - // Download stream content as ReadableStream - const content = await ctx.stream.download(input.streamId); - - // Process the stream - const reader = content.getReader(); - const chunks: Uint8Array[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - - return { data: Buffer.concat(chunks).toString() }; - }, +const stream = await streams.create('exports', { + contentType: 'application/json', + compress: true, }); -``` - -### Deleting Streams -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('StreamCleaner', { - schema: { - input: s.object({ streamId: s.string() }), - }, - handler: async (ctx, input) => { - await ctx.stream.delete(input.streamId); - return { deleted: true }; - }, -}); +await stream.write({ status: 'ok' }); +await stream.close(); ``` -## Dual Stream Pattern +Compression is finalized when the stream closes. Clients that fetch the stream URL should support gzip, which browsers and most HTTP clients handle automatically. -Create two streams simultaneously: one for the client response, one for audit logging. +## Read, List, and Delete + +Use `get()` for metadata, `download()` for content, `list()` for filtered discovery, and `delete()` for cleanup. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { streamText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('DualStreamWriter', { - schema: { - input: s.object({ - userId: s.string(), - message: s.string(), - }), - }, - handler: async (ctx, input) => { - // Create two streams - const mainStream = await ctx.stream.create('output', { - contentType: 'text/plain', - }); - const auditStream = await ctx.stream.create('audit', { - contentType: 'application/json', - metadata: { userId: input.userId }, - }); - - // Return main stream URL immediately - const response = { streamUrl: mainStream.url }; - - // Process both streams in background - ctx.waitUntil(async () => { - const { textStream } = streamText({ - model: openai('gpt-5.4-nano'), - prompt: input.message, - }); - - const chunks: string[] = []; - for await (const chunk of textStream) { - // Write to client stream - await mainStream.write(chunk); - chunks.push(chunk); - } - - // Write audit log - await auditStream.write(JSON.stringify({ - timestamp: new Date().toISOString(), - userId: input.userId, - prompt: input.message, - response: chunks.join(''), - })); - - await mainStream.close(); - await auditStream.close(); - }); - - return response; - }, -}); -``` +import { StreamClient } from '@agentuity/stream'; -## Content Moderation While Streaming +const streams = new StreamClient(); -Buffer and evaluate content in real-time using an LLM-as-a-judge pattern: +const info = await streams.get('stream_abc123'); +const content = await streams.download(info.id); +const text = await new Response(content).text(); -```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateObject, streamText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { groq } from '@ai-sdk/groq'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('ModeratedStreamer', { - schema: { - input: s.object({ message: s.string() }), - }, - handler: async (ctx, input) => { - const stream = await ctx.stream.create('moderated', { - contentType: 'text/plain', - }); - - ctx.waitUntil(async () => { - const { textStream } = streamText({ - model: openai('gpt-5.4-nano'), - prompt: input.message, - }); - - let buffer = ''; - const sentenceEnd = /[.!?]\s/; - - for await (const chunk of textStream) { - buffer += chunk; - - // Check complete sentences - if (sentenceEnd.test(buffer)) { - const isAppropriate = await moderateContent(buffer); - - if (isAppropriate) { - await stream.write(buffer); - } else { - ctx.logger.warn('Content blocked', { content: buffer }); - await stream.write('[Content filtered]'); - } - buffer = ''; - } - } - - // Flush remaining buffer - if (buffer) { - await stream.write(buffer); - } - - await stream.close(); - }); - - return { streamUrl: stream.url }; - }, +const recentExports = await streams.list({ + namespace: 'monthly-reports', + metadata: { accountId: 'acct_123' }, + limit: 25, }); -// Use Groq for low-latency moderation -async function moderateContent(text: string): Promise { - const { object } = await generateObject({ - model: groq('meta-llama/llama-4-scout-17b-16e-instruct'), - schema: s.object({ - safe: s.boolean(), - reason: s.optional(s.string()), - }), - prompt: `Is this content appropriate? Respond with safe=true if appropriate, safe=false if it contains harmful content.\n\nContent: "${text}"`, - }); - - return object.safe; -} +await streams.delete(info.id); ``` -## Using in Routes +`list()` returns `{ success, message, streams, total }`. Each listed stream includes `id`, `namespace`, `metadata`, `url`, `sizeBytes`, and `expiresAt`. -Routes have the same stream access via `c.var.stream`: +## Background Work -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; +Create streams inside the worker, job, or route that owns the generated output. If the work should outlive the request, enqueue it first and store status separately. -const router = new Hono(); +```typescript +import { StreamClient } from '@agentuity/stream'; -interface ExportRow { - readonly name: string; - readonly email: string; +interface ReportJob { + readonly accountId: string; + readonly month: string; } -router.post('/export', async (c) => { - const { data } = await c.req.json<{ data: readonly ExportRow[] }>(); +const streams = new StreamClient(); - const stream = await c.var.stream.create('export', { +export async function writeReport(job: ReportJob): Promise { + const stream = await streams.create('monthly-reports', { contentType: 'text/csv', + metadata: { accountId: job.accountId, month: job.month }, + ttl: 60 * 60 * 24 * 90, }); - // Return URL immediately, write in background - c.waitUntil(async () => { - await stream.write('Name,Email\n'); - for (const row of data) { - await stream.write(`${row.name},${row.email}\n`); - } + try { + await stream.write('account_id,month,total\n'); + await stream.write(`${job.accountId},${job.month},42817\n`); + } finally { await stream.close(); - }); + } - return c.json({ url: stream.url }); -}); - -export default router; + return stream.url; +} ``` - -Need to create streams from a Next.js backend or other external service? Create authenticated routes that expose stream operations, then call them via HTTP. See [SDK Utilities for External Apps](/cookbook/patterns/server-utilities). - +See [Background Work](/patterns/background-work): it pairs queues, KV status records, and durable streams for jobs that should continue after a request returns. -## Standalone Usage +## Hono -Create streams from background processes with `createAgentContext()`: +In Hono apps, `@agentuity/hono` initializes `StreamClient` once and exposes it on `c.var.stream`. -```typescript -import { createApp, createAgentContext } from '@agentuity/runtime'; +```bash +bun add @agentuity/hono@alpha hono +``` -const app = await createApp(); -export default app; +```typescript +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; -// Background job to generate reports -async function generateDailyReport(): Promise { - const ctx = createAgentContext({ trigger: 'cron' }); +type Variables = Pick; - await ctx.invoke(async () => { - const stream = await ctx.stream.create('report', { - contentType: 'text/csv', - metadata: { date: new Date().toISOString() }, - }); +const app = new Hono<{ Variables: Variables }>(); - await stream.write('Date,Metric,Value\n'); - // ... write report data - await stream.close(); +app.use('*', agentuity()); - ctx.logger.info('Report generated', { url: stream.url }); +app.post('/exports', async (c) => { + const stream = await c.var.stream.create('exports', { + contentType: 'text/plain', }); -} -``` - -See [Running Agents Without HTTP](/agents/standalone-execution) for more patterns including Discord bots, CLI tools, and queue workers. -## Stream Properties - -| Property | Description | -|----------|-------------| -| `stream.id` | Unique stream identifier | -| `stream.url` | Public URL to access the stream | -| `stream.bytesWritten` | Total bytes written so far | + try { + await stream.write('hello\n'); + } finally { + await stream.close(); + } -## Best Practices + return c.json({ id: stream.id, url: stream.url }); +}); -- **Use compression**: Enable `compress: true` for text-heavy streams (CSV, JSON, logs). Compression is applied server-side when the stream is closed, so it works with any write pattern -- **Return URLs early**: Use `ctx.waitUntil()` to return the stream URL while writing in the background -- **Clean up**: Delete streams after they're no longer needed to free storage -- **Set content types**: Always specify the correct MIME type for proper browser handling +export default app; +``` ## Next Steps -- [Key-Value Storage](/services/storage/key-value): Fast caching and configuration -- [Vector Storage](/services/storage/vector): Semantic search and embeddings -- [Object Storage (S3)](/services/storage/object): File and media storage -- [Database](/services/database): Relational data with queries and transactions -- [Streaming Responses](/agents/streaming-responses): HTTP streaming patterns +- [Background Work](/patterns/background-work): run long jobs with queues, status records, and streams +- [Object Storage](/services/storage/object): store files and binary blobs with Bun S3 +- [Key-Value Storage](/services/storage/key-value): store job status and exact-key state diff --git a/docs/src/web/content/services/storage/index.mdx b/docs/src/web/content/services/storage/index.mdx index 4e27aaa44..0536b8d35 100644 --- a/docs/src/web/content/services/storage/index.mdx +++ b/docs/src/web/content/services/storage/index.mdx @@ -1,11 +1,11 @@ --- title: Storage -description: Persistent storage options for agents and routes +description: Persistent storage clients for framework apps and deployed services --- import { Database, Key, Search, HardDrive, Activity } from 'lucide-react'; -Agentuity provides multiple storage options to match different data access patterns. +Choose storage by access pattern first: key lookup, semantic search, files, streams, or SQL. Start with the direct storage client or API, and use Hono `c.var.*` when you want the same managed clients injected into routes. ## Storage Types @@ -38,41 +38,28 @@ Agentuity provides multiple storage options to match different data access patte ## When to Use Each Type -| Storage Type | Best For | Access Pattern | -|--------------|----------|----------------| -| [Key-Value](/services/storage/key-value) | Caching, session data, rate limits | Fast key-based lookups | -| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG | Similarity search | -| [Object](/services/storage/object) | Files, images, large blobs | URL-based access | -| [Durable Streams](/services/storage/durable-streams) | Logs, exports, event streams | Append-only, ordered reads | -| [Database](/services/database) | Relational data, complex queries | SQL queries | +| Storage Type | Best For | Client Path | +|--------------|----------|-------------| +| [Key-Value](/services/storage/key-value) | Caching, session data, rate limits | `@agentuity/keyvalue` | +| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG | `@agentuity/vector` | +| [Object](/services/storage/object) | Files, images, large blobs | Bun S3 APIs with Agentuity-provided credentials | +| [Durable Streams](/services/storage/durable-streams) | Logs, exports, event streams | `@agentuity/stream` | +| [Database](/services/database) | Relational data, complex queries | `@agentuity/postgres`, `@agentuity/drizzle`, or `@agentuity/db` | -## Built-in State vs Storage - -Agents have built-in state for request-local work and thread-level conversation history: - -| State Type | Scope | Use For | -|------------|-------|---------| -| `ctx.state` | Request | Temporary values used inside the current handler | -| `ctx.session.state` | Request | Values needed during the current request or session-completion callbacks | -| `ctx.thread.state` | Thread | Conversation context across requests | - -Use storage services when you need: -- Custom TTL or expiration -- Data shared across agents -- Durable data beyond thread state -- External system integration +Use storage services when data must outlive the current request, be shared across workers or routes, or be inspected and managed as an Agentuity resource. ## Access Patterns -| Context | KV | Vector | Stream | -|---------|----|--------|--------| -| Agents | `ctx.kv` | `ctx.vector` | `ctx.stream` | -| Routes | `c.var.kv` | `c.var.vector` | `c.var.stream` | +| App Shape | KV | Vector | Database | +|-----------|----|--------|----------| +| Any server framework or script | `new KeyValueClient()` | `new VectorClient()` | `postgres()`, `createPostgresDrizzle()`, or `new DBClient(...)` | +| Hono with `@agentuity/hono` | `c.var.kv` | `c.var.vector` | use a direct database client | +| Browser code | Call your backend route | Call your backend route | Call your backend route | - -Database and object storage are not exposed on `ctx.*` or `c.var.*`. Use Bun's native `sql` and `s3` APIs for those services. See [Storage APIs](/reference/sdk-reference/storage) for the current breakdown. + +`@agentuity/hono` creates the same dedicated clients used in standalone examples and stores them on `c.var.*`. Database clients are not injected by the Hono middleware; create them directly where your app initializes database access. ## Custom Storage -For runtime services that need a different backend, you can implement [custom Key-Value, Vector, or Durable Stream storage](/services/storage/custom). +For service code that needs a different backend, you can implement [custom Key-Value, Vector, or Durable Stream storage](/services/storage/custom). diff --git a/docs/src/web/content/services/storage/key-value.mdx b/docs/src/web/content/services/storage/key-value.mdx index 7565230b3..06a4006de 100644 --- a/docs/src/web/content/services/storage/key-value.mdx +++ b/docs/src/web/content/services/storage/key-value.mdx @@ -1,309 +1,132 @@ --- title: Key-Value Storage short_title: Key-Value -description: Fast key-based storage for caching, session data, and configuration +description: Store small durable values by namespace and key with optional TTL --- -Key-value ("KV") storage provides fast data access for agents. Use it for caching, configuration, rate limiting, and data that needs quick lookups. +Use key-value storage for cache entries, user preferences, rate-limit counters, feature flags, and other data you fetch by exact key. Start with `KeyValueClient`; Hono apps can use `c.var.kv` after installing the Agentuity middleware. - -Use the [`@agentuity/keyvalue`](/reference/standalone-packages#key-value-storage) standalone package to access this service from any Node.js or Bun app without the runtime. - +```bash +bun add @agentuity/keyvalue@alpha +``` -## When to Use Key-Value Storage +```typescript +import { KeyValueClient } from '@agentuity/keyvalue'; -| Storage Type | Best For | -|--------------|----------| -| **Key-Value** | Fast lookups, caching, configuration, rate limits | -| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG | -| [Object (S3)](/services/storage/object) | Files, images, documents, media | -| [Database](/services/database) | Structured data, complex queries, transactions | -| [Durable Streams](/services/storage/durable-streams) | Large exports, audit logs, real-time data | +interface UserPreferences { + readonly theme: 'light' | 'dark'; + readonly notifications: boolean; +} - -Use built-in state (`ctx.state`, `ctx.thread.state`, `ctx.session.state`) for data tied to active requests and conversations. Use KV when you need custom TTL, *persistent data across sessions*, or *shared state across agents*. - +const kv = new KeyValueClient(); -## Access Patterns +await kv.set( + 'preferences', + 'user_123', + { theme: 'dark', notifications: true } satisfies UserPreferences, + { ttl: 60 * 60 * 24 * 30 } +); -| Context | Access | Details | -|---------|--------|---------| -| Agents | `ctx.kv` | See examples below | -| Routes | `c.var.kv` | See [Using in Routes](#using-in-routes) | -| Standalone | `createAgentContext()` | See [Standalone Usage](#standalone-usage) | -| External backends | HTTP routes | [SDK Utilities for External Apps](/cookbook/patterns/server-utilities) | -| Frontend | Via routes | [React Hooks](/frontend/react-hooks) | +const result = await kv.get('preferences', 'user_123'); - -The runtime surface is the same in agents and routes. `ctx.kv.get()` and `c.var.kv.get()` call the same storage interface. See [Accessing Services](/reference/sdk-reference/router#accessing-services) for the full reference. - +const preferences = result.exists + ? result.data + : { theme: 'light', notifications: true } satisfies UserPreferences; +``` -## Basic Operations +`KeyValueClient` reads `AGENTUITY_SDK_KEY` by default. Agentuity project code can keep that key in `.env` for local development and deployed environments can receive it through project environment configuration. -Access key-value storage through `ctx.kv` in agents or `c.var.kv` in routes. Namespaces are created when you write the first key. +## When to Use KV - -Local development KV supports `get()`, `set()`, and `delete()`. Managed cloud KV also supports search, key listing, namespace stats, and namespace management. - +| Need | Use | +|------|-----| +| exact key lookup | [Key-Value](/services/storage/key-value) | +| semantic search | [Vector](/services/storage/vector) | +| files or binary data | [Object Storage](/services/storage/object) | +| relational joins or transactions | [Database](/services/database) | +| append-only ordered data | [Durable Streams](/services/storage/durable-streams) | -### Storing Data +## TTL -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('CacheManager', { - handler: async (ctx) => { - const responseData = { status: 'ok', cachedAt: new Date().toISOString() }; - - // Store with custom TTL - await ctx.kv.set('cache', 'api-response', responseData, { - ttl: 3600, // expires in 1 hour - contentType: 'application/json', - }); - - // Store with default TTL (7 days) - await ctx.kv.set('cache', 'user-prefs', { theme: 'dark' }); - - // Store with no expiration (persists indefinitely) - await ctx.kv.set('config', 'feature-flags', { - darkMode: true, - betaFeatures: false, - }, { - ttl: null, // never expires (0 also works) - }); - - return { success: true }; - }, -}); -``` +Keys inherit the namespace default TTL unless you pass `ttl` to `set()`. Namespaces created on first write use a 7-day default. -### Retrieving Data +| Value | Behavior | +|-------|----------| +| omitted | inherit the namespace default TTL | +| `null` or `0` | never expire | +| `>= 60` | expire after that many seconds | ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('CacheRetriever', { - handler: async (ctx) => { - const result = await ctx.kv.get('cache', 'api-response'); - - if (result.exists) { - ctx.logger.info('Cache hit', { - contentType: result.contentType, - expiresAt: result.expiresAt, // ISO timestamp when key expires (if TTL set) - }); - return { data: result.data }; - } - - ctx.logger.info('Cache miss'); - return { data: null }; - }, -}); +await kv.set('cache', 'home', { html: '
...
' }, { ttl: 300 }); +await kv.set('config', 'feature-flags', { betaSearch: true }, { ttl: null }); ``` -### Deleting Data - -```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const agent = createAgent('SessionCleaner', { - schema: { - input: s.object({ sessionId: s.string() }), - output: s.object({ deleted: s.boolean() }), - }, - handler: async (ctx, input) => { - await ctx.kv.delete('sessions', input.sessionId); - return { deleted: true }; - }, -}); -``` + +Managed KV clamps per-key TTL values below 60 seconds to 60 seconds and values above 365 days to 365 days. Namespace defaults must be `0` or between 60 seconds and 365 days. + -## Type Safety +## Search and Inspect Keys -Use generics for type-safe data access: +Use `search()` when you need matching keys with stored value metadata. Use `getKeys()` when you only need the keys in a namespace. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; - -const UserPreferencesSchema = s.object({ - theme: s.enum(['light', 'dark']), - language: s.string(), - notifications: s.boolean(), -}); - -type UserPreferences = s.infer; - -const agent = createAgent('PreferenceLoader', { - schema: { - input: s.object({ userId: s.string() }), - output: s.object({ theme: s.enum(['light', 'dark']) }), - }, - handler: async (ctx, input) => { - const result = await ctx.kv.get('prefs', input.userId); - - if (result.exists) { - // TypeScript knows the shape of result.data - const theme = result.data.theme; // Type: 'light' | 'dark' - return { theme }; - } +interface CachedResponse { + readonly status: number; + readonly body: string; +} - return { theme: 'light' }; // default - }, -}); +const matches = await kv.search('cache', 'api:users'); +const keys = await kv.getKeys('cache'); +const namespaces = await kv.getNamespaces(); +const stats = await kv.getStats('cache'); ``` -## Additional Methods - -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('StorageExplorer', { - handler: async (ctx) => { - // Search keys by keyword (returns keys with metadata) - const matches = await ctx.kv.search('cache', 'user-'); - - // List all keys in a namespace - const keys = await ctx.kv.getKeys('cache'); - - // List all namespaces - const namespaces = await ctx.kv.getNamespaces(); - - // Get statistics for a namespace - const stats = await ctx.kv.getStats('cache'); - - // Get statistics for all namespaces - const allStats = await ctx.kv.getAllStats(); - - return { keys, namespaces, stats, allStats }; - }, -}); -``` + +`@agentuity/local` is useful when you want local reads and writes without managed credentials. For search, key listing, stats, or namespace operations, test against the managed KV service. + -### Namespace Management +## Namespace Management -Create and delete namespaces programmatically: +Create namespaces when you want a default TTL before the first write. Delete namespaces only when you mean to remove every key inside them. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('NamespaceManager', { - handler: async (ctx) => { - // Create a namespace with default TTL for all keys - await ctx.kv.createNamespace('cache', { - defaultTTLSeconds: 3600, // all keys expire in 1 hour by default - }); - - // Create a namespace with no expiration - await ctx.kv.createNamespace('config', { - defaultTTLSeconds: 0, // keys never expire - }); - - // Delete a namespace (removes all keys) - await ctx.kv.deleteNamespace('old-cache'); - - return { success: true }; - }, +await kv.createNamespace('sessions', { + defaultTTLSeconds: 60 * 60 * 24, }); -``` - -**TTL semantics:** - -| Value | Behavior | -|-------|----------| -| `undefined` | Inherits the namespace default TTL (7 days for auto-created namespaces) | -| `null` or `0` | Keys never expire | -| `>= 60` | Custom TTL in seconds (minimum 60 seconds, maximum 365 days) | - -Managed cloud KV clamps per-key TTL values passed to `set()` that are less than 60 seconds to 60 seconds, and greater than 365 days to 365 days. Namespace defaults passed to `createNamespace()` must be `0` or between 60 seconds and 365 days. Out-of-range namespace defaults return a validation error. A TTL of `0` means the key, or namespace default, never expires. - - - -Managed cloud KV extends a key when it is read with less than 50% of its TTL remaining. This keeps frequently accessed data alive without manual renewal. - +await kv.deleteNamespace('old-cache'); +``` - -`deleteNamespace()` permanently removes the namespace and all its keys. This operation cannot be undone. + +`deleteNamespace()` permanently removes the namespace and all keys inside it. -## TTL Strategy +## Hono -Keys expire after 7 days by default unless a namespace-level or per-key TTL is set. Use TTL for temporary data: +In Hono apps, `@agentuity/hono` initializes `KeyValueClient` once and exposes it on `c.var.kv`. -| Data Type | Suggested TTL | -|-----------|---------------| -| API cache | 5-60 minutes (300-3600s) | -| Session data | 24-48 hours (86400-172800s) | -| Rate limit counters | Until period reset | -| Feature flags | No TTL (persistent) | +```bash +bun add @agentuity/hono@alpha hono +``` ```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; -interface UserSession { +interface Session { readonly userId: string; readonly email: string; - readonly loginAt: string; - readonly preferences: { readonly theme: string }; } -const agent = createAgent('SessionManager', { - schema: { - input: s.object({ - token: s.string(), - userId: s.string(), - email: s.string(), - }), - output: s.object({ - session: s.object({ - userId: s.string(), - email: s.string(), - loginAt: s.string(), - preferences: s.object({ theme: s.string() }), - }), - }), - }, - handler: async (ctx, input) => { - const sessionKey = `session:${input.token}`; - - // Check for existing session - const existing = await ctx.kv.get('sessions', sessionKey); - if (existing.exists) { - return { session: existing.data }; - } - - // Create new session with 24-hour TTL - const session: UserSession = { - userId: input.userId, - email: input.email, - loginAt: new Date().toISOString(), - preferences: { theme: 'light' }, - }; - - await ctx.kv.set('sessions', sessionKey, session, { - ttl: 86400, // 24 hours - }); - - return { session }; - }, -}); -``` - -## Using in Routes +type Variables = Pick; -Routes have the same KV access via `c.var.kv`: +const app = new Hono<{ Variables: Variables }>(); -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; - -const router = new Hono(); +app.use('*', agentuity()); -router.get('/session/:id', async (c) => { - const sessionId = c.req.param('id'); - const result = await c.var.kv.get<{ userId: string; email: string }>('sessions', sessionId); +app.get('/sessions/:id', async (c) => { + const result = await c.var.kv.get('sessions', c.req.param('id')); if (!result.exists) { return c.json({ error: 'Session not found' }, 404); @@ -312,53 +135,11 @@ router.get('/session/:id', async (c) => { return c.json({ session: result.data }); }); -export default router; -``` - - -Need to access KV from a Next.js backend or other external service? Create authenticated routes that expose storage operations, then call them via HTTP. See [SDK Utilities for External Apps](/cookbook/patterns/server-utilities). - - -## Standalone Usage - -Use KV storage outside of agent handlers with `createAgentContext()`: - -```typescript -import { createApp, createAgentContext } from '@agentuity/runtime'; - -const app = await createApp(); export default app; - -async function ensurePreferences(userId: string): Promise { - const ctx = createAgentContext(); - - await ctx.invoke(async () => { - // Check user preferences - const prefs = await ctx.kv.get('prefs', userId); - - if (!prefs.exists) { - await ctx.kv.set('prefs', userId, { - theme: 'dark', - notifications: true, - }); - } - }); -} ``` -See [Running Agents Without HTTP](/agents/standalone-execution) for more patterns including Discord bots, CLI tools, and queue workers. - -## Best Practices - -- **Use descriptive keys**: `user:{userId}:prefs` instead of `u123` -- **Set appropriate TTLs**: Prevent storage bloat with expiring cache entries -- **Handle missing keys**: Always check `result.exists` before accessing data -- **Keep values small**: KV is optimized for small-to-medium values; use Object Storage for large files - ## Next Steps -- [Vector Storage](/services/storage/vector): Semantic search and embeddings -- [Object Storage (S3)](/services/storage/object): File and media storage -- [Database](/services/database): Relational data with queries and transactions -- [Durable Streams](/services/storage/durable-streams): Large data exports -- [State Management](/agents/state-management): Built-in request/thread/session state +- [Vector Storage](/services/storage/vector): use semantic search when exact keys are not enough +- [Database](/services/database): use Postgres for joins, constraints, and transactions +- [Using Standalone Packages](/reference/standalone-packages): configure service clients outside Agentuity projects diff --git a/docs/src/web/content/services/storage/object.mdx b/docs/src/web/content/services/storage/object.mdx index c6556b2ec..993333913 100644 --- a/docs/src/web/content/services/storage/object.mdx +++ b/docs/src/web/content/services/storage/object.mdx @@ -1,20 +1,25 @@ --- title: Object Storage (S3) short_title: Object -description: Durable file storage using Bun's native S3 APIs +description: Store files and binary data with Bun's native S3 APIs --- -Object storage provides durable file storage for documents, images, media, and binary content using [Bun's native S3 APIs](https://bun.sh/docs/runtime/s3). +Use object storage for files, images, documents, media, backups, and other data that should live outside your database. Use [Bun's S3 APIs](https://bun.sh/docs/runtime/s3) from server code with the bucket credentials linked to your project. -## When to Use Object Storage +```typescript +import { s3 } from 'bun'; + +const file = s3.file('reports/monthly-summary.json'); + +await file.write(JSON.stringify({ month: '2026-04', total: 42817 }), { + type: 'application/json', +}); + +const exists = await file.exists(); +const summary = exists ? await file.json() : undefined; +``` -| Storage Type | Best For | -|--------------|----------| -| **Object (S3)** | Files, images, documents, media, backups | -| [Key-Value](/services/storage/key-value) | Fast lookups, caching, configuration | -| [Vector](/services/storage/vector) | Semantic search, embeddings, RAG | -| [Database](/services/database) | Structured data, complex queries, transactions | -| [Durable Streams](/services/storage/durable-streams) | Large exports, audit logs | +`s3.file()` creates a lazy file reference. Network work starts when you read, write, check existence, delete, or stream the file. ## Setup @@ -22,194 +27,182 @@ Object storage requires a storage bucket linked to your project. ### New Projects -When you run `agentuity project create`, the CLI prompts you to create or select a storage bucket. If you opt in, the bucket is linked and credentials are written to `.env` automatically. +When you run `agentuity project create`, the CLI can create or select a storage bucket for the project. If you opt in, the bucket is linked and credentials are written to `.env`. ### Existing Projects -For projects that don't have a bucket yet: - -1. Create a bucket using the CLI or by signing in to the [Agentuity web app](https://app.agentuity.com/sign-in): - -```bash -agentuity cloud storage create -``` - -2. Link it to your project: +Create a bucket, then link it to the project: ```bash -agentuity project add storage +agentuity cloud storage create --name app-uploads +agentuity project add storage app-uploads ``` -`agentuity project add storage` links the bucket and writes credentials to `.env`. `agentuity cloud storage create` also writes credentials if run from a project directory. +`agentuity cloud storage create` writes credentials to `.env` when you run it from a project directory. `agentuity project add storage` links an existing bucket and writes the same credentials. -The credentials written to `.env`: +The credentials written to `.env` are: - `S3_ACCESS_KEY_ID` - `S3_SECRET_ACCESS_KEY` - `S3_BUCKET` - `S3_ENDPOINT` - -`agentuity dev` reads S3 credentials from `.env`. If you see `ERR_S3_MISSING_CREDENTIALS`, run `agentuity project add storage` to link a bucket and write the credentials. - -After cloning a project where `.env` is not checked in, run `agentuity project add storage` to re-link the bucket. Alternatively, if the project has been deployed before, run `agentuity cloud env pull` to restore all project environment variables from the cloud. + +Do not expose bucket credentials to browser code. Create presigned URLs from a server route when a browser needs direct upload or download access. - -When deployed to Agentuity Cloud, S3 credentials for linked buckets are available automatically. - +## When to Use Object Storage + +| Need | Use | +|------|-----| +| files or binary data | [Object Storage](/services/storage/object) | +| exact key lookup | [Key-Value](/services/storage/key-value) | +| semantic search or RAG retrieval | [Vector](/services/storage/vector) | +| append-only generated output | [Durable Streams](/services/storage/durable-streams) | +| relational joins or transactions | [Database](/services/database) | -## Quick Start +## Read, Write, and Delete + +Use the global `s3` client when your app uses the bucket credentials from the environment. ```typescript import { s3 } from 'bun'; -// Create a file reference -const file = s3.file('documents/report.pdf'); +const avatar = s3.file('users/user_123/avatar.png'); -// Write content -await file.write('Hello, World!'); -await file.write(JSON.stringify({ createdAt: new Date().toISOString() }), { - type: 'application/json', +await avatar.write(await fetch('https://example.com/avatar.png'), { + type: 'image/png', }); -// Read content -const text = await file.text(); -const json = await file.json(); -const bytes = await file.bytes(); - -// Check existence and delete -if (await file.exists()) { - await file.delete(); +if (await avatar.exists()) { + const bytes = await avatar.bytes(); + const contentType = avatar.type; } + +await avatar.delete(); ``` -## Using in Agents +For large files, write incrementally instead of holding the whole payload in memory: ```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; import { s3 } from 'bun'; -const agent = createAgent('FileProcessor', { - schema: { - input: s.object({ userId: s.string() }), - output: s.object({ - data: s.unknown().optional(), - error: s.string().optional(), - }), - }, - handler: async (ctx, input) => { - const file = s3.file(`uploads/${input.userId}/data.json`); - - if (!(await file.exists())) { - return { error: 'File not found' }; - } - - const data = await file.json(); - ctx.logger.info('File loaded', { userId: input.userId }); - return { data }; - }, +const file = s3.file('exports/large-report.csv'); +const writer = file.writer({ + type: 'text/csv', + partSize: 5 * 1024 * 1024, + queueSize: 4, }); + +writer.write('account_id,total\n'); +writer.write('acct_123,42817\n'); +await writer.end(); ``` -## Using in Routes +## Presigned URLs + +Generate presigned URLs from server code when clients should upload or download directly. ```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; import { s3 } from 'bun'; -const router = new Hono(); - -// File upload -router.post('/upload/:filename', async (c) => { - const filename = c.req.param('filename'); - const file = s3.file(`uploads/${filename}`); - - const buffer = await c.req.arrayBuffer(); - await file.write(new Uint8Array(buffer), { - type: c.req.header('content-type') || 'application/octet-stream', - }); - - return c.json({ success: true, url: file.presign({ expiresIn: 3600 }) }); +const downloadUrl = s3.presign('reports/monthly-summary.json', { + method: 'GET', + expiresIn: 60 * 15, }); -// File download (redirects to S3) -router.get('/download/:filename', async (c) => { - const file = s3.file(`uploads/${c.req.param('filename')}`); - if (!(await file.exists())) { - return c.json({ error: 'Not found' }, 404); - } - return new Response(file); +const uploadUrl = s3.presign('uploads/invoice.pdf', { + method: 'PUT', + expiresIn: 60 * 10, + type: 'application/pdf', }); - -export default router; ``` - -Passing an `S3File` to `new Response()` returns a 302 redirect to a presigned URL, so clients download directly from S3. - +`presign()` is synchronous. It signs a URL from local credentials and does not call S3. -## Presigned URLs +## Uploads from a Hono Route -Generate time-limited URLs for direct client access: +Keep browser uploads direct to S3 by returning a short-lived upload URL from your backend. -```typescript +```typescript title="src/index.ts" +import { Hono } from 'hono'; import { s3 } from 'bun'; +import { s } from '@agentuity/schema'; -// Download URL (default: GET, 24 hours) -const downloadUrl = s3.presign('uploads/document.pdf', { - expiresIn: 3600, +const uploadRequestSchema = s.object({ + filename: s.string(), + contentType: s.string(), }); -// Upload URL -const uploadUrl = s3.presign('uploads/new-file.pdf', { - method: 'PUT', - expiresIn: 900, - type: 'application/pdf', +const app = new Hono(); + +app.post('/api/uploads/presign', async (c) => { + const body: unknown = await c.req.json(); + const input = uploadRequestSchema.parse(body); + const key = `uploads/${crypto.randomUUID()}-${input.filename}`; + + const url = s3.presign(key, { + method: 'PUT', + expiresIn: 60 * 10, + type: input.contentType, + }); + + return c.json({ key, url }); }); + +app.get('/api/uploads/download', async (c) => { + const key = c.req.query('key'); + + if (!key) { + return c.json({ error: 'Missing key' }, 400); + } + + const file = s3.file(key); + + if (!(await file.exists())) { + return c.json({ error: 'File not found' }, 404); + } + + return new Response(file); +}); + +export default app; ``` +Passing an `S3File` to `new Response()` redirects the client to a presigned download URL, so the file does not flow through your route process. + ## Custom S3 Clients -For multiple buckets or external S3-compatible services: +Use `S3Client` when you need a specific bucket, external S3-compatible service, or explicit credentials. ```typescript import { S3Client } from 'bun'; function requireEnv(name: string): string { const value = process.env[name]; + if (!value) { throw new Error(`${name} is required`); } + return value; } -// Cloudflare R2 -const r2 = new S3Client({ - accessKeyId: requireEnv('R2_ACCESS_KEY'), - secretAccessKey: requireEnv('R2_SECRET_KEY'), - bucket: 'my-bucket', - endpoint: `https://${requireEnv('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`, +const uploads = new S3Client({ + accessKeyId: requireEnv('S3_ACCESS_KEY_ID'), + secretAccessKey: requireEnv('S3_SECRET_ACCESS_KEY'), + bucket: requireEnv('S3_BUCKET'), + endpoint: requireEnv('S3_ENDPOINT'), + region: 'auto', }); -// AWS S3 -const aws = new S3Client({ - accessKeyId: requireEnv('AWS_ACCESS_KEY_ID'), - secretAccessKey: requireEnv('AWS_SECRET_ACCESS_KEY'), - bucket: 'my-bucket', - region: 'us-east-1', -}); +await uploads.write('healthcheck.txt', 'ok', { type: 'text/plain' }); ``` -## Bun S3 Documentation - -For complete API documentation including streaming, multipart uploads, file metadata, listing objects, and partial reads, see the [Bun S3 documentation](https://bun.sh/docs/runtime/s3). +For Agentuity-managed buckets, prefer the environment variables written by the CLI. For other providers, pass that provider's endpoint, bucket, and credentials to `S3Client`. ## Next Steps -- [Key-Value Storage](/services/storage/key-value): Fast caching and configuration -- [Database](/services/database): Relational data with Bun's SQL support -- [Vector Storage](/services/storage/vector): Semantic search and embeddings -- [Durable Streams](/services/storage/durable-streams): Streaming large data exports +- [Key-Value Storage](/services/storage/key-value): store exact-key cache and state +- [Durable Streams](/services/storage/durable-streams): write generated output that should remain accessible by URL +- [Database](/services/database): store relational data with SQL and transactions diff --git a/docs/src/web/content/services/storage/vector.mdx b/docs/src/web/content/services/storage/vector.mdx index f63903c5e..07fb0b2fa 100644 --- a/docs/src/web/content/services/storage/vector.mdx +++ b/docs/src/web/content/services/storage/vector.mdx @@ -1,441 +1,185 @@ --- title: Vector Storage short_title: Vector -description: Semantic search and retrieval for knowledge bases and RAG systems +description: Store documents and embeddings for semantic search and retrieval --- -Vector storage enables semantic search, allowing agents to find information by *meaning* rather than keywords. Use it for knowledge bases, RAG systems, recommendations, and persistent agent memory. +Use vector storage when exact keys or SQL filters are not enough, for example knowledge search, RAG retrieval, recommendations, or long-lived memory. Start with `VectorClient`; Hono apps can use `c.var.vector` after installing the Agentuity middleware. - -Use the [`@agentuity/vector`](/reference/standalone-packages#vector-search) standalone package to access this service from any Node.js or Bun app without the runtime. - - -## When to Use Vector Storage +```bash +bun add @agentuity/vector@alpha +``` -| Storage Type | Best For | -|--------------|----------| -| **Vector** | Semantic search, embeddings, RAG, recommendations | -| [Key-Value](/services/storage/key-value) | Fast lookups, caching, configuration | -| [Object (S3)](/services/storage/object) | Files, images, documents, media | -| [Database](/services/database) | Structured data, complex queries, transactions | -| [Durable Streams](/services/storage/durable-streams) | Large exports, audit logs | - -## Access Patterns - -| Context | Access | Details | -|---------|--------|---------| -| Agents | `ctx.vector` | See examples below | -| Routes | `c.var.vector` | See [Using in Routes](#using-in-routes) | -| Standalone | `createAgentContext()` | See [Standalone Usage](#standalone-usage) | -| External backends | HTTP routes | [SDK Utilities for External Apps](/cookbook/patterns/server-utilities) | -| Frontend | Via routes | [React Hooks](/frontend/react-hooks) | - - -The Vector API is identical in all contexts. `ctx.vector.search()` and `c.var.vector.search()` work the same way. See [Accessing Services](/reference/sdk-reference/router#accessing-services) for the full reference. - +```typescript +import { VectorClient } from '@agentuity/vector'; -## Upserting Documents +interface DocumentMetadata { + readonly category: 'guide' | 'api'; + readonly source: string; +} -Store documents with automatic embedding generation. The `upsert` operation is idempotent: using an existing key updates the vector rather than creating a duplicate. +const vector = new VectorClient(); -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('KnowledgeLoader', { - handler: async (ctx) => { - // Upsert with text (auto-generates embeddings) - const results = await ctx.vector.upsert('knowledge-base', - { - key: 'doc-1', - document: 'Agentuity is an agent-native cloud platform', - metadata: { category: 'platform', source: 'docs' }, - ttl: 86400 * 7, // expires in 7 days - }, - { - key: 'doc-2', - document: 'Vector storage enables semantic search capabilities', - metadata: { category: 'features', source: 'docs' }, - // No TTL specified: uses 30-day default - } - ); - - // Returns: [{ key: 'doc-1', id: 'internal-id' }, ...] - return { inserted: results.length }; +await vector.upsert( + 'docs', + { + key: 'deploy-guide', + document: 'Deploy framework apps to Agentuity with agentuity deploy.', + metadata: { category: 'guide', source: 'docs' } satisfies DocumentMetadata, }, + { + key: 'service-clients', + document: 'Agentuity services can be used through dedicated client packages.', + metadata: { category: 'api', source: 'docs' } satisfies DocumentMetadata, + } +); + +const results = await vector.search('docs', { + query: 'How do I use Agentuity services from my app?', + limit: 3, + similarity: 0.7, }); ``` -**TTL semantics:** +`VectorClient` reads `AGENTUITY_SDK_KEY` by default. Agentuity project code can keep that key in `.env` for local development and deployed environments can receive it through project environment configuration. -| Value | Behavior | -|-------|----------| -| `undefined` | Vectors expire after 30 days (default) | -| `null` or `0` | Vectors never expire | -| `>= 60` | Custom TTL in seconds (minimum 60 seconds, maximum 90 days) | +## When to Use Vector Storage - -TTL is enforced only in cloud deployments. During local development, vectors persist indefinitely regardless of the TTL value. The `expiresAt` field will not be populated in local search results. - +| Need | Use | +|------|-----| +| semantic search or RAG retrieval | [Vector](/services/storage/vector) | +| exact key lookup | [Key-Value](/services/storage/key-value) | +| relational joins or transactions | [Database](/services/database) | +| files or binary data | [Object Storage](/services/storage/object) | +| append-only ordered data | [Durable Streams](/services/storage/durable-streams) | + +## Upsert Data -**With pre-computed embeddings:** +Pass `document` when you want Agentuity to generate embeddings. Pass `embeddings` when you already have vectors from another model. ```typescript -await ctx.vector.upsert('custom-embeddings', { - key: 'embedding-1', - embeddings: [0.12, 0.34, 0.56, 0.78], - metadata: { source: 'external' }, - ttl: null, // never expires +await vector.upsert('products', { + key: 'chair-001', + document: 'Ergonomic office chair with lumbar support', + metadata: { category: 'furniture', source: 'catalog' }, + ttl: 60 * 60 * 24 * 90, }); -``` - - -## Searching -Find semantically similar documents: - -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('SemanticSearch', { - handler: async (ctx) => { - const results = await ctx.vector.search('knowledge-base', { - query: 'What is an AI agent?', - limit: 5, - similarity: 0.7, // minimum similarity threshold - metadata: { category: 'platform' }, // filter by metadata - }); - - // Each result includes: id, key, similarity, metadata, expiresAt - return { - results: results.map(r => ({ - key: r.key, - similarity: r.similarity, - metadata: r.metadata, - expiresAt: r.expiresAt, // ISO timestamp when vector expires - })), - }; - }, +await vector.upsert('custom-embeddings', { + key: 'embedding-001', + embeddings: [0.12, 0.34, 0.56, 0.78], + metadata: { source: 'external-model' }, + ttl: null, }); ``` -## Direct Retrieval - -### get() - Single Item +The operation is idempotent by key: upserting the same key updates the stored vector. -Retrieve a specific vector by key without similarity search: +## TTL -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('DocumentRetriever', { - handler: async (ctx) => { - const result = await ctx.vector.get('knowledge-base', 'doc-1'); +Vectors expire after 30 days unless you pass `ttl`. - if (result.exists) { - return { - id: result.data.id, - key: result.data.key, - metadata: result.data.metadata, - }; - } +| Value | Behavior | +|-------|----------| +| omitted | expire after 30 days | +| `null` or `0` | never expire | +| `>= 60` | expire after that many seconds | - return { error: 'Document not found' }; - }, -}); -``` + +Managed vector storage clamps TTL values below 60 seconds to 60 seconds and values above 90 days to 90 days. + -### getMany() - Batch Retrieval +## Search -Retrieve multiple vectors efficiently: +Search accepts a natural-language query, optional result limit, optional similarity threshold, and optional metadata filter. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('BatchRetriever', { - handler: async (ctx) => { - const keys = ['doc-1', 'doc-2', 'doc-3']; - const resultMap = await ctx.vector.getMany('knowledge-base', ...keys); - - // resultMap is Map - return { - found: resultMap.size, - documents: Array.from(resultMap.values()).map(doc => ({ - key: doc.key, - content: doc.document, - })), - }; - }, +const furniture = await vector.search('products', { + query: 'comfortable seating for a desk', + limit: 5, + similarity: 0.65, + metadata: { category: 'furniture', source: 'catalog' }, }); -``` - -### exists() - Check Namespace - -Check if a namespace contains any vectors: -```typescript -const hasData = await ctx.vector.exists('knowledge-base'); -if (!hasData) { - return { error: 'Knowledge base not initialized' }; -} +const rankedKeys = furniture.map((result) => result.key); ``` - -`exists()` returns `false` for namespaces that exist but contain no vectors. Use this to verify your knowledge base has been populated with data before searching. - - -## Deleting Vectors - -```typescript -// Delete single vector -await ctx.vector.delete('knowledge-base', 'doc-1'); - -// Delete multiple vectors, returns count deleted -const count = await ctx.vector.delete('knowledge-base', 'doc-1', 'doc-2', 'doc-3'); -``` +Search results include `id`, `key`, `similarity`, optional `metadata`, and optional `expiresAt`. -## Type Safety +## Retrieve and Delete -Use generics for type-safe metadata: +Use `get()` when you already know the key, `getMany()` for batches, and `delete()` when you want to remove one or more vectors. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { s } from '@agentuity/schema'; +const document = await vector.get('products', 'chair-001'); -interface DocumentMetadata { - readonly title: string; - readonly category: 'guide' | 'api' | 'tutorial'; - readonly author: string; +if (document.exists) { + const metadata = document.data.metadata; } -const agent = createAgent('TypedVectorAgent', { - schema: { - input: s.object({ question: s.string() }), - output: s.object({ titles: s.array(s.string()) }), - }, - handler: async (ctx, input) => { - // Type-safe upsert - await ctx.vector.upsert('docs', { - key: 'guide-1', - document: 'Getting started with agents', - metadata: { - title: 'Getting Started', - category: 'guide', - author: 'team', - }, - }); - - // Type-safe search - const results = await ctx.vector.search('docs', { - query: input.question, - }); - - // TypeScript knows metadata shape - const titles = results - .map((r) => r.metadata?.title) - .filter((title): title is string => title !== undefined); - - return { titles }; - }, -}); +const batch = await vector.getMany('products', 'chair-001', 'desk-001'); +const deleted = await vector.delete('products', 'chair-001', 'desk-001'); ``` -## Simple RAG Example +## Namespaces and Stats -Search for relevant context and generate an informed response: +Vector namespaces are created on first upsert. Use stats and namespace methods to inspect or clean up stored data. ```typescript -import { createAgent } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { s } from '@agentuity/schema'; - -interface KnowledgeMetadata { - readonly content: string; -} +const hasProducts = await vector.exists('products'); +const stats = await vector.getStats('products'); +const namespaces = await vector.getNamespaces(); -const ragAgent = createAgent('RAG', { - schema: { - input: s.object({ question: s.string() }), - output: s.object({ - answer: s.string(), - sources: s.array(s.string()), - }), - }, - handler: async (ctx, input) => { - // Search for relevant documents - const results = await ctx.vector.search('knowledge-base', { - query: input.question, - limit: 3, - similarity: 0.7, - }); - - if (results.length === 0) { - return { - answer: "I couldn't find relevant information.", - sources: [], - }; - } - - // Build context from results - const context = results - .map((r) => r.metadata?.content ?? '') - .join('\n\n'); - - // Generate response using context - const { text } = await generateText({ - model: openai('gpt-5.4-nano'), - prompt: `Answer based on this context:\n\n${context}\n\nQuestion: ${input.question}`, - }); - - return { - answer: text, - sources: results.map(r => r.key), - }; - }, -}); +await vector.deleteNamespace('old-products'); ``` -## Using in Routes - -Routes have the same vector access via `c.var.vector`: - -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; + +`deleteNamespace()` permanently removes the namespace and all vectors inside it. + -const router = new Hono(); + +Local vector storage is useful for development and API-shape checks. Validate embedding quality, ranking, and TTL behavior against the managed vector service before relying on search relevance. + -router.post('/search', async (c) => { - const { query } = await c.req.json<{ query: string }>(); - const results = await c.var.vector.search('knowledge-base', { - query, - limit: 5, - }); +## Hono - return c.json({ results }); -}); +In Hono apps, `@agentuity/hono` initializes `VectorClient` once and exposes it on `c.var.vector`. -export default router; +```bash +bun add @agentuity/hono@alpha hono ``` -### Route-Based RAG - -Build a RAG endpoint that searches vectors and generates responses: - ```typescript import { Hono } from 'hono'; -import { validator } from '@agentuity/runtime'; -import type { Env } from '@agentuity/runtime'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; -const router = new Hono(); +type Variables = Pick; -interface KnowledgeMetadata { - readonly content: string; -} +const app = new Hono<{ Variables: Variables }>(); -const questionSchema = z.object({ - question: z.string().describe('User question'), -}); - -router.post('/ask', - validator({ input: questionSchema }), - async (c) => { - const { question } = c.req.valid('json'); - - // Search for relevant context - const results = await c.var.vector.search('knowledge-base', { - query: question, - limit: 3, - similarity: 0.7, - }); - - if (results.length === 0) { - return c.json({ - answer: "I couldn't find relevant information.", - sources: [], - }); - } - - // Build context from search results - const context = results - .map((r) => r.metadata?.content ?? '') - .join('\n\n'); - - // Generate answer - const { text } = await generateText({ - model: openai('gpt-5.4-nano'), - prompt: `Answer based on this context:\n\n${context}\n\nQuestion: ${question}`, - }); - - c.var.logger.info('RAG query completed', { - question, - sourcesFound: results.length, - }); - - return c.json({ - answer: text, - sources: results.map(r => r.key), - }); - } -); +app.use('*', agentuity()); -export default router; -``` +app.get('/search', async (c) => { + const query = c.req.query('q'); - -Need to access vector storage from a Next.js backend or other external service? Create authenticated routes that expose storage operations, then call them via HTTP. See [SDK Utilities for External Apps](/cookbook/patterns/server-utilities). - - -## Standalone Usage + if (!query) { + return c.json({ error: 'Missing q query parameter' }, 400); + } -Use vector storage in background jobs or external scripts with `createAgentContext()`: + const results = await c.var.vector.search('docs', { + query, + limit: 5, + }); -```typescript -import { createApp, createAgentContext } from '@agentuity/runtime'; + return c.json({ results }); +}); -const app = await createApp(); export default app; - -// CLI tool to index documents -async function indexDocuments(files: readonly string[]): Promise { - const ctx = createAgentContext(); - - await ctx.invoke(async () => { - for (const file of files) { - const content = await Bun.file(file).text(); - await ctx.vector.upsert('knowledge-base', { - key: file, - document: content, - metadata: { source: 'cli-import' }, - }); - } - ctx.logger.info('Indexed documents', { count: files.length }); - }); -} ``` -See [Running Agents Without HTTP](/agents/standalone-execution) for more patterns including Discord bots, CLI tools, and queue workers. - -## Troubleshooting - -- **Empty results**: Lower your similarity threshold (try 0.5 instead of 0.8) or check metadata filters -- **Duplicates**: Ensure consistent key naming; upsert with same key updates rather than duplicates -- **Poor relevance**: Results with similarity < 0.7 may be weak matches; filter post-search if needed - -## Best Practices - -- **Include context in documents**: Store enough text so documents are meaningful when retrieved -- **Use descriptive metadata**: Include title, category, tags for filtering and identification -- **Batch upserts**: Insert documents in batches of 100-500 for better performance -- **Combine get + search**: Use `search` for finding, `getMany` for fetching full details - ## Next Steps -- [Key-Value Storage](/services/storage/key-value): Fast caching and configuration -- [Object Storage (S3)](/services/storage/object): File and media storage -- [Database](/services/database): Relational data with queries and transactions -- [Durable Streams](/services/storage/durable-streams): Streaming large data exports -- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): Quality checks for RAG systems +- [Key-Value Storage](/services/storage/key-value): store exact-key cache and state +- [Database](/services/database): store relational data with SQL and transactions +- [LLM as a Judge](/cookbook/patterns/llm-as-a-judge): evaluate retrieval quality diff --git a/docs/src/web/content/services/tasks.mdx b/docs/src/web/content/services/tasks.mdx index 70c9d44ea..69e7c0fc0 100644 --- a/docs/src/web/content/services/tasks.mdx +++ b/docs/src/web/content/services/tasks.mdx @@ -1,498 +1,276 @@ --- title: Tasks -description: Track work items, issues, and agent activity with built-in lifecycle management +description: Track work items, issues, and agent activity with lifecycle management --- -Use `ctx.task` to create and manage structured work items: bugs, features, epics, and general tasks, across agents and human collaborators, with built-in status tracking, comments, tags, and file attachments. +Use tasks when work needs a durable lifecycle: status, priority, assignee, comments, tags, attachments, and history. Start with `TaskClient`; Hono apps can use `c.var.task` after installing the Agentuity middleware. - -Use the [`@agentuity/task`](/reference/standalone-packages#tasks) standalone package to access this service from any Node.js or Bun app without the runtime. - - -## When to Use Tasks - -| Service | Best For | -|---------|----------| -| **Tasks** | Structured work items with lifecycle, assignments, comments, tags | -| [Queues](/services/queues) | Async message passing for background processing | -| [Key-Value](/services/storage/key-value) | Simple state, caching, counters | - -Use tasks when you need to: - -- Track bugs or issues an agent discovers during execution -- Assign work items to humans or agents with explicit status transitions -- Attach files, comments, and audit trails to work items -- Build hierarchical project structures (epics, features, subtasks) -- Query task history by status, priority, type, or assignee - -## Managing Tasks - -| Method | Best For | -|--------|----------| -| **SDK** (`ctx.task`) | Agents and routes creating or updating tasks programmatically | -| **CLI** (`agentuity cloud task`) | Human and agent CLI workflows, scripting | -| **[Web App](https://app.agentuity.com/services/task)** | Visual task board, manual triage | - -## Creating Tasks - -`ctx.task.create()` requires `title`, `type`, and `created_id`. Include `creator` (a `UserEntityRef`) as well to preserve display name and attribution type. All other fields are optional. +```bash +bun add @agentuity/task@alpha +``` ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('BugReporter', { - handler: async (ctx, input) => { - const task = await ctx.task.create({ // [!code highlight] - title: 'Null pointer in payment flow', - type: 'bug', // [!code highlight] - priority: 'high', - description: `Observed at ${new Date().toISOString()}: ${input.errorMessage}`, - created_id: ctx.current.id, - creator: { - id: ctx.current.id, - name: ctx.current.name, - type: 'agent', // distinguish from human users // [!code highlight] - }, - metadata: { - source: 'payment-service', - traceId: input.traceId, - }, - }); - - ctx.logger.info('Bug filed', { taskId: task.id, status: task.status }); - return { taskId: task.id }; - }, -}); -``` +import { TaskClient } from '@agentuity/task'; +import type { UserEntityRef } from '@agentuity/task'; + +const tasks = new TaskClient(); + +const reporter = { + id: 'agent_triage', + name: 'Triage Agent', + type: 'agent', +} satisfies UserEntityRef; + +export async function fileBug(errorMessage: string, traceId: string): Promise { + const task = await tasks.create({ + title: 'Payment flow error', + type: 'bug', + priority: 'high', + description: errorMessage, + created_id: reporter.id, + creator: reporter, + metadata: { traceId, source: 'checkout' }, + }); -**`CreateTaskParams` fields:** + return task.id; +} +``` -| Field | Required | Description | -|-------|----------|-------------| -| `title` | Yes | Task title, max 1,024 characters | -| `type` | Yes | Task classification (`'epic'`, `'feature'`, `'enhancement'`, `'bug'`, `'task'`) | -| `created_id` | Yes | ID of the creating user or agent | -| `creator` | No | `UserEntityRef` with `id`, `name`, and optional `type`; adds display name alongside `created_id` | -| `description` | No | Detailed description, max 65,536 characters | -| `priority` | No | `'high'`, `'medium'`, `'low'`, or `'none'` (default: `'none'`) | -| `status` | No | Initial status (default: `'open'`) | -| `assignee` | No | `UserEntityRef` to assign the task to | -| `parent_id` | No | ID of a parent task for hierarchical organization | -| `tag_ids` | No | Array of tag IDs to attach at creation | -| `metadata` | No | Arbitrary key-value metadata for custom fields | -| `project` | No | `EntityRef` linking the task to a project | - -## Task Lifecycle - -Tasks move through a defined set of statuses. The server automatically records timestamps when each transition occurs. - -| Status | Description | Date field set | -|--------|-------------|----------------| -| `open` | Created, not yet started | `open_date` | -| `in_progress` | Actively being worked on | `in_progress_date` | -| `done` | Work completed | `closed_date` | -| `cancelled` | Abandoned | `cancelled_date` | - - -The SDK accepts shorthand values that normalize to canonical statuses before sending the request: - -- `completed` and `closed` both normalize to `done` -- `started` normalizes to `in_progress` - -The four canonical statuses are `open`, `in_progress`, `done`, and `cancelled`. Use these in queries and comparisons. - +`TaskClient` reads `AGENTUITY_SDK_KEY` by default. In Agentuity projects, keep the key in `.env` for local `agentuity dev` and configure the same environment variable for deployed apps. -```typescript -import { createAgent } from '@agentuity/runtime'; +## When to Use Tasks -const agent = createAgent('TaskWorker', { - handler: async (ctx, input) => { - const task = await ctx.task.get(input.taskId); - if (!task) return { error: 'Task not found' }; +| Need | Use | +|------|-----| +| work items with status, priority, comments, and audit history | [Tasks](/services/tasks) | +| fire-and-forget async handoff | [Queues](/services/queues) | +| recurring timed delivery | [Schedules](/services/schedules) | +| exact key lookup or counters | [Key-Value Storage](/services/storage/key-value) | - // Claim the task before starting work - await ctx.task.update(task.id, { // [!code highlight] - status: 'in_progress', - assignee: { id: ctx.current.id, name: ctx.current.name, type: 'agent' }, - }); +## Client Setup - // Do the work +Construct the client once at module scope and reuse it from handlers, routes, or scripts. - // Mark complete when done - await ctx.task.update(task.id, { - status: 'done', // [!code highlight] - }); +```typescript +import { TaskClient } from '@agentuity/task'; - return { completed: task.id }; - }, +const tasks = new TaskClient({ + orgId: process.env.AGENTUITY_ORG_ID, }); ``` - -`ctx.task.close(id)` sends a dedicated close request that marks the task as `'done'` and records `closed_date` server-side. For `'cancelled'`, use `ctx.task.update(id, { status: 'cancelled' })`. - - -## Task Types and Priority - -**Types** classify what a task represents: +| Option | Description | +|--------|-------------| +| `apiKey` | Optional API key. Defaults to `AGENTUITY_SDK_KEY`, then `AGENTUITY_CLI_KEY`. | +| `orgId` | Optional organization ID for org-scoped requests. | +| `url` | Optional Task API URL. Defaults to `AGENTUITY_TASK_URL`, then the regional Agentuity service URL. | +| `logger` | Optional logger instance. | -| Type | When to use | -|------|-------------| -| `epic` | Large initiatives spanning multiple features or tasks | -| `feature` | New capabilities to build | -| `enhancement` | Improvements to existing functionality | -| `bug` | Defects to fix | -| `task` | General work items | +## Create Tasks -**Priority** signals urgency: - -| Priority | Description | -|----------|-------------| -| `high` | Requires immediate attention | -| `medium` | Standard priority | -| `low` | Background or nice-to-have work | -| `none` | No priority assigned (default) | - -## Human vs Agent Attribution - -`UserEntityRef` carries a `type` field that distinguishes human users from AI agents. This lets dashboards and queries filter by who (or what) created or is working on tasks. - - -`creator` (a `UserEntityRef` with `id`, `name`, and `type`) is the preferred field. `created_id` is a legacy string-only field that remains supported. Use `creator` in new code so display names and type information are preserved. - +`create()` requires `title`, `type`, and `created_id`. Pass `creator` when you want dashboards and history to preserve a display name and whether the actor is human or an agent. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TaskRouter', { - handler: async (ctx, input) => { - // Created by this agent, assigned to a human for review - const task = await ctx.task.create({ - title: 'Review anomaly in transaction log', - type: 'task', - created_id: ctx.current.id, - creator: { - id: ctx.current.id, - name: ctx.current.name, - type: 'agent', // [!code highlight] - }, - assignee: { - id: input.reviewerId, - name: input.reviewerName, - type: 'human', // [!code highlight] - }, - priority: 'medium', - }); - - ctx.logger.info('Task routed to human', { - taskId: task.id, - assignee: task.assignee?.name, - }); - - return { taskId: task.id }; +const reviewer = { + id: 'user_123', + name: 'Maya Chen', + type: 'human', +} satisfies UserEntityRef; + +const task = await tasks.create({ + title: 'Review suspicious refund', + type: 'task', + priority: 'medium', + created_id: reporter.id, + creator: reporter, + assignee: reviewer, + metadata: { + refundId: 'rf_123', + riskScore: 0.91, }, }); ``` -## Comments - -Add threaded comments to any task. `userId` identifies the commenter; `author` is an optional `{ id, name }` reference that adds a display name. +| Field | Required | Description | +|-------|----------|-------------| +| `title` | Yes | Task title. | +| `type` | Yes | `epic`, `feature`, `enhancement`, `bug`, or `task`. | +| `created_id` | Yes | ID of the creating user, service, or agent. | +| `creator` | No | `{ id, name, type }` reference for readable attribution. | +| `description` | No | Detailed task body. | +| `priority` | No | `high`, `medium`, `low`, or `none`. Defaults to `none`. | +| `status` | No | Initial status. Defaults to `open`. | +| `assignee` | No | `{ id, name, type }` reference for the assigned actor. | +| `parent_id` | No | Parent task ID for epics, features, and subtasks. | +| `tag_ids` | No | Existing tag IDs to attach at creation. | +| `metadata` | No | JSON metadata for integration IDs, traces, or custom fields. | +| `project` | No | `{ id, name }` project reference. | + +## Update Lifecycle + +Tasks use four canonical statuses. The client also accepts `started`, `completed`, and `closed`, then normalizes them to canonical values. + +| Status | Meaning | +|--------|---------| +| `open` | Created, not started. | +| `in_progress` | Actively being worked on. | +| `done` | Work completed. | +| `cancelled` | Work abandoned. | ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TaskCommentWriter', { - handler: async (ctx, input) => { - // Add a comment - const comment = await ctx.task.createComment( // [!code highlight] - input.taskId, - `Analysis complete. Root cause: ${input.finding}`, - ctx.current.id, - { id: ctx.current.id, name: ctx.current.name }, - ); - - // Retrieve all comments with pagination - const { comments, total } = await ctx.task.listComments(input.taskId, { - limit: 20, - offset: 0, - }); - - ctx.logger.info('Comments loaded', { total }); - return { commentId: comment.id, total }; - }, +const claimed = await tasks.update(task.id, { + status: 'in_progress', + assignee: reporter, }); + +const closed = await tasks.close(claimed.id); ``` -## Tags +Use `close(id)` for completed work. Use `update(id, { status: 'cancelled' })` when the task should be abandoned. -Tags are org-wide labels you create once and apply to multiple tasks. Create a tag, then associate it. +## Comments and Tags -```typescript -import { createAgent } from '@agentuity/runtime'; +Comments need the task ID, comment body, and author ID. Pass `author` for display name and type. -const agent = createAgent('TagManager', { - handler: async (ctx, input) => { - // Create a reusable tag (once per org) - const tag = await ctx.task.createTag('payment-system', '#ff6600'); // [!code highlight] +```typescript +const comment = await tasks.createComment( + task.id, + 'Refund matched the manual review policy.', + reporter.id, + reporter +); - // Apply it to a task - await ctx.task.addTagToTask(input.taskId, tag.id); // [!code highlight] +const { comments } = await tasks.listComments(task.id, { limit: 20, offset: 0 }); - // List all tags on a task - const tags = await ctx.task.listTagsForTask(input.taskId); +const tag = await tasks.createTag('payments', '#00FFFF'); +await tasks.addTagToTask(task.id, tag.id); - ctx.logger.info('Tags applied', { tagCount: tags.length }); - return { tags }; - }, -}); +const tags = await tasks.listTagsForTask(task.id); ``` ## Attachments -Attachments use a two-phase upload: first call `uploadAttachment()` to get a presigned S3 URL, PUT your file bytes directly to that URL, then call `confirmAttachment()` to mark the upload complete. - - -Local task storage supports tasks, comments, tags, and changelog data. Attachment methods throw `AttachmentNotSupportedError` in local storage. - +Attachments use a two-step upload. First ask the task service for a presigned upload URL, then PUT the bytes to that URL and confirm the attachment. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('AttachmentUploader', { - handler: async (ctx, input) => { - // Phase 1: request a presigned upload URL - const { attachment, presigned_url } = await ctx.task.uploadAttachment( // [!code highlight] - input.taskId, - { - filename: 'error-trace.pdf', - content_type: 'application/pdf', - size: input.fileBytes.length, - }, - ); - - // Phase 2: upload directly to S3 (bypasses Agentuity servers) - const uploadResponse = await fetch(presigned_url, { // [!code highlight] - method: 'PUT', - body: input.fileBytes, - headers: { 'Content-Type': 'application/pdf' }, - }); - - if (!uploadResponse.ok) { - ctx.logger.error('Upload failed', { status: uploadResponse.status }); - return { error: 'Upload failed' }; - } - - // Phase 3: confirm the upload so the attachment becomes visible - const confirmed = await ctx.task.confirmAttachment(attachment.id); // [!code highlight] - ctx.logger.info('Attachment confirmed', { attachmentId: confirmed.id }); - - // Download: get a presigned URL to read the file back - const { presigned_url: downloadUrl } = await ctx.task.downloadAttachment(attachment.id); - - return { attachmentId: confirmed.id, downloadUrl }; - }, +const { attachment, presigned_url } = await tasks.uploadAttachment(task.id, { + filename: 'refund-log.json', + content_type: 'application/json', + size: 1024, }); + +const upload = await fetch(presigned_url, { + method: 'PUT', + body: JSON.stringify({ refundId: 'rf_123' }), + headers: { 'Content-Type': 'application/json' }, +}); + +if (!upload.ok) { + throw new Error(`Upload failed with status ${upload.status}`); +} + +const confirmed = await tasks.confirmAttachment(attachment.id); +const { presigned_url: downloadUrl } = await tasks.downloadAttachment(confirmed.id); ``` - -Both upload and download URLs expire after a short window (`expiry_seconds` on the response). Do not cache or share these URLs; request a new one when needed. - +Presigned upload and download URLs expire. Request a fresh URL when a user needs to upload or read the file again. -## Filtering and Pagination +## List and Inspect -`ctx.task.list()` accepts filters to narrow results. All parameters are optional. +Use `list()` for filtered views and `changelog()` when you need the field-level history for one task. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TaskDashboard', { - handler: async (ctx, input) => { - // High-priority open bugs, newest first - const { tasks, total } = await ctx.task.list({ // [!code highlight] - type: 'bug', - status: 'open', - priority: 'high', - sort: '-created_at', // descending by creation date // [!code highlight] - limit: 25, - offset: 0, - }); - - // Subtasks of a specific epic - const { tasks: subtasks } = await ctx.task.list({ - parent_id: input.epicId, // [!code highlight] - limit: 50, - }); - - ctx.logger.info('Tasks loaded', { total, subtaskCount: subtasks.length }); - return { tasks, subtasks }; - }, +const { tasks: openBugs, total } = await tasks.list({ + type: 'bug', + status: 'open', + priority: 'high', + include: ['metadata', 'tags'], + sort: '-created_at', + limit: 25, + offset: 0, }); + +const { changelog } = await tasks.changelog(task.id, { limit: 50, offset: 0 }); ``` -**`ListTasksParams` options:** - -| Field | Description | -|-------|-------------| -| `status` | Filter by lifecycle status | -| `type` | Filter by task type | -| `priority` | Filter by priority level | -| `assigned_id` | Filter by assigned user ID | -| `created_id` | Filter by creator user ID | -| `parent_id` | Filter by parent task (returns subtasks) | -| `project_id` | Filter by project ID | -| `tag_id` | Filter by a specific tag | -| `include` | Include additional fields such as `description`, `metadata`, `tags`, `subtask_count`, `created_id`, or `deleted` | -| `sort` | Sort field: `'created_at'`, `'updated_at'`, `'priority'`. Prefix with `-` for descending | -| `order` | Sort direction: `'asc'` or `'desc'` | -| `limit` | Maximum results to return | -| `offset` | Results to skip for pagination | -| `deleted` | Include soft-deleted tasks (default: `false`) | - -## Changelog - -Every field change on a task is automatically recorded. Use `ctx.task.changelog()` to retrieve the full audit trail. +| Filter | Description | +|--------|-------------| +| `status` | Filter by lifecycle status. | +| `type` | Filter by task type. | +| `priority` | Filter by priority. | +| `assigned_id` | Filter by assigned actor ID. | +| `created_id` | Filter by creator ID. | +| `parent_id` | Return subtasks for a parent task. | +| `project_id` | Filter by project ID. | +| `tag_id` | Filter by tag ID. | +| `include` | Include fields such as `description`, `metadata`, `tags`, `subtask_count`, `created_id`, or `deleted`. | +| `sort` | Sort by `created_at`, `updated_at`, or `priority`. Prefix with `-` for descending. | +| `order` | Optional sort direction, `asc` or `desc`. | +| `limit` / `offset` | Pagination controls. | +| `deleted` | Include soft-deleted tasks. | + +## Activity + +Use `getActivity()` for daily task activity counts. ```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TaskAuditor', { - handler: async (ctx, input) => { - const { changelog, total } = await ctx.task.changelog(input.taskId, { // [!code highlight] - limit: 50, - offset: 0, - }); - - for (const entry of changelog) { - ctx.logger.info('Field changed', { - field: entry.field, - from: entry.old_value, - to: entry.new_value, - at: entry.created_at, - }); - } - - return { changes: total }; - }, -}); +const activity = await tasks.getActivity({ days: 30 }); ``` -Each `TaskChangelogEntry` records the `field` name, `old_value`, `new_value`, and `created_at` timestamp. Status transitions, priority changes, and reassignments all appear automatically, with no extra instrumentation needed. +The `days` option is optional and defaults to 7. -## Batch Operations +## Hono -`batchDelete` soft-deletes tasks matching a set of filters. Soft-deleted tasks are hidden from normal queries unless you pass `deleted: true` to `list()`. +In Hono apps, `@agentuity/hono` initializes `TaskClient` once and exposes it on `c.var.task`. -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('TaskCleaner', { - handler: async (ctx, input) => { - // Remove cancelled tasks older than 30 days (up to 200 at a time) - const { deleted, count } = await ctx.task.batchDelete({ // [!code highlight] - status: 'cancelled', - older_than: '30d', // Go-style duration: '30m', '24h', '7d', '2w' // [!code highlight] - limit: 200, - }); - - ctx.logger.info('Batch delete complete', { count }); - return { deletedCount: count }; - }, -}); +```bash +bun add @agentuity/hono@alpha hono ``` - -`batchDelete` requires at least one filter (`status`, `type`, `priority`, `parent_id`, `created_id`, or `older_than`). Calling it without filters throws an error. The maximum per call is 200 tasks. - +```typescript +import { Hono } from 'hono'; +import { agentuity } from '@agentuity/hono'; +import type { Services } from '@agentuity/hono'; -## Hierarchical Tasks +type Variables = Pick; -Set `parent_id` to build epic-to-feature-to-task hierarchies. Query subtasks with `list({ parent_id: epicId })`. Pass `include: ['subtask_count']` when you need direct child counts without fetching every subtask. +const app = new Hono<{ Variables: Variables }>(); -```typescript -import { createAgent } from '@agentuity/runtime'; - -const agent = createAgent('ProjectPlanner', { - handler: async (ctx, input) => { - // Create the top-level epic - const epic = await ctx.task.create({ - title: 'Auth system overhaul', - type: 'epic', - priority: 'high', - created_id: ctx.current.id, - }); - - // Create features under the epic - const feature = await ctx.task.create({ - title: 'Implement OAuth2 login', - type: 'feature', - parent_id: epic.id, // [!code highlight] - created_id: ctx.current.id, - }); - - // Create a task under the feature - await ctx.task.create({ - title: 'Write OAuth2 callback handler', - type: 'task', - parent_id: feature.id, // [!code highlight] - created_id: ctx.current.id, - }); - - ctx.logger.info('Project hierarchy created', { epicId: epic.id }); - return { epicId: epic.id }; - }, -}); -``` +app.use('*', agentuity()); -## Using in Routes +app.post('/tasks', async (c) => { + const title = c.req.query('title'); + const userId = c.req.query('userId'); -Routes access the same task service via `c.var.task`: + if (!title || !userId) { + return c.json({ error: 'title and userId are required' }, 400); + } -```typescript -import { Hono } from 'hono'; -import type { Env } from '@agentuity/runtime'; - -const router = new Hono(); - -router.post('/tasks', async (c) => { - const body = await c.req.json(); - - const task = await c.var.task.create({ // [!code highlight] - title: body.title, - type: body.type ?? 'task', - created_id: body.userId, - creator: { - id: body.userId, - name: body.userName, - type: 'human', - }, + const task = await c.var.task.create({ + title, + type: 'task', + created_id: userId, + creator: { id: userId, name: userId, type: 'human' }, }); return c.json({ taskId: task.id }, 201); }); -router.get('/tasks/:id', async (c) => { - const task = await c.var.task.get(c.req.param('id')); // [!code highlight] +app.get('/tasks/:id', async (c) => { + const task = await c.var.task.get(c.req.param('id')); if (!task) { - return c.json({ error: 'Not found' }, 404); + return c.json({ error: 'Task not found' }, 404); } - return c.json(task); + return c.json({ task }); }); -export default router; +export default app; ``` -## Best Practices - -- **Use `creator` instead of `created_id`**: The `creator` field stores display name and type alongside the ID, which makes task history readable without joining user records. -- **Set `type: 'agent'` for programmatic tasks**: This lets dashboards distinguish automated work items from human-filed tickets. -- **Prefer `update()` over ad-hoc field sets**: Partial updates only modify provided fields, so you won't accidentally overwrite data you didn't intend to change. -- **Use `metadata` for integration fields**: Store external IDs, trace IDs, or service names in `metadata` rather than encoding them in the title or description. -- **Page results with `limit` and `offset`**: Set an explicit `limit` when listing tasks in agents so pagination stays predictable. - ## Next Steps -- [Queues](/services/queues): Async message passing for background processing between agents -- [Key-Value Storage](/services/storage/key-value): Simple per-session state and counters -- [Agents](/agents/creating-agents): Creating agents that use `ctx.task` and other services +- [Queues](/services/queues): hand off task-related work to background processors +- [Schedules](/services/schedules): create recurring checks that file or update tasks +- [Using Standalone Packages](/reference/standalone-packages): configure service clients outside Agentuity projects diff --git a/docs/src/web/content/services/webhooks.mdx b/docs/src/web/content/services/webhooks.mdx index 44d7dc3ff..3555aa7ad 100644 --- a/docs/src/web/content/services/webhooks.mdx +++ b/docs/src/web/content/services/webhooks.mdx @@ -3,246 +3,234 @@ title: Webhooks description: Create webhook endpoints to receive HTTP callbacks with delivery tracking and retry --- -Use the webhook service when you need a managed ingest endpoint for callbacks from services like Stripe, GitHub, or Slack, with a full audit trail of received payloads and delivery attempts. +Use webhooks when an external service needs a stable Agentuity ingest URL and you want receipts, destination delivery history, and retry. Start with `WebhookClient`; this service is not injected by `@agentuity/hono`, so use the client directly in any framework. - -Use the [`@agentuity/webhook`](/reference/standalone-packages#webhooks) standalone package to access this service from any Node.js or Bun app without the runtime. - - -## When to Use Webhooks - -| Approach | Best For | -|----------|----------| -| **Webhook Service** | Managed ingest endpoints with receipts, delivery tracking, and retry | -| [Route Handlers](/cookbook/patterns/webhook-handler) | Processing webhooks directly in your routes with signature verification | -| [Queues](/services/queues) | Internal async message passing between your own services | - - -To handle incoming webhooks directly in an agent or route, including signature verification for Stripe or GitHub, see the [Webhook Handler Pattern](/cookbook/patterns/webhook-handler). - - -## Managing Webhooks - -| Method | Access | Details | -|--------|--------|---------| -| SDK client | `WebhookClient` from `@agentuity/webhook` | Client-style management for common operations | -| Server utilities | `@agentuity/server` | Function-style management, including destination updates | -| CLI | `agentuity cloud webhook` | Create and inspect webhooks from the terminal | -| Web App | [Web App](https://app.agentuity.com/services/webhook) | Manage webhooks in the web app | - - -Webhooks are organization-level resources, not tied to a specific project. A single webhook can receive payloads from any external service and forward them to multiple destinations. - - -## Setup - -All webhook functions take an `APIClient` as their first parameter. +```bash +bun add @agentuity/webhook@alpha +``` ```typescript -import { APIClient, createLogger, getServiceUrls } from '@agentuity/server'; - -const logger = createLogger(); -const region = process.env.AGENTUITY_REGION ?? 'usc'; -const client = new APIClient(getServiceUrls(region).catalyst, logger); -``` +import { WebhookClient } from '@agentuity/webhook'; + +const webhooks = new WebhookClient(); + +export async function createStripeWebhook(destinationUrl: string): Promise { + const { webhook } = await webhooks.create({ + name: 'stripe-events', + description: 'Stripe payment events', + }); + + await webhooks.createDestination(webhook.id, { + type: 'url', + config: { + url: destinationUrl, + headers: { + 'X-Webhook-Source': 'stripe-events', + }, + }, + }); -The examples below use `@agentuity/server` function exports because they expose the full management surface, including destination updates. For client-style usage, instantiate `WebhookClient` from `@agentuity/webhook`; it wraps the same service for common operations. + if (!webhook.url) { + throw new Error('Webhook create response did not include an ingest URL'); + } -## Creating Webhooks + return webhook.url; +} +``` -Create a webhook to get a unique ingest URL. Point external services at this URL to start receiving their callbacks. +`WebhookClient` reads `AGENTUITY_SDK_KEY` by default. In Agentuity projects, keep the key in `.env` for local `agentuity dev` and configure the same environment variable for deployed apps. -```typescript -import { APIClient, createLogger, createWebhook, getServiceUrls } from '@agentuity/server'; +## When to Use Webhooks -const logger = createLogger(); -const region = process.env.AGENTUITY_REGION ?? 'usc'; -const client = new APIClient(getServiceUrls(region).catalyst, logger); +| Need | Use | +|------|-----| +| managed ingest URL with receipts and delivery tracking | [Webhooks](/services/webhooks) | +| process callbacks directly in your app route | [Webhook Handler Pattern](/cookbook/patterns/webhook-handler) | +| hand received events to background processing | [Queues](/services/queues) | +| send inbound email to an HTTP endpoint | [Email](/services/email) | -const webhook = await createWebhook(client, { // [!code highlight] - name: 'stripe-events', - description: 'Payment webhooks from Stripe', // optional -}); // [!code highlight] +## Client Setup -// webhook.url contains the ingest URL (only present on create) -// Format: https:///webhook/- -``` +Construct the client once at module scope and reuse it from handlers, routes, or scripts. -Via the CLI: +```typescript +import { WebhookClient } from '@agentuity/webhook'; -```bash -agentuity cloud webhook create --name stripe-events --description "Payment webhooks from Stripe" +const webhooks = new WebhookClient({ + orgId: process.env.AGENTUITY_ORG_ID, +}); ``` -The `url` field on the created webhook is the ingest URL to configure in the external service. The URL format is `https:///webhook/-`. +| Option | Description | +|--------|-------------| +| `apiKey` | Optional API key. Defaults to `AGENTUITY_SDK_KEY`, then `AGENTUITY_CLI_KEY`. | +| `orgId` | Optional organization ID for org-scoped requests. | +| `url` | Optional Webhook API URL. Defaults to `AGENTUITY_WEBHOOK_URL`, then the regional Agentuity service URL. | +| `logger` | Optional logger instance. | - -The `url` field is only present in the create response. Store it at creation time: it cannot be retrieved afterward via `getWebhook`. + +`@agentuity/hono` injects KV, vector, stream, queue, email, schedule, task, and sandbox clients. It does not inject `c.var.webhook`, so create a `WebhookClient` directly in Hono, Next.js, scripts, or any other server runtime. -## Listing and Retrieving Webhooks +## Create Webhooks -```typescript -import { listWebhooks, getWebhook } from '@agentuity/server'; +`create()` returns the webhook record. The ingest URL is only included in the create response, so store it when you create the webhook. -// List all webhooks (supports limit/offset pagination) -const { webhooks } = await listWebhooks(client, { limit: 10 }); // [!code highlight] - -for (const wh of webhooks) { - // wh.id, wh.name, wh.description, wh.created_at -} +```typescript +const { webhook } = await webhooks.create({ + name: 'github-events', + description: 'GitHub push and pull request events', +}); -// Get a specific webhook by ID -const webhook = await getWebhook(client, 'wh_abc123'); // [!code highlight] +const ingestUrl = webhook.url; ``` -## Updating and Deleting Webhooks - -```typescript -import { updateWebhook, deleteWebhook } from '@agentuity/server'; - -// Update the webhook (name is required, description is optional) -const updated = await updateWebhook(client, 'wh_abc123', { // [!code highlight] - name: 'stripe-events-v2', - description: 'Updated description', -}); // [!code highlight] +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Human-readable webhook name. | +| `description` | No | Optional purpose or source note. | -// Delete a webhook (permanently removes all destinations, receipts, and deliveries) -await deleteWebhook(client, 'wh_abc123'); // [!code highlight] -``` + +The `url` field is only present on the create response. Later `get()` calls return the webhook metadata and destinations, but not the ingest URL. + -## Adding Destinations +## Add Destinations -Destinations are the endpoints that receive forwarded payloads when a request arrives at your ingest URL. Add one or more URL destinations per webhook. +Destinations receive forwarded payloads when the ingest URL receives a request. The current destination type is `url`. ```typescript -import { createWebhookDestination } from '@agentuity/server'; - -const destination = await createWebhookDestination(client, 'wh_abc123', { // [!code highlight] +const { destination } = await webhooks.createDestination(webhook.id, { type: 'url', config: { - url: 'https://example.com/handle-stripe', // required - headers: { // optional custom headers - 'X-Webhook-Source': 'agentuity', + url: 'https://api.example.com/webhooks/github', + headers: { + 'X-Agentuity-Webhook': 'github-events', }, }, -}); // [!code highlight] -``` - -Via the CLI: +}); -```bash -agentuity cloud webhook destinations create url wh_abc123 https://example.com/handle-stripe +const { destinations } = await webhooks.listDestinations(webhook.id); ``` -**Destination config for `url` type:** +| Field | Description | +|-------|-------------| +| `type` | Destination type. Use `url`. | +| `config.url` | Public HTTP or HTTPS endpoint to receive forwarded payloads. | +| `config.headers` | Optional headers sent with each forwarded request. | -| Field | Type | Description | -|-------|------|-------------| -| `url` | `string` | Target URL. Must use `http` or `https`. | -| `headers` | `Record` | Optional headers added to each forwarded request. | +A webhook can have multiple destinations. Each receipt creates one delivery attempt per destination. -A webhook can have multiple destinations. Each incoming request is forwarded to all configured destinations independently. +## List, Get, and Update -## Managing Destinations +Use `list()` for admin views, `get()` for one webhook plus its destinations, and `update()` to change the name or description. ```typescript -import { - listWebhookDestinations, - updateWebhookDestination, - deleteWebhookDestination, -} from '@agentuity/server'; +const { webhooks: page, total } = await webhooks.list({ limit: 20, offset: 0 }); -// List all destinations for a webhook -const destinations = await listWebhookDestinations(client, 'wh_abc123'); // [!code highlight] +const details = await webhooks.get(webhook.id); -for (const dest of destinations) { - // dest.id, dest.type, dest.config, dest.webhook_id -} - -// Update a destination's config -const updated = await updateWebhookDestination(client, 'wh_abc123', 'whds_def456', { // [!code highlight] - config: { url: 'https://example.com/handle-stripe/v2' }, -}); // [!code highlight] - -// Delete a destination -await deleteWebhookDestination(client, 'wh_abc123', 'whds_def456'); // [!code highlight] +const { webhook: updated } = await webhooks.update(webhook.id, { + name: 'github-events-primary', + description: 'Primary GitHub webhook', +}); ``` +`update()` requires a `name`; `description` is optional. + ## Receipts -Every HTTP request received at a webhook's ingest URL is recorded as a receipt, capturing the original headers and payload for auditing. +Every request received at the ingest URL is stored as a receipt with headers and payload. ```typescript -import { listWebhookReceipts, getWebhookReceipt } from '@agentuity/server'; - -// List recent receipts (supports limit/offset pagination) -const { receipts } = await listWebhookReceipts(client, 'wh_abc123', { limit: 50 }); // [!code highlight] - -for (const receipt of receipts) { - // receipt.id, receipt.date, receipt.headers, receipt.payload -} - -// Fetch the full payload for a specific receipt -const receipt = await getWebhookReceipt(client, 'wh_abc123', 'whrc_def456'); // [!code highlight] +const { receipts } = await webhooks.listReceipts(webhook.id, { + limit: 50, + offset: 0, +}); +const firstReceipt = receipts.at(0); + +const receipt = firstReceipt + ? await webhooks.getReceipt(webhook.id, firstReceipt.id) + : null; ``` -Receipts let you replay a request by passing `receipt.payload` directly to your processing logic, without needing the external service to resend. +Receipts are useful for audit trails and replaying a payload through your own handler code. -## Delivery Tracking +## Delivery Tracking and Retry -Each receipt triggers a delivery attempt for every configured destination. Track these attempts to see which succeeded and which need retrying. +Every receipt produces delivery records for the webhook destinations. Use `listDeliveries()` to inspect delivery status and `retryDelivery()` for failed attempts. ```typescript -import { listWebhookDeliveries } from '@agentuity/server'; - -const { deliveries } = await listWebhookDeliveries(client, 'wh_abc123'); // [!code highlight] +const { deliveries } = await webhooks.listDeliveries(webhook.id, { + limit: 50, + offset: 0, +}); for (const delivery of deliveries) { - // delivery.status: 'pending' | 'success' | 'failed' - // delivery.retries: number of retry attempts - // delivery.error: reason string (when failed) - // delivery.webhook_destination_id: which destination this delivery targeted - // delivery.webhook_receipt_id: which receipt triggered this delivery + if (delivery.status === 'failed') { + await webhooks.retryDelivery(webhook.id, delivery.id); + } } ``` -**Delivery statuses:** - | Status | Meaning | |--------|---------| -| `pending` | Queued, waiting to be sent to the destination | -| `success` | Forwarded successfully | -| `failed` | Delivery attempt failed; `error` field contains the reason | +| `pending` | Delivery is queued. | +| `success` | Destination received the forwarded payload. | +| `failed` | Delivery failed. The `error` field contains the reason when available. | -Each delivery record links back to its source receipt via `webhook_receipt_id`, so you can trace a failure to the original incoming payload. +Delivery records include the destination ID and receipt ID, so you can trace a failed delivery back to the incoming request. -## Retrying Failed Deliveries +## Analytics -Only deliveries with `status: 'failed'` can be retried. The retry enqueues a new delivery attempt to the same destination. +Use org-level analytics for received, delivered, and failed counts across webhooks. ```typescript -import { listWebhookDeliveries, retryWebhookDelivery } from '@agentuity/server'; +const analytics = await webhooks.getOrgAnalytics({ + granularity: 'day', +}); + +const series = await webhooks.getOrgTimeSeries({ + granularity: 'day', +}); +``` -const { deliveries } = await listWebhookDeliveries(client, 'wh_abc123'); +Optional `start` and `end` values accept ISO 8601 timestamps. -for (const delivery of deliveries) { - if (delivery.status === 'failed') { - await retryWebhookDelivery(client, 'wh_abc123', delivery.id); // [!code highlight] - } -} +## Delete + +Delete destinations when you want the webhook to keep receiving requests but stop forwarding to one target. Delete the webhook when the ingest endpoint is no longer needed. + +```typescript +await webhooks.deleteDestination(webhook.id, destination.id); +await webhooks.delete(webhook.id); ``` -## Best Practices + +`delete()` permanently removes the webhook, destinations, receipts, and delivery records. + -- **One webhook per source**: Use separate webhooks for Stripe, GitHub, Slack, etc., so receipts stay organized and you can route them to different destinations independently. -- **Verify signatures in your destination**: The webhook service records and forwards payloads as received; signature verification (HMAC, Stripe-Signature header, etc.) belongs in your destination handler. See the [Webhook Handler Pattern](/cookbook/patterns/webhook-handler) for an example of processing webhooks in your routes with signature verification. -- **Use receipts as an audit log**: Before debugging a missed event, check `listWebhookReceipts` to confirm the payload was received at the ingest URL. -- **Poll deliveries after destination changes**: If you update a destination URL, retry any recent `failed` deliveries so they reach the correct endpoint. -- **Set custom headers for authentication**: Add a shared secret in `headers` when creating a destination so your handler can verify requests came from the webhook service. +## Framework Routes + +Use the same direct client inside Hono, Next.js route handlers, or server functions. + +```typescript +import { Hono } from 'hono'; +import { WebhookClient } from '@agentuity/webhook'; + +const webhooks = new WebhookClient(); +const app = new Hono(); + +app.get('/admin/webhooks', async (c) => { + const { webhooks: items, total } = await webhooks.list({ limit: 20 }); + + return c.json({ webhooks: items, total }); +}); + +export default app; +``` ## Next Steps -- [Queues](/services/queues): Async message passing between your own agents and services -- [Webhook Handler Pattern](/cookbook/patterns/webhook-handler): Handling incoming webhooks directly in routes with signature verification -- [Email](/services/email): Send transactional email from agents +- [Webhook Handler Pattern](/cookbook/patterns/webhook-handler): verify signatures and process payloads in your app +- [Queues](/services/queues): move webhook processing out of request paths +- [Email](/services/email): receive inbound email through managed addresses and destinations diff --git a/docs/src/web/demo-config.tsx b/docs/src/web/demo-config.tsx index d22280db7..26593da0c 100644 --- a/docs/src/web/demo-config.tsx +++ b/docs/src/web/demo-config.tsx @@ -85,7 +85,7 @@ export const DEMOS: DemoConfig[] = [ to see what tools are available inside your handler. ), - docsUrl: '/agents/creating-agents', + docsUrl: '/patterns/agents-as-a-pattern', category: 'basics', component: HelloDemo, codeExample: CODE_EXAMPLES.hello, @@ -236,7 +236,7 @@ export const DEMOS: DemoConfig[] = [ with the Vercel AI SDK for streaming and structured output. ), - docsUrl: '/agents/ai-gateway', + docsUrl: '/services/ai-gateway', category: 'services', component: AIGatewayDemo, codeExample: CODE_EXAMPLES['ai-gateway'], @@ -269,7 +269,7 @@ export const DEMOS: DemoConfig[] = [ . ), - docsUrl: '/agents/streaming-responses', + docsUrl: '/patterns/chat-and-streaming', category: 'io-patterns', component: StreamingDemo, codeExample: CODE_EXAMPLES.streaming, @@ -301,7 +301,7 @@ export const DEMOS: DemoConfig[] = [ . ), - docsUrl: '/routes/sse', + docsUrl: '/patterns/chat-and-streaming', category: 'io-patterns', component: SSEStreamDemo, codeExample: CODE_EXAMPLES['sse-stream'], @@ -333,7 +333,7 @@ export const DEMOS: DemoConfig[] = [ . ), - docsUrl: '/routes/websockets', + docsUrl: '/patterns/chat-and-streaming', category: 'io-patterns', component: WebSocketDemo, codeExample: CODE_EXAMPLES.websocket, @@ -390,7 +390,7 @@ export const DEMOS: DemoConfig[] = [ background tasks. ), - docsUrl: '/agents/calling-other-agents', + docsUrl: '/patterns/agents-as-a-pattern', category: 'io-patterns', component: AgentCallsDemo, codeExample: CODE_EXAMPLES['agent-calls'], diff --git a/docs/src/web/lib/docs-redirects.ts b/docs/src/web/lib/docs-redirects.ts index 75fa86418..edf536e67 100644 --- a/docs/src/web/lib/docs-redirects.ts +++ b/docs/src/web/lib/docs-redirects.ts @@ -1,10 +1,13 @@ export const docsRedirects = { demo: '/explorer', explorerEvals: '/cookbook/patterns/llm-as-a-judge', - apis: '/agents', - apisCallingAgents: '/routes/calling-agents', - apisWhenToUse: '/agents/when-to-use', - agentsWorkbench: '/agents', + apis: '/patterns/agents-as-a-pattern', + apisCallingAgents: '/patterns/agents-as-a-pattern', + apisWhenToUse: '/patterns/agents-as-a-pattern', + agentsWorkbench: '/patterns/agents-as-a-pattern', + agentsAiGateway: '/services/ai-gateway', + agentsStreaming: '/patterns/chat-and-streaming', + routesStreaming: '/patterns/chat-and-streaming', frontendWorkbench: '/frontend', agentsEvaluations: '/cookbook/patterns/llm-as-a-judge', referenceApiEvaluations: '/reference/api', @@ -15,11 +18,28 @@ export const docRedirectRules = [ { paths: ['/demo', '/demo/'], target: docsRedirects.demo }, { paths: ['/explorer/evals', '/explorer/evals/'], target: docsRedirects.explorerEvals }, { paths: ['/apis', '/apis/'], target: docsRedirects.apis }, + { paths: ['/agents', '/agents/'], target: docsRedirects.apis }, { paths: ['/apis/calling-agents', '/apis/calling-agents/'], target: docsRedirects.apisCallingAgents, }, { paths: ['/apis/when-to-use', '/apis/when-to-use/'], target: docsRedirects.apisWhenToUse }, + { + paths: ['/agents/ai-gateway', '/agents/ai-gateway/'], + target: docsRedirects.agentsAiGateway, + }, + { + paths: ['/agents/streaming-responses', '/agents/streaming-responses/'], + target: docsRedirects.agentsStreaming, + }, + { + paths: ['/agents/calling-other-agents', '/agents/calling-other-agents/'], + target: docsRedirects.apisCallingAgents, + }, + { + paths: ['/routes/sse', '/routes/sse/', '/routes/websockets', '/routes/websockets/'], + target: docsRedirects.routesStreaming, + }, { paths: ['/agents/workbench', '/agents/workbench/'], target: docsRedirects.agentsWorkbench }, { paths: ['/frontend/workbench', '/frontend/workbench/'], diff --git a/docs/src/web/public/images/integrations/astro.svg b/docs/src/web/public/images/integrations/astro.svg new file mode 100644 index 000000000..8f339f110 --- /dev/null +++ b/docs/src/web/public/images/integrations/astro.svg @@ -0,0 +1 @@ +Astro diff --git a/docs/src/web/public/images/integrations/hono.svg b/docs/src/web/public/images/integrations/hono.svg new file mode 100644 index 000000000..9a50df786 --- /dev/null +++ b/docs/src/web/public/images/integrations/hono.svg @@ -0,0 +1 @@ +Hono diff --git a/docs/src/web/public/images/integrations/nuxt.svg b/docs/src/web/public/images/integrations/nuxt.svg new file mode 100644 index 000000000..2e6e5a358 --- /dev/null +++ b/docs/src/web/public/images/integrations/nuxt.svg @@ -0,0 +1 @@ +Nuxt diff --git a/docs/src/web/public/images/integrations/react-router.svg b/docs/src/web/public/images/integrations/react-router.svg new file mode 100644 index 000000000..75016f603 --- /dev/null +++ b/docs/src/web/public/images/integrations/react-router.svg @@ -0,0 +1 @@ +React Router diff --git a/docs/src/web/public/images/integrations/svelte.svg b/docs/src/web/public/images/integrations/svelte.svg new file mode 100644 index 000000000..ad54a4142 --- /dev/null +++ b/docs/src/web/public/images/integrations/svelte.svg @@ -0,0 +1 @@ +Svelte diff --git a/docs/src/web/public/images/integrations/vite.svg b/docs/src/web/public/images/integrations/vite.svg new file mode 100644 index 000000000..65d4263ff --- /dev/null +++ b/docs/src/web/public/images/integrations/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/docs/src/web/routes/_docs/agents/ai-sdk-integration.tsx b/docs/src/web/routes/_docs/agents/ai-sdk-integration.tsx deleted file mode 100644 index f39559a42..000000000 --- a/docs/src/web/routes/_docs/agents/ai-sdk-integration.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/ai-sdk-integration')({ - component: () => , - staticData: { crumb: 'AI SDK Integration' }, -}); diff --git a/docs/src/web/routes/_docs/agents/calling-other-agents.tsx b/docs/src/web/routes/_docs/agents/calling-other-agents.tsx deleted file mode 100644 index 059cb8a7b..000000000 --- a/docs/src/web/routes/_docs/agents/calling-other-agents.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/calling-other-agents')({ - component: () => , - staticData: { crumb: 'Calling Other Agents' }, -}); diff --git a/docs/src/web/routes/_docs/agents/creating-agents.tsx b/docs/src/web/routes/_docs/agents/creating-agents.tsx deleted file mode 100644 index abf3a6ea5..000000000 --- a/docs/src/web/routes/_docs/agents/creating-agents.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/creating-agents')({ - component: () => , - staticData: { crumb: 'Creating Agents' }, -}); diff --git a/docs/src/web/routes/_docs/agents/events-lifecycle.tsx b/docs/src/web/routes/_docs/agents/events-lifecycle.tsx deleted file mode 100644 index 17fdba3b7..000000000 --- a/docs/src/web/routes/_docs/agents/events-lifecycle.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/events-lifecycle')({ - component: () => , - staticData: { crumb: 'Events & Lifecycle' }, -}); diff --git a/docs/src/web/routes/_docs/agents/index.tsx b/docs/src/web/routes/_docs/agents/index.tsx deleted file mode 100644 index 7db893a65..000000000 --- a/docs/src/web/routes/_docs/agents/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/')({ - component: () => , - staticData: { crumb: 'Agents' }, -}); diff --git a/docs/src/web/routes/_docs/agents/schema-libraries.tsx b/docs/src/web/routes/_docs/agents/schema-libraries.tsx deleted file mode 100644 index 85cf1ee37..000000000 --- a/docs/src/web/routes/_docs/agents/schema-libraries.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/schema-libraries')({ - component: () => , - staticData: { crumb: 'Schema Libraries' }, -}); diff --git a/docs/src/web/routes/_docs/agents/standalone-execution.tsx b/docs/src/web/routes/_docs/agents/standalone-execution.tsx deleted file mode 100644 index 611f84d8c..000000000 --- a/docs/src/web/routes/_docs/agents/standalone-execution.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/standalone-execution')({ - component: () => , - staticData: { crumb: 'Standalone Execution' }, -}); diff --git a/docs/src/web/routes/_docs/agents/state-management.tsx b/docs/src/web/routes/_docs/agents/state-management.tsx deleted file mode 100644 index c4b96df49..000000000 --- a/docs/src/web/routes/_docs/agents/state-management.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/state-management')({ - component: () => , - staticData: { crumb: 'State Management' }, -}); diff --git a/docs/src/web/routes/_docs/agents/streaming-responses.tsx b/docs/src/web/routes/_docs/agents/streaming-responses.tsx deleted file mode 100644 index 4a4169086..000000000 --- a/docs/src/web/routes/_docs/agents/streaming-responses.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/streaming-responses')({ - component: () => , - staticData: { crumb: 'Streaming Responses' }, -}); diff --git a/docs/src/web/routes/_docs/agents/when-to-use.tsx b/docs/src/web/routes/_docs/agents/when-to-use.tsx deleted file mode 100644 index 16bee2b6c..000000000 --- a/docs/src/web/routes/_docs/agents/when-to-use.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/agents/when-to-use')({ - component: () => , - staticData: { crumb: 'When to Use' }, -}); diff --git a/docs/src/web/routes/_docs/agents/workbench.tsx b/docs/src/web/routes/_docs/agents/workbench.tsx deleted file mode 100644 index 28b017fa4..000000000 --- a/docs/src/web/routes/_docs/agents/workbench.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createFileRoute, redirect } from '@tanstack/react-router'; -import { RedirectFallback } from '../../../components/docs/RedirectFallback'; -import { docsRedirects } from '../../../lib/docs-redirects'; - -const target = docsRedirects.agentsWorkbench; - -export const Route = createFileRoute('/_docs/agents/workbench')({ - beforeLoad: () => { - if (typeof window !== 'undefined') { - throw redirect({ to: target, replace: true, statusCode: 301 }); - } - }, - component: () => , - staticData: { crumb: 'Workbench' }, -}); diff --git a/docs/src/web/routes/_docs/deploy-operate/deploy-framework-apps.tsx b/docs/src/web/routes/_docs/deploy-operate/deploy-framework-apps.tsx new file mode 100644 index 000000000..aba5b3180 --- /dev/null +++ b/docs/src/web/routes/_docs/deploy-operate/deploy-framework-apps.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/deploy-operate/deploy-framework-apps')({ + component: () => , + staticData: { crumb: 'Deploy Framework Apps' }, +}); diff --git a/docs/src/web/routes/_docs/deploy-operate/environment-variables.tsx b/docs/src/web/routes/_docs/deploy-operate/environment-variables.tsx new file mode 100644 index 000000000..a3f0cd9f9 --- /dev/null +++ b/docs/src/web/routes/_docs/deploy-operate/environment-variables.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/deploy-operate/environment-variables')({ + component: () => , + staticData: { crumb: 'Environment Variables' }, +}); diff --git a/docs/src/web/routes/_docs/deploy-operate/index.tsx b/docs/src/web/routes/_docs/deploy-operate/index.tsx new file mode 100644 index 000000000..2209a8786 --- /dev/null +++ b/docs/src/web/routes/_docs/deploy-operate/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/deploy-operate/')({ + component: () => , + staticData: { crumb: 'Deploy & Operate' }, +}); diff --git a/docs/src/web/routes/_docs/deploy-operate/local-development.tsx b/docs/src/web/routes/_docs/deploy-operate/local-development.tsx new file mode 100644 index 000000000..2ebfe22a6 --- /dev/null +++ b/docs/src/web/routes/_docs/deploy-operate/local-development.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/deploy-operate/local-development')({ + component: () => , + staticData: { crumb: 'Local Development' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/astro.tsx b/docs/src/web/routes/_docs/frameworks/astro.tsx new file mode 100644 index 000000000..3839ee3e5 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/astro.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/astro')({ + component: () => , + staticData: { crumb: 'Astro' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/hono.tsx b/docs/src/web/routes/_docs/frameworks/hono.tsx new file mode 100644 index 000000000..6892476a9 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/hono.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/hono')({ + component: () => , + staticData: { crumb: 'Hono' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/index.tsx b/docs/src/web/routes/_docs/frameworks/index.tsx new file mode 100644 index 000000000..83ea95d3a --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/')({ + component: () => , + staticData: { crumb: 'Frameworks' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/nextjs.tsx b/docs/src/web/routes/_docs/frameworks/nextjs.tsx new file mode 100644 index 000000000..d55902c03 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/nextjs.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/nextjs')({ + component: () => , + staticData: { crumb: 'Next.js' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/nuxt.tsx b/docs/src/web/routes/_docs/frameworks/nuxt.tsx new file mode 100644 index 000000000..2d50cc765 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/nuxt.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/nuxt')({ + component: () => , + staticData: { crumb: 'Nuxt' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/react-router.tsx b/docs/src/web/routes/_docs/frameworks/react-router.tsx new file mode 100644 index 000000000..831a0fa7e --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/react-router.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/react-router')({ + component: () => , + staticData: { crumb: 'React Router' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/sveltekit.tsx b/docs/src/web/routes/_docs/frameworks/sveltekit.tsx new file mode 100644 index 000000000..97899c5cf --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/sveltekit.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/sveltekit')({ + component: () => , + staticData: { crumb: 'SvelteKit' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/tanstack-start.tsx b/docs/src/web/routes/_docs/frameworks/tanstack-start.tsx new file mode 100644 index 000000000..daa9fdd44 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/tanstack-start.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/tanstack-start')({ + component: () => , + staticData: { crumb: 'TanStack Start' }, +}); diff --git a/docs/src/web/routes/_docs/frameworks/vite-react.tsx b/docs/src/web/routes/_docs/frameworks/vite-react.tsx new file mode 100644 index 000000000..124fe5c28 --- /dev/null +++ b/docs/src/web/routes/_docs/frameworks/vite-react.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/frameworks/vite-react')({ + component: () => , + staticData: { crumb: 'Vite + React' }, +}); diff --git a/docs/src/web/routes/_docs/migration/from-v2.tsx b/docs/src/web/routes/_docs/migration/from-v2.tsx new file mode 100644 index 000000000..e224d5f38 --- /dev/null +++ b/docs/src/web/routes/_docs/migration/from-v2.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/migration/from-v2')({ + component: () => , + staticData: { crumb: 'From v2' }, +}); diff --git a/docs/src/web/routes/_docs/migration/index.tsx b/docs/src/web/routes/_docs/migration/index.tsx new file mode 100644 index 000000000..cd4c55567 --- /dev/null +++ b/docs/src/web/routes/_docs/migration/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/migration/')({ + component: () => , + staticData: { crumb: 'Migration' }, +}); diff --git a/docs/src/web/routes/_docs/migration/migrate-cli.tsx b/docs/src/web/routes/_docs/migration/migrate-cli.tsx new file mode 100644 index 000000000..fae96d5d2 --- /dev/null +++ b/docs/src/web/routes/_docs/migration/migrate-cli.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/migration/migrate-cli')({ + component: () => , + staticData: { crumb: 'Migration CLI' }, +}); diff --git a/docs/src/web/routes/_docs/migration/runtime-to-frameworks.tsx b/docs/src/web/routes/_docs/migration/runtime-to-frameworks.tsx new file mode 100644 index 000000000..bf305867b --- /dev/null +++ b/docs/src/web/routes/_docs/migration/runtime-to-frameworks.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/migration/runtime-to-frameworks')({ + component: () => , + staticData: { crumb: 'Runtime to Frameworks' }, +}); diff --git a/docs/src/web/routes/_docs/patterns/agents-as-a-pattern.tsx b/docs/src/web/routes/_docs/patterns/agents-as-a-pattern.tsx new file mode 100644 index 000000000..a513d186b --- /dev/null +++ b/docs/src/web/routes/_docs/patterns/agents-as-a-pattern.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/patterns/agents-as-a-pattern')({ + component: () => , + staticData: { crumb: 'Agents as a Pattern' }, +}); diff --git a/docs/src/web/routes/_docs/patterns/background-work.tsx b/docs/src/web/routes/_docs/patterns/background-work.tsx new file mode 100644 index 000000000..9bac2d483 --- /dev/null +++ b/docs/src/web/routes/_docs/patterns/background-work.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/patterns/background-work')({ + component: () => , + staticData: { crumb: 'Background Work' }, +}); diff --git a/docs/src/web/routes/_docs/patterns/chat-and-streaming.tsx b/docs/src/web/routes/_docs/patterns/chat-and-streaming.tsx new file mode 100644 index 000000000..566c1ef30 --- /dev/null +++ b/docs/src/web/routes/_docs/patterns/chat-and-streaming.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/patterns/chat-and-streaming')({ + component: () => , + staticData: { crumb: 'Chat and Streaming' }, +}); diff --git a/docs/src/web/routes/_docs/patterns/index.tsx b/docs/src/web/routes/_docs/patterns/index.tsx new file mode 100644 index 000000000..ab8220b20 --- /dev/null +++ b/docs/src/web/routes/_docs/patterns/index.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { MDXPage } from '../../../components/docs/mdx-page'; + +export const Route = createFileRoute('/_docs/patterns/')({ + component: () => , + staticData: { crumb: 'Patterns' }, +}); diff --git a/docs/src/web/routes/_docs/routes/calling-agents.tsx b/docs/src/web/routes/_docs/routes/calling-agents.tsx deleted file mode 100644 index 400c9feaa..000000000 --- a/docs/src/web/routes/_docs/routes/calling-agents.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/calling-agents')({ - component: () => , - staticData: { crumb: 'Calling Agents' }, -}); diff --git a/docs/src/web/routes/_docs/routes/cron.tsx b/docs/src/web/routes/_docs/routes/cron.tsx deleted file mode 100644 index 6e920d6e5..000000000 --- a/docs/src/web/routes/_docs/routes/cron.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/cron')({ - component: () => , - staticData: { crumb: 'Cron' }, -}); diff --git a/docs/src/web/routes/_docs/routes/explicit-routing.tsx b/docs/src/web/routes/_docs/routes/explicit-routing.tsx deleted file mode 100644 index 9a18fa6dd..000000000 --- a/docs/src/web/routes/_docs/routes/explicit-routing.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/explicit-routing')({ - component: () => , - staticData: { crumb: 'Explicit Routing' }, -}); diff --git a/docs/src/web/routes/_docs/routes/http.tsx b/docs/src/web/routes/_docs/routes/http.tsx deleted file mode 100644 index cc5a9f4f5..000000000 --- a/docs/src/web/routes/_docs/routes/http.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/http')({ - component: () => , - staticData: { crumb: 'HTTP' }, -}); diff --git a/docs/src/web/routes/_docs/routes/index.tsx b/docs/src/web/routes/_docs/routes/index.tsx deleted file mode 100644 index f74e09cf0..000000000 --- a/docs/src/web/routes/_docs/routes/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/')({ - component: () => , - staticData: { crumb: 'Routes' }, -}); diff --git a/docs/src/web/routes/_docs/routes/middleware.tsx b/docs/src/web/routes/_docs/routes/middleware.tsx deleted file mode 100644 index 5fbf729d6..000000000 --- a/docs/src/web/routes/_docs/routes/middleware.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/middleware')({ - component: () => , - staticData: { crumb: 'Middleware' }, -}); diff --git a/docs/src/web/routes/_docs/routes/sse.tsx b/docs/src/web/routes/_docs/routes/sse.tsx deleted file mode 100644 index 21953a5f2..000000000 --- a/docs/src/web/routes/_docs/routes/sse.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/sse')({ - component: () => , - staticData: { crumb: 'SSE' }, -}); diff --git a/docs/src/web/routes/_docs/routes/webrtc.tsx b/docs/src/web/routes/_docs/routes/webrtc.tsx deleted file mode 100644 index cab508e68..000000000 --- a/docs/src/web/routes/_docs/routes/webrtc.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/webrtc')({ - component: () => , - staticData: { crumb: 'WebRTC' }, -}); diff --git a/docs/src/web/routes/_docs/routes/websockets.tsx b/docs/src/web/routes/_docs/routes/websockets.tsx deleted file mode 100644 index a1c5ee525..000000000 --- a/docs/src/web/routes/_docs/routes/websockets.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { MDXPage } from '../../../components/docs/mdx-page'; - -export const Route = createFileRoute('/_docs/routes/websockets')({ - component: () => , - staticData: { crumb: 'WebSockets' }, -}); diff --git a/docs/src/web/routes/_docs/agents/ai-gateway.tsx b/docs/src/web/routes/_docs/services/ai-gateway.tsx similarity index 55% rename from docs/src/web/routes/_docs/agents/ai-gateway.tsx rename to docs/src/web/routes/_docs/services/ai-gateway.tsx index 8cb57f5ec..bc7666ce1 100644 --- a/docs/src/web/routes/_docs/agents/ai-gateway.tsx +++ b/docs/src/web/routes/_docs/services/ai-gateway.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from '@tanstack/react-router'; import { MDXPage } from '../../../components/docs/mdx-page'; -export const Route = createFileRoute('/_docs/agents/ai-gateway')({ - component: () => , +export const Route = createFileRoute('/_docs/services/ai-gateway')({ + component: () => , staticData: { crumb: 'AI Gateway' }, }); diff --git a/docs/src/web/routes/index.tsx b/docs/src/web/routes/index.tsx index 27a7b180e..35648fe06 100644 --- a/docs/src/web/routes/index.tsx +++ b/docs/src/web/routes/index.tsx @@ -3,10 +3,7 @@ import { Rocket, Download, Zap, - Bot, - Route as RouteIcon, Server, - Monitor, Play, BookOpen, Users, @@ -42,18 +39,18 @@ function HomePage() { sandboxes, observability, and more.

- Start with a guide, explore interactive demos, or dive into the reference docs. + Start with your framework, add Agentuity services, then deploy the app.

- + - Try the SDK in Your Browser + Start with the v3 quickstart - The SDK Explorer has live, interactive demos for agents, storage, streaming, and - more. No setup required. + Create a framework app, run it with `agentuity dev`, and validate the build before + deploying. @@ -77,7 +74,7 @@ function HomePage() { } /> @@ -87,16 +84,10 @@ function HomePage() { } - /> - } + href="/frameworks" + title="Frameworks" + description="Start from Next.js, Nuxt, Hono, SvelteKit, Astro, or another app shape" + icon={} /> } /> } + href="/deploy-operate" + title="Build & Deploy" + description="Run local development, build deployable output, and manage environment values" + icon={} + /> + } /> @@ -117,9 +114,9 @@ function HomePage() { } /> } /> { @@ -46,11 +49,13 @@ async function frameworkDefToDetected( return { name: slug, - runtime: 'node', + runtime: runtime ?? 'node', packageManager: pm, buildCommand: resolvedBuildCommand, buildOutput: resolvedOutputDir, staticDir: resolvedStaticDir, + startCommand, + serverEntry, confidence: 'high', }; } @@ -74,6 +79,9 @@ export async function detectFramework(projectDir: string): Promise ['bunx', 'nuxi@latest', 'init', dir, '--packageManager', 'bun'], - dependencies: ['ai', '@ai-sdk/openai'], + createCommand: (dir) => [ + 'bunx', + 'nuxi@latest', + 'init', + dir, + '--template', + 'minimal', + '--packageManager', + 'bun', + ], + dependencies: ['openai', '@agentuity/keyvalue'], scripts: { deploy: 'agentuity deploy', }, @@ -130,7 +139,7 @@ export const frameworkCatalog: FrameworkScaffold[] = [ '--package-manager', 'bun', ], - dependencies: ['ai', '@ai-sdk/openai'], + dependencies: ['openai', '@agentuity/keyvalue'], scripts: { deploy: 'agentuity deploy', }, @@ -149,8 +158,12 @@ export const frameworkCatalog: FrameworkScaffold[] = [ 'minimal', '--types', 'ts', + '--no-add-ons', + '--install', + 'bun', ], - dependencies: ['ai', '@ai-sdk/openai'], + dependencies: ['openai', '@agentuity/keyvalue'], + devDependencies: ['@sveltejs/adapter-node'], scripts: { deploy: 'agentuity deploy', }, @@ -171,7 +184,7 @@ export const frameworkCatalog: FrameworkScaffold[] = [ '--typescript', 'strict', ], - dependencies: ['ai', '@ai-sdk/openai'], + dependencies: ['openai', '@agentuity/keyvalue', '@astrojs/node'], scripts: { deploy: 'agentuity deploy', }, @@ -191,8 +204,10 @@ export const frameworkCatalog: FrameworkScaffold[] = [ '--pm', 'bun', ], - dependencies: ['ai', '@ai-sdk/openai'], + dependencies: ['openai', '@agentuity/keyvalue'], scripts: { + build: 'bun build src/index.ts --target=bun --outdir=dist', + start: 'bun dist/index.js', deploy: 'agentuity deploy', }, overlayDir: 'hono', @@ -202,9 +217,11 @@ export const frameworkCatalog: FrameworkScaffold[] = [ name: 'Vite + React', description: 'React SPA with Vite bundler', createCommand: (dir) => ['bunx', 'create-vite@latest', dir, '--template', 'react-ts'], - dependencies: ['ai', '@ai-sdk/openai', '@tanstack/react-query'], + dependencies: ['openai', '@agentuity/keyvalue', '@tanstack/react-query'], devDependencies: ['tailwindcss', '@tailwindcss/vite'], scripts: { + build: 'tsc -b && vite build && bun build server.ts --target=bun --outfile=dist/server.js', + start: 'NODE_ENV=production bun dist/server.js', deploy: 'agentuity deploy', }, overlayDir: 'vite-react', diff --git a/packages/cli/src/cmd/project/index.ts b/packages/cli/src/cmd/project/index.ts index e71ffd6bb..85289019c 100644 --- a/packages/cli/src/cmd/project/index.ts +++ b/packages/cli/src/cmd/project/index.ts @@ -14,7 +14,10 @@ export const command = createCommand({ description: 'Project related commands', tags: ['fast', 'requires-auth'], examples: [ - { command: getCommand('project create my-agent'), description: 'Create a new project' }, + { + command: getCommand('project create --name my-agent'), + description: 'Create a new project', + }, { command: getCommand('project import'), description: 'Import an existing project' }, { command: getCommand('project list'), description: 'List all projects' }, { command: getCommand('project add database'), description: 'Link an existing database' }, diff --git a/packages/cli/src/cmd/project/templates/astro/astro.config.mjs b/packages/cli/src/cmd/project/templates/astro/astro.config.mjs new file mode 100644 index 000000000..24aebb762 --- /dev/null +++ b/packages/cli/src/cmd/project/templates/astro/astro.config.mjs @@ -0,0 +1,10 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import node from '@astrojs/node'; + +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone', + }), +}); diff --git a/packages/cli/src/cmd/project/templates/astro/src/pages/api/translate.ts b/packages/cli/src/cmd/project/templates/astro/src/pages/api/translate.ts index 2af50556e..070a017f1 100644 --- a/packages/cli/src/cmd/project/templates/astro/src/pages/api/translate.ts +++ b/packages/cli/src/cmd/project/templates/astro/src/pages/api/translate.ts @@ -1,22 +1,126 @@ -import type { APIRoute } from 'astro'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; +import { KeyValueClient } from '@agentuity/keyvalue'; +import type { APIContext, APIRoute } from 'astro'; +import OpenAI from 'openai'; -export const POST: APIRoute = async ({ request }) => { - const { text, toLanguage, model = 'gpt-4o-mini' } = await request.json(); +const HISTORY_NAMESPACE = 'translation-history'; +const HISTORY_LIMIT = 5; +const SESSION_COOKIE = 'agentuity_session'; +const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; - const { text: translation, usage } = await generateText({ - model: openai(model), - prompt: `Translate the following text to ${toLanguage}. Return only the translation, nothing else.\n\n${text}`, +interface HistoryEntry { + readonly model: string; + readonly sessionId: string; + readonly text: string; + readonly timestamp: string; + readonly tokens: number; + readonly toLanguage: string; + readonly translation: string; +} + +interface HistoryState { + readonly history: readonly HistoryEntry[]; + readonly translationCount: number; +} + +const kv = new KeyValueClient(); + +function createSessionId(): string { + return `sess_${crypto.randomUUID().replaceAll('-', '').slice(0, 24)}`; +} + +function truncate(value: string, length: number): string { + return value.length > length ? `${value.slice(0, length)}...` : value; +} + +function getSessionId(cookies: APIContext['cookies']): string { + const sessionId = cookies.get(SESSION_COOKIE)?.value ?? createSessionId(); + + cookies.set(SESSION_COOKIE, sessionId, { + httpOnly: true, + maxAge: SESSION_TTL_SECONDS, + path: '/', + sameSite: 'lax', + secure: import.meta.env.PROD, + }); + + return sessionId; +} + +function json(data: unknown): Response { + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); +} + +async function readHistory(sessionId: string): Promise { + const result = await kv.get(HISTORY_NAMESPACE, sessionId); + return result.exists ? result.data : { history: [], translationCount: 0 }; +} + +async function saveHistory(sessionId: string, entry: HistoryEntry): Promise { + const previous = await readHistory(sessionId); + const history = [...previous.history, entry].slice(-HISTORY_LIMIT); + const next = { + history, + translationCount: previous.translationCount + 1, + }; + + await kv.set(HISTORY_NAMESPACE, sessionId, next, { ttl: SESSION_TTL_SECONDS }); + + return next; +} + +export const GET: APIRoute = async ({ cookies }) => { + const sessionId = getSessionId(cookies); + const history = await readHistory(sessionId); + + return json({ + history: history.history, + sessionId, + translationCount: history.translationCount, }); +}; - return new Response( - JSON.stringify({ - translation, - tokens: usage?.totalTokens ?? 0, - model, - toLanguage, - }), - { headers: { 'Content-Type': 'application/json' } }, - ); +export const DELETE: APIRoute = async ({ cookies }) => { + const sessionId = getSessionId(cookies); + await kv.delete(HISTORY_NAMESPACE, sessionId); + + return json({ + history: [], + sessionId, + translationCount: 0, + }); +}; + +export const POST: APIRoute = async ({ cookies, request }) => { + const { text, toLanguage, model = 'gpt-5.4-nano' } = await request.json(); + const prompt = `Translate to ${toLanguage}:\n\n${text}`; + const openai = new OpenAI(); + const sessionId = getSessionId(cookies); + + const completion = await openai.chat.completions.create({ + model, + messages: [{ role: 'user', content: prompt }], + }); + const translation = completion.choices[0]?.message?.content ?? ''; + const tokens = completion.usage?.total_tokens ?? 0; + const history = await saveHistory(sessionId, { + model, + sessionId, + text: truncate(text, 50), + timestamp: new Date().toISOString(), + tokens, + toLanguage, + translation: truncate(translation, 50), + }); + + return json({ + history: history.history, + sessionId, + translation, + translationCount: history.translationCount, + tokens, + model, + toLanguage, + }); }; diff --git a/packages/cli/src/cmd/project/templates/astro/src/pages/index.astro b/packages/cli/src/cmd/project/templates/astro/src/pages/index.astro index 846245de6..435513095 100644 --- a/packages/cli/src/cmd/project/templates/astro/src/pages/index.astro +++ b/packages/cli/src/cmd/project/templates/astro/src/pages/index.astro @@ -1,8 +1,12 @@ --- const LANGUAGES = ['Spanish', 'French', 'German', 'Chinese'] as const; -const MODELS = ['gpt-4o-mini', 'gpt-4o', 'gpt-4.1-nano'] as const; +const MODELS = [ + { value: 'gpt-5.4-nano', label: 'GPT-5.4 Nano' }, + { value: 'gpt-5.4-mini', label: 'GPT-5.4 Mini' }, + { value: 'gpt-5.4', label: 'GPT-5.4' }, +] as const; const DEFAULT_TEXT = - 'Welcome to Agentuity! This translation demo shows what you can build with the platform. It connects to AI models through our gateway — no separate API keys needed. Try translating this text into different languages to see it in action.'; + 'Welcome to Agentuity! This starter app translates text, stores recent requests in key-value storage, and shows how a typed framework route can call models through Agentuity’s AI Gateway. Try a few languages or switch models, then check the app session, model, and token details below.'; --- @@ -10,10 +14,7 @@ const DEFAULT_TEXT = Agentuity + Astro - +
@@ -42,9 +43,15 @@ const DEFAULT_TEXT = fill-rule="evenodd" /> -

Welcome to Agentuity

+

+ Welcome to Agentuity +

- Astro + AI Gateway + The Full-Stack Platform for AI Agents

@@ -57,7 +64,7 @@ const DEFAULT_TEXT = using
@@ -84,17 +91,44 @@ const DEFAULT_TEXT =
+ +
+
+

Recent translations

+ +
+
+
History will appear here
+
+ +
+

How it works

+
+
+ +
+
+

Key-value history

+

KeyValueClient stores recent translations for this browser session.

+
+

AI Gateway routing

-

agentuity dev automatically sets OPENAI_API_KEY and OPENAI_BASE_URL so the AI SDK routes through the Agentuity gateway.

+

agentuity dev automatically sets OPENAI_API_KEY and OPENAI_BASE_URL so the OpenAI SDK routes through Agentuity's AI Gateway.

@@ -121,12 +155,91 @@ const DEFAULT_TEXT =
diff --git a/packages/cli/src/cmd/project/templates/hono/src/index.ts b/packages/cli/src/cmd/project/templates/hono/src/index.ts index 64cd204fd..b505caa05 100644 --- a/packages/cli/src/cmd/project/templates/hono/src/index.ts +++ b/packages/cli/src/cmd/project/templates/hono/src/index.ts @@ -1,27 +1,129 @@ +import { KeyValueClient } from '@agentuity/keyvalue'; +import { getCookie, setCookie } from 'hono/cookie'; import { Hono } from 'hono'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; +import type { Context } from 'hono'; +import OpenAI from 'openai'; + +const HISTORY_NAMESPACE = 'translation-history'; +const HISTORY_LIMIT = 5; +const SESSION_COOKIE = 'agentuity_session'; +const SESSION_TTL_SECONDS = 60 * 60 * 24 * 30; +const DEFAULT_TEXT = + 'Welcome to Agentuity! This starter app translates text, stores recent requests in key-value storage, and shows how a typed framework route can call models through Agentuity’s AI Gateway. Try a few languages or switch models, then check the app session, model, and token details below.'; + +interface HistoryEntry { + readonly model: string; + readonly sessionId: string; + readonly text: string; + readonly timestamp: string; + readonly tokens: number; + readonly toLanguage: string; + readonly translation: string; +} + +interface HistoryState { + readonly history: readonly HistoryEntry[]; + readonly translationCount: number; +} const app = new Hono(); +const kv = new KeyValueClient(); + +function createSessionId(): string { + return `sess_${crypto.randomUUID().replaceAll('-', '').slice(0, 24)}`; +} + +function truncate(value: string, length: number): string { + return value.length > length ? `${value.slice(0, length)}...` : value; +} + +function getSessionId(c: Context): string { + const sessionId = getCookie(c, SESSION_COOKIE) ?? createSessionId(); + + setCookie(c, SESSION_COOKIE, sessionId, { + httpOnly: true, + maxAge: SESSION_TTL_SECONDS, + path: '/', + sameSite: 'Lax', + secure: process.env.NODE_ENV === 'production', + }); + + return sessionId; +} + +async function readHistory(sessionId: string): Promise { + const result = await kv.get(HISTORY_NAMESPACE, sessionId); + return result.exists ? result.data : { history: [], translationCount: 0 }; +} + +async function saveHistory(sessionId: string, entry: HistoryEntry): Promise { + const previous = await readHistory(sessionId); + const history = [...previous.history, entry].slice(-HISTORY_LIMIT); + const next = { + history, + translationCount: previous.translationCount + 1, + }; + + await kv.set(HISTORY_NAMESPACE, sessionId, next, { ttl: SESSION_TTL_SECONDS }); + + return next; +} + +app.get('/api/translate', async (c) => { + const sessionId = getSessionId(c); + const history = await readHistory(sessionId); + + return c.json({ + history: history.history, + sessionId, + translationCount: history.translationCount, + }); +}); + +app.delete('/api/translate', async (c) => { + const sessionId = getSessionId(c); + await kv.delete(HISTORY_NAMESPACE, sessionId); + + return c.json({ + history: [], + sessionId, + translationCount: 0, + }); +}); -// API route app.post('/api/translate', async (c) => { - const { text, toLanguage, model = 'gpt-4o-mini' } = await c.req.json(); + const { text, toLanguage, model = 'gpt-5.4-nano' } = await c.req.json(); + const prompt = `Translate to ${toLanguage}:\n\n${text}`; + const openai = new OpenAI(); + const sessionId = getSessionId(c); - const { text: translation, usage } = await generateText({ - model: openai(model), - prompt: `Translate the following text to ${toLanguage}. Return only the translation, nothing else.\n\n${text}`, + const completion = await openai.chat.completions.create({ + model, + messages: [{ role: 'user', content: prompt }], + }); + const translation = completion.choices[0]?.message?.content ?? ''; + const tokens = completion.usage?.total_tokens ?? 0; + const history = await saveHistory(sessionId, { + model, + sessionId, + text: truncate(text, 50), + timestamp: new Date().toISOString(), + tokens, + toLanguage, + translation: truncate(translation, 50), }); return c.json({ + history: history.history, + sessionId, translation, - tokens: usage?.totalTokens ?? 0, + translationCount: history.translationCount, + tokens, model, toLanguage, }); }); -// Landing page app.get('/', (c) => { return c.html(` @@ -29,7 +131,7 @@ app.get('/', (c) => { Agentuity + Hono - +