|
| 1 | +'use agent'; |
| 2 | +import { useModel, useTool } from '@flue/runtime'; |
| 3 | +import * as Sentry from '@sentry/node'; |
| 4 | +import * as v from 'valibot'; |
| 5 | +import DataLoader from 'dataloader'; |
| 6 | + |
| 7 | +// The `'use agent'` directive is how `@flue/vite` finds this module and binds an identity to it at |
| 8 | +// build time. That binding is the part a hand-written scenario cannot reproduce, so it is the main |
| 9 | +// reason this app exists alongside the node-integration-test suite. |
| 10 | +export function Hello() { |
| 11 | + useModel('openrouter/anthropic/claude-haiku-4.5'); |
| 12 | + |
| 13 | + useTool({ |
| 14 | + name: 'get_weather', |
| 15 | + description: 'Get the current weather for a city.', |
| 16 | + input: v.object({ city: v.string() }), |
| 17 | + // Wrapped in a manual span: Flue runs the tool while the SDK's `execute_tool` span is active, |
| 18 | + // so this should nest directly under it rather than landing beside it. |
| 19 | + run: ({ city }) => |
| 20 | + Sentry.startSpan( |
| 21 | + { name: 'resolve-weather', attributes: { 'weather.source': 'static-table', 'weather.city': city } }, |
| 22 | + () => { |
| 23 | + return `It is 21 degrees and sunny in ${city}.`; |
| 24 | + }, |
| 25 | + ), |
| 26 | + }); |
| 27 | + |
| 28 | + // Called from inside a tool on purpose: the dataloader span then lands under `execute_tool` in |
| 29 | + // the agent's trace, which is what "captured alongside the AI spans" has to mean. |
| 30 | + useTool({ |
| 31 | + name: 'count_items', |
| 32 | + description: 'Count items by loading them. Call this when the user asks to count items.', |
| 33 | + input: v.object({}), |
| 34 | + // Constructed per execution, like node-eve does: a module-level loader caches its keys, so a |
| 35 | + // second call would skip the batch function and emit no span. |
| 36 | + run: async () => { |
| 37 | + const loader = new DataLoader<number, number>(async keys => keys.map(key => key * 2)); |
| 38 | + const doubled = await Promise.all([loader.load(1), loader.load(2), loader.load(3)]); |
| 39 | + return `Loaded ${doubled.length} items: ${doubled.join(', ')}.`; |
| 40 | + }, |
| 41 | + }); |
| 42 | + |
| 43 | + useTool({ |
| 44 | + name: 'fail_now', |
| 45 | + description: 'Always throws an error. Call this when the user asks to trigger a failure.', |
| 46 | + input: v.object({}), |
| 47 | + run: () => { |
| 48 | + throw new Error('Intentional flue tool failure'); |
| 49 | + }, |
| 50 | + }); |
| 51 | + |
| 52 | + return 'You are a helpful assistant. Use get_weather when asked about weather, count_items when asked to count items, and fail_now when asked to fail.'; |
| 53 | +} |
0 commit comments