eve 0.52.1 → 0.52.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/src/channel/types.d.ts +1 -0
  3. package/dist/src/chunks/{use-eve-agent-CxkGSusZ.js → use-eve-agent-BR8EpIOX.js} +46 -19
  4. package/dist/src/chunks/{use-eve-agent-Crmh_YB-.js → use-eve-agent-XnfjQt1Z.js} +46 -19
  5. package/dist/src/cli/commands/deploy.js +1 -1
  6. package/dist/src/cli/commands/extension-init.d.ts +2 -1
  7. package/dist/src/cli/commands/extension-init.js +1 -1
  8. package/dist/src/cli/commands/init.d.ts +2 -13
  9. package/dist/src/cli/commands/init.js +1 -1
  10. package/dist/src/cli/dev/run-interactive-ui.d.ts +3 -0
  11. package/dist/src/cli/dev/run-interactive-ui.js +1 -1
  12. package/dist/src/cli/dev/tui/prompt-command-handler.js +1 -1
  13. package/dist/src/cli/dev/tui/runner.d.ts +6 -0
  14. package/dist/src/cli/dev/tui/runner.js +1 -1
  15. package/dist/src/cli/dev/tui/setup-commands.d.ts +5 -3
  16. package/dist/src/cli/dev/tui/setup-commands.js +2 -2
  17. package/dist/src/cli/dev/tui/status-line.js +1 -1
  18. package/dist/src/cli/dev/tui/subagent-pump.js +1 -1
  19. package/dist/src/cli/dev/tui/tui.d.ts +4 -0
  20. package/dist/src/cli/dev/tui/tui.js +1 -1
  21. package/dist/src/cli/run.d.ts +1 -1
  22. package/dist/src/cli/run.js +2 -2
  23. package/dist/src/cli/telemetry/index.d.ts +15 -0
  24. package/dist/src/cli/telemetry/index.js +2 -2
  25. package/dist/src/client/eve-agent-store.js +1 -1
  26. package/dist/src/client/ndjson.js +1 -1
  27. package/dist/src/client/session.js +1 -1
  28. package/dist/src/compiled/.vendor-stamp.json +1 -1
  29. package/dist/src/compiler/compile-from-memory.d.ts +1 -0
  30. package/dist/src/compiler/compile-from-memory.js +1 -1
  31. package/dist/src/compiler/default-tool-policy.d.ts +6 -0
  32. package/dist/src/compiler/default-tool-policy.js +1 -0
  33. package/dist/src/compiler/extension-compatibility.d.ts +18 -18
  34. package/dist/src/compiler/extension-compatibility.js +1 -1
  35. package/dist/src/compiler/manifest.js +1 -1
  36. package/dist/src/compiler/normalize-agent-config.js +1 -1
  37. package/dist/src/compiler/normalize-manifest.js +1 -1
  38. package/dist/src/context/build-dynamic-tools.d.ts +2 -1
  39. package/dist/src/context/build-dynamic-tools.js +1 -1
  40. package/dist/src/context/dynamic-tool-lifecycle.d.ts +3 -3
  41. package/dist/src/context/dynamic-tool-lifecycle.js +1 -1
  42. package/dist/src/context/dynamic-tool-metadata.d.ts +1 -1
  43. package/dist/src/context/hook-lifecycle.js +1 -1
  44. package/dist/src/context/keys.d.ts +2 -0
  45. package/dist/src/context/keys.js +1 -1
  46. package/dist/src/evals/cli/eval.js +1 -1
  47. package/dist/src/eve-channel/index.js +1 -1
  48. package/dist/src/execution/activity-events.d.ts +1 -0
  49. package/dist/src/execution/activity-events.js +1 -1
  50. package/dist/src/execution/node-step.js +1 -1
  51. package/dist/src/execution/sandbox/prewarm.js +1 -1
  52. package/dist/src/execution/session-activity-projection.js +1 -1
  53. package/dist/src/execution/session-activity.js +1 -1
  54. package/dist/src/execution/settle-cancelled-turn-step.js +1 -1
  55. package/dist/src/execution/tasks/parent/dispatch.js +1 -1
  56. package/dist/src/execution/tasks/parent/task-cancel.d.ts +2 -2
  57. package/dist/src/execution/tasks/parent/task-cancel.js +1 -1
  58. package/dist/src/execution/tasks/parent/tool-execution.js +1 -1
  59. package/dist/src/execution/tools/sleep-workflow.js +1 -1
  60. package/dist/src/execution/workflow-runtime.js +1 -1
  61. package/dist/src/execution/workflow-steps.js +1 -1
  62. package/dist/src/execution/workflow-webhook-route.js +1 -1
  63. package/dist/src/framework/sources/registry.js +1 -1
  64. package/dist/src/harness/action-presentation.d.ts +13 -0
  65. package/dist/src/harness/action-presentation.js +1 -0
  66. package/dist/src/harness/approval-delivery-coordinator.js +1 -1
  67. package/dist/src/harness/compaction-prompt.d.ts +1 -0
  68. package/dist/src/harness/compaction-prompt.js +1 -1
  69. package/dist/src/harness/compaction.js +1 -1
  70. package/dist/src/harness/coordination.js +1 -1
  71. package/dist/src/harness/emission.d.ts +1 -6
  72. package/dist/src/harness/emission.js +1 -1
  73. package/dist/src/harness/execute-tool.d.ts +2 -1
  74. package/dist/src/harness/finish-reason.d.ts +3 -0
  75. package/dist/src/harness/finish-reason.js +1 -0
  76. package/dist/src/harness/provider-tool-schemas.d.ts +1 -0
  77. package/dist/src/harness/provider-tool-schemas.js +1 -1
  78. package/dist/src/harness/step-hooks.js +1 -1
  79. package/dist/src/harness/stream-actions.d.ts +2 -2
  80. package/dist/src/harness/stream-actions.js +1 -1
  81. package/dist/src/harness/tool-call-input-errors.d.ts +4 -3
  82. package/dist/src/harness/tool-call-input-errors.js +1 -1
  83. package/dist/src/harness/tool-loop.js +2 -2
  84. package/dist/src/harness/tool-presentation.d.ts +5 -0
  85. package/dist/src/harness/tool-presentation.js +1 -0
  86. package/dist/src/harness/turn-client-context.d.ts +1 -0
  87. package/dist/src/harness/workflow-lifecycle.d.ts +2 -3
  88. package/dist/src/harness/workflow-lifecycle.js +1 -1
  89. package/dist/src/internal/application/compiled-artifacts.d.ts +3 -0
  90. package/dist/src/internal/application/compiled-artifacts.js +1 -1
  91. package/dist/src/internal/application/package.js +1 -1
  92. package/dist/src/internal/authored-definition/core.js +1 -1
  93. package/dist/src/internal/authored-definition/schema-backed.js +1 -1
  94. package/dist/src/internal/authored-module-conditions.d.ts +2 -0
  95. package/dist/src/internal/authored-module-conditions.js +1 -0
  96. package/dist/src/internal/authored-module-loader.d.ts +3 -0
  97. package/dist/src/internal/authored-module-loader.js +3 -3
  98. package/dist/src/internal/authored-module-map-loader.d.ts +3 -1
  99. package/dist/src/internal/authored-module-map-loader.js +1 -1
  100. package/dist/src/internal/authored-runtime-modules.d.ts +2 -0
  101. package/dist/src/internal/authored-runtime-modules.js +1 -1
  102. package/dist/src/internal/bundler/nitro-rolldown.d.ts +1 -0
  103. package/dist/src/internal/bundler/nitro-rolldown.js +1 -1
  104. package/dist/src/internal/nitro/development-generation.d.ts +2 -0
  105. package/dist/src/internal/nitro/development-generation.js +1 -1
  106. package/dist/src/internal/nitro/host/configure-nitro-routes.js +1 -1
  107. package/dist/src/internal/nitro/host/create-application-nitro.js +1 -1
  108. package/dist/src/internal/nitro/host/prepare-application-host.js +1 -1
  109. package/dist/src/internal/nitro/routes/channel-request-instrumentation.js +1 -1
  110. package/dist/src/internal/node-esm-compat-banner.d.ts +19 -6
  111. package/dist/src/internal/node-esm-compat-banner.js +2 -2
  112. package/dist/src/internal/workflow-bundle/builder-support.d.ts +5 -0
  113. package/dist/src/internal/workflow-bundle/builder.js +2 -2
  114. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.d.ts +1 -1
  115. package/dist/src/internal/workflow-bundle/dynamic-tool-transform.js +1 -1
  116. package/dist/src/internal/workflow-bundle/workflow-core-shim.js +1 -1
  117. package/dist/src/protocol/activity.d.ts +6 -0
  118. package/dist/src/protocol/activity.js +1 -1
  119. package/dist/src/protocol/message.d.ts +13 -1
  120. package/dist/src/protocol/message.js +1 -1
  121. package/dist/src/public/channels/slack/activity-plan.js +1 -1
  122. package/dist/src/public/channels/slack/activity.js +2 -2
  123. package/dist/src/public/channels/slack/connections.d.ts +2 -2
  124. package/dist/src/public/channels/slack/connections.js +1 -1
  125. package/dist/src/public/memory/file/backends/default.js +1 -1
  126. package/dist/src/public/models/openai/chatgpt/credential-store.d.ts +6 -0
  127. package/dist/src/public/models/openai/chatgpt/credential-store.js +1 -0
  128. package/dist/src/public/models/openai/chatgpt/model.d.ts +1 -1
  129. package/dist/src/public/models/openai/chatgpt/oauth.d.ts +20 -0
  130. package/dist/src/public/models/openai/chatgpt/oauth.js +1 -0
  131. package/dist/src/public/models/openai/chatgpt/token-broker.d.ts +4 -3
  132. package/dist/src/public/models/openai/chatgpt/token-broker.js +1 -1
  133. package/dist/src/public/models/openai/chatgpt/transport.d.ts +1 -2
  134. package/dist/src/public/models/openai/index.d.ts +3 -4
  135. package/dist/src/public/tools/agent.d.ts +1 -0
  136. package/dist/src/public/tools/agent.js +1 -0
  137. package/dist/src/public/tools/ask-question.d.ts +1 -0
  138. package/dist/src/public/tools/ask-question.js +1 -0
  139. package/dist/src/public/tools/connection-search.d.ts +1 -0
  140. package/dist/src/public/tools/connection-search.js +1 -0
  141. package/dist/src/public/tools/index.d.ts +1 -1
  142. package/dist/src/public/tools/task-cancel.d.ts +1 -0
  143. package/dist/src/public/tools/task-cancel.js +1 -0
  144. package/dist/src/public/tools/task-update.d.ts +1 -0
  145. package/dist/src/public/tools/task-update.js +1 -0
  146. package/dist/src/runtime/connections/openapi-schema.js +1 -1
  147. package/dist/src/runtime/prompt/compose.js +1 -1
  148. package/dist/src/runtime/resolve-agent.js +1 -1
  149. package/dist/src/runtime/resolve-tool.js +1 -1
  150. package/dist/src/runtime/subagents/dynamic-agent-config.js +1 -1
  151. package/dist/src/runtime/types.d.ts +2 -0
  152. package/dist/src/self-modification/agent.js +1 -1
  153. package/dist/src/self-modification/extension/instructions.d.ts +1 -3
  154. package/dist/src/self-modification/extension/instructions.js +4 -2
  155. package/dist/src/self-modification/filesystem.js +1 -1
  156. package/dist/src/setup/flows/chatgpt-auth.d.ts +14 -2
  157. package/dist/src/setup/flows/chatgpt-auth.js +1 -1
  158. package/dist/src/setup/flows/model.d.ts +3 -2
  159. package/dist/src/setup/flows/model.js +1 -1
  160. package/dist/src/setup/flows/provider.js +1 -1
  161. package/dist/src/setup/flows/registry.d.ts +4 -0
  162. package/dist/src/setup/flows/registry.js +1 -1
  163. package/dist/src/setup/integrations/file-memory/vercel.d.ts +2 -2
  164. package/dist/src/setup/integrations/file-memory/vercel.js +1 -1
  165. package/dist/src/setup/scaffold/create/project.js +1 -1
  166. package/dist/src/shared/agent-definition.d.ts +7 -0
  167. package/dist/src/shared/boundary-hook-error.d.ts +4 -0
  168. package/dist/src/shared/boundary-hook-error.js +1 -0
  169. package/dist/src/{execution/activity-text.d.ts → shared/presentation-text.d.ts} +1 -1
  170. package/dist/src/shared/presentation-text.js +1 -0
  171. package/dist/src/svelte/index.js +1 -1
  172. package/dist/src/svelte/use-eve-agent.js +1 -1
  173. package/dist/src/tools/definition.d.ts +29 -0
  174. package/dist/src/tools/definition.js +1 -1
  175. package/dist/src/tools/durable-callbacks.d.ts +40 -9
  176. package/dist/src/tools/durable-callbacks.js +1 -1
  177. package/dist/src/tools/dynamic.d.ts +4 -1
  178. package/dist/src/tools/framework/agent-contract.js +1 -1
  179. package/dist/src/tools/provided/bash.js +1 -1
  180. package/dist/src/tools/provided/glob.js +1 -1
  181. package/dist/src/tools/provided/grep.js +1 -1
  182. package/dist/src/tools/provided/load-skill.js +1 -1
  183. package/dist/src/tools/provided/read-file.js +1 -1
  184. package/dist/src/tools/provided/web-fetch.js +1 -1
  185. package/dist/src/tools/provided/write-file.js +1 -1
  186. package/dist/src/tools/tool-label.d.ts +1 -0
  187. package/dist/src/tools/tool-label.js +1 -0
  188. package/dist/src/tracing/agent-action-instrumentation.js +1 -1
  189. package/dist/src/tracing/agent-approval-instrumentation.js +1 -1
  190. package/dist/src/tracing/agent-channel-delivery-instrumentation.js +1 -1
  191. package/dist/src/tracing/agent-otel-provider.js +2 -2
  192. package/dist/src/tracing/agent-otel-session-context.js +1 -1
  193. package/dist/src/tracing/agent-otel-usage.d.ts +10 -0
  194. package/dist/src/tracing/agent-otel-usage.js +1 -1
  195. package/dist/src/tracing/agent-span-naming.d.ts +1 -0
  196. package/dist/src/tracing/agent-span-naming.js +1 -0
  197. package/dist/src/tracing/agent-tool-instrumentation.js +1 -1
  198. package/dist/src/vue/index.js +1 -1
  199. package/dist/src/vue/use-eve-agent.js +1 -1
  200. package/docs/channels/slack.mdx +2 -0
  201. package/docs/concepts/built-in-tools.md +397 -68
  202. package/docs/concepts/default-harness.md +6 -0
  203. package/docs/concepts/sessions-runs-and-streaming.md +4 -0
  204. package/docs/connections/openapi.mdx +4 -0
  205. package/docs/connections/overview.mdx +1 -1
  206. package/docs/evals/running.mdx +2 -0
  207. package/docs/guides/client/continuations.mdx +11 -0
  208. package/docs/guides/client/streaming.mdx +7 -1
  209. package/docs/guides/dynamic-capabilities.md +5 -5
  210. package/docs/guides/frontend/overview.mdx +1 -1
  211. package/docs/guides/hooks.md +1 -1
  212. package/docs/guides/instrumentation.md +40 -1
  213. package/docs/guides/remote-agents.md +2 -0
  214. package/docs/instructions.mdx +1 -1
  215. package/docs/memory/custom-provider.md +4 -1
  216. package/docs/memory/file.md +25 -12
  217. package/docs/memory/overview.mdx +45 -13
  218. package/docs/reference/cli.md +6 -0
  219. package/docs/reference/telemetry.md +2 -2
  220. package/docs/reference/typescript-api.md +13 -9
  221. package/docs/sandbox.mdx +14 -2
  222. package/docs/subagents/index.mdx +8 -4
  223. package/docs/tools/overview.mdx +44 -2
  224. package/docs/tools/workflows.mdx +9 -1
  225. package/docs/tutorial/connect-a-warehouse.mdx +49 -26
  226. package/docs/tutorial/first-agent.mdx +20 -19
  227. package/docs/tutorial/guard-the-spend.mdx +4 -4
  228. package/docs/tutorial/how-it-runs.mdx +2 -3
  229. package/docs/tutorial/meta.json +2 -2
  230. package/docs/tutorial/query-sample-data.mdx +30 -19
  231. package/docs/tutorial/remember-definitions.mdx +5 -5
  232. package/docs/tutorial/run-analysis.mdx +155 -18
  233. package/docs/tutorial/ship-it.mdx +71 -81
  234. package/docs/tutorial/team-playbooks.mdx +10 -10
  235. package/package.json +32 -7
  236. package/dist/src/execution/activity-text.js +0 -1
  237. package/dist/src/internal/workflow-bundle/authored-workflow-modules.d.ts +0 -9
  238. package/dist/src/internal/workflow-bundle/authored-workflow-modules.js +0 -1
  239. package/dist/src/public/models/openai/chatgpt/codex-app-server.d.ts +0 -40
  240. package/dist/src/public/models/openai/chatgpt/codex-app-server.js +0 -1
