@struct-ai/sdk 0.3.0 → 0.4.2
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 +101 -16
- package/dist/commonjs/context.d.ts +45 -0
- package/dist/commonjs/context.js +78 -1
- package/dist/commonjs/core.js +184 -29
- package/dist/commonjs/events.d.ts +17 -6
- package/dist/commonjs/events.js +82 -59
- package/dist/commonjs/genai-content.d.ts +52 -0
- package/dist/commonjs/genai-content.js +143 -0
- package/dist/commonjs/instrument.d.ts +47 -0
- package/dist/commonjs/instrument.js +158 -0
- package/dist/commonjs/integrations/anthropic-content.js +18 -6
- package/dist/commonjs/integrations/anthropic.d.ts +8 -1
- package/dist/commonjs/integrations/anthropic.js +515 -104
- package/dist/commonjs/integrations/index.js +8 -0
- package/dist/commonjs/integrations/langchain-callback.d.ts +182 -27
- package/dist/commonjs/integrations/langchain-callback.js +754 -87
- package/dist/commonjs/integrations/langchain-content.js +1 -1
- package/dist/commonjs/integrations/langchain.d.ts +3 -0
- package/dist/commonjs/integrations/langchain.js +353 -7
- package/dist/commonjs/integrations/openai-content.d.ts +34 -0
- package/dist/commonjs/integrations/openai-content.js +375 -0
- package/dist/commonjs/integrations/openai.d.ts +39 -0
- package/dist/commonjs/integrations/openai.js +305 -0
- package/dist/commonjs/semconv.d.ts +12 -0
- package/dist/commonjs/semconv.js +13 -1
- package/dist/commonjs/truncation.d.ts +29 -0
- package/dist/commonjs/truncation.js +184 -10
- package/dist/commonjs/version.d.ts +2 -0
- package/dist/commonjs/version.js +6 -0
- package/dist/esm/context.d.ts +45 -0
- package/dist/esm/context.js +74 -1
- package/dist/esm/core.js +185 -30
- package/dist/esm/events.d.ts +17 -6
- package/dist/esm/events.js +82 -61
- package/dist/esm/genai-content.d.ts +52 -0
- package/dist/esm/genai-content.js +137 -0
- package/dist/esm/instrument.d.ts +47 -0
- package/dist/esm/instrument.js +155 -0
- package/dist/esm/integrations/anthropic-content.js +19 -7
- package/dist/esm/integrations/anthropic.d.ts +8 -1
- package/dist/esm/integrations/anthropic.js +514 -107
- package/dist/esm/integrations/index.js +8 -0
- package/dist/esm/integrations/langchain-callback.d.ts +182 -27
- package/dist/esm/integrations/langchain-callback.js +756 -89
- package/dist/esm/integrations/langchain-content.js +1 -1
- package/dist/esm/integrations/langchain.d.ts +3 -0
- package/dist/esm/integrations/langchain.js +352 -7
- package/dist/esm/integrations/openai-content.d.ts +34 -0
- package/dist/esm/integrations/openai-content.js +360 -0
- package/dist/esm/integrations/openai.d.ts +39 -0
- package/dist/esm/integrations/openai.js +296 -0
- package/dist/esm/semconv.d.ts +12 -0
- package/dist/esm/semconv.js +12 -0
- package/dist/esm/truncation.d.ts +29 -0
- package/dist/esm/truncation.js +182 -10
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +3 -0
- package/package.json +11 -3
|
@@ -11,6 +11,14 @@ const INTEGRATIONS = [
|
|
|
11
11
|
await mod.patch(sdk);
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
|
+
{
|
|
15
|
+
name: "openai",
|
|
16
|
+
probe: () => import("openai"),
|
|
17
|
+
apply: async (sdk) => {
|
|
18
|
+
const mod = await import("./openai.js");
|
|
19
|
+
await mod.patch(sdk);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
14
22
|
{
|
|
15
23
|
name: "langchain",
|
|
16
24
|
probe: () => import("@langchain/core/language_models/chat_models"),
|
|
@@ -36,29 +36,37 @@ interface LLMResultLike {
|
|
|
36
36
|
*
|
|
37
37
|
* - `gen_ai.conversation.id` is the OTel GenAI-spec conversation identifier
|
|
38
38
|
* and Struct's UI grouping unit — one value = one entry in the sessions
|
|
39
|
-
* list.
|
|
40
|
-
*
|
|
39
|
+
* list. It is one id per run: every span belonging to a top-level agent
|
|
40
|
+
* invocation AND any subagents it spawns shares the SAME
|
|
41
|
+
* `gen_ai.conversation.id`, so the whole call tree collapses into one
|
|
42
|
+
* session (OTel's `conversation.id` models "the thread", not "the
|
|
43
|
+
* agent").
|
|
41
44
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* own
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* spans would collapse into the outer session, burying delegation.
|
|
45
|
+
* SUBAGENTS (an agent invoked from inside another's tool body) therefore
|
|
46
|
+
* INHERIT the parent run's `gen_ai.conversation.id` rather than minting
|
|
47
|
+
* their own. Structural linkage — "this invoke_agent span was spawned by
|
|
48
|
+
* that one" — is carried separately via the `struct.agent.parent_session_id`
|
|
49
|
+
* span attribute (what powers "Spawned by" / subagent navigation in the
|
|
50
|
+
* UI), not by giving the subagent a different session id.
|
|
49
51
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* A subagent MAY still be started with its own explicit thread_id (e.g. a
|
|
53
|
+
* LangGraph checkpoint key distinct from the parent's). When that happens
|
|
54
|
+
* we preserve it as `struct.agent.thread_id` for observability, but it does
|
|
55
|
+
* NOT split the session — `gen_ai.conversation.id` still follows the
|
|
56
|
+
* parent, because the grouping unit is the run, not the checkpoint.
|
|
57
|
+
*
|
|
58
|
+
* Chat / tool / retriever spans NEVER fabricate a conversation id. If no
|
|
59
|
+
* ancestor run, metadata thread_id, or ambient session supplies one, the
|
|
60
|
+
* span (and any log events it emits) simply omits `gen_ai.conversation.id`
|
|
61
|
+
* — an orphan span with no session is preferable to a fake one that can
|
|
62
|
+
* never be joined to anything else.
|
|
56
63
|
*
|
|
57
64
|
* End-user guidance:
|
|
58
65
|
* - Use thread_id per conversation; multi-turn chats reuse it.
|
|
59
|
-
* -
|
|
60
|
-
*
|
|
61
|
-
*
|
|
66
|
+
* - Subagents inherit the enclosing run's session automatically; you
|
|
67
|
+
* don't need to (and shouldn't) pass a different thread_id purely to
|
|
68
|
+
* separate them — use `struct.agent.parent_session_id` /
|
|
69
|
+
* "Spawned by" navigation in the UI instead.
|
|
62
70
|
*/
|
|
63
71
|
export declare class StructCallbackHandler {
|
|
64
72
|
private readonly sdk;
|
|
@@ -83,6 +91,54 @@ export declare class StructCallbackHandler {
|
|
|
83
91
|
readonly raiseError = false;
|
|
84
92
|
private readonly runs;
|
|
85
93
|
private readonly internalLogger;
|
|
94
|
+
/**
|
|
95
|
+
* Index of live `execute_tool` spans by their `langgraph_checkpoint_ns`,
|
|
96
|
+
* keyed to an ARRAY of currently-live spans rather than a single span.
|
|
97
|
+
*
|
|
98
|
+
* The re-parenting trick this supports: LangGraph stamps a namespace
|
|
99
|
+
* (`tools:<uuid>`) on a tool-call branch AND the same value on the
|
|
100
|
+
* sub-agent graph that tool triggers, so a sub-agent's chain-start (whose
|
|
101
|
+
* `parentRunId` points at the parent GRAPH — a sibling of the tool in the
|
|
102
|
+
* run tree, not the tool itself) can look itself up by ns and re-parent
|
|
103
|
+
* under its triggering tool call instead of landing as a sibling.
|
|
104
|
+
*
|
|
105
|
+
* That assumption — "ns is unique even across parallel same-named tool
|
|
106
|
+
* calls" — is FALSE on every `@langchain/langgraph` release checked, up
|
|
107
|
+
* to and including the latest published as of this writing (`1.4.7`, and
|
|
108
|
+
* confirmed byte-identical on `main` at HEAD): `ToolNode.run()`
|
|
109
|
+
* (`0.2.x`: `dist/prebuilt/tool_node.js`; `1.x`: moved to
|
|
110
|
+
* `libs/langgraph-core/src/prebuilt/tool_node.ts`) invokes every parallel
|
|
111
|
+
* `tool_call` within one step (`Promise.all(...map(call => this.runTool(
|
|
112
|
+
* call, config, input)))`) with the exact SAME `config` object reference
|
|
113
|
+
* — no per-call config cloning, no call-index-derived namespace — so
|
|
114
|
+
* `config.metadata.langgraph_checkpoint_ns` collides across parallel
|
|
115
|
+
* same-named tool calls in that step on every version, not just 0.2.x.
|
|
116
|
+
* (One opt-in exception: routing tool calls through LangGraph `Send`
|
|
117
|
+
* objects, added ~PR #1498, gets Pregel's normal per-task namespacing —
|
|
118
|
+
* but `createReactAgent`/`ToolNode`'s default path does not use it.) If a
|
|
119
|
+
* future release fixes this, update this comment with the version and
|
|
120
|
+
* consider re-enabling direct pairing unconditionally on it. (Python's
|
|
121
|
+
* `langgraph.prebuilt.tool_node.ToolNode._afunc` does not have this
|
|
122
|
+
* problem: it calls `get_config_list(config, len(tool_calls))` to build a
|
|
123
|
+
* genuinely per-call config before fanning out.)
|
|
124
|
+
*
|
|
125
|
+
* An array-per-ns (instead of one span per ns) makes the collision
|
|
126
|
+
* DETECTABLE instead of silently last-write-wins: at consume time
|
|
127
|
+
* (`handleChainStart`'s sub-agent lookup), exactly one live candidate for
|
|
128
|
+
* the ns is an unambiguous pair (re-parent as before); zero or more than
|
|
129
|
+
* one live candidates means we cannot tell which tool call the sub-agent
|
|
130
|
+
* actually belongs to, so we deliberately do NOT re-parent — the
|
|
131
|
+
* sub-agent falls back to the normal `resolveParent` chain (renders as a
|
|
132
|
+
* graph sibling: degraded, but never wrong). Cross-wiring two unrelated
|
|
133
|
+
* spans is worse than under-wiring one. Entries are evicted (their
|
|
134
|
+
* specific span removed, not the whole ns bucket) in
|
|
135
|
+
* `handleToolEnd`/`handleToolError` so the map never grows unboundedly and
|
|
136
|
+
* so a tool call that finishes frees its slot for ambiguity resolution.
|
|
137
|
+
* Parity: python `_tool_spans_by_ns` / `_checkpoint_ns` (python's
|
|
138
|
+
* `langgraph` >= 1.2.0 does not need this same guard, since its ns values
|
|
139
|
+
* never collide — see above).
|
|
140
|
+
*/
|
|
141
|
+
private readonly checkpointNsToolSpans;
|
|
86
142
|
constructor(sdk: StructSDK, tracer: Tracer, logger: Logger | undefined);
|
|
87
143
|
handleChainStart: (chain: Serialized, inputs: unknown, runId: string, parentRunId?: string, _tags?: string[], metadata?: Record<string, unknown>, runType?: string, runName?: string) => void;
|
|
88
144
|
handleChainEnd: (_outputs: unknown, runId: string) => void;
|
|
@@ -92,6 +148,14 @@ export declare class StructCallbackHandler {
|
|
|
92
148
|
handleLLMEnd: (output: LLMResultLike, runId: string) => void;
|
|
93
149
|
handleLLMError: (err: Error, runId: string) => void;
|
|
94
150
|
handleToolStart: (tool: Serialized, input: string, runId: string, parentRunId?: string, _tags?: string[], metadata?: Record<string, unknown>, runName?: string) => void;
|
|
151
|
+
/**
|
|
152
|
+
* Remove one specific span from its checkpoint-ns bucket (not the whole
|
|
153
|
+
* bucket) — a sibling parallel tool call under the same colliding ns may
|
|
154
|
+
* still be live and must keep its own candidate-list membership. Deletes
|
|
155
|
+
* the ns key entirely once its bucket is empty so the map never grows
|
|
156
|
+
* unboundedly.
|
|
157
|
+
*/
|
|
158
|
+
private evictCheckpointNsSpan;
|
|
95
159
|
handleToolEnd: (output: unknown, runId: string) => void;
|
|
96
160
|
handleToolError: (err: Error, runId: string) => void;
|
|
97
161
|
handleRetrieverStart: (retriever: Serialized, query: string, runId: string, parentRunId?: string, _tags?: string[], metadata?: Record<string, unknown>, runName?: string) => void;
|
|
@@ -100,7 +164,11 @@ export declare class StructCallbackHandler {
|
|
|
100
164
|
private resolveParent;
|
|
101
165
|
/**
|
|
102
166
|
* Conversation-id resolution for chat/tool/retriever spans — INHERIT from
|
|
103
|
-
* the parent
|
|
167
|
+
* the parent run so everything rolls up under one gen_ai.conversation.id.
|
|
168
|
+
* Returns `undefined` (never fabricates a UUID) when no ancestor run,
|
|
169
|
+
* metadata thread_id, or ambient session supplies one — an orphan span
|
|
170
|
+
* with no conversation.id is preferable to a fake one that can never be
|
|
171
|
+
* joined to anything else (parity: python `_resolve_session_id`).
|
|
104
172
|
*/
|
|
105
173
|
private resolveSessionId;
|
|
106
174
|
/**
|
|
@@ -117,18 +185,105 @@ export declare class StructCallbackHandler {
|
|
|
117
185
|
*/
|
|
118
186
|
private inheritedAgentSessionId;
|
|
119
187
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
188
|
+
* Nearest ``invoke_agent`` ancestor SPAN — O(1) lookup, mirrors
|
|
189
|
+
* ``inheritedAgentSessionId`` for the span pointer itself. Cached at every
|
|
190
|
+
* RunState creation so descendants (and prompt propagation) can reach the
|
|
191
|
+
* ancestor without walking a parent chain we don't retain.
|
|
124
192
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
193
|
+
* Unlike ``inheritedAgentSessionId``, a missing ``parentRunId`` falls back
|
|
194
|
+
* to the ambient `getAgentSpan()` (matching `resolveParent`'s top-level
|
|
195
|
+
* fallback) rather than `undefined` — a run with no LangChain parent may
|
|
196
|
+
* still be nested inside a manually-created `struct.agent()` span.
|
|
197
|
+
* Parity: python `_inherited_agent_span`.
|
|
198
|
+
*/
|
|
199
|
+
private inheritedAgentSpan;
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the pending-tool-call queue OBJECT a run should push/pop
|
|
202
|
+
* against for `gen_ai.tool.call.id` autofill, per `RunState.toolCallQueue`'s
|
|
203
|
+
* doc comment.
|
|
204
|
+
*
|
|
205
|
+
* PINNED AT THE RUN-TREE ROOT, INHERITED DOWN AS THE SAME OBJECT
|
|
206
|
+
* REFERENCE — this is the crux of the design, and it is the OBJECT that
|
|
207
|
+
* must be pinned, not merely a manual-vs-bare boolean. Whenever a parent
|
|
208
|
+
* run is known (`parentRunId` resolves to a live `RunState`), we copy its
|
|
209
|
+
* `toolCallQueue` reference VERBATIM rather than re-deriving anything from
|
|
210
|
+
* the live ALS frame. Only a run with no resolvable parent (a genuine
|
|
211
|
+
* root, or an orphaned `parentRunId` whose `RunState` was never
|
|
212
|
+
* registered) consults the live `getManualAgentSpan()` signal — and even
|
|
213
|
+
* then, only ONCE, at that run's creation.
|
|
214
|
+
*
|
|
215
|
+
* Why the OBJECT, not just a boolean: a descendant run's OWN callback can
|
|
216
|
+
* fire in a lost or foreign ALS frame — the LangChain FRAMEWORK layer
|
|
217
|
+
* (`langchain.ts`'s `BaseChatModel.generate`/`.stream` patch) opens its
|
|
218
|
+
* own short-lived ALS scope (`runWithContext({ suppressGenAi: true },
|
|
219
|
+
* fn)`) around each chat completion, entirely independent of any customer
|
|
220
|
+
* `struct.agent()` call; more generally, LangGraph's internal scheduling
|
|
221
|
+
* can run a node's callback outside the async context that originally
|
|
222
|
+
* held the manual scope. A design that pins only a manual-vs-bare
|
|
223
|
+
* ownership BOOLEAN but still routes the actual push (`handleLLMEnd`) and
|
|
224
|
+
* pop (`handleToolStart`) through the ALS-reading
|
|
225
|
+
* `pushPendingToolCalls`/`popPendingToolCallId` helpers (context.ts) is
|
|
226
|
+
* still broken: those helpers read whatever `pendingToolCalls` object is
|
|
227
|
+
* on the CURRENT frame, so a push in one lost/foreign frame and a pop in a
|
|
228
|
+
* DIFFERENT lost/foreign frame silently disagree, even though both frames
|
|
229
|
+
* correctly resolved the same boolean. Operating directly on the
|
|
230
|
+
* RunState-pinned `toolCallQueue` object sidesteps the ALS frame entirely
|
|
231
|
+
* for both operations: whatever object the root observed is what the
|
|
232
|
+
* whole run tree pushes into and pops from, regardless of ALS frame state
|
|
233
|
+
* at either callback.
|
|
234
|
+
*
|
|
235
|
+
* Three cases:
|
|
236
|
+
* 1. `parentRunId` resolves to a registered parent `RunState` — inherit
|
|
237
|
+
* `toolCallQueue` from it verbatim (decided once at the tree root) —
|
|
238
|
+
* UNLESS a NEW manual scope began mid-tree (see FIX F below).
|
|
239
|
+
* 2. No parent (root, or an orphaned `parentRunId`) AND a MANUAL
|
|
240
|
+
* `struct.agent()` scope is live right now (`getManualAgentSpan()`
|
|
241
|
+
* set) — capture `getPendingToolCalls()`, the SAME object
|
|
242
|
+
* `struct.agent()` seeded onto the ALS store and that manual
|
|
243
|
+
* `struct.tool()` calls pop from via `popPendingToolCallId`
|
|
244
|
+
* (context.ts). Captured IN-FRAME here (this callback necessarily
|
|
245
|
+
* runs inside the live manual scope), so the RunState-pinned
|
|
246
|
+
* reference and the ALS store's `pendingToolCalls` stay identical.
|
|
247
|
+
* 3. No parent AND no manual scope — this run IS the top of a bare run
|
|
248
|
+
* tree; mint a fresh queue `{}`.
|
|
249
|
+
*
|
|
250
|
+
* Self-audit round 5 (FIX F): case 1's "inherit verbatim" is only correct
|
|
251
|
+
* when the parent's queue was captured under the SAME manual ownership
|
|
252
|
+
* that is live right now. A NESTED manual `struct.agent()` — opened from
|
|
253
|
+
* inside a tool body, whose own LangChain graph LangChain threads as a
|
|
254
|
+
* CHILD of the outer run tree, parented on a REAL tool span rather than
|
|
255
|
+
* reaching the manual span through skipped chains (so it never reaches
|
|
256
|
+
* the suppressed-twin branch in `handleChainStart`) — would otherwise
|
|
257
|
+
* silently inherit the OUTER run's
|
|
258
|
+
* queue object, even though the CURRENT ambient `getManualAgentSpan()` is
|
|
259
|
+
* the INNER agent's own (different) span and the inner agent's own
|
|
260
|
+
* `pendingToolCalls` (a fresh object seeded by that inner `struct.agent()`
|
|
261
|
+
* call) is what its descendants actually push/pop against. Detect this by
|
|
262
|
+
* comparing the live `getManualAgentSpan()` against the PARENT run's
|
|
263
|
+
* `queueManualSpan` (the manual span that owned the parent's queue): if
|
|
264
|
+
* they differ and the live one is truthy, a new manual scope has begun —
|
|
265
|
+
* re-capture `getPendingToolCalls()` fresh, exactly like case 2. When they
|
|
266
|
+
* match (including both `undefined`, i.e. bare on both sides — preserves
|
|
267
|
+
* the F2 frame-independence fix, where a merely lost/foreign ALS frame
|
|
268
|
+
* reads as `undefined` and must NOT trigger a spurious re-capture),
|
|
269
|
+
* inherit verbatim as before.
|
|
270
|
+
*/
|
|
271
|
+
private resolveQueueOwnership;
|
|
272
|
+
/**
|
|
273
|
+
* Conversation-id resolution for AGENT spans — INHERIT from the parent
|
|
274
|
+
* run first, so a subagent shares its outer agent's gen_ai.conversation.id
|
|
275
|
+
* (one id per run; parity: python `_resolve_agent_session_id`). Only when
|
|
276
|
+
* there's no parent session do we fall back to metadata.thread_id, the
|
|
277
|
+
* ambient session, or — as a last resort, since agent spans should always
|
|
278
|
+
* have a coherent id — a fresh UUID. A subagent's own divergent thread_id
|
|
279
|
+
* (if any) is preserved separately as `struct.agent.thread_id`; see the
|
|
280
|
+
* caller in `handleChainStart`.
|
|
129
281
|
*/
|
|
130
282
|
private resolveAgentSessionId;
|
|
131
283
|
private propagateUserPrompt;
|
|
132
284
|
}
|
|
285
|
+
declare function detectProviderFromSerialized(llm: Serialized): string;
|
|
286
|
+
/** @internal */
|
|
287
|
+
export declare const _detectProviderFromSerializedForTest: typeof detectProviderFromSerialized;
|
|
133
288
|
export {};
|
|
134
289
|
//# sourceMappingURL=langchain-callback.d.ts.map
|