keelwave
SDKsTypeScript

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>
ArgumentNotes
agentNameName the run is grouped under in the dashboard.
fnYour agent body. Receives the live Run.
opts.inputThe task/prompt that started the run.
opts.metadataArbitrary 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

MemberTypeNotes
idstringRun id, assigned by the server (or locally on fallback).
timestampstringRun start timestamp, needed by time-scoped query endpoints.
loopDetectedbooleantrue 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

FieldTypeNotes
tokensnumberAdded to the run's token total.
costUsdnumberAdded to the run's cost total.
metadataRecord<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

FieldTypeDefaultNotes
okbooleantrueWhether the tool succeeded.
tokensnumberAdded to the run's token total.
costUsdnumberAdded to the run's cost total.
latencyMsnumberTool latency in milliseconds.
metadataRecord<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 run

getCurrentRun() 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.

On this page