@amaster.ai/dsh-langfuse 0.1.25 → 0.1.27
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/lib/client.js +36 -7
- package/lib/index.js +276 -14
- package/lib/types/client.d.ts +5 -0
- package/lib/types/index.d.ts +23 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -252,18 +252,23 @@ export class LangfuseReporter {
|
|
|
252
252
|
if (!tracing || !api)
|
|
253
253
|
return null;
|
|
254
254
|
try {
|
|
255
|
-
const
|
|
255
|
+
const externalParent = parentContext(input.context?.traceparent);
|
|
256
|
+
const parent = externalParent === NO_PARENT && input.traceId
|
|
257
|
+
? { traceId: input.traceId, spanId: '0000000000000001', traceFlags: 1 }
|
|
258
|
+
: externalParent;
|
|
256
259
|
input = this.mask(input);
|
|
257
|
-
const root = api.context.with(api.ROOT_CONTEXT, () => tracing.startObservation(input.name, { metadata: input.metadata }, { parentSpanContext: parent }));
|
|
260
|
+
const root = api.context.with(api.ROOT_CONTEXT, () => tracing.startObservation(input.name, { metadata: input.metadata }, { parentSpanContext: parent, startTime: input.startTime }));
|
|
258
261
|
this.stampTraceContext(root, {
|
|
259
262
|
sessionId: input.sessionId,
|
|
260
|
-
traceName:
|
|
263
|
+
traceName: externalParent === NO_PARENT ? input.name : undefined,
|
|
261
264
|
userId: input.context?.userId ?? this.config.userId,
|
|
262
265
|
tags: input.context?.tags ?? this.config.tags,
|
|
263
266
|
});
|
|
264
267
|
// External traces retain their owner's trace name and trace-level IO.
|
|
265
|
-
if (
|
|
268
|
+
if (externalParent === NO_PARENT) {
|
|
266
269
|
this.roots.add(root);
|
|
270
|
+
root.otelSpan.setAttribute(tracing.LangfuseOtelSpanAttributes.IS_APP_ROOT, true);
|
|
271
|
+
}
|
|
267
272
|
else
|
|
268
273
|
root.otelSpan.setAttribute(tracing.LangfuseOtelSpanAttributes.IS_APP_ROOT, false);
|
|
269
274
|
return root;
|
|
@@ -273,6 +278,25 @@ export class LangfuseReporter {
|
|
|
273
278
|
return null;
|
|
274
279
|
}
|
|
275
280
|
}
|
|
281
|
+
/** A running task keeps its trace identity; late host context is a link. */
|
|
282
|
+
updateContext(span, context) {
|
|
283
|
+
if (!span)
|
|
284
|
+
return;
|
|
285
|
+
try {
|
|
286
|
+
const previous = this.traceContext.get(span);
|
|
287
|
+
this.stampTraceContext(span, {
|
|
288
|
+
...previous,
|
|
289
|
+
userId: context.userId ?? previous?.userId,
|
|
290
|
+
tags: context.tags ?? previous?.tags,
|
|
291
|
+
});
|
|
292
|
+
const parent = parentContext(context.traceparent);
|
|
293
|
+
if (parent !== NO_PARENT)
|
|
294
|
+
span.otelSpan.addLink({ context: parent });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error('[dsh-langfuse] context update failed:', error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
276
300
|
/** Open a generation (one per LLM call) under any observation parent. */
|
|
277
301
|
startGeneration(parent, input) {
|
|
278
302
|
const api = this.api;
|
|
@@ -327,9 +351,14 @@ export class LangfuseReporter {
|
|
|
327
351
|
input: input.input,
|
|
328
352
|
metadata: input.metadata,
|
|
329
353
|
};
|
|
330
|
-
const span = api.context.with(api.ROOT_CONTEXT, () => input.
|
|
331
|
-
?
|
|
332
|
-
|
|
354
|
+
const span = api.context.with(api.ROOT_CONTEXT, () => input.startTime && this.tracing
|
|
355
|
+
? this.tracing.startObservation(input.name, attributes, {
|
|
356
|
+
parentSpanContext: parent.otelSpan.spanContext(),
|
|
357
|
+
startTime: input.startTime,
|
|
358
|
+
})
|
|
359
|
+
: input.asType === 'tool'
|
|
360
|
+
? parent.startObservation(input.name, attributes, { asType: 'tool' })
|
|
361
|
+
: parent.startObservation(input.name, attributes));
|
|
333
362
|
this.stampTraceContext(span, this.traceContext.get(parent));
|
|
334
363
|
return span;
|
|
335
364
|
}
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* (`turn/start` opens a root span — in the v5 SDK the root observation 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
|
+
* - Native A2A inbox tasks own a trace spanning admission through task termination;
|
|
20
|
+
* agent turns and queue controls are children of that task.
|
|
19
21
|
* - subagent child sessions (`session/created` with `header.parentSession`)
|
|
20
22
|
* ride the parent's trace: a `subagent` **span** opened under the enclosing
|
|
21
23
|
* delegation tool span, the child's generations/tool spans nested under it,
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
*
|
|
29
31
|
* @module dsh-langfuse
|
|
30
32
|
*/
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
31
34
|
import Schema from '@deepseek-ai/schemastery';
|
|
32
35
|
import { LangfuseReporter } from './client.js';
|
|
33
36
|
import { isHookInvoked, isHookResult } from './hooks.js';
|
|
@@ -203,6 +206,10 @@ export function apply(ctx, config) {
|
|
|
203
206
|
return;
|
|
204
207
|
const reporter = new LangfuseReporter(config);
|
|
205
208
|
const sessions = new Map();
|
|
209
|
+
const nativeInboxes = new Set();
|
|
210
|
+
const pendingTurns = new Map();
|
|
211
|
+
const inboxInputs = new Map();
|
|
212
|
+
const inboxTasks = new Map();
|
|
206
213
|
const children = new Map();
|
|
207
214
|
const completedTurns = new Map();
|
|
208
215
|
function rememberTurn(sessionId, turn, parent, turnTrace) {
|
|
@@ -268,11 +275,98 @@ export function apply(ctx, config) {
|
|
|
268
275
|
tags: context.tags ? [...context.tags] : undefined,
|
|
269
276
|
});
|
|
270
277
|
});
|
|
271
|
-
function openTrace(sessionId, metadata) {
|
|
278
|
+
function openTrace(sessionId, metadata, startTime) {
|
|
272
279
|
const context = sessionId ? traceContexts.get(sessionId) : undefined;
|
|
273
280
|
if (sessionId)
|
|
274
281
|
traceContexts.delete(sessionId);
|
|
275
|
-
return reporter.openTrace({ name: config.traceName, sessionId, metadata, context });
|
|
282
|
+
return reporter.openTrace({ name: config.traceName, sessionId, metadata, context, startTime });
|
|
283
|
+
}
|
|
284
|
+
const inboxKey = (contextId, taskId) => JSON.stringify([contextId, taskId]);
|
|
285
|
+
function taskTrace(event) {
|
|
286
|
+
if (!event.taskId)
|
|
287
|
+
return;
|
|
288
|
+
const key = inboxKey(event.contextId, event.taskId);
|
|
289
|
+
let task = inboxTasks.get(key);
|
|
290
|
+
if (!task) {
|
|
291
|
+
const context = traceContexts.get(event.contextId);
|
|
292
|
+
traceContexts.delete(event.contextId);
|
|
293
|
+
task = {
|
|
294
|
+
key,
|
|
295
|
+
contextId: event.contextId,
|
|
296
|
+
taskId: event.taskId,
|
|
297
|
+
trace: reporter.openTrace({
|
|
298
|
+
name: config.traceName === 'dsh-turn' ? 'a2a-task' : config.traceName,
|
|
299
|
+
sessionId: event.contextId,
|
|
300
|
+
metadata: { taskId: event.taskId, taskState: 'queued' },
|
|
301
|
+
traceId: createHash('sha256').update(key).digest('hex').slice(0, 32),
|
|
302
|
+
startTime: new Date(event.queuedAt ?? event.at),
|
|
303
|
+
}),
|
|
304
|
+
hasInput: false,
|
|
305
|
+
messageIds: new Set(),
|
|
306
|
+
nativeMessageIds: new Set(),
|
|
307
|
+
openTurns: 0,
|
|
308
|
+
};
|
|
309
|
+
if (context)
|
|
310
|
+
reporter.updateContext(task.trace, context);
|
|
311
|
+
inboxTasks.set(key, task);
|
|
312
|
+
}
|
|
313
|
+
if (event.messageId)
|
|
314
|
+
task.messageIds.add(event.messageId);
|
|
315
|
+
if (event.nativeMessageId)
|
|
316
|
+
task.nativeMessageIds.add(event.nativeMessageId);
|
|
317
|
+
if (!task.hasInput && config.captureContent && event.input) {
|
|
318
|
+
reporter.updateSpan(task.trace, { input: event.input });
|
|
319
|
+
task.hasInput = true;
|
|
320
|
+
}
|
|
321
|
+
reporter.updateSpan(task.trace, {
|
|
322
|
+
metadata: {
|
|
323
|
+
referenceTaskIds: event.referenceTaskIds?.length ? event.referenceTaskIds : undefined,
|
|
324
|
+
a2aMessageIds: [...task.messageIds],
|
|
325
|
+
nativeMessageIds: [...task.nativeMessageIds],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return task;
|
|
329
|
+
}
|
|
330
|
+
function closeTask(task, endedAt = new Date()) {
|
|
331
|
+
if (!task.end || task.openTurns)
|
|
332
|
+
return;
|
|
333
|
+
const end = task.end;
|
|
334
|
+
if (inboxInputs.get(task.contextId)?.[0]?.taskId === task.taskId)
|
|
335
|
+
inboxInputs.delete(task.contextId);
|
|
336
|
+
if (inboxTasks.get(task.key) === task)
|
|
337
|
+
inboxTasks.delete(task.key);
|
|
338
|
+
afterMedia(task.trace, (at) => reporter.endSpan(task.trace, {
|
|
339
|
+
output: end.phase === 'completed' && config.captureContent && task.lastOutput !== undefined
|
|
340
|
+
? task.lastOutput
|
|
341
|
+
: end.output,
|
|
342
|
+
level: end.level,
|
|
343
|
+
metadata: { taskState: end.phase },
|
|
344
|
+
}, at), new Date(Math.max(end.at, endedAt.getTime())));
|
|
345
|
+
}
|
|
346
|
+
function endTurn(state, update = {}, endedAt) {
|
|
347
|
+
const task = state.taskKey ? inboxTasks.get(state.taskKey) : undefined;
|
|
348
|
+
if (task && state.lastAssistantText !== undefined)
|
|
349
|
+
task.lastOutput = state.lastAssistantText;
|
|
350
|
+
afterMedia(state.trace, (at) => {
|
|
351
|
+
reporter.endSpan(state.trace, update, at);
|
|
352
|
+
if (task) {
|
|
353
|
+
task.openTurns--;
|
|
354
|
+
closeTask(task, at);
|
|
355
|
+
}
|
|
356
|
+
}, endedAt);
|
|
357
|
+
}
|
|
358
|
+
function suspendTasks(contextId) {
|
|
359
|
+
for (const task of [...inboxTasks.values()]) {
|
|
360
|
+
if (contextId !== undefined && task.contextId !== contextId)
|
|
361
|
+
continue;
|
|
362
|
+
task.end ??= {
|
|
363
|
+
phase: 'suspended',
|
|
364
|
+
at: Date.now(),
|
|
365
|
+
output: { state: 'suspended' },
|
|
366
|
+
level: 'DEFAULT',
|
|
367
|
+
};
|
|
368
|
+
closeTask(task);
|
|
369
|
+
}
|
|
276
370
|
}
|
|
277
371
|
function rootSessionId(sessionId) {
|
|
278
372
|
while (sessionId && children.has(sessionId))
|
|
@@ -314,10 +408,36 @@ export function apply(ctx, config) {
|
|
|
314
408
|
const existing = id ? sessions.get(id) : undefined;
|
|
315
409
|
if (existing)
|
|
316
410
|
return existing.trace;
|
|
317
|
-
const
|
|
411
|
+
const pending = id ? pendingTurns.get(id) : undefined;
|
|
412
|
+
const turn = pending?.turn;
|
|
413
|
+
if (id && nativeInboxes.has(id)) {
|
|
414
|
+
const selected = inboxInputs.get(id)?.[0];
|
|
415
|
+
const task = selected ? taskTrace(selected) : undefined;
|
|
416
|
+
if (!task || task.end)
|
|
417
|
+
return null;
|
|
418
|
+
const context = traceContexts.get(id);
|
|
419
|
+
if (context) {
|
|
420
|
+
reporter.updateContext(task.trace, context);
|
|
421
|
+
traceContexts.delete(id);
|
|
422
|
+
}
|
|
423
|
+
const trace = reporter.startSpan(task.trace, {
|
|
424
|
+
name: 'agent:turn',
|
|
425
|
+
metadata: { turn, taskId: task.taskId },
|
|
426
|
+
startTime: pending?.startTime,
|
|
427
|
+
});
|
|
428
|
+
pendingTurns.delete(id);
|
|
429
|
+
if (trace) {
|
|
430
|
+
task.openTurns++;
|
|
431
|
+
sessions.set(id, { trace, hasInput: turn === undefined, turn, taskKey: task.key });
|
|
432
|
+
}
|
|
433
|
+
return trace;
|
|
434
|
+
}
|
|
435
|
+
const trace = openTrace(id, turn === undefined ? undefined : { turn }, pending?.startTime);
|
|
436
|
+
if (id)
|
|
437
|
+
pendingTurns.delete(id);
|
|
318
438
|
// hasInput starts true: one-offs never take a turn's first-message input.
|
|
319
439
|
if (trace && id)
|
|
320
|
-
sessions.set(id, { trace, hasInput:
|
|
440
|
+
sessions.set(id, { trace, hasInput: turn === undefined, turn });
|
|
321
441
|
return trace;
|
|
322
442
|
}
|
|
323
443
|
/**
|
|
@@ -328,6 +448,129 @@ export function apply(ctx, config) {
|
|
|
328
448
|
const child = sessionId ? children.get(sessionId) : undefined;
|
|
329
449
|
return child?.span ?? traceFor(sessionId);
|
|
330
450
|
}
|
|
451
|
+
ctx.on('a2a/inbox', (event) => {
|
|
452
|
+
try {
|
|
453
|
+
const id = event.contextId;
|
|
454
|
+
nativeInboxes.add(id);
|
|
455
|
+
if (event.phase === 'resolved')
|
|
456
|
+
return;
|
|
457
|
+
const metadata = {
|
|
458
|
+
taskId: event.taskId,
|
|
459
|
+
taskIds: event.taskIds,
|
|
460
|
+
messageId: event.messageId,
|
|
461
|
+
nativeMessageId: event.nativeMessageId,
|
|
462
|
+
pending: event.pending,
|
|
463
|
+
target: event.target,
|
|
464
|
+
position: event.position,
|
|
465
|
+
agentStatus: event.agentStatus,
|
|
466
|
+
turn: event.turn,
|
|
467
|
+
step: event.step,
|
|
468
|
+
};
|
|
469
|
+
const input = config.captureContent ? event.input : undefined;
|
|
470
|
+
const waitMs = event.queuedAt === undefined
|
|
471
|
+
? undefined
|
|
472
|
+
: Math.max(0, (event.selectedAt ?? event.at) - event.queuedAt);
|
|
473
|
+
const output = {
|
|
474
|
+
state: event.phase,
|
|
475
|
+
target: event.target,
|
|
476
|
+
position: event.position,
|
|
477
|
+
pending: event.pending,
|
|
478
|
+
agentStatus: event.agentStatus,
|
|
479
|
+
};
|
|
480
|
+
if (event.phase === 'selected') {
|
|
481
|
+
const task = taskTrace(event);
|
|
482
|
+
if (task && task.selectedAt === undefined) {
|
|
483
|
+
task.selectedAt = event.selectedAt ?? event.at;
|
|
484
|
+
task.waitMs = waitMs;
|
|
485
|
+
}
|
|
486
|
+
if (task)
|
|
487
|
+
reporter.updateSpan(task.trace, {
|
|
488
|
+
metadata: { taskState: 'running', waitMs: task.waitMs },
|
|
489
|
+
});
|
|
490
|
+
const inputs = inboxInputs.get(id) ?? [];
|
|
491
|
+
if (!inputs.some((input) => input.nativeMessageId === event.nativeMessageId))
|
|
492
|
+
inputs.push(event);
|
|
493
|
+
inboxInputs.set(id, inputs);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (['prepared', 'prepare-failed', 'prepare-aborted'].includes(event.phase)) {
|
|
497
|
+
const parent = parentFor(id);
|
|
498
|
+
const inputs = inboxInputs.get(id) ?? [event];
|
|
499
|
+
const first = inputs[0];
|
|
500
|
+
const turnWaitMs = first.queuedAt === undefined
|
|
501
|
+
? undefined
|
|
502
|
+
: Math.max(0, (first.selectedAt ?? first.at) - first.queuedAt);
|
|
503
|
+
reporter.updateSpan(parent, {
|
|
504
|
+
input: sessions.get(id)?.hasInput ? undefined : input,
|
|
505
|
+
metadata: {
|
|
506
|
+
taskId: event.taskId,
|
|
507
|
+
waitMs: turnWaitMs,
|
|
508
|
+
a2aMessageIds: inputs.map((input) => input.messageId),
|
|
509
|
+
nativeMessageIds: inputs.map((input) => input.nativeMessageId),
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
const prepare = reporter.startSpan(parent, {
|
|
513
|
+
name: event.target === 'next-step' ? 'inbox:steering' : 'inbox:prepare',
|
|
514
|
+
input,
|
|
515
|
+
startTime: new Date(event.selectedAt ?? event.at),
|
|
516
|
+
metadata: {
|
|
517
|
+
...metadata,
|
|
518
|
+
waitMs,
|
|
519
|
+
prepareMs: event.selectedAt === undefined ? undefined : Math.max(0, event.at - event.selectedAt),
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
reporter.endSpan(prepare, {
|
|
523
|
+
output,
|
|
524
|
+
level: event.phase === 'prepare-failed'
|
|
525
|
+
? 'ERROR'
|
|
526
|
+
: event.phase === 'prepare-aborted'
|
|
527
|
+
? 'WARNING'
|
|
528
|
+
: 'DEFAULT',
|
|
529
|
+
}, new Date(event.at));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const tasks = event.taskId
|
|
533
|
+
? [
|
|
534
|
+
['completed', 'failed', 'canceled'].includes(event.phase)
|
|
535
|
+
? inboxTasks.get(inboxKey(id, event.taskId))
|
|
536
|
+
: taskTrace(event),
|
|
537
|
+
].filter((task) => !!task)
|
|
538
|
+
: [...inboxTasks.values()].filter((task) => task.contextId === id &&
|
|
539
|
+
!task.end &&
|
|
540
|
+
(event.taskIds === undefined || event.taskIds.includes(task.taskId)));
|
|
541
|
+
for (const task of tasks) {
|
|
542
|
+
if (task.end)
|
|
543
|
+
continue;
|
|
544
|
+
const details = { ...metadata, taskId: task.taskId };
|
|
545
|
+
const operation = reporter.startSpan(task.trace, {
|
|
546
|
+
name: `inbox:${event.phase}`,
|
|
547
|
+
input,
|
|
548
|
+
startTime: new Date(event.at),
|
|
549
|
+
metadata: details,
|
|
550
|
+
});
|
|
551
|
+
reporter.endSpan(operation, { output }, new Date(event.at));
|
|
552
|
+
const phase = event.phase === 'cleared' ? 'canceled' : event.phase;
|
|
553
|
+
if (['completed', 'failed', 'canceled'].includes(phase)) {
|
|
554
|
+
task.end = {
|
|
555
|
+
phase,
|
|
556
|
+
at: event.at,
|
|
557
|
+
output: { ...output, state: phase },
|
|
558
|
+
level: phase === 'failed' ? 'ERROR' : phase === 'canceled' ? 'WARNING' : 'DEFAULT',
|
|
559
|
+
};
|
|
560
|
+
closeTask(task, new Date(event.at));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (event.phase === 'disposed')
|
|
564
|
+
suspendTasks(id);
|
|
565
|
+
if (event.phase === 'disposed' || event.phase === 'cleared') {
|
|
566
|
+
nativeInboxes.delete(id);
|
|
567
|
+
inboxInputs.delete(id);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
console.error('[dsh-langfuse] inbox reporting failed:', error);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
331
574
|
// ------------------------------------------------------------------
|
|
332
575
|
// Subagent child sessions: session/created precedes both subagent/start
|
|
333
576
|
// and the child's first generation, and the durable header already links
|
|
@@ -385,8 +628,8 @@ export function apply(ctx, config) {
|
|
|
385
628
|
}, endedAt));
|
|
386
629
|
});
|
|
387
630
|
// ------------------------------------------------------------------
|
|
388
|
-
//
|
|
389
|
-
//
|
|
631
|
+
// Turn lifecycle: native tasks keep their root across turns; other sessions
|
|
632
|
+
// open one trace per turn. Subagents stay under their parent's trace.
|
|
390
633
|
// ------------------------------------------------------------------
|
|
391
634
|
ctx.on('session/event', (session, event) => {
|
|
392
635
|
const sessionId = session.id;
|
|
@@ -407,7 +650,9 @@ export function apply(ctx, config) {
|
|
|
407
650
|
if (previous?.root)
|
|
408
651
|
reporter.endSpan(previous.root, { level: 'WARNING' });
|
|
409
652
|
const child = children.get(sessionId);
|
|
410
|
-
const inTurn = child
|
|
653
|
+
const inTurn = child
|
|
654
|
+
? child.turn === data.turn
|
|
655
|
+
: (sessions.get(sessionId)?.turn ?? pendingTurns.get(sessionId)?.turn) === data.turn;
|
|
411
656
|
const completed = data.invocationId && !inTurn
|
|
412
657
|
? completedTurns.get(JSON.stringify([sessionId, data.turn]))
|
|
413
658
|
: undefined;
|
|
@@ -553,7 +798,13 @@ export function apply(ctx, config) {
|
|
|
553
798
|
closeHooks(sessionId, true);
|
|
554
799
|
const stale = sessions.get(sessionId);
|
|
555
800
|
if (stale)
|
|
556
|
-
|
|
801
|
+
endTurn(stale);
|
|
802
|
+
inboxInputs.delete(sessionId);
|
|
803
|
+
if (nativeInboxes.has(sessionId)) {
|
|
804
|
+
sessions.delete(sessionId);
|
|
805
|
+
pendingTurns.set(sessionId, { turn: event.data.turn, startTime: new Date(event.time) });
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
557
808
|
const trace = openTrace(sessionId, { turn: event.data.turn });
|
|
558
809
|
if (trace)
|
|
559
810
|
sessions.set(sessionId, { trace, hasInput: false, turn: event.data.turn });
|
|
@@ -575,13 +826,14 @@ export function apply(ctx, config) {
|
|
|
575
826
|
break;
|
|
576
827
|
}
|
|
577
828
|
case 'turn/end': {
|
|
829
|
+
if (pendingTurns.has(sessionId))
|
|
830
|
+
traceFor(sessionId);
|
|
578
831
|
const state = sessions.get(sessionId);
|
|
579
832
|
if (state) {
|
|
580
833
|
sessions.delete(sessionId);
|
|
581
834
|
rememberTurn(sessionId, event.data.turn, state.trace, state.trace);
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
afterMedia(state.trace, (endedAt) => reporter.endSpan(state.trace, {
|
|
835
|
+
// Native task roots stay open until the A2A terminal signal.
|
|
836
|
+
endTurn(state, {
|
|
585
837
|
output: state.lastAssistantText,
|
|
586
838
|
level: event.data.reason.kind === 'error'
|
|
587
839
|
? 'ERROR'
|
|
@@ -599,7 +851,7 @@ export function apply(ctx, config) {
|
|
|
599
851
|
? { errorCode: event.data.reason.error.code }
|
|
600
852
|
: {}),
|
|
601
853
|
},
|
|
602
|
-
}
|
|
854
|
+
});
|
|
603
855
|
}
|
|
604
856
|
break;
|
|
605
857
|
}
|
|
@@ -611,6 +863,9 @@ export function apply(ctx, config) {
|
|
|
611
863
|
const state = sessions.get(sessionId);
|
|
612
864
|
sessions.delete(sessionId);
|
|
613
865
|
traceContexts.delete(sessionId);
|
|
866
|
+
pendingTurns.delete(sessionId);
|
|
867
|
+
nativeInboxes.delete(sessionId);
|
|
868
|
+
inboxInputs.delete(sessionId);
|
|
614
869
|
for (const [key, completed] of completedTurns) {
|
|
615
870
|
if (completed.sessionId === sessionId)
|
|
616
871
|
completedTurns.delete(key);
|
|
@@ -621,7 +876,8 @@ export function apply(ctx, config) {
|
|
|
621
876
|
// trace) — v5 spans only export on end, so dropping it open would lose
|
|
622
877
|
// everything it recorded.
|
|
623
878
|
if (state)
|
|
624
|
-
|
|
879
|
+
endTurn(state);
|
|
880
|
+
suspendTasks(sessionId);
|
|
625
881
|
// Children: keep the entry — background/continuable subagents dispose
|
|
626
882
|
// their session BEFORE subagent/end fires, and the span's close must ride
|
|
627
883
|
// subagent/end (disposing first would either leak the span or close it
|
|
@@ -920,10 +1176,16 @@ export function apply(ctx, config) {
|
|
|
920
1176
|
// down — v5 spans only export on end, so unloading mid-turn (profile
|
|
921
1177
|
// reload) would otherwise silently drop them.
|
|
922
1178
|
for (const state of sessions.values())
|
|
923
|
-
|
|
1179
|
+
endTurn(state, {}, endedAt);
|
|
1180
|
+
suspendTasks();
|
|
1181
|
+
await drainMedia();
|
|
924
1182
|
for (const child of children.values())
|
|
925
1183
|
reporter.endSpan(child.span, {}, endedAt);
|
|
926
1184
|
sessions.clear();
|
|
1185
|
+
pendingTurns.clear();
|
|
1186
|
+
nativeInboxes.clear();
|
|
1187
|
+
inboxInputs.clear();
|
|
1188
|
+
inboxTasks.clear();
|
|
927
1189
|
completedTurns.clear();
|
|
928
1190
|
traceContexts.clear();
|
|
929
1191
|
children.clear();
|
package/lib/types/client.d.ts
CHANGED
|
@@ -106,7 +106,11 @@ export declare class LangfuseReporter {
|
|
|
106
106
|
sessionId?: string;
|
|
107
107
|
metadata?: Record<string, unknown>;
|
|
108
108
|
context?: LangfuseTraceContext;
|
|
109
|
+
startTime?: Date;
|
|
110
|
+
traceId?: string;
|
|
109
111
|
}): LangfuseSpan | null;
|
|
112
|
+
/** A running task keeps its trace identity; late host context is a link. */
|
|
113
|
+
updateContext(span: Observation | null, context: LangfuseTraceContext): void;
|
|
110
114
|
/** Open a generation (one per LLM call) under any observation parent. */
|
|
111
115
|
startGeneration(parent: Observation | null, input: {
|
|
112
116
|
name: string;
|
|
@@ -130,6 +134,7 @@ export declare class LangfuseReporter {
|
|
|
130
134
|
input?: unknown;
|
|
131
135
|
metadata?: Record<string, unknown>;
|
|
132
136
|
asType?: 'tool';
|
|
137
|
+
startTime?: Date;
|
|
133
138
|
}): LangfuseSpan | null;
|
|
134
139
|
/**
|
|
135
140
|
* Merge fields into an open span — subagent enrichment (label, provider),
|
package/lib/types/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* (`turn/start` opens a root span — in the v5 SDK the root observation 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
|
+
* - Native A2A inbox tasks own a trace spanning admission through task termination;
|
|
20
|
+
* agent turns and queue controls are children of that task.
|
|
19
21
|
* - subagent child sessions (`session/created` with `header.parentSession`)
|
|
20
22
|
* ride the parent's trace: a `subagent` **span** opened under the enclosing
|
|
21
23
|
* delegation tool span, the child's generations/tool spans nested under it,
|
|
@@ -33,8 +35,29 @@ import Schema from '@deepseek-ai/schemastery';
|
|
|
33
35
|
import { type LangfuseTraceContext } from './client.js';
|
|
34
36
|
export type { LangfuseTraceContext } from './client.js';
|
|
35
37
|
export { appendHookEvent, type HookInvoked, type HookResult } from './hooks.js';
|
|
38
|
+
/** Optional protocol telemetry; no dependency on the A2A plugin. */
|
|
39
|
+
interface A2aInboxEvent {
|
|
40
|
+
phase: string;
|
|
41
|
+
contextId: string;
|
|
42
|
+
at: number;
|
|
43
|
+
pending: number;
|
|
44
|
+
target?: 'next-turn' | 'next-step';
|
|
45
|
+
input?: string;
|
|
46
|
+
position?: number;
|
|
47
|
+
agentStatus?: string;
|
|
48
|
+
taskId?: string;
|
|
49
|
+
taskIds?: string[];
|
|
50
|
+
messageId?: string;
|
|
51
|
+
referenceTaskIds?: string[];
|
|
52
|
+
nativeMessageId?: string;
|
|
53
|
+
queuedAt?: number;
|
|
54
|
+
selectedAt?: number;
|
|
55
|
+
turn?: number;
|
|
56
|
+
step?: number;
|
|
57
|
+
}
|
|
36
58
|
declare module '@deepseek-ai/cordis' {
|
|
37
59
|
interface Events {
|
|
60
|
+
'a2a/inbox'(detail: A2aInboxEvent): void;
|
|
38
61
|
/** Bind standard labels/parent context before a session's next trace starts. */
|
|
39
62
|
'langfuse/context': (sessionId: string, context: LangfuseTraceContext) => void;
|
|
40
63
|
}
|
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.27",
|
|
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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
68
68
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
69
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
69
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
|
|
70
70
|
"@deepseek-ai/dsh-hook-protocol": "0.2.0-rc.2",
|
|
71
71
|
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
72
72
|
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|