Runs
Open agent runs, record steps and tool calls, and read the ambient run from anywhere in the call stack.
A run is one execution of your agent. Inside a run you record steps and tool calls; when the run closes, keelwave has the full trace — step order, tokens, cost, duration, output, and whether the agent looped.
import { Keelwave } from 'keelwave'
const client = new Keelwave({ apiKey: process.env.KEELWAVE_API_KEY ?? '' })
await client.run(
'demo-agent',
async (run) => {
await run.step('plan', 'break the task into steps')
await run.toolCall('web_search', { q: 'keelwave' }, { results: ['...'] })
run.setOutput('done')
},
{ input: 'TypeScript observability' },
)client.run() opens the run before your callback executes and closes it after
the callback settles — including when it throws.
client.run()
run(
agentName: string,
fn: (run: Run) => Promise<unknown>,
opts?: { input?: string; metadata?: Record<string, unknown> },
): Promise<unknown>| Argument | Notes |
|---|---|
agentName | Name the run is grouped under in the dashboard. |
fn | Your agent body. Receives the live Run. |
opts.input | The task/prompt that started the run. |
opts.metadata | Arbitrary JSON attached to the run. |
The callback's resolved value is returned. If the callback throws, the run is
finished with status failed and reason error, then the error is rethrown.
If the server is unreachable when the run opens, the SDK synthesises a local run id and still executes your callback. Tracing degrades; your agent does not.
Run properties
| Member | Type | Notes |
|---|---|---|
id | string | Run id, assigned by the server (or locally on fallback). |
timestamp | string | Run start timestamp, needed by time-scoped query endpoints. |
loopDetected | boolean | true once a repeated tool-call fingerprint is seen. |
step()
Records a decision step: reasoning, planning, an observation — anything that isn't a tool call.
await run.step('think', 'the query returned nothing useful, try a narrower one', {
tokens: 120,
costUsd: 0.0004,
metadata: { attempt: 2 },
})Signature: step(stepType: string, content?: string, opts?: StepOptions): Promise<void>.
StepOptions
| Field | Type | Notes |
|---|---|---|
tokens | number | Added to the run's token total. |
costUsd | number | Added to the run's cost total. |
metadata | Record<string, unknown> | Arbitrary JSON stored with the step. |
stepType is a free-form string — plan, think, observe, whatever suits
your agent. Each call increments the run's step index.
toolCall()
Records a tool invocation with its input and output, and fingerprints it for loop detection.
await run.toolCall(
'web_search',
{ q: 'keelwave observability' },
{ results: ['https://example.com'] },
{ ok: true, latencyMs: 240, tokens: 30 },
)Signature:
toolCall(
toolName: string,
input: Record<string, unknown>,
output: unknown,
opts?: ToolCallOptions,
): Promise<void>output may be any value. Plain objects are stored as-is; anything else
(strings, numbers, arrays, null) is wrapped as { value: String(output) }.
ToolCallOptions
| Field | Type | Default | Notes |
|---|---|---|---|
ok | boolean | true | Whether the tool succeeded. |
tokens | number | — | Added to the run's token total. |
costUsd | number | — | Added to the run's cost total. |
latencyMs | number | — | Tool latency in milliseconds. |
metadata | Record<string, unknown> | — | Arbitrary JSON stored with the step. |
Loop detection
Every toolCall is hashed into a SHA-256 fingerprint of the tool name plus its
input with keys sorted. A repeated fingerprint marks the run as looping and
records the step index where the loop began. This happens locally, so it works
even when ingest fails.
await client.run('repeat', async (run) => {
await run.toolCall('search', { q: 'same' }, { results: [] })
console.log(run.loopDetected) // false
await run.toolCall('search', { q: 'same' }, { results: [] })
console.log(run.loopDetected) // true
})Distinct inputs are not a loop:
await client.run('distinct', async (run) => {
await run.toolCall('search', { q: 'a' }, {})
await run.toolCall('search', { q: 'b' }, {})
console.log(run.loopDetected) // false
})Two related methods let you drive detection yourself:
run.checkFingerprint(toolName, input)— fingerprint a call you recorded some other way.run.markLoop(stepIndex?)— flag the run as looping from your own heuristic.
setOutput()
Stores the run's final output. It is sent when the run finishes.
run.setOutput('Found: https://example.com')Signature: setOutput(output: string): void.
finish()
client.run() calls finish() for you. Call it directly only if you are
managing a Run outside that helper.
await run.finish('completed', 'clean')
await run.finish('failed', 'error')Signature: finish(status?: 'completed' | 'failed', reason?: string): Promise<void>.
Defaults are 'completed' and 'clean'. The finish payload carries total
steps, total tokens, total cost, duration, the loop flag and its step index, and
the output set via setOutput.
getCurrentRun()
The active run is tracked with AsyncLocalStorage, so any helper called inside
the run can reach it without threading it through arguments.
import { Keelwave, getCurrentRun } from 'keelwave'
const client = new Keelwave({ apiKey: process.env.KEELWAVE_API_KEY ?? '' })
async function fetchDocs(q: string): Promise<Array<string>> {
const run = getCurrentRun()
await run?.step('observe', `fetching docs for ${q}`)
return [`doc for ${q}`]
}
await client.run('nested', async () => {
await fetchDocs('loop detection')
})
getCurrentRun() // undefined — outside any rungetCurrentRun() returns Run | undefined. The same value is available as
client.getCurrentRun().
Outside a run getCurrentRun() is undefined. Use optional chaining
(run?.step(...)) in helpers that may be called either way.
Decorator form
If you'd rather not open the run by hand, client.agent() wraps an async
function so calling it opens a run, records the return value as the run output,
and closes the run.
const runAgent = client.agent({ name: 'demo-agent' })(async (task: string) => {
const run = getCurrentRun()
await run?.step('plan', `working on ${task}`)
return `Found: ${task}`
})
await runAgent('TypeScript observability')See Span for agent, observe, and manual spans.