@struct-ai/sdk 0.2.1 → 0.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -8
- package/dist/commonjs/context.d.ts +26 -0
- package/dist/commonjs/context.js +20 -1
- package/dist/commonjs/core.d.ts +2 -0
- package/dist/commonjs/core.js +133 -28
- package/dist/commonjs/events.js +3 -3
- package/dist/commonjs/integrations/anthropic.d.ts +2 -0
- package/dist/commonjs/integrations/anthropic.js +470 -47
- package/dist/commonjs/integrations/langchain-callback.d.ts +179 -27
- package/dist/commonjs/integrations/langchain-callback.js +670 -81
- package/dist/commonjs/integrations/langchain.d.ts +3 -0
- package/dist/commonjs/integrations/langchain.js +353 -7
- package/dist/commonjs/semconv.d.ts +11 -0
- package/dist/commonjs/semconv.js +12 -1
- package/dist/commonjs/version.d.ts +2 -0
- package/dist/commonjs/version.js +6 -0
- package/dist/esm/context.d.ts +26 -0
- package/dist/esm/context.js +18 -1
- package/dist/esm/core.d.ts +2 -0
- package/dist/esm/core.js +135 -30
- package/dist/esm/events.js +3 -3
- package/dist/esm/integrations/anthropic.d.ts +2 -0
- package/dist/esm/integrations/anthropic.js +471 -50
- package/dist/esm/integrations/langchain-callback.d.ts +179 -27
- package/dist/esm/integrations/langchain-callback.js +672 -83
- package/dist/esm/integrations/langchain.d.ts +3 -0
- package/dist/esm/integrations/langchain.js +352 -7
- package/dist/esm/semconv.d.ts +11 -0
- package/dist/esm/semconv.js +11 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +3 -0
- package/package.json +13 -11
package/dist/esm/core.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { context as otelContext, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
1
|
+
import { context as otelContext, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
3
2
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
4
3
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
5
|
-
import {
|
|
4
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
6
5
|
import { BatchLogRecordProcessor, LoggerProvider, } from "@opentelemetry/sdk-logs";
|
|
7
6
|
import { BatchSpanProcessor, NodeTracerProvider, } from "@opentelemetry/sdk-trace-node";
|
|
8
7
|
import { ContentCaptureMode, captureContentFor, emitEventsFor, emitSpanContentFor, } from "./content-capture.js";
|
|
@@ -10,6 +9,7 @@ import { getAgentSpan, getSessionId, popPendingToolCallId, runWithContext, } fro
|
|
|
10
9
|
import { autoInstrument } from "./integrations/index.js";
|
|
11
10
|
import { ERROR_TYPE, GEN_AI, STRUCT, } from "./semconv.js";
|
|
12
11
|
import { safeJsonStringify } from "./truncation.js";
|
|
12
|
+
import { SDK_VERSION } from "./version.js";
|
|
13
13
|
export const DEFAULT_ENDPOINT = "https://ingest.struct.ai";
|
|
14
14
|
const LOG_ENABLED = process.env.STRUCT_SDK_LOG === "1";
|
|
15
15
|
const defaultInternalLogger = {
|
|
@@ -33,6 +33,43 @@ const defaultInternalLogger = {
|
|
|
33
33
|
* so consumers cannot reach it via the package's public API surface.
|
|
34
34
|
*/
|
|
35
35
|
export const firstFailureLogged = new Set();
|
|
36
|
+
/**
|
|
37
|
+
* OTel SDK 2.x providers accept processors only at construction time —
|
|
38
|
+
* `addSpanProcessor` / `addLogRecordProcessor` no longer exist on the
|
|
39
|
+
* providers. These fan-outs are registered at construction and delegate to a
|
|
40
|
+
* mutable list, preserving the SDK's test-only `addSpanProcessor` /
|
|
41
|
+
* `addLogRecordProcessor` hooks.
|
|
42
|
+
*/
|
|
43
|
+
class FanoutSpanProcessor {
|
|
44
|
+
delegates = [];
|
|
45
|
+
onStart(...args) {
|
|
46
|
+
for (const p of this.delegates)
|
|
47
|
+
p.onStart(...args);
|
|
48
|
+
}
|
|
49
|
+
onEnd(...args) {
|
|
50
|
+
for (const p of this.delegates)
|
|
51
|
+
p.onEnd(...args);
|
|
52
|
+
}
|
|
53
|
+
forceFlush() {
|
|
54
|
+
return Promise.all(this.delegates.map((p) => p.forceFlush())).then(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
shutdown() {
|
|
57
|
+
return Promise.all(this.delegates.map((p) => p.shutdown())).then(() => undefined);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
class FanoutLogRecordProcessor {
|
|
61
|
+
delegates = [];
|
|
62
|
+
onEmit(...args) {
|
|
63
|
+
for (const p of this.delegates)
|
|
64
|
+
p.onEmit(...args);
|
|
65
|
+
}
|
|
66
|
+
forceFlush() {
|
|
67
|
+
return Promise.all(this.delegates.map((p) => p.forceFlush())).then(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
shutdown() {
|
|
70
|
+
return Promise.all(this.delegates.map((p) => p.shutdown())).then(() => undefined);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
36
73
|
/**
|
|
37
74
|
* Run `fn`; swallow any exception. The first failure per `site` logs at WARN
|
|
38
75
|
* (with the error attached); subsequent failures at the same site log at
|
|
@@ -60,6 +97,8 @@ export class StructSDK {
|
|
|
60
97
|
_initialized = false;
|
|
61
98
|
_tracerProvider;
|
|
62
99
|
_loggerProvider;
|
|
100
|
+
_spanFanout;
|
|
101
|
+
_logFanout;
|
|
63
102
|
_ingestKey = "";
|
|
64
103
|
_endpoint = DEFAULT_ENDPOINT;
|
|
65
104
|
_contentCapture = ContentCaptureMode.EventOnly;
|
|
@@ -133,26 +172,31 @@ export class StructSDK {
|
|
|
133
172
|
maxExportBatchSize: 100,
|
|
134
173
|
scheduledDelayMillis: 1000,
|
|
135
174
|
});
|
|
136
|
-
const resource =
|
|
175
|
+
const resource = resourceFromAttributes({
|
|
137
176
|
"service.name": serviceName,
|
|
138
177
|
"service.version": serviceVersion,
|
|
139
178
|
"deployment.environment": environment,
|
|
140
179
|
});
|
|
180
|
+
this._spanFanout = new FanoutSpanProcessor();
|
|
141
181
|
this._tracerProvider = new NodeTracerProvider({
|
|
142
182
|
resource,
|
|
143
|
-
spanProcessors: [spanProcessor],
|
|
183
|
+
spanProcessors: [spanProcessor, this._spanFanout],
|
|
144
184
|
});
|
|
145
185
|
const logExporter = new OTLPLogExporter({
|
|
146
186
|
url: `${this._endpoint}/v1/logs`,
|
|
147
187
|
headers,
|
|
148
188
|
});
|
|
149
|
-
const logProcessor = new BatchLogRecordProcessor(
|
|
189
|
+
const logProcessor = new BatchLogRecordProcessor({
|
|
190
|
+
exporter: logExporter,
|
|
150
191
|
maxQueueSize: 10000,
|
|
151
192
|
maxExportBatchSize: 100,
|
|
152
193
|
scheduledDelayMillis: 1000,
|
|
153
194
|
});
|
|
154
|
-
this.
|
|
155
|
-
this._loggerProvider
|
|
195
|
+
this._logFanout = new FanoutLogRecordProcessor();
|
|
196
|
+
this._loggerProvider = new LoggerProvider({
|
|
197
|
+
resource,
|
|
198
|
+
processors: [logProcessor, this._logFanout],
|
|
199
|
+
});
|
|
156
200
|
this._initialized = true;
|
|
157
201
|
this._shutdownHook = () => {
|
|
158
202
|
void this.shutdown();
|
|
@@ -172,6 +216,8 @@ export class StructSDK {
|
|
|
172
216
|
this._initialized = false;
|
|
173
217
|
this._tracerProvider = undefined;
|
|
174
218
|
this._loggerProvider = undefined;
|
|
219
|
+
this._spanFanout = undefined;
|
|
220
|
+
this._logFanout = undefined;
|
|
175
221
|
this._shutdownHook = undefined;
|
|
176
222
|
this._readyPromise = Promise.resolve();
|
|
177
223
|
return;
|
|
@@ -182,24 +228,24 @@ export class StructSDK {
|
|
|
182
228
|
if (!this._tracerProvider) {
|
|
183
229
|
throw new Error("Call struct.init() before using the SDK");
|
|
184
230
|
}
|
|
185
|
-
return this._tracerProvider.getTracer(name);
|
|
231
|
+
return this._tracerProvider.getTracer(name, SDK_VERSION);
|
|
186
232
|
}
|
|
187
233
|
getLogger(name = "struct-sdk") {
|
|
188
234
|
if (!this._loggerProvider) {
|
|
189
235
|
throw new Error("Call struct.init() before using the SDK");
|
|
190
236
|
}
|
|
191
|
-
return this._loggerProvider.getLogger(name);
|
|
237
|
+
return this._loggerProvider.getLogger(name, SDK_VERSION);
|
|
192
238
|
}
|
|
193
239
|
getInternalLogger() {
|
|
194
240
|
return this._internalLogger;
|
|
195
241
|
}
|
|
196
242
|
/** Test-only: attach a custom span processor to the internal provider. */
|
|
197
243
|
addSpanProcessor(processor) {
|
|
198
|
-
this.
|
|
244
|
+
this._spanFanout?.delegates.push(processor);
|
|
199
245
|
}
|
|
200
246
|
/** Test-only: attach a custom log record processor. */
|
|
201
247
|
addLogRecordProcessor(processor) {
|
|
202
|
-
this.
|
|
248
|
+
this._logFanout?.delegates.push(processor);
|
|
203
249
|
}
|
|
204
250
|
/**
|
|
205
251
|
* Shut down the SDK and flush pending telemetry.
|
|
@@ -256,8 +302,7 @@ export class StructSDK {
|
|
|
256
302
|
return await fn();
|
|
257
303
|
}
|
|
258
304
|
const agentName = options.name;
|
|
259
|
-
const
|
|
260
|
-
const parentSessionId = getSessionId();
|
|
305
|
+
const explicitSessionId = options.sessionId;
|
|
261
306
|
const tracer = this.getTracer("struct-sdk");
|
|
262
307
|
// Parent the new agent span on the nearest enclosing agent span (if
|
|
263
308
|
// any) so nested `struct.agent()` / `struct.tool()` calls build a
|
|
@@ -266,16 +311,68 @@ export class StructSDK {
|
|
|
266
311
|
// nothing to hang off. We do NOT rely on the global OTel context
|
|
267
312
|
// manager being installed — the SDK manages its own parent chain via
|
|
268
313
|
// StructContext ALS to stay neutral in multi-tenant OTel setups.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
314
|
+
//
|
|
315
|
+
// Captured BEFORE we resolve/overwrite anything below — mirrors
|
|
316
|
+
// python's `enclosing_session_id` / `enclosing_agent_span` capture at
|
|
317
|
+
// core.py:625-626. Used to (1) resolve this agent's own session id via
|
|
318
|
+
// ambient inheritance, (2) detect break-out, and (3) decide whether to
|
|
319
|
+
// stamp `struct.agent.parent_session_id`.
|
|
320
|
+
const enclosingSessionId = getSessionId();
|
|
321
|
+
const enclosingAgentSpan = getAgentSpan();
|
|
322
|
+
// ── Resolve sessionId (REVISION R1 grouping model) ──────────────────
|
|
323
|
+
// Resolution order: explicit caller arg > ambient (enclosing agent) >
|
|
324
|
+
// session-less. We never fabricate a uuid — a caller that omits
|
|
325
|
+
// sessionId with no enclosing agent gets a SESSION-LESS run (no
|
|
326
|
+
// `gen_ai.conversation.id` attribute at all). Ports core.py:628-644
|
|
327
|
+
// exactly (`self._session_id = ""` there is `undefined` here).
|
|
328
|
+
const sessionId = explicitSessionId !== undefined
|
|
329
|
+
? explicitSessionId
|
|
330
|
+
: enclosingSessionId !== undefined
|
|
331
|
+
? enclosingSessionId
|
|
332
|
+
: undefined;
|
|
333
|
+
// ── Break-out detection (spawned-by Link) ───────────────────────────
|
|
334
|
+
// Caller supplied an EXPLICIT session id that differs from the
|
|
335
|
+
// enclosing agent's session AND there IS an enclosing Struct agent
|
|
336
|
+
// span in scope. This agent starts a fresh ROOT trace (no OTel
|
|
337
|
+
// parent) and carries a causal Link back to the enclosing agent's
|
|
338
|
+
// span context. Ports struct_sdk.core._AgentContext._start_span's
|
|
339
|
+
// `break_out` branch (struct-sdk-python/src/struct_sdk/core.py:651-656)
|
|
340
|
+
// exactly.
|
|
341
|
+
const breakOut = explicitSessionId !== undefined &&
|
|
342
|
+
enclosingSessionId !== undefined &&
|
|
343
|
+
explicitSessionId !== enclosingSessionId &&
|
|
344
|
+
enclosingAgentSpan !== undefined;
|
|
345
|
+
// ── Parentage: PURE NEST (Option D) ─────────────────────────────────
|
|
346
|
+
// A non-break-out agent nests under whatever OTel span is active — the
|
|
347
|
+
// host's ambient context — like every peer LLM/agent SDK. We do NOT
|
|
348
|
+
// second-guess the active span or unilaterally re-root: the old
|
|
349
|
+
// `foreignRoot` heuristic detached from ANY active span and so ripped
|
|
350
|
+
// agents out of legitimate host request traces. Cross-conversation
|
|
351
|
+
// grouping is carried by gen_ai.conversation.id, never by trace
|
|
352
|
+
// parentage. A runtime that propagates a stale/unwanted span across a
|
|
353
|
+
// boundary (e.g. our own persistent_agent Temporal workflow span) clears
|
|
354
|
+
// it at the SOURCE, never here. (A provenance-gated detach — re-root only
|
|
355
|
+
// a leaked Struct-OWNED span — is a documented, deferred follow-up.)
|
|
356
|
+
let startContext;
|
|
357
|
+
let links;
|
|
358
|
+
if (breakOut) {
|
|
359
|
+
// Explicit different session under another Struct agent: fresh ROOT
|
|
360
|
+
// trace + a spawned-by Link. The one deliberate, opt-in re-root.
|
|
361
|
+
// enclosingAgentSpan is guaranteed defined by the breakOut guard above.
|
|
362
|
+
links = [{ context: enclosingAgentSpan.spanContext() }];
|
|
363
|
+
startContext = ROOT_CONTEXT;
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
startContext = enclosingAgentSpan
|
|
367
|
+
? trace.setSpan(otelContext.active(), enclosingAgentSpan)
|
|
368
|
+
: otelContext.active();
|
|
369
|
+
}
|
|
273
370
|
// Span creation itself can fail (custom tracer, broken context). If it
|
|
274
371
|
// does, fall through to running `fn` directly so the user's call
|
|
275
372
|
// always runs uninstrumented rather than blowing up on telemetry.
|
|
276
373
|
let span;
|
|
277
374
|
safe(() => {
|
|
278
|
-
span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: SpanKind.INTERNAL },
|
|
375
|
+
span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: SpanKind.INTERNAL, links }, startContext);
|
|
279
376
|
}, "agent.start_span", this._internalLogger);
|
|
280
377
|
if (span === undefined) {
|
|
281
378
|
return await fn();
|
|
@@ -296,17 +393,24 @@ export class StructSDK {
|
|
|
296
393
|
startedSpan.setAttribute(GEN_AI.AGENT_VERSION, options.version);
|
|
297
394
|
}
|
|
298
395
|
// gen_ai.conversation.id is the spec-blessed name for
|
|
299
|
-
// session/thread id.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
396
|
+
// session/thread id. Omitted when session-less (never fabricated).
|
|
397
|
+
// Ports core.py:723-725 (`if self._session_id:`) exactly.
|
|
398
|
+
if (sessionId) {
|
|
399
|
+
startedSpan.setAttribute(GEN_AI.CONVERSATION_ID, sessionId);
|
|
400
|
+
}
|
|
401
|
+
// ``struct.agent.parent_session_id`` is a SPAWNED-BY marker — set it
|
|
402
|
+
// ONLY when this agent has an enclosing session that DIFFERS from
|
|
403
|
+
// its own resolved session id. An inline nested agent (same session
|
|
404
|
+
// as the enclosing agent, whether by explicit same id or ambient
|
|
405
|
+
// inheritance) already has its parent relationship encoded by the
|
|
406
|
+
// OTel span tree (ParentSpanId) — stamping
|
|
407
|
+
// parent_session_id === own sessionId would be self-referential
|
|
408
|
+
// noise. Structure comes from the tree/Link, not this attr
|
|
409
|
+
// (Link-canonical decision). Ports core.py:736-745 exactly (both
|
|
410
|
+
// the break-out and the "legacy" differing-id-no-span branches
|
|
411
|
+
// collapse to this one condition).
|
|
412
|
+
if (enclosingSessionId !== undefined && enclosingSessionId !== sessionId) {
|
|
413
|
+
startedSpan.setAttribute(STRUCT.AGENT_PARENT_SESSION_ID, enclosingSessionId);
|
|
310
414
|
}
|
|
311
415
|
if (options.metadata) {
|
|
312
416
|
for (const [key, value] of Object.entries(options.metadata)) {
|
|
@@ -319,6 +423,7 @@ export class StructSDK {
|
|
|
319
423
|
conversationId: sessionId,
|
|
320
424
|
agentSpan: startedSpan,
|
|
321
425
|
pendingToolCalls: {},
|
|
426
|
+
manualAgentSpan: startedSpan,
|
|
322
427
|
}, async () => {
|
|
323
428
|
const activeCtx = trace.setSpan(otelContext.active(), startedSpan);
|
|
324
429
|
try {
|
package/dist/esm/events.js
CHANGED
|
@@ -49,7 +49,7 @@ export function emitAnthropicMessageEvents(logger, messages, system) {
|
|
|
49
49
|
parts: truncateParts(parts),
|
|
50
50
|
}),
|
|
51
51
|
extraAttrs: {
|
|
52
|
-
[GEN_AI.
|
|
52
|
+
[GEN_AI.PROVIDER_NAME]: "anthropic",
|
|
53
53
|
[GEN_AI.MESSAGE_INDEX]: msgIndex,
|
|
54
54
|
},
|
|
55
55
|
});
|
|
@@ -67,7 +67,7 @@ export function emitAnthropicMessageEvents(logger, messages, system) {
|
|
|
67
67
|
eventName,
|
|
68
68
|
payload: safeJsonStringify({ role, parts: truncateParts(parts) }),
|
|
69
69
|
extraAttrs: {
|
|
70
|
-
[GEN_AI.
|
|
70
|
+
[GEN_AI.PROVIDER_NAME]: "anthropic",
|
|
71
71
|
[GEN_AI.MESSAGE_INDEX]: msgIndex,
|
|
72
72
|
},
|
|
73
73
|
});
|
|
@@ -92,7 +92,7 @@ export function emitAnthropicChoiceEvent(logger, contentBlocks, stopReason) {
|
|
|
92
92
|
eventName: EVENT_NAMES.CHOICE,
|
|
93
93
|
payload,
|
|
94
94
|
extraAttrs: {
|
|
95
|
-
[GEN_AI.
|
|
95
|
+
[GEN_AI.PROVIDER_NAME]: "anthropic",
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
98
|
}
|
|
@@ -22,6 +22,8 @@ export declare function patch(sdk: StructSDK): Promise<void>;
|
|
|
22
22
|
export declare function unpatch(): Promise<void>;
|
|
23
23
|
type CreateMethod = (this: unknown, params: CreateParams, opts?: unknown) => unknown;
|
|
24
24
|
type StreamMethod = (this: unknown, params: CreateParams, opts?: unknown) => unknown;
|
|
25
|
+
export declare function wrapCreate(original: CreateMethod): CreateMethod;
|
|
26
|
+
export declare function wrapStream(original: StreamMethod): StreamMethod;
|
|
25
27
|
/** @internal */
|
|
26
28
|
export type _PatchContextForTest = PatchContext;
|
|
27
29
|
/** @internal */
|