@amaster.ai/dsh-langfuse 0.1.34 → 0.1.36
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 +4 -1
- package/lib/client.js +36 -2
- package/lib/index.js +366 -29
- package/lib/types/client.d.ts +2 -0
- package/lib/types/index.d.ts +35 -32
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Langfuse observability for [DeepSeek Harness (dsh)](https://github.com/deepseek-
|
|
|
7
7
|
- one **generation** per LLM call (`llm/stream` waterfall), plus a nested `llm-request` span carrying the verbatim loop-built request
|
|
8
8
|
- one **tool observation** per tool call (`tools/pre-execute` through `tools/result`)
|
|
9
9
|
- one **span** per `hook/invoked` / `hook/result` pair, with hook point, decision, exit code, and duration (stderr summary follows `captureContent`)
|
|
10
|
-
- one **trace** per session turn (`session/event`) —
|
|
10
|
+
- one **trace** per session turn with observed activity (`session/event`) — the root is created on the first message or observed operation, retains the original turn start time, and ends at `turn/end`; completed turns without activity export nothing, while errors and cancellation remain visible
|
|
11
11
|
- subagent child sessions nested under the parent's tree (`session/created` header link + `subagent/start` / `subagent/end`)
|
|
12
12
|
- structured reasoning in generation output, optional image display, inherited user/tags, and final turn severity
|
|
13
13
|
- optional W3C parent context supplied per session, without adopting the host's ambient OTEL context
|
|
@@ -37,6 +37,7 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
37
37
|
baseUrl: https://cloud.langfuse.com
|
|
38
38
|
traceName: dsh-turn
|
|
39
39
|
captureContent: true # set false to record metadata only
|
|
40
|
+
maxPayloadBytes: 262144 # per-LLM-call request/raw/output cap (256 KiB)
|
|
40
41
|
userId: developer
|
|
41
42
|
tags: [coding, experiment-a]
|
|
42
43
|
environment: development
|
|
@@ -53,6 +54,8 @@ Buffered spans drain on `session/flush(session)`; the exporter shuts down with t
|
|
|
53
54
|
|
|
54
55
|
With `captureContent: true`, both `llm-call` and `llm-request` inputs contain every `llm/stream` request option except the non-serializable `AbortSignal`. New request fields pass through without a plugin update. For image-bearing requests, generation input is updated to a display projection; `llm-request` retains the original attachment references. Langfuse's separate generation `modelParameters` attribute summarizes the known sampling fields; it is not part of either request input. These are dsh-side values, not exact provider HTTP payloads: adapters may add defaults, project messages/tools, or rename fields after this seam. With `captureContent: false`, the collector keeps chunk/tool-call counts and usage without buffering text, reasoning, tool arguments, or raw chunks; inputs keep structural counts while the separate `modelParameters` attribute keeps safe parameter metadata; reply excerpts, reasoning, tool arguments/results, error text, and media are withheld from observation names and IO.
|
|
55
56
|
|
|
57
|
+
`maxPayloadBytes` independently bounds each captured request, raw chunk stream, and assembled output per LLM call (default 256 KiB). Oversized requests become a bounded preview; oversized streams retain only fitting chunks/prefixes and set `metadata.truncated`. These limits apply only to telemetry copies: forwarded chunks and original messages/results stay unchanged. Session disposal closes that session's active LLM/tool observations; a child whose activation rolls back before `subagent/start` closes immediately, while an already-started child still closes at `subagent/end`.
|
|
58
|
+
|
|
56
59
|
Configured Langfuse credentials and `pk-lf-*` / `sk-lf-*` values are redacted across reporter payloads, names, status messages, and labels. `redactFields` replaces matching payload fields recursively, including JSON-encoded arguments, ignoring case. Name excerpts are derived after redaction. Delta blocks are also checked as assembled text so fragmented credentials cannot bypass the mask: affected blocks keep their record count/type/index, with sanitized text in the first delta and later deltas cleared. Redaction only changes telemetry copies; original messages/results and the forwarded stream stay intact. The exporter applies the same mask to IO and metadata as a final safeguard. User/tags are explicit metadata and remain when content capture is off.
|
|
57
60
|
|
|
58
61
|
## Reasoning, images, and turn results
|
package/lib/client.js
CHANGED
|
@@ -115,16 +115,38 @@ export class LangfuseReporter {
|
|
|
115
115
|
this.config = config;
|
|
116
116
|
this.ready = this.init();
|
|
117
117
|
}
|
|
118
|
+
/** Whether a payload property should be replaced without inspecting its value. */
|
|
119
|
+
shouldRedactField(key) {
|
|
120
|
+
return !!this.config.redactFields?.some((field) => field.toLowerCase() === key.toLowerCase());
|
|
121
|
+
}
|
|
118
122
|
/** Clone telemetry only; never mutate harness-owned messages or tool results. */
|
|
119
123
|
mask(data) {
|
|
120
124
|
const secrets = [this.config.publicKey, this.config.secretKey].filter(Boolean);
|
|
121
125
|
const fields = new Set(this.config.redactFields?.map((key) => key.toLowerCase()));
|
|
122
126
|
const seen = new WeakSet();
|
|
123
127
|
const redactText = (text) => {
|
|
124
|
-
const
|
|
128
|
+
const markedRanges = Array.from(text.matchAll(/\[REDACTED\]/g), (match) => [match.index, match.index + match[0].length]);
|
|
129
|
+
const ranges = [];
|
|
130
|
+
let markerIndex = 0;
|
|
131
|
+
const addRange = (start, stop) => {
|
|
132
|
+
// Existing markers are atomic: a second mask must never expand them.
|
|
133
|
+
// Walk the sorted marker list once instead of rescanning it per match.
|
|
134
|
+
while (markerIndex < markedRanges.length && markedRanges[markerIndex][1] <= start)
|
|
135
|
+
markerIndex += 1;
|
|
136
|
+
if (markerIndex < markedRanges.length &&
|
|
137
|
+
start < markedRanges[markerIndex][1] &&
|
|
138
|
+
stop > markedRanges[markerIndex][0])
|
|
139
|
+
return;
|
|
140
|
+
ranges.push([start, stop]);
|
|
141
|
+
};
|
|
142
|
+
for (const match of text.matchAll(/(?:pk|sk)-lf-[\w-]+/g))
|
|
143
|
+
addRange(match.index, match.index + match[0].length);
|
|
125
144
|
for (const secret of secrets) {
|
|
145
|
+
// Each independent scan starts at the first marker. Do not reset for
|
|
146
|
+
// every match, or a large marker list becomes quadratic again.
|
|
147
|
+
markerIndex = 0;
|
|
126
148
|
for (let index = text.indexOf(secret); index !== -1; index = text.indexOf(secret, index + 1)) {
|
|
127
|
-
|
|
149
|
+
addRange(index, index + secret.length);
|
|
128
150
|
}
|
|
129
151
|
}
|
|
130
152
|
let output = '';
|
|
@@ -346,6 +368,12 @@ export class LangfuseReporter {
|
|
|
346
368
|
// v5 observation attributes have no `name` — renames ride the OTEL span.
|
|
347
369
|
if (update.name)
|
|
348
370
|
generation.otelSpan.updateName(update.name);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
console.error('[dsh-langfuse] generation end failed:', error);
|
|
374
|
+
}
|
|
375
|
+
// An update failure must not strand the observation: v5 exports only ended spans.
|
|
376
|
+
try {
|
|
349
377
|
generation.end(endedAt);
|
|
350
378
|
}
|
|
351
379
|
catch (error) {
|
|
@@ -418,6 +446,12 @@ export class LangfuseReporter {
|
|
|
418
446
|
if (update.output !== undefined && this.roots.has(span)) {
|
|
419
447
|
span.setTraceIO({ output: update.output });
|
|
420
448
|
}
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
console.error('[dsh-langfuse] span end failed:', error);
|
|
452
|
+
}
|
|
453
|
+
// Always close the span, even when its final attributes could not be written.
|
|
454
|
+
try {
|
|
421
455
|
span.end(endedAt);
|
|
422
456
|
}
|
|
423
457
|
catch (error) {
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* shared identity).
|
|
14
14
|
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
|
15
15
|
* - `session/event` emit → one Langfuse **trace** per agent turn
|
|
16
|
-
* (`turn/start`
|
|
16
|
+
* (`turn/start` records the start; actual activity opens the root — in v5 it IS
|
|
17
17
|
* the trace — the turn's first `user/message` sets the input, `turn/end`
|
|
18
18
|
* stamps the end reason and ends the root, which is what exports it).
|
|
19
19
|
* - Native A2A inbox tasks own a trace spanning admission through task termination;
|
|
@@ -54,6 +54,11 @@ export const Config = Schema.object({
|
|
|
54
54
|
traceName: Schema.string().default('dsh-turn'),
|
|
55
55
|
/** Include full message/tool payloads in Langfuse IO. Disable to log metadata only. */
|
|
56
56
|
captureContent: Schema.boolean().default(true),
|
|
57
|
+
maxPayloadBytes: Schema.number()
|
|
58
|
+
.min(1)
|
|
59
|
+
.step(1)
|
|
60
|
+
.default(256 * 1024)
|
|
61
|
+
.description('maximum captured request, raw-stream, and assembled-output bytes per LLM call'),
|
|
57
62
|
redactFields: Schema.array(Schema.string())
|
|
58
63
|
.default([])
|
|
59
64
|
.description('case-insensitive payload fields to redact'),
|
|
@@ -125,6 +130,260 @@ function requestBodyOf(options, captureContent) {
|
|
|
125
130
|
const { signal: _, ...body } = options;
|
|
126
131
|
return body;
|
|
127
132
|
}
|
|
133
|
+
const newPayloadBudget = (maxBytes) => ({
|
|
134
|
+
remaining: maxBytes,
|
|
135
|
+
truncated: false,
|
|
136
|
+
});
|
|
137
|
+
/** Take a UTF-8 prefix without ever exceeding the remaining content-byte budget. */
|
|
138
|
+
function takePayloadText(value, budget) {
|
|
139
|
+
const bytes = Buffer.byteLength(value);
|
|
140
|
+
if (bytes <= budget.remaining) {
|
|
141
|
+
budget.remaining -= bytes;
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
budget.truncated = true;
|
|
145
|
+
if (budget.remaining <= 0)
|
|
146
|
+
return '';
|
|
147
|
+
let low = 0;
|
|
148
|
+
let high = value.length;
|
|
149
|
+
while (low < high) {
|
|
150
|
+
const middle = Math.ceil((low + high) / 2);
|
|
151
|
+
if (Buffer.byteLength(value.slice(0, middle)) <= budget.remaining)
|
|
152
|
+
low = middle;
|
|
153
|
+
else
|
|
154
|
+
high = middle - 1;
|
|
155
|
+
}
|
|
156
|
+
budget.remaining = 0;
|
|
157
|
+
return value.slice(0, low);
|
|
158
|
+
}
|
|
159
|
+
/** Build a preview whose complete JSON value, including its wrapper, fits the remaining cap. */
|
|
160
|
+
function previewPayload(serialized, budget) {
|
|
161
|
+
const remaining = budget.remaining;
|
|
162
|
+
const marker = { truncated: true };
|
|
163
|
+
const withPreview = { ...marker, preview: '' };
|
|
164
|
+
if (Buffer.byteLength(JSON.stringify(withPreview)) <= remaining) {
|
|
165
|
+
let low = 0;
|
|
166
|
+
let high = serialized.length;
|
|
167
|
+
while (low < high) {
|
|
168
|
+
const middle = Math.ceil((low + high) / 2);
|
|
169
|
+
const candidate = { ...marker, preview: serialized.slice(0, middle) };
|
|
170
|
+
if (Buffer.byteLength(JSON.stringify(candidate)) <= remaining)
|
|
171
|
+
low = middle;
|
|
172
|
+
else
|
|
173
|
+
high = middle - 1;
|
|
174
|
+
}
|
|
175
|
+
budget.remaining = 0;
|
|
176
|
+
return { ...marker, preview: serialized.slice(0, low) };
|
|
177
|
+
}
|
|
178
|
+
if (Buffer.byteLength(JSON.stringify(marker)) <= remaining) {
|
|
179
|
+
budget.remaining = 0;
|
|
180
|
+
return marker;
|
|
181
|
+
}
|
|
182
|
+
// Even the marker is too large: retain a plain string prefix, still capped
|
|
183
|
+
// after JSON quoting/escaping rather than by source-string bytes.
|
|
184
|
+
let low = 0;
|
|
185
|
+
let high = serialized.length;
|
|
186
|
+
while (low < high) {
|
|
187
|
+
const middle = Math.ceil((low + high) / 2);
|
|
188
|
+
if (Buffer.byteLength(JSON.stringify(serialized.slice(0, middle))) <= remaining)
|
|
189
|
+
low = middle;
|
|
190
|
+
else
|
|
191
|
+
high = middle - 1;
|
|
192
|
+
}
|
|
193
|
+
budget.remaining = 0;
|
|
194
|
+
return low > 0 ? serialized.slice(0, low) : remaining >= 2 ? '' : 0;
|
|
195
|
+
}
|
|
196
|
+
const MAX_ATOMIC_MASK_BYTES = 64 * 1024;
|
|
197
|
+
/** Redact leaves while serializing, so an oversized payload never needs a full mask or stringify. */
|
|
198
|
+
function boundMaskedPayload(input, budget, reporter) {
|
|
199
|
+
const seen = new WeakSet();
|
|
200
|
+
const parts = [];
|
|
201
|
+
let remaining = budget.remaining;
|
|
202
|
+
let overflow = false;
|
|
203
|
+
const append = (text) => {
|
|
204
|
+
if (overflow || text === undefined)
|
|
205
|
+
return;
|
|
206
|
+
const bytes = Buffer.byteLength(text);
|
|
207
|
+
if (bytes > remaining) {
|
|
208
|
+
// The node was already masked atomically; a serialized prefix is safe.
|
|
209
|
+
parts.push(takePayloadText(text, { remaining, truncated: true }));
|
|
210
|
+
remaining = 0;
|
|
211
|
+
overflow = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
parts.push(text);
|
|
215
|
+
remaining -= bytes;
|
|
216
|
+
};
|
|
217
|
+
const write = (value) => {
|
|
218
|
+
if (overflow || value === undefined)
|
|
219
|
+
return undefined;
|
|
220
|
+
if (value === null || typeof value !== 'object' || value instanceof Date) {
|
|
221
|
+
if (typeof value === 'string' &&
|
|
222
|
+
Buffer.byteLength(value) > MAX_ATOMIC_MASK_BYTES &&
|
|
223
|
+
Buffer.byteLength(value) > remaining) {
|
|
224
|
+
overflow = true;
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const masked = reporter.mask(value);
|
|
228
|
+
append(JSON.stringify(masked));
|
|
229
|
+
return masked;
|
|
230
|
+
}
|
|
231
|
+
if (seen.has(value)) {
|
|
232
|
+
append(JSON.stringify('[Circular]'));
|
|
233
|
+
return '[Circular]';
|
|
234
|
+
}
|
|
235
|
+
seen.add(value);
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
append('[');
|
|
238
|
+
const result = [];
|
|
239
|
+
for (const [index, item] of value.entries()) {
|
|
240
|
+
if (overflow)
|
|
241
|
+
break;
|
|
242
|
+
if (index > 0)
|
|
243
|
+
append(',');
|
|
244
|
+
if (item === undefined) {
|
|
245
|
+
append('null');
|
|
246
|
+
result.push(null);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const masked = write(item);
|
|
250
|
+
if (!overflow)
|
|
251
|
+
result.push(masked);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
append(']');
|
|
255
|
+
seen.delete(value);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
append('{');
|
|
259
|
+
const result = Object.create(null);
|
|
260
|
+
let first = true;
|
|
261
|
+
for (const key in value) {
|
|
262
|
+
if (overflow)
|
|
263
|
+
break;
|
|
264
|
+
if (!Object.hasOwn(value, key))
|
|
265
|
+
continue;
|
|
266
|
+
const child = value[key];
|
|
267
|
+
if (child === undefined && !reporter.shouldRedactField(key))
|
|
268
|
+
continue;
|
|
269
|
+
if (!first)
|
|
270
|
+
append(',');
|
|
271
|
+
first = false;
|
|
272
|
+
append(JSON.stringify(reporter.mask(key)));
|
|
273
|
+
append(':');
|
|
274
|
+
if (reporter.shouldRedactField(key)) {
|
|
275
|
+
append(JSON.stringify('[REDACTED]'));
|
|
276
|
+
result[key] = '[REDACTED]';
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const masked = write(child);
|
|
280
|
+
if (!overflow)
|
|
281
|
+
result[key] = masked;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
append('}');
|
|
285
|
+
seen.delete(value);
|
|
286
|
+
return result;
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const masked = write(input);
|
|
290
|
+
if (!overflow) {
|
|
291
|
+
budget.remaining = remaining;
|
|
292
|
+
return masked;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
budget.truncated = true;
|
|
297
|
+
return previewPayload('', budget);
|
|
298
|
+
}
|
|
299
|
+
budget.truncated = true;
|
|
300
|
+
return previewPayload(parts.join(''), budget);
|
|
301
|
+
}
|
|
302
|
+
/** Bound a structured payload without changing its shape when it fits. */
|
|
303
|
+
function boundPayload(input, budget, reporter) {
|
|
304
|
+
if (reporter)
|
|
305
|
+
return boundMaskedPayload(input, budget, reporter);
|
|
306
|
+
let serialized;
|
|
307
|
+
try {
|
|
308
|
+
serialized = JSON.stringify(input) ?? '';
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
budget.truncated = true;
|
|
312
|
+
return previewPayload('', budget);
|
|
313
|
+
}
|
|
314
|
+
const bytes = Buffer.byteLength(serialized);
|
|
315
|
+
if (bytes <= budget.remaining) {
|
|
316
|
+
budget.remaining -= bytes;
|
|
317
|
+
return input;
|
|
318
|
+
}
|
|
319
|
+
budget.truncated = true;
|
|
320
|
+
return previewPayload(serialized, budget);
|
|
321
|
+
}
|
|
322
|
+
/** Keep only whole chunks that fit; stream accounting still processes every original chunk. */
|
|
323
|
+
function capturePayloadChunk(chunk, chunks, budget) {
|
|
324
|
+
if (budget.remaining <= 0) {
|
|
325
|
+
budget.truncated = true;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let serialized;
|
|
329
|
+
try {
|
|
330
|
+
serialized = JSON.stringify(chunk) ?? '';
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
chunks.length = 0;
|
|
334
|
+
budget.truncated = true;
|
|
335
|
+
budget.remaining = 0;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// Account for the final raw array too: [] around the first chunk and one
|
|
339
|
+
// comma per additional retained chunk, after redaction has changed lengths.
|
|
340
|
+
const bytes = Buffer.byteLength(serialized) + (chunks.length === 0 ? 2 : 1);
|
|
341
|
+
if (bytes > budget.remaining) {
|
|
342
|
+
// Partial groups are unsafe: a retained prefix may be a secret fragment.
|
|
343
|
+
chunks.length = 0;
|
|
344
|
+
budget.truncated = true;
|
|
345
|
+
budget.remaining = 0;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
chunks.push(chunk);
|
|
349
|
+
budget.remaining -= bytes;
|
|
350
|
+
}
|
|
351
|
+
/** Append only while the full assembled text fits; otherwise drop the whole channel. */
|
|
352
|
+
function appendAssembledText(current, delta, budget) {
|
|
353
|
+
const bytes = Buffer.byteLength(delta);
|
|
354
|
+
if (bytes > budget.remaining) {
|
|
355
|
+
budget.truncated = true;
|
|
356
|
+
budget.remaining = 0;
|
|
357
|
+
return '';
|
|
358
|
+
}
|
|
359
|
+
budget.remaining -= bytes;
|
|
360
|
+
return current + delta;
|
|
361
|
+
}
|
|
362
|
+
/** Charge the whole tool-call envelope, not only its argument text. */
|
|
363
|
+
function captureToolCall(call, calls, budget) {
|
|
364
|
+
if (budget.remaining <= 0) {
|
|
365
|
+
budget.truncated = true;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const envelope = {
|
|
369
|
+
...call,
|
|
370
|
+
function: { ...call.function, arguments: '' },
|
|
371
|
+
};
|
|
372
|
+
const bytes = Buffer.byteLength(JSON.stringify(envelope));
|
|
373
|
+
if (bytes > budget.remaining) {
|
|
374
|
+
budget.truncated = true;
|
|
375
|
+
budget.remaining = 0;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
budget.remaining -= bytes;
|
|
379
|
+
calls.push({
|
|
380
|
+
...call,
|
|
381
|
+
function: {
|
|
382
|
+
...call.function,
|
|
383
|
+
arguments: takePayloadText(call.function.arguments, budget),
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
128
387
|
/** Generation names carry the reply's first non-empty line (capped) for list readability. */
|
|
129
388
|
function enhanceWithFirstLine(base, text) {
|
|
130
389
|
const firstLine = text
|
|
@@ -269,6 +528,7 @@ export function apply(ctx, config) {
|
|
|
269
528
|
const toolResults = new WeakMap();
|
|
270
529
|
const openHooks = new Map();
|
|
271
530
|
const openOperations = new Set();
|
|
531
|
+
const operationSessions = new Map();
|
|
272
532
|
const traceContexts = new Map();
|
|
273
533
|
const pendingMedia = new Set();
|
|
274
534
|
const mediaFor = new WeakMap();
|
|
@@ -330,8 +590,13 @@ export function apply(ctx, config) {
|
|
|
330
590
|
}
|
|
331
591
|
});
|
|
332
592
|
function openTrace(sessionId, metadata, startTime) {
|
|
333
|
-
const
|
|
334
|
-
|
|
593
|
+
const pending = sessionId ? pendingTurns.get(sessionId) : undefined;
|
|
594
|
+
const context = pending
|
|
595
|
+
? pending.context
|
|
596
|
+
: sessionId
|
|
597
|
+
? traceContexts.get(sessionId)
|
|
598
|
+
: undefined;
|
|
599
|
+
if (sessionId && !pending)
|
|
335
600
|
traceContexts.delete(sessionId);
|
|
336
601
|
return reporter.openTrace({ name: config.traceName, sessionId, metadata, context, startTime });
|
|
337
602
|
}
|
|
@@ -635,10 +900,11 @@ export function apply(ctx, config) {
|
|
|
635
900
|
if (!parentSessionId)
|
|
636
901
|
return;
|
|
637
902
|
const childId = session.id;
|
|
638
|
-
// Parent under the enclosing delegation tool span when
|
|
639
|
-
//
|
|
640
|
-
// parent
|
|
641
|
-
const
|
|
903
|
+
// Parent under the enclosing delegation tool span only when it is unique;
|
|
904
|
+
// concurrent tools have no reliable stack-top ordering, so use the known
|
|
905
|
+
// parent subagent/root instead of attributing the child to an arbitrary tool.
|
|
906
|
+
const openTools = openToolSpans.get(parentSessionId) ?? [];
|
|
907
|
+
const parentObservation = (openTools.length === 1 ? openTools[0] : undefined) ??
|
|
642
908
|
children.get(parentSessionId)?.span ??
|
|
643
909
|
traceFor(parentSessionId);
|
|
644
910
|
const span = reporter.startSpan(parentObservation, {
|
|
@@ -651,6 +917,7 @@ export function apply(ctx, config) {
|
|
|
651
917
|
children.set(childId, {
|
|
652
918
|
parentSessionId,
|
|
653
919
|
span,
|
|
920
|
+
started: false,
|
|
654
921
|
hasInput: false,
|
|
655
922
|
turnTrace: children.get(parentSessionId)?.turnTrace ??
|
|
656
923
|
sessions.get(rootSessionId(parentSessionId) ?? '')?.trace,
|
|
@@ -660,13 +927,17 @@ export function apply(ctx, config) {
|
|
|
660
927
|
const child = children.get(info.id);
|
|
661
928
|
if (!child)
|
|
662
929
|
return; // remote (ACP) runs have no local session — nothing to nest
|
|
930
|
+
child.started = true;
|
|
663
931
|
reporter.updateSpan(child.span, { metadata: { provider: info.provider, local: info.local } });
|
|
664
932
|
});
|
|
665
933
|
ctx.on('subagent/end', (info) => {
|
|
666
934
|
const child = children.get(info.id);
|
|
667
935
|
if (!child)
|
|
668
936
|
return;
|
|
669
|
-
|
|
937
|
+
// A background child can finish after the parent moved to another turn.
|
|
938
|
+
// Only mark the root that was active when this child was created.
|
|
939
|
+
if (info.stopReason === 'error' &&
|
|
940
|
+
sessions.get(rootSessionId(info.id) ?? '')?.trace === child.turnTrace)
|
|
670
941
|
noteError(info.id);
|
|
671
942
|
children.delete(info.id);
|
|
672
943
|
afterMedia(child.span, (endedAt) => reporter.endSpan(child.span, {
|
|
@@ -854,17 +1125,20 @@ export function apply(ctx, config) {
|
|
|
854
1125
|
if (stale)
|
|
855
1126
|
endTurn(stale);
|
|
856
1127
|
inboxInputs.delete(sessionId);
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if
|
|
864
|
-
|
|
1128
|
+
sessions.delete(sessionId);
|
|
1129
|
+
pendingTurns.set(sessionId, {
|
|
1130
|
+
turn: event.data.turn,
|
|
1131
|
+
startTime: new Date(event.time),
|
|
1132
|
+
context: nativeInboxes.has(sessionId) ? undefined : traceContexts.get(sessionId),
|
|
1133
|
+
});
|
|
1134
|
+
// Ordinary turns consume request context at start, even if they remain empty.
|
|
1135
|
+
if (!nativeInboxes.has(sessionId))
|
|
1136
|
+
traceContexts.delete(sessionId);
|
|
865
1137
|
break;
|
|
866
1138
|
}
|
|
867
1139
|
case 'user/message': {
|
|
1140
|
+
if (pendingTurns.has(sessionId))
|
|
1141
|
+
traceFor(sessionId);
|
|
868
1142
|
const state = sessions.get(sessionId);
|
|
869
1143
|
if (state && !state.hasInput && config.captureContent) {
|
|
870
1144
|
setMessageInput(state.trace, event.data.content);
|
|
@@ -873,6 +1147,8 @@ export function apply(ctx, config) {
|
|
|
873
1147
|
break;
|
|
874
1148
|
}
|
|
875
1149
|
case 'assistant/message': {
|
|
1150
|
+
if (pendingTurns.has(sessionId))
|
|
1151
|
+
traceFor(sessionId);
|
|
876
1152
|
const state = sessions.get(sessionId);
|
|
877
1153
|
if (state && config.captureContent) {
|
|
878
1154
|
state.lastAssistantText = messageText(event.data.message) ?? state.lastAssistantText;
|
|
@@ -880,8 +1156,10 @@ export function apply(ctx, config) {
|
|
|
880
1156
|
break;
|
|
881
1157
|
}
|
|
882
1158
|
case 'turn/end': {
|
|
883
|
-
if (pendingTurns.has(sessionId)
|
|
1159
|
+
if (pendingTurns.has(sessionId) &&
|
|
1160
|
+
(nativeInboxes.has(sessionId) || event.data.reason.kind !== 'completed'))
|
|
884
1161
|
traceFor(sessionId);
|
|
1162
|
+
pendingTurns.delete(sessionId);
|
|
885
1163
|
const state = sessions.get(sessionId);
|
|
886
1164
|
if (state) {
|
|
887
1165
|
sessions.delete(sessionId);
|
|
@@ -913,6 +1191,16 @@ export function apply(ctx, config) {
|
|
|
913
1191
|
});
|
|
914
1192
|
ctx.on('session/disposed', (session) => {
|
|
915
1193
|
const sessionId = session.id;
|
|
1194
|
+
for (const close of [...openOperations]) {
|
|
1195
|
+
if (operationSessions.get(close) === sessionId)
|
|
1196
|
+
close();
|
|
1197
|
+
}
|
|
1198
|
+
const child = children.get(sessionId);
|
|
1199
|
+
if (child && !child.started) {
|
|
1200
|
+
// Activation rollback disposes the session without subagent/start/end.
|
|
1201
|
+
children.delete(sessionId);
|
|
1202
|
+
afterMedia(child.span, (endedAt) => reporter.endSpan(child.span, { level: 'WARNING', statusMessage: 'subagent activation rolled back' }, endedAt));
|
|
1203
|
+
}
|
|
916
1204
|
closeHooks(sessionId);
|
|
917
1205
|
const state = sessions.get(sessionId);
|
|
918
1206
|
sessions.delete(sessionId);
|
|
@@ -951,7 +1239,12 @@ export function apply(ctx, config) {
|
|
|
951
1239
|
const oneOffRoot = options.sessionId ? null : parent;
|
|
952
1240
|
const generationName = options.purpose ? `llm-call [${options.purpose}]` : 'llm-call';
|
|
953
1241
|
const modelParameters = modelParametersOf(options, config.captureContent);
|
|
954
|
-
const
|
|
1242
|
+
const maxPayloadBytes = config.maxPayloadBytes ?? 256 * 1024;
|
|
1243
|
+
const requestBudget = newPayloadBudget(maxPayloadBytes);
|
|
1244
|
+
const rawBudget = newPayloadBudget(maxPayloadBytes);
|
|
1245
|
+
const outputBudget = newPayloadBudget(maxPayloadBytes);
|
|
1246
|
+
const requestBody = requestBodyOf(options, config.captureContent);
|
|
1247
|
+
const input = boundPayload(requestBody, requestBudget, reporter);
|
|
955
1248
|
const generation = reporter.startGeneration(parent, {
|
|
956
1249
|
name: generationName,
|
|
957
1250
|
model: options.model,
|
|
@@ -966,7 +1259,12 @@ export function apply(ctx, config) {
|
|
|
966
1259
|
enrichInput(generation, Promise.all(options.messages.map(async (message) => ({
|
|
967
1260
|
...message,
|
|
968
1261
|
content: await displayBlocks(message.content, ctx.get('attachments'), mediaOptions, options.signal, budget),
|
|
969
|
-
}))).then((messages) =>
|
|
1262
|
+
}))).then((messages) => {
|
|
1263
|
+
const enrichmentBudget = newPayloadBudget(maxPayloadBytes);
|
|
1264
|
+
const enriched = boundPayload({ ...requestBody, messages }, enrichmentBudget, reporter);
|
|
1265
|
+
requestBudget.truncated ||= enrichmentBudget.truncated;
|
|
1266
|
+
return enriched;
|
|
1267
|
+
}));
|
|
970
1268
|
}
|
|
971
1269
|
// The rawest request a plugin can observe: the loop-built GenerateOptions
|
|
972
1270
|
// verbatim, pre-adapter (the provider HTTP body is assembled inside the
|
|
@@ -1005,6 +1303,7 @@ export function apply(ctx, config) {
|
|
|
1005
1303
|
closed = true;
|
|
1006
1304
|
const endedAt = new Date();
|
|
1007
1305
|
openOperations.delete(closeCall);
|
|
1306
|
+
operationSessions.delete(closeCall);
|
|
1008
1307
|
const failure = finish && (finish.kind === 'error' || finish.kind === 'aborted') ? finish : undefined;
|
|
1009
1308
|
// No terminal finish chunk (the consumer abandoned the stream or
|
|
1010
1309
|
// the adapter cut it short): the call's outcome is unknown — mark
|
|
@@ -1026,20 +1325,33 @@ export function apply(ctx, config) {
|
|
|
1026
1325
|
level = 'WARNING';
|
|
1027
1326
|
statusMessage = 'stream closed before the terminal finish chunk';
|
|
1028
1327
|
}
|
|
1328
|
+
// Redact complete grouped deltas before the final serialized bound;
|
|
1329
|
+
// streaming capture has already bounded memory and dropped overflow whole.
|
|
1330
|
+
const finalRawBudget = newPayloadBudget(maxPayloadBytes);
|
|
1331
|
+
const rawOutput = boundPayload(reporter.mask(redactChunks(rawChunks, reporter)), finalRawBudget);
|
|
1332
|
+
rawBudget.truncated ||= finalRawBudget.truncated;
|
|
1029
1333
|
reporter.endSpan(requestSpan, {
|
|
1030
|
-
output: config.captureContent ?
|
|
1334
|
+
output: config.captureContent ? rawOutput : undefined,
|
|
1031
1335
|
level,
|
|
1032
1336
|
statusMessage,
|
|
1033
|
-
metadata: {
|
|
1337
|
+
metadata: {
|
|
1338
|
+
chunkCount,
|
|
1339
|
+
truncated: requestBudget.truncated || rawBudget.truncated || undefined,
|
|
1340
|
+
},
|
|
1034
1341
|
}, endedAt);
|
|
1035
1342
|
if (level === 'ERROR')
|
|
1036
1343
|
noteError(options.sessionId);
|
|
1344
|
+
// A final independent safety bound covers the complete output envelope;
|
|
1345
|
+
// an exhausted streaming budget must not erase an otherwise valid prefix.
|
|
1346
|
+
const finalOutputBudget = newPayloadBudget(maxPayloadBytes);
|
|
1347
|
+
const output = boundPayload(outputBody(), finalOutputBudget, reporter);
|
|
1348
|
+
outputBudget.truncated ||= finalOutputBudget.truncated;
|
|
1037
1349
|
// Partial progress counts on every path: whatever streamed is kept.
|
|
1038
1350
|
afterMedia(generation, (endedAt) => reporter.endGeneration(generation, {
|
|
1039
1351
|
name: config.captureContent
|
|
1040
1352
|
? enhanceWithFirstLine(generationName, reporter.mask(text))
|
|
1041
1353
|
: undefined,
|
|
1042
|
-
output
|
|
1354
|
+
output,
|
|
1043
1355
|
usage,
|
|
1044
1356
|
completionStartTime,
|
|
1045
1357
|
level,
|
|
@@ -1049,11 +1361,16 @@ export function apply(ctx, config) {
|
|
|
1049
1361
|
errorCode: failure?.failure?.code,
|
|
1050
1362
|
incomplete: finish ? undefined : true,
|
|
1051
1363
|
toolCallCount: toolCallCount > 0 ? toolCallCount : undefined,
|
|
1364
|
+
truncated: requestBudget.truncated ||
|
|
1365
|
+
rawBudget.truncated ||
|
|
1366
|
+
outputBudget.truncated ||
|
|
1367
|
+
undefined,
|
|
1052
1368
|
},
|
|
1053
1369
|
}, endedAt), endedAt);
|
|
1054
1370
|
reporter.endSpan(oneOffRoot, {}, endedAt);
|
|
1055
1371
|
};
|
|
1056
1372
|
openOperations.add(closeCall);
|
|
1373
|
+
operationSessions.set(closeCall, options.sessionId);
|
|
1057
1374
|
let stream;
|
|
1058
1375
|
try {
|
|
1059
1376
|
stream = next();
|
|
@@ -1073,7 +1390,7 @@ export function apply(ctx, config) {
|
|
|
1073
1390
|
}
|
|
1074
1391
|
chunkCount += 1;
|
|
1075
1392
|
if (config.captureContent)
|
|
1076
|
-
|
|
1393
|
+
capturePayloadChunk(chunk, rawChunks, rawBudget);
|
|
1077
1394
|
// TTFT is the first real token — block boundaries, usage frames and
|
|
1078
1395
|
// empty heartbeat deltas are not completions (the predicate above).
|
|
1079
1396
|
if (completionStartTime === undefined && isTokenDelta(chunk)) {
|
|
@@ -1082,21 +1399,39 @@ export function apply(ctx, config) {
|
|
|
1082
1399
|
switch (chunk.type) {
|
|
1083
1400
|
case 'text-delta':
|
|
1084
1401
|
if (config.captureContent)
|
|
1085
|
-
text
|
|
1402
|
+
text = appendAssembledText(text, chunk.text, outputBudget);
|
|
1086
1403
|
break;
|
|
1087
1404
|
case 'reasoning-delta':
|
|
1088
1405
|
if (config.captureContent)
|
|
1089
|
-
reasoning
|
|
1406
|
+
reasoning = appendAssembledText(reasoning, chunk.text, outputBudget);
|
|
1090
1407
|
break;
|
|
1091
1408
|
case 'block-end':
|
|
1092
1409
|
if (chunk.block.type === 'tool-call') {
|
|
1093
1410
|
toolCallCount += 1;
|
|
1094
|
-
if (config.captureContent)
|
|
1095
|
-
|
|
1411
|
+
if (config.captureContent) {
|
|
1412
|
+
if (outputBudget.remaining <= 0) {
|
|
1413
|
+
outputBudget.truncated = true;
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
const envelopeBytes = Buffer.byteLength(JSON.stringify({
|
|
1417
|
+
id: chunk.block.id,
|
|
1418
|
+
type: 'function',
|
|
1419
|
+
function: { name: chunk.block.name, arguments: '' },
|
|
1420
|
+
}));
|
|
1421
|
+
if (envelopeBytes > outputBudget.remaining) {
|
|
1422
|
+
outputBudget.truncated = true;
|
|
1423
|
+
outputBudget.remaining = 0;
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
captureToolCall({
|
|
1096
1427
|
id: chunk.block.id,
|
|
1097
1428
|
type: 'function',
|
|
1098
|
-
function: {
|
|
1099
|
-
|
|
1429
|
+
function: {
|
|
1430
|
+
name: chunk.block.name,
|
|
1431
|
+
arguments: reporter.mask(chunk.block.arguments),
|
|
1432
|
+
},
|
|
1433
|
+
}, toolCalls, outputBudget);
|
|
1434
|
+
}
|
|
1100
1435
|
}
|
|
1101
1436
|
break;
|
|
1102
1437
|
case 'usage':
|
|
@@ -1159,6 +1494,7 @@ export function apply(ctx, config) {
|
|
|
1159
1494
|
closed = true;
|
|
1160
1495
|
const endedAt = new Date();
|
|
1161
1496
|
openOperations.delete(closeTool);
|
|
1497
|
+
operationSessions.delete(closeTool);
|
|
1162
1498
|
toolResults.delete(exec);
|
|
1163
1499
|
if (sessionId && toolCalls.get(sessionId)?.get(exec.callId)?.span === span) {
|
|
1164
1500
|
const calls = toolCalls.get(sessionId);
|
|
@@ -1186,6 +1522,7 @@ export function apply(ctx, config) {
|
|
|
1186
1522
|
end();
|
|
1187
1523
|
}
|
|
1188
1524
|
openOperations.add(closeTool);
|
|
1525
|
+
operationSessions.set(closeTool, sessionId);
|
|
1189
1526
|
toolResults.set(exec, (result) => {
|
|
1190
1527
|
if (closed)
|
|
1191
1528
|
return;
|
package/lib/types/client.d.ts
CHANGED
|
@@ -92,6 +92,8 @@ export declare class LangfuseReporter {
|
|
|
92
92
|
/** Settles (never rejects) once the lazy SDK import+wiring finished. */
|
|
93
93
|
readonly ready: Promise<void>;
|
|
94
94
|
constructor(config: LangfuseConnectionConfig);
|
|
95
|
+
/** Whether a payload property should be replaced without inspecting its value. */
|
|
96
|
+
shouldRedactField(key: string): boolean;
|
|
95
97
|
/** Clone telemetry only; never mutate harness-owned messages or tool results. */
|
|
96
98
|
mask<T>(data: T): T;
|
|
97
99
|
/** Dynamically import the SDK and wire the isolated export pipeline. */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* shared identity).
|
|
14
14
|
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
|
15
15
|
* - `session/event` emit → one Langfuse **trace** per agent turn
|
|
16
|
-
* (`turn/start`
|
|
16
|
+
* (`turn/start` records the start; actual activity opens the root — in v5 it IS
|
|
17
17
|
* the trace — the turn's first `user/message` sets the input, `turn/end`
|
|
18
18
|
* stamps the end reason and ends the root, which is what exports it).
|
|
19
19
|
* - Native A2A inbox tasks own a trace spanning admission through task termination;
|
|
@@ -70,39 +70,41 @@ export declare const name = "dsh-langfuse";
|
|
|
70
70
|
* prefer fail-fast wiring.
|
|
71
71
|
*/
|
|
72
72
|
export declare const inject: string[];
|
|
73
|
-
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
74
|
-
enabled: Schema<boolean, boolean>;
|
|
75
|
-
publicKey: Schema<string, string>;
|
|
76
|
-
secretKey: Schema<string, string>;
|
|
77
|
-
baseUrl: Schema<string, string>;
|
|
78
|
-
traceName: Schema<string, string>;
|
|
73
|
+
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
74
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
75
|
+
publicKey: Schema<string, string, "defined">;
|
|
76
|
+
secretKey: Schema<string, string, "defined">;
|
|
77
|
+
baseUrl: Schema<string, string, "defined">;
|
|
78
|
+
traceName: Schema<string, string, "defined">;
|
|
79
79
|
/** Include full message/tool payloads in Langfuse IO. Disable to log metadata only. */
|
|
80
|
-
captureContent: Schema<boolean, boolean>;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
captureContent: Schema<boolean, boolean, "defined">;
|
|
81
|
+
maxPayloadBytes: Schema<number, number, "defined">;
|
|
82
|
+
redactFields: Schema<string[], string[], "defined">;
|
|
83
|
+
userId: Schema<string, string, "defined">;
|
|
84
|
+
tags: Schema<string[], string[], "defined">;
|
|
85
|
+
environment: Schema<string, string, "defined">;
|
|
86
|
+
release: Schema<string, string, "defined">;
|
|
87
|
+
captureMedia: Schema<boolean, boolean, "defined">;
|
|
88
|
+
maxMediaBytes: Schema<number, number, "defined">;
|
|
89
|
+
mediaTimeoutMs: Schema<number, number, "defined">;
|
|
90
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
91
|
+
enabled: Schema<boolean, boolean, "defined">;
|
|
92
|
+
publicKey: Schema<string, string, "defined">;
|
|
93
|
+
secretKey: Schema<string, string, "defined">;
|
|
94
|
+
baseUrl: Schema<string, string, "defined">;
|
|
95
|
+
traceName: Schema<string, string, "defined">;
|
|
95
96
|
/** Include full message/tool payloads in Langfuse IO. Disable to log metadata only. */
|
|
96
|
-
captureContent: Schema<boolean, boolean>;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
captureContent: Schema<boolean, boolean, "defined">;
|
|
98
|
+
maxPayloadBytes: Schema<number, number, "defined">;
|
|
99
|
+
redactFields: Schema<string[], string[], "defined">;
|
|
100
|
+
userId: Schema<string, string, "defined">;
|
|
101
|
+
tags: Schema<string[], string[], "defined">;
|
|
102
|
+
environment: Schema<string, string, "defined">;
|
|
103
|
+
release: Schema<string, string, "defined">;
|
|
104
|
+
captureMedia: Schema<boolean, boolean, "defined">;
|
|
105
|
+
maxMediaBytes: Schema<number, number, "defined">;
|
|
106
|
+
mediaTimeoutMs: Schema<number, number, "defined">;
|
|
107
|
+
}>>, "plain">;
|
|
106
108
|
export interface LangfusePluginConfig {
|
|
107
109
|
enabled: boolean;
|
|
108
110
|
publicKey: string;
|
|
@@ -110,6 +112,7 @@ export interface LangfusePluginConfig {
|
|
|
110
112
|
baseUrl: string;
|
|
111
113
|
traceName: string;
|
|
112
114
|
captureContent: boolean;
|
|
115
|
+
maxPayloadBytes?: number;
|
|
113
116
|
redactFields?: string[];
|
|
114
117
|
userId?: string;
|
|
115
118
|
tags?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amaster.ai/dsh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Langfuse observability plugin for DeepSeek Harness: one generation per LLM call with a nested llm-request span, one span per tool call, one trace per session turn, subagent child sessions nested under the parent's tree.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.5"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -65,16 +65,16 @@
|
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "4.0.5",
|
|
68
|
-
"@deepseek-ai/dsh-agent": "0.2.1-rc.
|
|
69
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-rc.
|
|
70
|
-
"@deepseek-ai/dsh-hook-protocol": "0.2.1-rc.
|
|
71
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.
|
|
72
|
-
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.
|
|
73
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.
|
|
74
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.
|
|
75
|
-
"@deepseek-ai/dsh-subagent": "0.2.1-rc.
|
|
76
|
-
"@deepseek-ai/dsh-tools": "0.2.1-rc.
|
|
77
|
-
"@deepseek-ai/dsh-util-time": "0.2.1-rc.
|
|
68
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.5",
|
|
69
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.5",
|
|
70
|
+
"@deepseek-ai/dsh-hook-protocol": "0.2.1-rc.5",
|
|
71
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.5",
|
|
72
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-rc.5",
|
|
73
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.5",
|
|
74
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.5",
|
|
75
|
+
"@deepseek-ai/dsh-subagent": "0.2.1-rc.5",
|
|
76
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.5",
|
|
77
|
+
"@deepseek-ai/dsh-util-time": "0.2.1-rc.5",
|
|
78
78
|
"@langfuse/otel": "5.10.1",
|
|
79
79
|
"@langfuse/tracing": "5.10.1",
|
|
80
80
|
"@opentelemetry/api": "^1.9.1",
|