@@ -1,58 +1,81 @@
1
1
  ---
2
- title: "Connect a Warehouse"
3
- description: "Part 4 of the Build an Agent tutorial. Let each user connect their own warehouse over an OAuth MCP via Vercel Connect."
2
+ title: "Connect a Warehouse (Optional)"
3
+ description: "Optional follow-up to the Build an Agent tutorial. Let each user connect their own warehouse over an OAuth MCP via Vercel Connect."
4
4
  ---
5
5
 
6
- The sample dataset got the analytics assistant running, but it's a stand-in. Now point the agent at a real warehouse and let each user connect their own by signing in through their browser. That's what a connection is for. It's an MCP server the model reaches through tools, with auth that eve drives for you.
6
+ Complete [the tutorial with sample data](./first-agent) first. When you have a warehouse with an MCP server, you can connect it to the analytics assistant and let each user sign in through their browser. That's what a connection is for. It's an MCP server the model reaches through tools, with auth that eve drives for you.
7
7
 
8
- This step depends on Vercel Connect, which is in private beta. No Connect access? Keep the Step 3 sample dataset and read this step for the connection model. Steps 5 through 9 work against the sample dataset, so you can complete the tutorial without a warehouse.
8
+ [Vercel Connect is generally available](https://vercel.com/changelog/vercel-connect-secure-access-to-external-services-for-your-agents). You can complete the tutorial with the [sample dataset](./query-sample-data); Connect is only needed for the OAuth integration shown on this page.
9
9
 
10
- The filename sets the runtime name. Put the file at `agent/connections/warehouse.ts` and it registers as `"warehouse"`, with its tools surfaced as `warehouse__<tool>`.
10
+ ## Before you start
11
+
12
+ This integration needs:
13
+
14
+ - A warehouse account and a working MCP server for that warehouse, with its full endpoint URL. Connect manages credentials; it does not create a warehouse or turn a SQL database into an MCP server.
15
+ - Permission to authorize that server, with read-only access to the data you want the agent to query.
16
+ - A Vercel account and a linked project for Connect.
17
+ - An authenticated user on the eve session. Complete the route-auth setup in [Ship it](./ship-it#replace-placeholderauth) before trying per-user OAuth from a web app.
18
+
19
+ If you are working through the tutorial for the first time, keep `run_sql` and [continue to Run analysis](./run-analysis). You can add this integration later.
20
+
21
+ ## Register the connector
22
+
23
+ Run these commands from `analytics-assistant/`. Replace the example URL with your MCP server's actual endpoint, including its path:
24
+
25
+ ```bash
26
+ npm install @vercel/connect
27
+ npx vercel@latest link
28
+ npx vercel@latest connect create "https://your-warehouse.example/mcp" --name warehouse
29
+ ```
30
+
31
+ Follow the provider-specific registration prompts. Copy the **connector UID returned by the CLI**. The display name `warehouse` is not the connector UID. Attach the returned connector to this project, then pull the local environment:
32
+
33
+ ```bash
34
+ npx vercel@latest connect attach "<returned-connector-uid>" --yes
35
+ npx vercel@latest env pull
36
+ ```
37
+
38
+ `vercel env pull` provides `VERCEL_OIDC_TOKEN` for local requests to Connect. That token identifies your Vercel project; it does not sign an end user into the app. The steps below use the authenticated deployment from [Ship it](./ship-it#replace-placeholderauth). The [Connect reference](https://vercel.com/docs/connect) covers service-specific setup and project attachments.
11
39
 
12
40
  ## Declare the connection
13
41
 
14
- The warehouse exposes a generic SQL MCP behind OAuth. Pass `connect()` from `@vercel/connect/eve` as the auth, and Vercel Connect handles the OAuth flow, stores the tokens, and refreshes them for you:
42
+ Create `agent/connections/warehouse.ts`. Replace both the endpoint URL and connector UID below with the values you just configured:
15
43
 
16
44
  ```ts title="agent/connections/warehouse.ts"
17
45
  import { connect } from "@vercel/connect/eve";
18
46
  import { defineMcpClientConnection } from "eve/connections";
19
47
 
20
48
  export default defineMcpClientConnection({
21
- url: "https://mcp.your-warehouse.example/sse",
49
+ url: "https://your-warehouse.example/mcp",
22
50
  description: "The team's data warehouse: run read-only SQL and list tables and columns.",
23
- auth: connect("warehouse"),
51
+ auth: connect("<returned-connector-uid>"),
24
52
  });
25
53
  ```
26
54
 
27
- `"warehouse"` is the UID you chose when registering the Connect client. By default this OAuth is user-scoped. Each end-user authorizes in their own browser, and eve resolves that user's token before every tool call.
55
+ The filename registers the eve connection as `"warehouse"`, with tools named `warehouse__<tool>`. This eve name is independent of the Connect connector UID. The remote server determines which tools exist; check its tool inventory and read-only permissions before using it.
28
56
 
29
- Before testing the warehouse from a web app, make sure the eve channel route auth maps your signed-in app user to `principalType: "user"`. A Connect-backed connection can only start per-user OAuth when the active session already has an authenticated user principal. If route auth still only accepts `localDev()`, a runtime token, or a placeholder guard, the first warehouse tool call fails with `reason: "principal_required"` instead of showing the sign-in challenge.
57
+ `connect("...")` is user-scoped by default. Each end-user authorizes in their own browser, and eve resolves that user's token before a tool call. The eve channel's route auth must map the signed-in app user to `principalType: "user"`. `localDev()`, a runtime token, or a placeholder guard cannot supply that identity; those sessions fail with `reason: "principal_required"` before OAuth starts. For a shared app credential, see [app vs. user auth](../connections#choose-app-vs-user-auth).
30
58
 
31
- Once Connect is enabled on your account, wire it up:
59
+ ## Deploy and try it
32
60
 
33
- 1. Install the package: `npm install @vercel/connect`.
34
- 2. Create the Connect client: `vercel connect create <type> --name warehouse`.
35
- 3. Link the client to your project.
36
- 4. Run `vercel link` and `vercel env pull` so `VERCEL_OIDC_TOKEN` is available locally.
61
+ Deploy the updated app so the authenticated web app includes the new connection:
37
62
 
38
- For the full reference, see [MCP connections](../connections/mcp).
39
-
40
- ## What the user sees
63
+ ```bash
64
+ npx vercel@latest deploy
65
+ ```
41
66
 
42
- Ask a question that needs the warehouse:
67
+ Open the new HTTPS preview URL, sign in with the credentials configured in [Ship it](./ship-it#replace-placeholderauth), and create a new session. The tutorial's development server bypasses the browser login and uses `localDev()`; restarting `npm run dev` alone does not provide the user identity this connection requires.
43
68
 
44
- ```text
45
- How many enterprise customers signed up last month?
46
- ```
69
+ From your authenticated web app, ask the agent to inspect the warehouse's available tables and then query a table you have access to. Use the server's schema rather than assuming it has the tutorial's `orders` and `customers` tables.
47
70
 
48
- The first time, the model picks a warehouse tool but there's no token yet, so the turn parks and the channel shows a "Sign in" affordance. You authorize in the browser, and once the OAuth callback completes, the turn resumes from exactly that step (the durable parking from [Step 2](./how-it-runs)) and the query runs. Later calls in the session reuse the cached per-user token, so there's no prompt.
71
+ If the user has not authorized the connector, the turn parks and the channel shows a sign-in link. Complete authorization in the browser; after the callback succeeds, the turn resumes and retries the tool call. Later calls use the existing grant while it remains valid. See [MCP connection troubleshooting](../connections/mcp#troubleshooting) if registration succeeds but the tool cannot connect.
49
72
 
50
73
  ## The token never reaches the model
51
74
 
52
- Right before each request to the MCP server, eve resolves the bearer and sends it as `Authorization: Bearer <token>`. The model only ever sees tool names, descriptions, and results. The credential stays out of its reach.
75
+ Right before each request to the MCP server, eve resolves the bearer and sends it as `Authorization: Bearer <token>`. The model sees tool names, descriptions, and results. The credential stays in the app runtime.
53
76
 
54
- If you want more control, gate the connection behind approval (`approval: once()`) or narrow which tools the model sees (`tools.allow`). See [MCP connections](../connections/mcp).
77
+ Gate the connection behind approval (`approval: once()`) or narrow which tools the model sees (`tools.allow`) when needed. See [MCP connections](../connections/mcp).
55
78
 
56
- → Next: [Run analysis](./run-analysis)
79
+ → Return to the tutorial: [Run analysis](./run-analysis)
57
80
 
58
81
  Learn more: [MCP connections](../connections/mcp) · [Authentication](../guides/auth-and-route-protection)
@@ -1,16 +1,22 @@
1
1
  ---
2
2
  title: "Your First Agent"
3
- description: "Part 1 of the Build an Agent tutorial. Scaffold the analytics assistant, give it an analyst persona, run it, and ask a question."
3
+ description: "Scaffold the analytics assistant, give it an analyst persona, run it, and ask a question."
4
4
  ---
5
5
 
6
- The Build an Agent tutorial constructs one app end to end, a data analytics assistant. You ask in natural language, and over the next nine steps it learns to query a warehouse, run analysis in a sandbox, remember your team's metric definitions, and refuse to exceed your query budget without asking.
6
+ For a bespoke learning experience, start with the [eve template gallery](https://eve.dev/templates) or ask your coding agent to build your use case. For example:
7
7
 
8
- Step 1 gets it talking. The scaffold bundles a small sample dataset, so your first question works with zero setup.
8
+ - “Build an eve Slack agent that answers questions from our team’s documentation.”
9
+ - “Build an eve incident-response agent that investigates alerts using our observability tools.”
10
+ - “Build an eve GitHub maintainer that triages issues and summarizes pull requests.”
11
+
12
+ If you prefer a step-by-step walkthrough, continue with this tutorial. It constructs one app end to end: a data analytics assistant. You ask in natural language, and over eight steps it learns to query sample data, run analysis in a sandbox, remember your team's metric definitions, and refuse to exceed your query budget without asking.
13
+
14
+ Step 1 gets it talking. In Step 3 you create a small local dataset that you use throughout the tutorial. You can complete the tutorial without a warehouse or Vercel Connect account. [Connect a warehouse](./connect-a-warehouse) is an optional follow-up once your agent works.
9
15
 
10
16
  ## Prerequisites
11
17
 
12
18
  - Node 24 or newer and npm.
13
- - A model credential. The scaffold's default model goes through the [Vercel AI Gateway](../getting-started), so you need `AI_GATEWAY_API_KEY` (or `VERCEL_OIDC_TOKEN` pulled via `vercel link`). A direct provider model like `anthropic("claude-opus-4-8")` instead needs that provider's AI SDK package and key, here `@ai-sdk/anthropic` and `ANTHROPIC_API_KEY`.
19
+ - A working model credential, configured through [Getting Started](../getting-started). Keep the model and credential you already tested there.
14
20
 
15
21
  If you have not run eve before, complete [Getting Started](../getting-started) first. Without a credential, "Run the agent" below fails when the runtime tries to reach the model; the dev TUI's `/model` flow walks you through pasting a key or linking a project.
16
22
 
@@ -21,24 +27,19 @@ npx eve@latest init analytics-assistant
21
27
  cd analytics-assistant
22
28
  ```
23
29
 
24
- The command writes the starter agent with eve's default model and built-in HTTP API
25
- channel (`agent/channels/eve.ts`), installs dependencies, initializes Git, and
26
- starts the development server. Stop the server before continuing with the edits
27
- below. It does not create a Vercel project or deploy. `init` creates the
30
+ The command writes the starter agent and built-in HTTP API channel
31
+ (`agent/channels/eve.ts`), installs dependencies, initializes Git, and offers
32
+ to start the development server. Stop the server if you started it before
33
+ continuing with the edits below. It does not create a Vercel project or deploy. `init` creates the
28
34
  `analytics-assistant/` directory, so `cd` into it before running further
29
35
  commands.
30
36
 
31
- ## Set the model
32
-
33
- `agent/agent.ts` holds the model and config. Use a capable model for analysis work:
37
+ ## Keep your configured model
34
38
 
35
- ```ts
36
- import { defineAgent } from "eve";
37
-
38
- export default defineAgent({
39
- model: "anthropic/claude-opus-4.8",
40
- });
41
- ```
39
+ `agent/agent.ts` holds the model and config. Use the same model and credentials
40
+ that worked in Getting Started. The tutorial does not require switching providers.
41
+ If you need to configure this new project, use `/model` in the dev TUI before
42
+ sending your first question.
42
43
 
43
44
  ## Give it an analyst persona
44
45
 
@@ -53,7 +54,7 @@ You are a senior data analyst. You answer questions about the team's data.
53
54
  the data, say so plainly.
54
55
  ```
55
56
 
56
- Instructions are identity and standing rules. On-demand procedures belong in skills (Step 7), and actions belong in tools (Step 3). See [Instructions](../instructions).
57
+ Instructions are identity and standing rules. On-demand procedures belong in skills (Step 6), and actions belong in tools (Step 3). See [Instructions](../instructions).
57
58
 
58
59
  ## Run the agent
59
60
 
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  title: "Guard the Spend"
3
- description: "Part 8 of the Build an Agent tutorial. Gate expensive queries with cost-based approval. The agent pauses, asks, and resumes."
3
+ description: "Part 7 of the Build an Agent tutorial. Gate expensive queries with cost-based approval. The agent pauses, asks, and resumes."
4
4
  ---
5
5
 
6
- A single warehouse query can scan terabytes and run up the bill. So before the analytics assistant fires off an expensive scan, make it stop and check with you. The agent pauses, asks you, and resumes with your answer. That's human-in-the-loop, and you wire it up with one field on the tool.
6
+ Use the sample dataset to practice an approval before a tool runs. The example assigns a simulated scan cost to each query; it does not incur warehouse charges. Make the analytics assistant stop and check with you before a query exceeds that threshold. The agent pauses, asks you, and resumes with your answer. That's human-in-the-loop, and you wire it up with one field on the tool.
7
7
 
8
8
  `approval` runs before `execute`. Return `"user-approval"` and the turn parks on an approval request; you answer, and the run picks up from that exact step. The function gets the tool input, so you can make the decision cost-based.
9
9
 
10
10
  ## Estimate, then gate
11
11
 
12
- This step keeps `run_sql` on the Step 3 sample dataset so you can demo the gate locally. With a real warehouse you'd gate the warehouse connection tool from Step 4 the same way, on a dry-run byte estimate instead of the toy heuristic below.
12
+ This step keeps `run_sql` on the Step 3 sample dataset so you can demo the gate locally. If you later [connect a warehouse](./connect-a-warehouse), use the provider's dry-run estimate to decide when approval is needed.
13
13
 
14
14
  Add a cheap estimator and gate `run_sql` on it:
15
15
 
@@ -30,7 +30,7 @@ const THRESHOLD_GB = 50;
30
30
 
31
31
  export default defineTool({
32
32
  description: "Run a read-only SQL query against the analytics tables.",
33
- inputSchema: z.object({ sql: z.string() }),
33
+ inputSchema: z.object({ sql: z.string().max(10_000) }),
34
34
  // Cost-based gate: only the expensive queries need a human yes.
35
35
  approval: ({ toolInput }) =>
36
36
  estimateScanGb(toolInput?.sql ?? "") > THRESHOLD_GB ? "user-approval" : "not-applicable",
@@ -15,9 +15,8 @@ Each turn runs as a durable workflow, and eve saves progress at every step. Comp
15
15
 
16
16
  That's why the features in the rest of this tutorial work the way they do:
17
17
 
18
- - The warehouse sign-in in Step 4 parks the turn until you authorize in the browser. A few minutes is fine.
19
- - The metric glossary in Step 6 survives across turns. State is checkpointed at step boundaries, so it sticks.
20
- - The spend approval in Step 8 pauses the turn on your yes/no, then picks up exactly where it left off.
18
+ - The metric glossary in Step 5 survives across turns. State is checkpointed at step boundaries, so it sticks.
19
+ - The spend approval in Step 7 pauses the turn on your yes/no, then picks up exactly where it left off.
21
20
 
22
21
  You author capabilities, including tools, instructions, channels, and skills. eve drives the model-to-tool loop and decides when a turn continues, waits, or ends. You never write that loop yourself.
23
22
 
@@ -5,11 +5,11 @@
5
5
  "first-agent",
6
6
  "how-it-runs",
7
7
  "query-sample-data",
8
- "connect-a-warehouse",
9
8
  "run-analysis",
10
9
  "remember-definitions",
11
10
  "team-playbooks",
12
11
  "guard-the-spend",
13
- "ship-it"
12
+ "ship-it",
13
+ "connect-a-warehouse"
14
14
  ]
15
15
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Query Sample Data"
3
- description: "Part 3 of the Build an Agent tutorial. Add a run_sql tool over the bundled sample dataset and watch the tool loop."
3
+ description: "Part 3 of the Build an Agent tutorial. Add a run_sql tool over a local sample dataset and watch the tool loop."
4
4
  ---
5
5
 
6
6
  The analytics assistant can hold a conversation, but it can't see a single row of data. Give it a tool. A tool is the action primitive. Typed input goes in, your code runs, structured output comes back. The name the model sees is the filename, so `agent/tools/run_sql.ts` becomes the tool `run_sql`.
@@ -14,27 +14,21 @@ npm install sql.js
14
14
  npm install --save-dev @types/sql.js
15
15
  ```
16
16
 
17
- `sql.js` loads its WebAssembly binary from the installed package at runtime. Keep the package external so the binary remains next to its JavaScript:
17
+ `sql.js` loads its WebAssembly binary from the installed package at runtime. Keep the package external so the binary remains next to its JavaScript. Add this `build` property inside your existing `defineAgent({ ... })` in `agent/agent.ts`, keeping your configured `model` and its imports:
18
18
 
19
- ```ts title="agent/agent.ts" {5-7}
20
- import { defineAgent } from "eve";
21
-
22
- export default defineAgent({
23
- model: "anthropic/claude-opus-4.8",
24
- build: {
25
- externalDependencies: ["sql.js"],
26
- },
27
- });
19
+ ```ts
20
+ build: {
21
+ externalDependencies: ["sql.js"],
22
+ },
28
23
  ```
29
24
 
30
25
  Restart the dev server after changing `externalDependencies`. eve reads this setting when the server starts.
31
26
 
32
27
  ## A tiny sample dataset
33
28
 
34
- Store a small in-memory dataset under `agent/lib/`. Keep it tiny. This is throwaway scaffolding, not the real warehouse (that comes in Step 4).
29
+ Create `agent/lib/sample-db.ts` with the dataset below. These four orders and three customers supply the data for every remaining tutorial step. No database server, account, or connection string is needed. The dataset is recreated when the app restarts.
35
30
 
36
31
  ```ts title="agent/lib/sample-db.ts"
37
- // A toy SQLite-in-memory stand-in. Swap for your real warehouse in Step 4.
38
32
  import initSqlJs from "sql.js";
39
33
 
40
34
  const SEED = `
@@ -60,12 +54,27 @@ async function db() {
60
54
 
61
55
  export async function runReadOnlySql(sql: string) {
62
56
  const database = await db();
63
- const [result] = database.exec(sql);
64
- if (!result) return { columns: [], rows: [] as unknown[][] };
65
- return { columns: result.columns, rows: result.values };
57
+ const query = `SELECT * FROM (\n${sql.trim().replace(/;$/, "")}\n) LIMIT 501`;
58
+ const statement = database.prepare(query);
59
+ try {
60
+ // prepare() accepts the first statement; reject any unparsed trailing SQL.
61
+ if (statement.getSQL() !== query) {
62
+ throw new Error("Provide a single SELECT query.");
63
+ }
64
+ const columns = statement.getColumnNames();
65
+ const rows: unknown[][] = [];
66
+ while (rows.length < 501 && statement.step()) rows.push(statement.get());
67
+ return { columns, rows };
68
+ } finally {
69
+ statement.free();
70
+ }
66
71
  }
67
72
  ```
68
73
 
74
+ The outer `SELECT` lets SQLite accept queries, including `WITH` queries, while rejecting writes and configuration statements. The helper prepares one statement, rejects trailing SQL, and reads at most 501 rows. The extra row lets the tool report that its 500-row output was truncated. It frees the statement even when a query fails.
75
+
76
+ This is an in-memory tutorial database. A row limit does not bound the work needed to compute a query; use database-enforced read-only permissions and query timeouts when adapting the tool to a production database.
77
+
69
78
  ## Define the run_sql tool
70
79
 
71
80
  ```ts title="agent/tools/run_sql.ts"
@@ -78,7 +87,7 @@ export default defineTool({
78
87
  "Run a read-only SQL query against the analytics tables (orders, customers) " +
79
88
  "and return the columns and rows.",
80
89
  inputSchema: z.object({
81
- sql: z.string().describe("A single read-only SELECT statement."),
90
+ sql: z.string().max(10_000).describe("A single read-only SELECT statement."),
82
91
  }),
83
92
  async execute({ sql }) {
84
93
  const { columns, rows } = await runReadOnlySql(sql);
@@ -98,8 +107,10 @@ Restart the dev server with `npm run dev` and ask:
98
107
  Which customer has spent the most, and how much?
99
108
  ```
100
109
 
101
- Watch the loop play out in the TUI. The model emits a `run_sql` call, eve runs your `execute`, and the rows come back as a tool result. The model reads them and answers with a real number. eve drove the whole loop. All you supplied was the tool.
110
+ Watch the loop play out in the TUI. The model emits a `run_sql` call, eve runs your `execute`, and the rows come back as a tool result. The model reads them and answers with a real number. The result is Globex with 9,900 cents ($99.00). The amounts are stored in cents, so divide by 100 when reporting dollars. eve drove the whole loop; you supplied the tool.
111
+
112
+ Keep `run_sql` and the sample database for the rest of the tutorial. When you have your own data service, [Connect a warehouse](./connect-a-warehouse) explains the optional integration.
102
113
 
103
- → Next: [Connect a warehouse](./connect-a-warehouse)
114
+ → Next: [Run analysis](./run-analysis)
104
115
 
105
116
  Learn more: [Tools](../tools)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Remember Definitions"
3
- description: "Part 6 of the Build an Agent tutorial. Use defineState to remember the team's metric glossary across turns."
3
+ description: "Part 5 of the Build an Agent tutorial. Use defineState to remember the team's metric glossary across turns."
4
4
  ---
5
5
 
6
6
  Every team has house definitions for the analytics assistant. "Active" means a purchase in the last 30 days, revenue is net of refunds, a "week" starts Monday. Re-explaining all of that on every turn is a waste. State gives the agent a place to keep them.
@@ -55,15 +55,15 @@ export default defineTool({
55
55
  ## See it persist
56
56
 
57
57
  ```text
58
- > For us, an active customer is one with a purchase in the last 30 days.
58
+ > For this dataset, a high-value customer has at least $50 in total orders.
59
59
  Remember that.
60
- → calls define_metric("active customer", "purchase in the last 30 days")
60
+ → calls define_metric("high-value customer", "at least $50 in total orders")
61
61
 
62
- > How many active customers do we have?
62
+ > How many high-value customers do we have?
63
63
  → recalls the definition, writes the matching SQL, answers
64
64
  ```
65
65
 
66
- The second turn is a separate turn in the same session, yet the definition is still there. State checkpoints at step boundaries, so it's the same durability from [Step 2](./how-it-runs), now applied to your own data.
66
+ The sample data has two high-value customers: Acme ($57) and Globex ($99). This definition works with the same fixed dataset whenever you run the tutorial. The second turn is a separate turn in the same session, yet the definition is still there. State checkpoints at step boundaries, so it's the same durability from [Step 2](./how-it-runs), now applied to your own data.
67
67
 
68
68
  State is scoped to a session and isolated per agent, so a subagent starts with fresh state and never sees the parent's. Need to reset something each turn? Call `update(() => fresh)` in a lifecycle hook. More in [State](../concepts/state).
69
69
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: "Run Analysis"
3
- description: "Part 5 of the Build an Agent tutorial. Seed the warehouse schema into the sandbox workspace, then compute and chart beyond SQL."
3
+ description: "Part 4 of the Build an Agent tutorial. Seed the sample database schema into the sandbox workspace, then compute and chart beyond SQL."
4
4
  ---
5
5
 
6
6
  SQL tells the analytics assistant the numbers, but a cohort curve, a forecast, or a chart needs real computation. That's what the sandbox is for. It's an isolated bash environment with a `/workspace` filesystem, and every agent gets exactly one.
@@ -9,7 +9,7 @@ This takes two pieces. First seed reference files the model can read, then compu
9
9
 
10
10
  ## Seed the schema into the workspace
11
11
 
12
- Mount the warehouse schema into the sandbox so the model isn't guessing at table shapes. Seeding uses the folder sandbox layout, where anything under `agent/sandbox/workspace/` lands in the live `/workspace` cwd at session bootstrap.
12
+ Mount the sample database schema into the sandbox so the model isn't guessing at table shapes. Seeding uses the folder sandbox layout, where anything under `agent/sandbox/workspace/` lands in the live `/workspace` cwd at session bootstrap.
13
13
 
14
14
  ```text
15
15
  agent/sandbox/
@@ -21,65 +21,202 @@ agent/sandbox/
21
21
  ```sql
22
22
  -- agent/sandbox/workspace/schema.sql
23
23
  -- Reference only: table shapes the analyst can read before writing queries.
24
- CREATE TABLE orders (id INT, customer_id INT, amount_cents INT, created_at DATE);
25
- CREATE TABLE customers (id INT, name TEXT, plan TEXT, signed_up_at DATE);
24
+ CREATE TABLE orders (id INTEGER, customer_id INTEGER, amount_cents INTEGER, created_at TEXT);
25
+ CREATE TABLE customers (id INTEGER, name TEXT, plan TEXT);
26
26
  ```
27
27
 
28
- Top-level workspace entries get advertised to the model automatically, so it knows `schema.sql` is there to read. No `agent/sandbox/sandbox.ts` required. A `workspace/` folder keeps the default sandbox and seeds your files into it.
28
+ Top-level workspace entries get advertised to the model automatically, so it knows `schema.sql` is there to read. A `workspace/` folder seeds your files without requiring a sandbox definition. Add one next to install the chart dependencies.
29
+
30
+ ## Install the chart dependencies
31
+
32
+ Python needs a sandbox that runs real binaries. For local development, start a Docker-compatible daemon and run `docker info` to verify it is reachable before restarting `npm run dev`. eve also supports [microsandbox](../sandbox#microsandbox) on compatible hosts. The `just-bash` fallback cannot run Python or install packages; installing Python on your laptop does not add it to the sandbox. You can [continue to the next chapter](./remember-definitions) if you do not want to set up a local container or VM yet.
33
+
34
+ The default eve images for Docker, microsandbox, and Vercel Sandbox include Python, but not matplotlib. Add this definition alongside `workspace/` to install matplotlib in a virtual environment. It uses the same environment for installation and chart execution, without changing the system Python:
35
+
36
+ ```ts title="agent/sandbox/sandbox.ts"
37
+ import { defineSandbox } from "eve/sandbox";
38
+
39
+ export default defineSandbox({
40
+ async bootstrap({ use }) {
41
+ const sandbox = await use();
42
+ const commands = [
43
+ "sudo apt-get update && sudo apt-get install -y python3 python3-venv",
44
+ "python3 -m venv /workspace/.venv",
45
+ "/workspace/.venv/bin/python -m pip install matplotlib==3.10.8",
46
+ "/workspace/.venv/bin/python -c \"import matplotlib.pyplot; print('Chart dependencies ready')\"",
47
+ ];
48
+ for (const command of commands) {
49
+ const result = await sandbox.run({ command });
50
+ if (result.exitCode !== 0) {
51
+ throw new Error(
52
+ `Chart setup failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
53
+ );
54
+ }
55
+ }
56
+ },
57
+ });
58
+ ```
59
+
60
+ Bootstrap runs when eve builds the sandbox template, and sessions inherit the installed environment. The first build needs network access to Ubuntu package repositories and PyPI. Keep the default network policy while following this example; a custom `deny-all` or allow-list policy must permit these downloads during bootstrap. The same definition uses Vercel Sandbox after deployment to Vercel.
61
+
62
+ Restart `npm run dev` after adding the definition. Before asking for a chart, send this message in the TUI:
63
+
64
+ ```text
65
+ Run /workspace/.venv/bin/python -c "import matplotlib; print(matplotlib.__version__)" in the sandbox.
66
+ ```
67
+
68
+ The command should print `3.10.8`. If setup fails, check the dev server's sandbox logs for the failing command. Resolve the package download or backend error before continuing.
29
69
 
30
70
  ## Compute and chart in the sandbox
31
71
 
32
72
  The built-in `bash`, `read_file`, and `write_file` tools already target the sandbox. When you write your own analysis steps, grab a live handle with `ctx.getSandbox()`:
33
73
 
34
74
  ```ts title="agent/tools/chart_series.ts"
75
+ import { randomUUID } from "node:crypto";
35
76
  import { defineTool } from "eve/tools";
36
77
  import { z } from "zod";
37
78
 
38
79
  export default defineTool({
39
80
  description:
40
81
  "Plot a time series to a PNG in the workspace. Pass {date, value} points; " +
41
- "returns the chart path.",
82
+ "returns the chart path and PNG data for the client to save.",
42
83
  inputSchema: z.object({
43
- title: z.string(),
44
- points: z.array(z.object({ date: z.string(), value: z.number() })),
84
+ title: z.string().min(1).max(120),
85
+ points: z
86
+ .array(z.object({ date: z.string().max(40), value: z.number() }))
87
+ .min(1)
88
+ .max(366),
45
89
  }),
46
90
  async execute({ title, points }, ctx) {
47
91
  const sandbox = await ctx.getSandbox();
92
+ const directory = `analysis/${randomUUID()}`;
48
93
  await sandbox.writeTextFile({
49
- path: "analysis/series.json",
94
+ path: `${directory}/series.json`,
50
95
  content: JSON.stringify({ title, points }),
51
96
  });
52
97
  await sandbox.writeTextFile({
53
- path: "analysis/plot.py",
98
+ path: `${directory}/plot.py`,
54
99
  content: [
55
100
  "import json, matplotlib",
56
101
  "matplotlib.use('Agg')",
57
102
  "import matplotlib.pyplot as plt",
58
103
  "d = json.load(open('series.json'))",
104
+ "plt.figure(figsize=(8, 4), dpi=100)",
59
105
  "plt.plot([p['date'] for p in d['points']], [p['value'] for p in d['points']])",
60
106
  "plt.title(d['title']); plt.savefig('chart.png')",
61
107
  ].join("\n"),
62
108
  });
63
- const root = sandbox.resolvePath("analysis");
64
- await sandbox.run({ command: `cd ${JSON.stringify(root)} && python plot.py` });
65
- return { chart: `${root}/chart.png` };
109
+ const root = sandbox.resolvePath(directory);
110
+ const result = await sandbox.run({
111
+ command: `cd ${JSON.stringify(root)} && /workspace/.venv/bin/python plot.py`,
112
+ });
113
+ if (result.exitCode !== 0) {
114
+ throw new Error(
115
+ `Chart generation failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`,
116
+ );
117
+ }
118
+ const chart = `${root}/chart.png`;
119
+ const png = await sandbox.readBinaryFile({ path: chart });
120
+ if (!png || png.byteLength === 0) {
121
+ throw new Error("The chart command did not produce a PNG.");
122
+ }
123
+ if (png.byteLength > 1024 * 1024) {
124
+ throw new Error("The chart exceeds the 1 MiB download limit. Plot fewer points.");
125
+ }
126
+ return { chart, pngBase64: Buffer.from(png).toString("base64") };
127
+ },
128
+ toModelOutput({ chart }) {
129
+ return {
130
+ type: "text",
131
+ value: `Created ${chart}. The PNG is available in the tool result for the client to save.`,
132
+ };
66
133
  },
67
134
  });
68
135
  ```
69
136
 
70
- This tool shells out to `python` with matplotlib, which the sandbox base image does not preinstall. Install the runtime in sandbox bootstrap (or bake it into a custom image) so `python plot.py` resolves. See [Sandbox](../sandbox) for where bootstrap runs.
137
+ The tool uses the Python environment installed in bootstrap. `matplotlib.use('Agg')` selects a renderer that writes PNGs without a desktop display.
71
138
 
72
- Now ask for something past plain SQL. If you skipped Step 4, this still works against the Step 3 sample dataset:
139
+ `sandbox.run()` returns an exit code even when the command fails. Check `exitCode` before returning a chart path. Throwing an error gives the model the failed command's output, including a missing Python executable or matplotlib import, so it can report the failure instead of claiming a chart exists.
140
+
141
+ Ask for a chart using the sample data from Step 3:
73
142
 
74
143
  ```text
75
- Plot total order revenue per customer.
144
+ Plot daily order revenue in dollars for May 2026.
145
+ ```
146
+
147
+ The model queries `run_sql` for daily totals, divides `amount_cents` by 100, and passes date/value points to `chart_series`. The sample data has four dates with revenue of $42, $15, $99, and $8. The chart uses the same data you queried in Step 3.
148
+
149
+ ## Save the chart to your computer
150
+
151
+ A sandbox path is not a download URL or a file on your laptop. `readBinaryFile` copies the generated PNG out of the sandbox into the tool result, where your client can use it. `toModelOutput` sends the model only a short description, so the image's base64 data does not fill its context. The TUI shows tool results as text; it does not save or preview this PNG automatically.
152
+
153
+ Create this script in your project root. It asks for a chart through the same local HTTP API as the TUI, then saves the successful tool result as `chart.png` in the directory where you run the script:
154
+
155
+ ```ts title="save-chart.ts"
156
+ import { writeFile } from "node:fs/promises";
157
+ import { resolve } from "node:path";
158
+ import { Client } from "eve/client";
159
+ import { z } from "zod";
160
+
161
+ const host = process.argv[2];
162
+ if (!host) throw new Error("Usage: node save-chart.ts <dev-server-url>");
163
+
164
+ const client = new Client({ host });
165
+ const { response } = await client.sessions.create({
166
+ message: "Use run_sql and chart_series to plot daily order revenue in May 2026.",
167
+ });
168
+ const turn = await response.result();
169
+ if (turn.status === "failed") throw new Error("The agent turn failed. Check the dev server logs.");
170
+ if (turn.inputRequests.length > 0) {
171
+ const prompts = turn.inputRequests.map((request) => request.prompt).join("\n");
172
+ throw new Error(
173
+ `Session ${turn.sessionId} needs your input before it can create the chart:\n${prompts}`,
174
+ );
175
+ }
176
+
177
+ const chartOutput = z.object({ pngBase64: z.string().min(1).max(1_398_104) });
178
+ let saved = false;
179
+ for (const event of turn.events) {
180
+ if (event.type !== "action.result") continue;
181
+ const result = event.data.result;
182
+ if (result.kind !== "tool-result" || result.toolName !== "chart_series" || result.isError)
183
+ continue;
184
+
185
+ const { pngBase64 } = chartOutput.parse(result.output);
186
+ const png = Buffer.from(pngBase64, "base64");
187
+ if (
188
+ png.byteLength > 1024 * 1024 ||
189
+ !png.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
190
+ ) {
191
+ throw new Error("The tool result is not a PNG within the 1 MiB download limit.");
192
+ }
193
+ await writeFile("chart.png", png);
194
+ saved = true;
195
+ }
196
+ if (!saved) {
197
+ console.error(turn.message ?? "The agent returned no explanation.");
198
+ throw new Error("No chart was returned. Check the agent's response above and sandbox setup.");
199
+ }
200
+ console.log(`Saved ${resolve("chart.png")}`);
201
+ ```
202
+
203
+ Keep `npm run dev` running. In a second terminal, run the script with the actual URL printed by the dev server; for example:
204
+
205
+ ```bash
206
+ node save-chart.ts http://localhost:2000
76
207
  ```
77
208
 
78
- The model queries for the numbers (the warehouse from Step 4, or the sample dataset if you skipped it), checks `schema.sql` to get the grain right, then calls `chart_series` to render the PNG in `/workspace`.
209
+ Open the saved `chart.png` in your image viewer. This creates a separate session from your TUI conversation and overwrites an existing `chart.png`. A failed tool call or a turn without a chart fails the script instead of claiming a file was saved.
210
+
211
+ This step does not require approvals. If you return after adding the approval gate in [Guard the spend](./guard-the-spend), the script reports any pending request with its session ID. [Respond through the client](../guides/client/messages#answer-human-input-requests) before collecting that session's chart result.
212
+
213
+ The same client can save a chart from a hosted agent: pass the deployment URL and configure [client authentication](../guides/client/overview#authentication) for that deployment. The file is saved on the computer running the script. In a web UI, consume the same `action.result` output to build an image preview or download control. Returning bytes from a tool does not add that UI automatically.
214
+
215
+ This example caps charts at 1 MiB because tool outputs are persisted with session events. For larger files, copy the bytes to your application's file storage and return an authorized download URL instead. Sandbox files themselves are not permanent artifact storage.
79
216
 
80
217
  ## Secrets stay out of the sandbox
81
218
 
82
- The sandbox has no `process.env` and no access to your app's secrets. Your warehouse token lives in the app runtime, and firewall brokering is the only path it takes to the warehouse host. It never enters the sandbox process.
219
+ The sandbox has no `process.env` and no access to your app's secrets. The `run_sql` tool and sample database run in your app. You pass only the query results to the chart tool; the sandbox does not need database credentials.
83
220
 
84
221
  The local backend runs the sandbox on your laptop during `eve dev`; on Vercel it runs on Vercel Sandbox. Lifecycle, backends, and network policy are in [Sandbox](../sandbox).
85
222