@introspection-sdk/introspection-pi
v0.9.2
Published
Introspection observability extension for the Pi Agent SDK — OTEL GenAI semantic-convention spans for chat completions and tool execution
Readme
introspection-pi
Introspection observability extension for the Pi Agent SDK — emits OpenTelemetry GenAI semantic-convention spans for chat completions and tool execution.
Installation
npm install @introspection-sdk/introspection-pi \
@opentelemetry/api \
@earendil-works/pi-ai \
@earendil-works/pi-agent-coreUsage
import { trace } from "@opentelemetry/api";
import { Agent } from "@earendil-works/pi-agent-core";
import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
import {
instrumentAgent,
instrumentStream,
type AgentMeta,
} from "@introspection-sdk/introspection-pi";
const tracer = trace.getTracer("my-app");
const meta: AgentMeta = {
conversationId: "conv_123",
agentId: "support-agent",
agentName: "Support",
};
const agent = new Agent({
initialState: {
model: getBuiltinModel("anthropic", "claude-sonnet-4-6"),
systemPrompt: "You are a helpful support agent.",
},
});
// One chat span per LLM call
agent.streamFn = instrumentStream(agent.streamFn, { tracer, meta });
// One execute_tool span per tool call
const tools = instrumentAgent(agent, { tracer, meta });
await agent.prompt("Help me understand my latest invoice.");
// Later, on shutdown:
tools.stop();Adding caller-specific attributes
Use the extraAttributes hook to layer non-semconv attributes on every
chat span (tenant labels, correlation IDs, feature flags):
agent.streamFn = instrumentStream(agent.streamFn, {
tracer,
meta,
extraAttributes: (model, ctx) => ({
"introspection.byok": !process.env.PROXY_KEY,
"tenant.id": meta.conversationId,
}),
});Parenting spans under a turn span
If you wrap an entire user turn in your own span, pass
getParentContext so each chat / tool span lands under it:
const turnSpan = tracer.startSpan(`turn ${meta.agentName}`);
const turnContext = trace.setSpan(context.active(), turnSpan);
agent.streamFn = instrumentStream(agent.streamFn, {
tracer,
meta,
getParentContext: () => turnContext,
});What gets emitted
For each LLM call (chat ${provider} span):
gen_ai.conversation.id,gen_ai.agent.id,gen_ai.agent.namegen_ai.operation.name = "chat"gen_ai.provider.name,gen_ai.request.model,gen_ai.response.modelgen_ai.request.stream = truegen_ai.system_instructions,gen_ai.tool.definitionsgen_ai.input.messages,gen_ai.output.messagesgen_ai.response.id,gen_ai.response.finish_reasonsgen_ai.response.time_to_first_chunkgen_ai.conversation.compactedwhen compacted history was sentgen_ai.usage.input_tokens,gen_ai.usage.output_tokensgen_ai.usage.reasoning.output_tokenswhen reportedgen_ai.usage.cache_read.input_tokens,gen_ai.usage.cache_creation.input_tokens(when > 0)gen_ai.cost.usd(when reported)introspection.termination_reason = "cancelled" | "awaiting_user"for requested aborts
Requested aborts are not recorded as errors. A user/runtime cancellation or an
interrupt pause ends the span with gen_ai.response.finish_reasons = ["aborted"]
and introspection.termination_reason, but without setStatus(ERROR) or a
synthetic exception. Unclaimed aborts and provider/model failures are still
recorded as errors with a standard exception span event.
For each tool call (execute_tool ${tool_name} span):
gen_ai.conversation.id,gen_ai.agent.id,gen_ai.agent.namegen_ai.operation.name = "execute_tool"gen_ai.tool.name,gen_ai.tool.type,gen_ai.tool.call.idgen_ai.tool.call.arguments,gen_ai.tool.call.result- Tool results are retained for both successful and failed executions so the
conversation can be reconstructed losslessly. Tool errors are recorded with
setStatus(ERROR). Tool calls cut short by a requested abort are marked withintrospection.termination_reason = "cancelled"and are not marked as errors.
License
Apache-2.0
