mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
e6ecaf6fb3
Signed-off-by: Andrew Barba <barba@hey.com>
63 lines
3.1 KiB
Plaintext
63 lines
3.1 KiB
Plaintext
---
|
|
title: "Guard the Spend"
|
|
description: "Part 7 of the Build an Agent tutorial. Gate expensive queries with cost-based approval. The agent pauses, asks, and resumes."
|
|
---
|
|
|
|
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.
|
|
|
|
`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.
|
|
|
|
## Estimate, then gate
|
|
|
|
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.
|
|
|
|
Add a cheap estimator and gate `run_sql` on it:
|
|
|
|
```ts title="agent/lib/cost.ts"
|
|
// Illustrative: a real warehouse exposes a dry-run byte estimate.
|
|
export function estimateScanGb(sql: string): number {
|
|
return /\bwhere\b/i.test(sql) ? 1 : 200; // unfiltered scans are the expensive ones
|
|
}
|
|
```
|
|
|
|
```ts title="agent/tools/run_sql.ts"
|
|
import { defineTool } from "eve/tools";
|
|
import { z } from "zod";
|
|
import { runReadOnlySql } from "../lib/sample-db";
|
|
import { estimateScanGb } from "../lib/cost";
|
|
|
|
const THRESHOLD_GB = 50;
|
|
|
|
export default defineTool({
|
|
description: "Run a read-only SQL query against the analytics tables.",
|
|
inputSchema: z.object({ sql: z.string().max(10_000) }),
|
|
// Cost-based gate: only the expensive queries need a human yes.
|
|
approval: ({ toolInput }) =>
|
|
estimateScanGb(toolInput?.sql ?? "") > THRESHOLD_GB ? "user-approval" : "not-applicable",
|
|
async execute({ sql }) {
|
|
const { columns, rows } = await runReadOnlySql(sql);
|
|
return { columns, rows: rows.slice(0, 500), truncated: rows.length > 500 };
|
|
},
|
|
});
|
|
```
|
|
|
|
Cheap queries run straight through. A query estimated above the threshold trips the gate.
|
|
|
|
## Pause, ask, resume
|
|
|
|
Ask for something that forces a large unfiltered scan:
|
|
|
|
```text
|
|
Total revenue across all customers, all time, broken out by day.
|
|
```
|
|
|
|
The model proposes the query, `approval` returns `"user-approval"`, and the turn parks. The stream emits `input.requested`, then `session.waiting`. How the prompt looks depends on the channel, whether buttons in the TUI, Block Kit in Slack, or a UI control on the web. Approve it and the run resumes from exactly that step, then the query runs. Deny it and the tool is skipped, with the model told why.
|
|
|
|
Each session has exactly one active continuation. Answer an approval against a stale handle and it's rejected, so there's no way to double-resume the same parked turn.
|
|
|
|
The same machinery backs the built-in `ask_question` tool, where the model asks you mid-turn, and per-connection approval via `approval: once()`. See [Tools and human-in-the-loop](../tools).
|
|
|
|
→ Next: [Ship it](./ship-it)
|
|
|
|
Learn more: [Tools and human-in-the-loop](../tools)
|