TypeScript Quickstart
Install the TypeScript SDK, wrap your agent, and see runs in the dashboard in five minutes.
Instrument a Vercel AI SDK agent and see its decision trace, token usage, and any loops in the dashboard.
Prerequisites
- A running keelwave server. Follow Self-hosting with Docker.
The API and dashboard both listen on
http://localhost:8080. - An API key.
make seedincore/creates a dev project and prints a plaintextkw_...key. The server only stores its SHA-256 hash, so copy it when it is printed. - Node.js 18 or newer.
Keep the kw_... key out of source control. Every example below reads it
from the environment.
Install
npm install keelwaveThe Vercel AI SDK is an optional peer dependency — core tracing never imports
it. Install it only if you use the keelwave/vercel-ai adapter:
npm install ai @ai-sdk/openai zodzod is used below to describe tool parameters to the AI SDK; it is not a
keelwave dependency.
Set your environment
export KEELWAVE_API_KEY=kw_... # from `make seed`
export KEELWAVE_ENDPOINT=http://localhost:8080
export OPENAI_API_KEY=sk-...KEELWAVE_API_KEY and KEELWAVE_ENDPOINT are a convention used by the
examples, not magic names — the SDK reads nothing from the environment on its
own. You pass both to the constructor explicitly.
Initialize the client
import { Keelwave } from 'keelwave'
const client = new Keelwave({
apiKey: process.env.KEELWAVE_API_KEY ?? '',
endpoint: process.env.KEELWAVE_ENDPOINT ?? 'http://localhost:8080',
})endpoint defaults to http://localhost:8080 and a trailing slash is
stripped. By default the SDK is fail-soft: ingest errors are logged as warnings
and never thrown into your agent. Pass raiseOnError: true to surface them
instead — useful while you are wiring things up.
Check the connection before you go further:
console.log(await client.health())Instrument the agent
Three pieces do the work:
wrapModel(client, model)fromkeelwave/vercel-aiwraps anyLanguageModelV1in middleware. EverygenerateText/streamTextcall through it is recorded as an LLM trace with model, provider, tokens, latency, and status.client.observe({ name, stepType })returns a decorator that wraps a function. With the defaultstepTypeof'tool_call'it emits a tool call step whose input is fingerprinted, so a repeated call with identical arguments flags the run as looping.client.agent({ name })wraps an async function in a run. It opens the run before the body and closes it after, recordingcompleted/failed, duration, and the totals. Steps emitted inside link to the run throughAsyncLocalStorage, so you never pass a run object around.
Save this as agent.ts:
import { openai } from '@ai-sdk/openai'
import { generateText, tool } from 'ai'
import { Keelwave } from 'keelwave'
import { wrapModel } from 'keelwave/vercel-ai'
import { z } from 'zod'
const client = new Keelwave({
apiKey: process.env.KEELWAVE_API_KEY ?? '',
endpoint: process.env.KEELWAVE_ENDPOINT ?? 'http://localhost:8080',
})
const model = wrapModel(client, openai('gpt-4o'))
const webSearch = client.observe({ name: 'web_search', stepType: 'tool_call' })(
async (q: string): Promise<{ results: Array<string> }> => {
// Replace with a real search implementation.
return { results: [`result for: ${q}`] }
},
)
const runAgent = client.agent({ name: 'research-agent' })(async (
question: string,
): Promise<string> => {
const { text } = await generateText({
model,
prompt: question,
tools: {
web_search: tool({
description: 'Search the web for a query.',
parameters: z.object({ q: z.string() }),
execute: async ({ q }) => webSearch(q),
}),
},
maxSteps: 8,
})
return text
})
console.log(await runAgent('what changed in our churn numbers last week?'))Run it
npm install -D tsx
node --import tsx agent.tstsx runs the TypeScript file directly. On Node 22.6+ you can instead use
node --experimental-strip-types agent.ts, or compile with tsc first.
The agent prints its answer as usual. Instrumentation is fail-soft by default: if the keelwave server is unreachable, the SDK warns and your agent keeps running.
See it in the dashboard
Open http://localhost:8080 and sign in. The user make seed creates has no
password and is not email-verified, so it cannot sign in — register an account
through the dashboard for UI access, as described in
Docker.
- Runs — your
research-agentrun appears with its status, step count, token total, cost, and duration, alongside the project-wide completion rate and loop rate. - Run detail — click the run for its step timeline in order: each
web_searchtool call with its input, output, and success flag. - Tools —
web_searchshows up in the tool registry with its success rate and latency.
If the same search runs twice with identical arguments, the second call matches the first fingerprint and the run is flagged as looping, with the step index where the repetition began.
Without decorators
If decorators don't fit, open the run yourself. client.run takes the agent
name and a callback that receives the Run; it POSTs the run before the
callback, sends each step as it happens, and finishes the run afterwards —
including marking it failed when the callback throws.
await client.run('research-agent', async (run) => {
await run.step('think', 'need to search first')
await run.toolCall('web_search', { q: question }, { results: [] })
run.setOutput('done')
}, { input: question })run.toolCall fingerprints the tool name and input for you. run.step does
not — call run.checkFingerprint(name, input) yourself, or run.markLoop()
when you have detected a loop by other means.
Spans
For work that isn't a tool call, client.span emits a custom step on the
active run:
const span = client.span('retrieval', 'fetch_docs').start()
const docs = await fetchDocs(query)
span.set(`fetched ${docs.length} docs`)
await span.end()Span also implements Symbol.asyncDispose, so with the explicit resource
management syntax you can write await using span = client.span('retrieval')
and let it emit at the end of scope.
Reading the current run
getCurrentRun() returns the Run for the enclosing client.run or
client.agent call, or undefined outside one:
import { getCurrentRun } from 'keelwave'
const run = getCurrentRun()
run?.markLoop()