@amaster.ai/dsh-langfuse 0.1.24 → 0.1.25
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 +3 -1
- package/lib/index.js +35 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ Tool hooks should include the execution's `callId` in both records. The plugin m
|
|
|
134
134
|
|
|
135
135
|
`hook/result.outcome` describes process execution independently of `decision`: `success` is normal completion, `failed` is execution failure, `timed-out` is deadline termination, and `cancelled` is cancellation. The producer maps its process semantics to these values; an exit code expressing a policy denial can still be a successful execution. Optional `exitCode` and `signal` preserve process details. Failed/timed-out executions receive ERROR observations, cancellation receives WARNING, and successful executions retain decision-based warnings. Records without an explicit outcome preserve the legacy exit-code interpretation (0/2 are non-error). The plugin records `durationMs` and never infers a timeout or cancellation from a missing exit code.
|
|
136
136
|
|
|
137
|
-
For records admitted by the session, runs with `invocationId` retain correlation after `turn/end` and a subsequent `turn/start`; a late result ends the original hook span without attaching it to the new turn.
|
|
137
|
+
For records admitted by the session, runs with `invocationId` retain correlation after `turn/end` and a subsequent `turn/start`; a late result ends the original hook span without attaching it to the new turn. Delayed invocations, including `UserCancel`, reuse their originating turn's parent span even after the next turn starts. The plugin retains the latest 128 completed-turn associations across all sessions, clearing a session's associations on disposal and all associations on unload. Reporting never reopens the ended root or changes its end time or final status; each hook records its own duration. When the originating parent is unavailable or evicted, the invocation receives a separate root with `detachedHook: true` and the original turn number; its result ends both hook and fallback root. Neither path consumes `langfuse/context` queued for the next turn. Detached producers must finish reporting before the session is disposed. Session disposal/plugin unload closes unfinished runs with `hook result not observed`; this means missing telemetry, not proof of execution failure. Legacy records without `invocationId` still close at turn boundaries.
|
|
138
138
|
|
|
139
139
|
Custom names, hook input, parsed results and raw process output follow `captureContent`; credentials and `redactFields` are masked by the existing reporter. New outcomes, correlation IDs, dialect, point, duration and process metadata remain available with content capture disabled. The event writer does not sanitize the session log itself: application producers remain responsible for what they persist. Applications must explicitly replace any private hook events with this standard producer path, reuse the same `invocationId`, and report the actual outcome; installing/upgrading the plugin does not adapt an application bridge or repair historical Trace overview IO (Issue #59).
|
|
140
140
|
|
|
@@ -146,6 +146,8 @@ Run `pnpm test:sdk` at the repository root for a secrets-free check using the re
|
|
|
146
146
|
|
|
147
147
|
The independent `dsh-langfuse.tool-hooks` E2E leg boots the packed plugin in the real headless profile, asks the model to read a marker file, and executes BeforeTool/AfterTool scripts through an E2E-only producer. It requires both gateway and Langfuse credentials, then polls the real Observations API to verify named hooks, tool-call parenting, enclosing tool timestamps, script input and parsed output, credential masking, and stdout truncation. The existing `dsh-langfuse.hook` leg continues to check the upstream Codex UserPromptSubmit bridge; the new fixture verifies the shared event contract and does not substitute for the application's Gemini bridge integration.
|
|
148
148
|
|
|
149
|
+
The `dsh-langfuse.cancel-hook` E2E leg cancels a real agent at a tool boundary, queues its next turn, and asynchronously runs a UserCancel process after that turn starts. Real Langfuse API read-back must show the hook under the cancelled turn's root and trace ID, with the root still aborted and ended before the hook's own duration. It requires the existing gateway and Langfuse secrets in `integration.yml`.
|
|
150
|
+
|
|
149
151
|
### Trace overview compatibility
|
|
150
152
|
|
|
151
153
|
Overall input/output belongs to the `dsh-turn` root observation. The root is exported when the turn ends; while a long turn is running, child observations can already be visible before the root appears. After completion, allow asynchronous ingestion to settle.
|
package/lib/index.js
CHANGED
|
@@ -204,6 +204,17 @@ export function apply(ctx, config) {
|
|
|
204
204
|
const reporter = new LangfuseReporter(config);
|
|
205
205
|
const sessions = new Map();
|
|
206
206
|
const children = new Map();
|
|
207
|
+
const completedTurns = new Map();
|
|
208
|
+
function rememberTurn(sessionId, turn, parent, turnTrace) {
|
|
209
|
+
if (!parent)
|
|
210
|
+
return;
|
|
211
|
+
const key = JSON.stringify([sessionId, turn]);
|
|
212
|
+
completedTurns.delete(key);
|
|
213
|
+
completedTurns.set(key, { sessionId, parent, turnTrace });
|
|
214
|
+
// Bound retained SDK handles across all sessions; older hooks use the standalone fallback.
|
|
215
|
+
if (completedTurns.size > 128)
|
|
216
|
+
completedTurns.delete(completedTurns.keys().next().value);
|
|
217
|
+
}
|
|
207
218
|
/** Per-session stack of currently open tool pipeline spans (for subagent parenting). */
|
|
208
219
|
const openToolSpans = new Map();
|
|
209
220
|
const toolCalls = new Map();
|
|
@@ -397,7 +408,10 @@ export function apply(ctx, config) {
|
|
|
397
408
|
reporter.endSpan(previous.root, { level: 'WARNING' });
|
|
398
409
|
const child = children.get(sessionId);
|
|
399
410
|
const inTurn = child ? child.turn === data.turn : sessions.get(sessionId)?.turn === data.turn;
|
|
400
|
-
const
|
|
411
|
+
const completed = data.invocationId && !inTurn
|
|
412
|
+
? completedTurns.get(JSON.stringify([sessionId, data.turn]))
|
|
413
|
+
: undefined;
|
|
414
|
+
const root = data.invocationId && !inTurn && !completed
|
|
401
415
|
? // A late hook must not consume context queued for the next turn.
|
|
402
416
|
reporter.openTrace({
|
|
403
417
|
name: config.traceName,
|
|
@@ -407,7 +421,12 @@ export function apply(ctx, config) {
|
|
|
407
421
|
: undefined;
|
|
408
422
|
const tool = data.callId ? toolCalls.get(sessionId)?.get(data.callId) : undefined;
|
|
409
423
|
const hookName = config.captureContent ? reporter.mask({ name: data.name }).name : undefined;
|
|
410
|
-
const span = reporter.startSpan(
|
|
424
|
+
const span = reporter.startSpan(completed?.parent ??
|
|
425
|
+
(root === undefined
|
|
426
|
+
? tool?.turn === data.turn
|
|
427
|
+
? tool.span
|
|
428
|
+
: parentFor(sessionId)
|
|
429
|
+
: root), {
|
|
411
430
|
name: `hook:${data.point}${hookName ? `:${hookName}` : ''}`,
|
|
412
431
|
input: config.captureContent ? data.input : undefined,
|
|
413
432
|
metadata: {
|
|
@@ -429,7 +448,11 @@ export function apply(ctx, config) {
|
|
|
429
448
|
handlerId: data.handlerId,
|
|
430
449
|
persistent: !!data.invocationId,
|
|
431
450
|
callId: data.callId,
|
|
432
|
-
turnTrace:
|
|
451
|
+
turnTrace: completed
|
|
452
|
+
? completed.turnTrace
|
|
453
|
+
: root === undefined
|
|
454
|
+
? (child?.turnTrace ?? sessions.get(sessionId)?.trace)
|
|
455
|
+
: undefined,
|
|
433
456
|
});
|
|
434
457
|
openHooks.set(sessionId, pending);
|
|
435
458
|
return;
|
|
@@ -500,8 +523,10 @@ export function apply(ctx, config) {
|
|
|
500
523
|
if (child) {
|
|
501
524
|
if (event.type === 'turn/start')
|
|
502
525
|
child.turn = event.data.turn;
|
|
503
|
-
if (event.type === 'turn/end')
|
|
526
|
+
if (event.type === 'turn/end') {
|
|
527
|
+
rememberTurn(sessionId, event.data.turn, child.span, child.turnTrace ?? null);
|
|
504
528
|
child.turn = undefined;
|
|
529
|
+
}
|
|
505
530
|
// The delegated prompt (the child's first user/message) is the
|
|
506
531
|
// subagent span's input; the descriptor enriches the span name.
|
|
507
532
|
if (event.type === 'user/message' && !child.hasInput && config.captureContent) {
|
|
@@ -553,6 +578,7 @@ export function apply(ctx, config) {
|
|
|
553
578
|
const state = sessions.get(sessionId);
|
|
554
579
|
if (state) {
|
|
555
580
|
sessions.delete(sessionId);
|
|
581
|
+
rememberTurn(sessionId, event.data.turn, state.trace, state.trace);
|
|
556
582
|
// The turn's final answer becomes the trace's output; ending the
|
|
557
583
|
// root span is what exports the trace (v5 exports spans on end).
|
|
558
584
|
afterMedia(state.trace, (endedAt) => reporter.endSpan(state.trace, {
|
|
@@ -585,6 +611,10 @@ export function apply(ctx, config) {
|
|
|
585
611
|
const state = sessions.get(sessionId);
|
|
586
612
|
sessions.delete(sessionId);
|
|
587
613
|
traceContexts.delete(sessionId);
|
|
614
|
+
for (const [key, completed] of completedTurns) {
|
|
615
|
+
if (completed.sessionId === sessionId)
|
|
616
|
+
completedTurns.delete(key);
|
|
617
|
+
}
|
|
588
618
|
openToolSpans.delete(sessionId);
|
|
589
619
|
toolCalls.delete(sessionId);
|
|
590
620
|
// End an abandoned root (crash-orphaned turn or between-turns one-off
|
|
@@ -894,6 +924,7 @@ export function apply(ctx, config) {
|
|
|
894
924
|
for (const child of children.values())
|
|
895
925
|
reporter.endSpan(child.span, {}, endedAt);
|
|
896
926
|
sessions.clear();
|
|
927
|
+
completedTurns.clear();
|
|
897
928
|
traceContexts.clear();
|
|
898
929
|
children.clear();
|
|
899
930
|
openToolSpans.clear();
|
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.25",
|
|
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",
|