Telemetry & Tracing
Stamp spans on your app's work so it shows up in the instance's traces, without importing OpenTelemetry.
Agent-readable version: Markdown · llms.txt
Everything a Rome instance does — handling a channel message, running an agent turn, executing an action — is traced. When your app wraps its own work in a span, that work appears in the same trace tree as the host's spans, nested under whatever triggered it (an action:* or hook:* span, for example). You get timing, attributes, and error status in the instance's observability stack with a few lines of code.
@rome-os/app-runtime exposes this without any OpenTelemetry dependency. All OTEL machinery stays inside the host; the SDK functions forward to a bridge the host installs at startup. Your app only imports from the SDK:
import { withRomeSpan, withSdkSpan, currentSessionId } from "@rome-os/app-runtime";The API
| Function | What it does |
|---|---|
withRomeSpan(name, attrs, fn) | Runs fn inside a span named name with attrs attached. Ends the span when the promise settles; a thrown error is recorded and marks the span ERROR. This is the main call. |
withSdkSpan(methodName, attrs, fn) | Convenience wrapper: same as withRomeSpan with the name prefixed as sdk:<methodName> and sdk.method stamped as an attribute. |
startRomeSpan(name, attrs?) | Emits a detached marker span. The SDK does not return a span handle, so you cannot end it yourself — prefer withRomeSpan, which brackets your work. |
currentSessionId() | The session id of the agent session your code is running under, or undefined outside one. |
runWithSession(sessionId, fn) | Runs fn with a session context established, so spans and logs inside it correlate to that session. |
SpanAttributes is just Record<string, unknown> — the type of the attrs argument. Namespace your keys ("myapp.batch_size": 12) so they don't collide with host attributes.
Everything is a no-op without the host
The SDK functions only work when a bridge is registered, which the host does when your app runs inside a Rome instance. When no bridge is present — a plain unit test, a script running your module directly — withRomeSpan falls through to calling fn() with no span emitted, currentSessionId() returns undefined, and runWithSession runs fn verbatim. You never need to stub telemetry in tests, and instrumented code never crashes outside Rome.
The host also never blocks on telemetry: span export is fire-and-forget, so instrumentation costs your app nothing on the request path.
Session correlation
Rome keys its trace views on sessions. When your code runs inside an agent session — an action execute, a hook fired during a turn — the host has already established the session context, and withRomeSpan stamps session.id on your span automatically. You don't need to do anything.
currentSessionId() and runWithSession() exist for the gap: work that escapes the ambient context. If your action kicks off background work (a setTimeout, a detached promise) that should still show up under the session, capture the id first and re-establish it:
const sessionId = currentSessionId();
queueBackgroundJob(() => {
if (!sessionId) return doWork();
return runWithSession(sessionId, () => doWork());
});Example
The system app's local summon action (called as system:summon) wraps each
nested agent run in its own span, so the trace tree shows a summon:<child>
node between the parent action:system:summon span and the child agent's own
spans:
import { withRomeSpan } from "@rome-os/app-runtime";
export async function executeSummon(deps: SummonDeps, agentName: string, prompt: string) {
// The parent action span already exists; this nests a dedicated span for
// the child agent's run inside it, with the child's name as an attribute.
return withRomeSpan(`summon:${agentName}`, { "rome.summon.child_agent": agentName }, async () => {
let result = "";
for await (const msg of deps.agentRunner.run({ agentName, prompt })) {
if (msg.type === "result") result = msg.content;
}
return { result };
});
}The same pattern fits any unit of app work worth timing: an external API call, a batch import, a per-item loop body. Name spans as <domain>:<what> (the host uses action:*, hook:*, agent:*, sdk:*) and put variable detail in attributes, not the name, so spans of the same kind group together in queries.
Watch out: through the SDK,
startRomeSpanstarts and immediately ends the span — it records a zero-duration point in the trace, not an open interval, because the SDK never hands you a span to end later. Use it only as an event marker. Anything with a duration belongs inwithRomeSpan/withSdkSpan.
You will also see setTelemetryBridge and the TelemetryBridge interface in the SDK's exports. Those are host wiring — the Rome core calls setTelemetryBridge at startup to install the real implementations. Apps never call it.