New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@struct-ai/sdk

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@struct-ai/sdk

Struct agent observability SDK — auto-instruments AI agent frameworks with OpenTelemetry

npmnpm
Version
0.1.0
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

@struct-ai/sdk

Struct agent observability SDK for TypeScript/Node.js. Auto-instruments AI agent frameworks — the Anthropic SDK and LangChain.js — and emits OpenTelemetry traces + logs to struct.ai with zero config.

This is the TypeScript port of struct-sdk (Python). Span names, attribute keys, and log event shapes are identical across the two SDKs so the server processes both uniformly.

Install

npm install @struct-ai/sdk
# optional — the SDK auto-instruments these if present
npm install @anthropic-ai/sdk @langchain/core @langchain/langgraph

Requires Node 18+.

Quickstart

import { struct } from "@struct-ai/sdk";
// Initialize once, as early as possible in your process
struct.init({
  ingestKey: process.env.STRUCT_INGEST_KEY!,
  serviceName: "my-agent",
  environment: "production",
});

// Use your agent code as normal — spans + log events are emitted automatically.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();

await struct.agent({ name: "checkout" }, async () => {
  const msg = await client.messages.create({
    model: "claude-3-5-sonnet-20241022",
    max_tokens: 1024,
    messages: [{ role: "user", content: "plan my checkout flow" }],
  });

  // tool_call_id is auto-filled from the preceding Anthropic response
  await struct.tool({ name: "search" }, async () => {
    return await search(msg);
  });
});

What gets instrumented

LibraryHookSpan typeNotes
@anthropic-ai/sdkMessages.prototype.create, .streamchat {model}Cache-token accounting, streaming with tool-use reconstruction
@anthropic-ai/bedrock-sdk, @anthropic-ai/vertex-sdkMessages.prototype.*chat {model}Best-effort, if installed
@langchain/core BaseChatModel.invoke, .streamchat {model}Skipped when a provider-direct instrumentor is active (e.g. ChatAnthropic + Anthropic patch → single span)
@langchain/core StructuredTool.invokeexecute_tool {name}Extracts tool_call_id from LangChain ToolCall input or pending queue
@langchain/core BaseRetriever.invokeretrieval {name}
@langchain/langgraph Pregel.invoke, .streaminvoke_agent {name}Covers createReactAgent, custom graphs. thread_id → gen_ai.conversation.id

Content capture

The SDK supports four capture modes controlling how prompt/response content is emitted.

import { struct, ContentCaptureMode } from "@struct-ai/sdk";
struct.init({
  ingestKey: ...,
  contentCapture: ContentCaptureMode.EventOnly, // default
  // or ContentCaptureMode.None, SpanOnly, SpanAndEvent
});
  • EventOnly (default): per-message content lands on OTel log records (gen_ai.{user,assistant,system,tool}.message, gen_ai.choice). Spans carry metadata only.
  • SpanOnly: content on span attributes (gen_ai.input.messages, gen_ai.output.messages).
  • SpanAndEvent: both.
  • None: no content captured. Token counts, tool call IDs, finish reasons, and other metadata still flow.

Set captureContent: false for the legacy bool API (equivalent to ContentCaptureMode.None).

Manual scopes

struct.agent() and struct.tool() create invoke_agent and execute_tool spans. These are optional — LangChain's Pregel patch creates agent spans automatically when your graph has a thread_id in the config.

await struct.agent(
  { name: "onboarding", sessionId: conversationId, metadata: { tenant: "acme" } },
  async () => {
    await struct.tool({ name: "fetch-profile" }, async () => {
      return fetchProfile();
    });
  }
);

Nested agents set struct.agent.parent_session_id on the inner span, linking subagents back to the parent.

Semantic conventions

Emits attributes per the OTel GenAI semantic conventions:

  • gen_ai.operation.name — chat, execute_tool, invoke_agent, retrieval
  • gen_ai.provider.name — anthropic, openai, langchain, struct, …
  • gen_ai.request.{model, max_tokens, temperature, top_p, top_k, stop_sequences}
  • gen_ai.response.{model, id, finish_reasons}
  • gen_ai.usage.{input_tokens, output_tokens, cache_read.input_tokens, cache_creation.input_tokens}
  • gen_ai.conversation.id
  • gen_ai.tool.{name, call.id, call.arguments, call.result}
  • error.type + StatusCode.ERROR on failures

Note: gen_ai.usage.input_tokens for Anthropic is the TRUE total — we add back cache_read_input_tokens + cache_creation_input_tokens (which Anthropic's raw response excludes). Matches the Python SDK.

Troubleshooting

  • Spans missing after instrumenting: Import @struct-ai/sdk (or struct.init()) before the instrumented libraries, so their class prototypes are patched before any instance is constructed. In most Node setups this is automatic, but some bundlers tree-shake aggressively.
  • No logs appearing: LogRecords only emit when sdk.emitEvents is true (EventOnly or SpanAndEvent capture mode, which is the default). If you set captureContent: false you disable them.
  • Duplicate chat spans: The LangChain integration suppresses its own chat span when a provider-direct patch is active (e.g. ChatAnthropic calls through to @anthropic-ai/sdk, which emits its own chat span). If you see doubles, confirm both integrations are auto-instrumenting (check struct.initialized).
  • Subagent in a different trace / missing from parent's "Subagents" list: If you invoke a nested agent (subagent.invoke(...)) from inside a tool body, define the outer tool with tool(func, { name, description, schema }) from @langchain/core/tools — not new DynamicTool({...}). The tool() factory wraps your function in AsyncLocalStorageProviderSingleton.runWithConfig(...), which is what lets the nested invoke inherit the tool's callback chain and share the parent's trace_id. DynamicTool skips that wrap, so the subagent starts a new trace and parent↔subagent linkage breaks. The struct.agent.parent_session_id attribute is still set, so "Spawned by" on the child side still renders — but the parent's forward link to the subagent won't appear.

Parity with the Python SDK

Span names, attribute keys, and log event shapes match struct-sdk-python byte-for-byte. Running the same agent logic in both SDKs against the same tenant produces structurally identical traces in the Struct UI.

One deliberate divergence: the TS SDK finalizes streaming chat spans via the finalMessage event, closing a known gap in the Python SDK where messages.stream() didn't populate the pending-tool-call queue.

Development

pnpm install
pnpm -F @struct-ai/sdk typecheck
pnpm -F @struct-ai/sdk test
pnpm -F @struct-ai/sdk build

License

Apache-2.0

Keywords

struct

FAQs

Package last updated on 30 Apr 2026

Related posts