Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mortemlabs.com/llms.txt

Use this file to discover all available pages before exploring further.

Mortem integrates with the Vercel AI SDK by wrapping your language model and tools before passing them to generateText, streamText, or any other AI SDK function. The wrappers intercept every call transparently — no changes to your prompts, model configuration, or tool definitions required.

What gets captured

Mortem records the following for each agent run:
  • LLM calls — model ID, input messages, parameters, output text, and streamed content
  • Tool invocations — tool name, input arguments, and output values
  • Token counts — reported by the model provider where available
  • Cost estimates — calculated from token usage and recorded on each llm_call event

Prerequisites

Install the SDK and create an agent in the dashboard before continuing. You need MORTEM_API_KEY and MORTEM_AGENT_ID set in your environment.

Integration

1

Initialize the Mortem client

Create a Mortem instance at the top of your agent module. Pass your API key, agent ID, and the environment your bot runs on.
import { Mortem } from "@mortemlabs/sdk"

const mortem = new Mortem({
  apiKey: process.env.MORTEM_API_KEY ?? "",
  agentId: process.env.MORTEM_AGENT_ID,
  verifyToken: process.env.MORTEM_VERIFY_TOKEN, // remove after first verified run
  environment: "devnet",
})
verifyToken is only needed during your first deployment. Once the dashboard shows the agent as verified, remove MORTEM_VERIFY_TOKEN from your environment and code.
2

Wrap your model and tools

Call mortem.wrapLanguageModel and mortem.wrapTools before starting the session. Both wrappers mutate the object in place and return the same reference, so you can pass them directly to generateText.
import { openai } from "@ai-sdk/openai"

const model = openai("gpt-4o")
const tracedModel = mortem.wrapLanguageModel(model)

const tools = {
  getPrice: {
    description: "Get the current token price",
    parameters: z.object({ mint: z.string() }),
    execute: async ({ mint }) => fetchPrice(mint),
  },
}
const tracedTools = mortem.wrapTools(tools)
Both wrappers are idempotent — wrapping the same model or tools object twice has no effect.
3

Start a session and run the agent

Create a session with mortem.startSession, then execute your agent logic inside session.run. The session.run callback keeps Mortem’s async trace context active so all child LLM and tool events are associated with this trace.
import { generateText } from "ai"

const session = await mortem.startSession({
  inputSummary: "Evaluate whether the bot should open a token position",
  tags: ["swap", "devnet"],
})

try {
  const result = await session.run(async () => {
    return generateText({
      model: tracedModel,
      tools: tracedTools,
      maxSteps: 5,
      prompt: "Should I swap 1 SOL for JUP right now?",
    })
  })

  await session.complete(result.text)
} catch (error) {
  await session.fail(error)
} finally {
  await mortem.close()
}
Always call mortem.close() in a finally block. It flushes the trace buffer and ensures all events are delivered before the process exits.

Complete example

The following is the full integration pattern in a single file:
import { Mortem } from "@mortemlabs/sdk"
import { openai } from "@ai-sdk/openai"
import { generateText } from "ai"
import { z } from "zod"

const mortem = new Mortem({
  apiKey: process.env.MORTEM_API_KEY ?? "",
  agentId: process.env.MORTEM_AGENT_ID,
  verifyToken: process.env.MORTEM_VERIFY_TOKEN,
  environment: "devnet",
})

const model = openai("gpt-4o")
const tracedModel = mortem.wrapLanguageModel(model)

const tools = {
  getPrice: {
    description: "Get the current token price",
    parameters: z.object({ mint: z.string() }),
    execute: async ({ mint }: { mint: string }) => fetchPrice(mint),
  },
}
const tracedTools = mortem.wrapTools(tools)

const session = await mortem.startSession({
  inputSummary: "Evaluate whether the bot should open a token position",
  tags: ["swap", "devnet"],
})

try {
  const result = await session.run(async () => {
    return generateText({
      model: tracedModel,
      tools: tracedTools,
      maxSteps: 5,
      prompt: "Should I swap 1 SOL for JUP right now?",
    })
  })

  await session.complete(result.text)
} catch (error) {
  await session.fail(error)
} finally {
  await mortem.close()
}

Streaming

mortem.wrapLanguageModel captures streaming responses too. When you use streamText instead of generateText, the wrapper taps the underlying doStream method and accumulates content deltas as they arrive. The llm_call event is completed with the full assembled content when the stream closes. No extra configuration is needed — the same tracedModel works for both generateText and streamText.

Why session.run matters

session.run establishes an async context that child events — LLM calls, tool invocations — attach to. Without it, a top-level trace is created but the child events from inside generateText have no session to bind to and are silently dropped.
If you call generateText or streamText from multiple async paths in the same run, nest all of them inside a single session.run callback. The context propagates through promise chains and async iterators automatically.