Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.trodo.ai/docs/llms.txt

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

wrapAgent (the main pattern) handles the common case: an agent loop that runs to completion inside a single function call. But sometimes a logical “run” lives across multiple HTTP requests over many minutes — for example a long-lived chat session pinned to a websocket, or a scheduled job that gets resumed on a different worker. For those cases use startRun (returns a runId) and endRun (finalises the run later — possibly from a different process). Between the two, any process can use joinRun(runId, …) to add child spans.
Not for MCP servers. If you’re building an MCP server, use trackMcp instead — runless spans, no session lifecycle to manage. MCP servers proxy tool calls but don’t see the user’s prompt or the LLM’s final answer, so a Run wrapping an MCP session has no analytical content; and MCP has no clean session-end signal, so Runs created at initialize get stuck in running forever.

When to reach for this

SituationUse
In-process agent: open → work → close in one functionwrapAgent
Downstream service joins an in-flight runjoinRun
MCP server (proxies tool calls; doesn’t see prompts)trackMcp
Run spans many HTTP requests over hours, AND server owns prompt+answerstartRun + joinRun + endRun

Example — websocket-pinned chat

import trodo from 'trodo-node';
import { redis } from './redis.js';

trodo.init({ siteId: process.env.TRODO_SITE_ID });

// On connection: open a run for the whole conversation.
async function onConnect(ws, userId) {
  const runId = await trodo.startRun('chat', {
    distinctId: userId,
    conversationId: ws.conversationId,
  });
  await redis.set(`ws:run:${ws.id}`, runId, 'EX', 86400);
  ws.runId = runId;
}

// Each message: append a span (LLM call + nested tool calls).
async function onMessage(ws, message) {
  return trodo.joinRun(ws.runId, null, async (span) => {
    span.setInput({ message });
    const reply = await callLlm(message);
    span.setOutput({ reply });
    return reply;
  }, { name: 'chat.turn', kind: 'agent' });
}

// On disconnect: finalise.
async function onClose(ws, reason) {
  if (ws.runId) {
    await trodo.endRun(ws.runId, { status: reason === 'normal' ? 'ok' : 'error' });
  }
}
Same runId threads through startRun → many joinRun calls → endRun. The dashboard shows one run, with every tool call as a child span beneath it.

API

startRun

OptionNotes
agentNameRequired. Free-text identifier (e.g. "chat", "ingest_pipeline").
runIdOptional. Caller-supplied UUID for cross-process correlation. If omitted the SDK mints one and returns it.
distinctId / distinct_idThe end user. Lets you filter runs per user across the dashboard.
conversationId / conversation_idGroups runs that belong to one user-facing conversation.
parentRunId / parent_run_idMarks this as a sub-agent run.
metadataFree-form JSON tags.
inputRecorded as the run’s input for the dashboard.
Returns the runId (string). The Run row exists in your dashboard immediately after this call resolves with status: "running".

endRun

OptionNotes
runIdRequired (positional).
outputFinal output payload. Truncated at 64 KB.
status"ok" (default) or "error".
errorSummary / error_summaryHuman-readable error message — set when status="error".
metadataOptional final metadata to merge into the Run record.
endRun aggregates any locally-buffered spans, finalises the row server-side, and unmarks the run as joined.

Why not wrapAgent?

wrapAgent is a single-context-manager block — open → work → close all happen in one call stack. That doesn’t fit when the run lives across multiple HTTP requests served by potentially different workers. startRun / endRun decompose the lifecycle so each phase can run in a different place. For everything except long-lived multi-process sessions, prefer wrapAgent — it’s one HTTP call instead of two and keeps the surface area smaller.

Next