@amaster.ai/dsh-langfuse 0.1.25 → 0.1.26
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 +282 -14
- package/lib/types/client.d.ts +5 -0
- package/lib/types/index.d.ts +24 -0
- package/package.json +1 -1
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,135 @@ 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
|
+
paused: event.paused,
|
|
466
|
+
agentStatus: event.agentStatus,
|
|
467
|
+
turn: event.turn,
|
|
468
|
+
step: event.step,
|
|
469
|
+
};
|
|
470
|
+
const input = config.captureContent ? event.input : undefined;
|
|
471
|
+
const waitMs = event.queuedAt === undefined
|
|
472
|
+
? undefined
|
|
473
|
+
: Math.max(0, (event.selectedAt ?? event.at) - event.queuedAt);
|
|
474
|
+
const output = {
|
|
475
|
+
state: event.phase,
|
|
476
|
+
target: event.target,
|
|
477
|
+
position: event.position,
|
|
478
|
+
pending: event.pending,
|
|
479
|
+
paused: event.paused,
|
|
480
|
+
agentStatus: event.agentStatus,
|
|
481
|
+
};
|
|
482
|
+
if (event.phase === 'selected') {
|
|
483
|
+
const task = taskTrace(event);
|
|
484
|
+
if (task && task.selectedAt === undefined) {
|
|
485
|
+
task.selectedAt = event.selectedAt ?? event.at;
|
|
486
|
+
task.waitMs = waitMs;
|
|
487
|
+
}
|
|
488
|
+
if (task)
|
|
489
|
+
reporter.updateSpan(task.trace, {
|
|
490
|
+
metadata: { taskState: 'running', waitMs: task.waitMs },
|
|
491
|
+
});
|
|
492
|
+
const inputs = inboxInputs.get(id) ?? [];
|
|
493
|
+
if (!inputs.some((input) => input.nativeMessageId === event.nativeMessageId))
|
|
494
|
+
inputs.push(event);
|
|
495
|
+
inboxInputs.set(id, inputs);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (['prepared', 'prepare-failed', 'prepare-aborted'].includes(event.phase)) {
|
|
499
|
+
const parent = parentFor(id);
|
|
500
|
+
const inputs = inboxInputs.get(id) ?? [event];
|
|
501
|
+
const first = inputs[0];
|
|
502
|
+
const turnWaitMs = first.queuedAt === undefined
|
|
503
|
+
? undefined
|
|
504
|
+
: Math.max(0, (first.selectedAt ?? first.at) - first.queuedAt);
|
|
505
|
+
reporter.updateSpan(parent, {
|
|
506
|
+
input: sessions.get(id)?.hasInput ? undefined : input,
|
|
507
|
+
metadata: {
|
|
508
|
+
taskId: event.taskId,
|
|
509
|
+
waitMs: turnWaitMs,
|
|
510
|
+
a2aMessageIds: inputs.map((input) => input.messageId),
|
|
511
|
+
nativeMessageIds: inputs.map((input) => input.nativeMessageId),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
const prepare = reporter.startSpan(parent, {
|
|
515
|
+
name: event.target === 'next-step' ? 'inbox:steering' : 'inbox:prepare',
|
|
516
|
+
input,
|
|
517
|
+
startTime: new Date(event.selectedAt ?? event.at),
|
|
518
|
+
metadata: {
|
|
519
|
+
...metadata,
|
|
520
|
+
waitMs,
|
|
521
|
+
prepareMs: event.selectedAt === undefined ? undefined : Math.max(0, event.at - event.selectedAt),
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
reporter.endSpan(prepare, {
|
|
525
|
+
output,
|
|
526
|
+
level: event.phase === 'prepare-failed'
|
|
527
|
+
? 'ERROR'
|
|
528
|
+
: event.phase === 'prepare-aborted'
|
|
529
|
+
? 'WARNING'
|
|
530
|
+
: 'DEFAULT',
|
|
531
|
+
}, new Date(event.at));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const tasks = event.taskId
|
|
535
|
+
? [
|
|
536
|
+
['completed', 'failed', 'canceled'].includes(event.phase)
|
|
537
|
+
? inboxTasks.get(inboxKey(id, event.taskId))
|
|
538
|
+
: taskTrace(event),
|
|
539
|
+
].filter((task) => !!task)
|
|
540
|
+
: [...inboxTasks.values()].filter((task) => task.contextId === id &&
|
|
541
|
+
!task.end &&
|
|
542
|
+
(event.taskIds === undefined || event.taskIds.includes(task.taskId)));
|
|
543
|
+
for (const task of tasks) {
|
|
544
|
+
if (task.end)
|
|
545
|
+
continue;
|
|
546
|
+
const details = { ...metadata, taskId: task.taskId };
|
|
547
|
+
const operation = reporter.startSpan(task.trace, {
|
|
548
|
+
name: `inbox:${event.phase}`,
|
|
549
|
+
input,
|
|
550
|
+
startTime: new Date(event.at),
|
|
551
|
+
metadata: details,
|
|
552
|
+
});
|
|
553
|
+
reporter.endSpan(operation, { output }, new Date(event.at));
|
|
554
|
+
const phase = event.phase === 'cleared' ? 'canceled' : event.phase;
|
|
555
|
+
if (['completed', 'failed', 'canceled'].includes(phase)) {
|
|
556
|
+
task.end = {
|
|
557
|
+
phase,
|
|
558
|
+
at: event.at,
|
|
559
|
+
output: { ...output, state: phase },
|
|
560
|
+
level: phase === 'failed' ? 'ERROR' : phase === 'canceled' ? 'WARNING' : 'DEFAULT',
|
|
561
|
+
};
|
|
562
|
+
closeTask(task, new Date(event.at));
|
|
563
|
+
}
|
|
564
|
+
else
|
|
565
|
+
reporter.updateSpan(task.trace, {
|
|
566
|
+
metadata: { taskState: event.phase === 'paused' ? 'paused' : undefined },
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (event.phase === 'disposed')
|
|
570
|
+
suspendTasks(id);
|
|
571
|
+
if (event.phase === 'disposed' || event.phase === 'cleared') {
|
|
572
|
+
nativeInboxes.delete(id);
|
|
573
|
+
inboxInputs.delete(id);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
console.error('[dsh-langfuse] inbox reporting failed:', error);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
331
580
|
// ------------------------------------------------------------------
|
|
332
581
|
// Subagent child sessions: session/created precedes both subagent/start
|
|
333
582
|
// and the child's first generation, and the durable header already links
|
|
@@ -385,8 +634,8 @@ export function apply(ctx, config) {
|
|
|
385
634
|
}, endedAt));
|
|
386
635
|
});
|
|
387
636
|
// ------------------------------------------------------------------
|
|
388
|
-
//
|
|
389
|
-
//
|
|
637
|
+
// Turn lifecycle: native tasks keep their root across turns; other sessions
|
|
638
|
+
// open one trace per turn. Subagents stay under their parent's trace.
|
|
390
639
|
// ------------------------------------------------------------------
|
|
391
640
|
ctx.on('session/event', (session, event) => {
|
|
392
641
|
const sessionId = session.id;
|
|
@@ -407,7 +656,9 @@ export function apply(ctx, config) {
|
|
|
407
656
|
if (previous?.root)
|
|
408
657
|
reporter.endSpan(previous.root, { level: 'WARNING' });
|
|
409
658
|
const child = children.get(sessionId);
|
|
410
|
-
const inTurn = child
|
|
659
|
+
const inTurn = child
|
|
660
|
+
? child.turn === data.turn
|
|
661
|
+
: (sessions.get(sessionId)?.turn ?? pendingTurns.get(sessionId)?.turn) === data.turn;
|
|
411
662
|
const completed = data.invocationId && !inTurn
|
|
412
663
|
? completedTurns.get(JSON.stringify([sessionId, data.turn]))
|
|
413
664
|
: undefined;
|
|
@@ -553,7 +804,13 @@ export function apply(ctx, config) {
|
|
|
553
804
|
closeHooks(sessionId, true);
|
|
554
805
|
const stale = sessions.get(sessionId);
|
|
555
806
|
if (stale)
|
|
556
|
-
|
|
807
|
+
endTurn(stale);
|
|
808
|
+
inboxInputs.delete(sessionId);
|
|
809
|
+
if (nativeInboxes.has(sessionId)) {
|
|
810
|
+
sessions.delete(sessionId);
|
|
811
|
+
pendingTurns.set(sessionId, { turn: event.data.turn, startTime: new Date(event.time) });
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
557
814
|
const trace = openTrace(sessionId, { turn: event.data.turn });
|
|
558
815
|
if (trace)
|
|
559
816
|
sessions.set(sessionId, { trace, hasInput: false, turn: event.data.turn });
|
|
@@ -575,13 +832,14 @@ export function apply(ctx, config) {
|
|
|
575
832
|
break;
|
|
576
833
|
}
|
|
577
834
|
case 'turn/end': {
|
|
835
|
+
if (pendingTurns.has(sessionId))
|
|
836
|
+
traceFor(sessionId);
|
|
578
837
|
const state = sessions.get(sessionId);
|
|
579
838
|
if (state) {
|
|
580
839
|
sessions.delete(sessionId);
|
|
581
840
|
rememberTurn(sessionId, event.data.turn, state.trace, state.trace);
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
afterMedia(state.trace, (endedAt) => reporter.endSpan(state.trace, {
|
|
841
|
+
// Native task roots stay open until the A2A terminal signal.
|
|
842
|
+
endTurn(state, {
|
|
585
843
|
output: state.lastAssistantText,
|
|
586
844
|
level: event.data.reason.kind === 'error'
|
|
587
845
|
? 'ERROR'
|
|
@@ -599,7 +857,7 @@ export function apply(ctx, config) {
|
|
|
599
857
|
? { errorCode: event.data.reason.error.code }
|
|
600
858
|
: {}),
|
|
601
859
|
},
|
|
602
|
-
}
|
|
860
|
+
});
|
|
603
861
|
}
|
|
604
862
|
break;
|
|
605
863
|
}
|
|
@@ -611,6 +869,9 @@ export function apply(ctx, config) {
|
|
|
611
869
|
const state = sessions.get(sessionId);
|
|
612
870
|
sessions.delete(sessionId);
|
|
613
871
|
traceContexts.delete(sessionId);
|
|
872
|
+
pendingTurns.delete(sessionId);
|
|
873
|
+
nativeInboxes.delete(sessionId);
|
|
874
|
+
inboxInputs.delete(sessionId);
|
|
614
875
|
for (const [key, completed] of completedTurns) {
|
|
615
876
|
if (completed.sessionId === sessionId)
|
|
616
877
|
completedTurns.delete(key);
|
|
@@ -621,7 +882,8 @@ export function apply(ctx, config) {
|
|
|
621
882
|
// trace) — v5 spans only export on end, so dropping it open would lose
|
|
622
883
|
// everything it recorded.
|
|
623
884
|
if (state)
|
|
624
|
-
|
|
885
|
+
endTurn(state);
|
|
886
|
+
suspendTasks(sessionId);
|
|
625
887
|
// Children: keep the entry — background/continuable subagents dispose
|
|
626
888
|
// their session BEFORE subagent/end fires, and the span's close must ride
|
|
627
889
|
// subagent/end (disposing first would either leak the span or close it
|
|
@@ -920,10 +1182,16 @@ export function apply(ctx, config) {
|
|
|
920
1182
|
// down — v5 spans only export on end, so unloading mid-turn (profile
|
|
921
1183
|
// reload) would otherwise silently drop them.
|
|
922
1184
|
for (const state of sessions.values())
|
|
923
|
-
|
|
1185
|
+
endTurn(state, {}, endedAt);
|
|
1186
|
+
suspendTasks();
|
|
1187
|
+
await drainMedia();
|
|
924
1188
|
for (const child of children.values())
|
|
925
1189
|
reporter.endSpan(child.span, {}, endedAt);
|
|
926
1190
|
sessions.clear();
|
|
1191
|
+
pendingTurns.clear();
|
|
1192
|
+
nativeInboxes.clear();
|
|
1193
|
+
inboxInputs.clear();
|
|
1194
|
+
inboxTasks.clear();
|
|
927
1195
|
completedTurns.clear();
|
|
928
1196
|
traceContexts.clear();
|
|
929
1197
|
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,30 @@ 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
|
+
paused?: boolean;
|
|
48
|
+
agentStatus?: string;
|
|
49
|
+
taskId?: string;
|
|
50
|
+
taskIds?: string[];
|
|
51
|
+
messageId?: string;
|
|
52
|
+
referenceTaskIds?: string[];
|
|
53
|
+
nativeMessageId?: string;
|
|
54
|
+
queuedAt?: number;
|
|
55
|
+
selectedAt?: number;
|
|
56
|
+
turn?: number;
|
|
57
|
+
step?: number;
|
|
58
|
+
}
|
|
36
59
|
declare module '@deepseek-ai/cordis' {
|
|
37
60
|
interface Events {
|
|
61
|
+
'a2a/inbox'(detail: A2aInboxEvent): void;
|
|
38
62
|
/** Bind standard labels/parent context before a session's next trace starts. */
|
|
39
63
|
'langfuse/context': (sessionId: string, context: LangfuseTraceContext) => void;
|
|
40
64
|
}
|
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.26",
|
|
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",
|